Procházet zdrojové kódy

Merge branch 'ctkdoublespinbox-decimalsbykey'

* ctkdoublespinbox-decimalsbykey:
  Expose publicly ctkRangeWidget slider, min and max spinbox
  Fix ctkSliderWidget assert when changing decimals with Ctrl+'+'
  Fix ctkSliderWidget assert when changing decimals with DecimalsByValue
  Expose ctkDoubleSpinBox::DecimalsOption to other widgets
  Add ctkDoubleSpinBox::DecimalsByKey, DecimalsByValue and ReplaceDecimals
Julien Finet před 12 roky
rodič
revize
1f3efbb8d3

+ 1 - 1
Libs/Widgets/Resources/UI/ctkScreenshotDialog.ui

@@ -156,7 +156,7 @@
          <string>Select an integer scale factor (between 0.5 and 5) for the image file, e.g. a value of &quot;2&quot; will save an image twice the size.</string>
         </property>
         <property name="decimalsOption">
-         <enum>ctkDoubleSpinBox::Fixed</enum>
+         <enum>ctkDoubleSpinBox::FixedDecimals</enum>
         </property>
         <property name="decimals">
          <number>1</number>

+ 6 - 0
Libs/Widgets/Testing/Cpp/CMakeLists.txt

@@ -29,6 +29,7 @@ set(TEST_SOURCES
   ctkDoubleRangeSliderTest2.cpp
   ctkDoubleSliderTest1.cpp
   ctkDoubleSliderTest2.cpp
+  ctkDoubleSpinBoxTest.cpp
   ctkDoubleSpinBoxTest1.cpp
   ctkDynamicSpacerTest1.cpp
   ctkDynamicSpacerTest2.cpp
@@ -70,6 +71,7 @@ set(TEST_SOURCES
   ctkSettingsTest1.cpp
   ctkSettingsDialogTest1.cpp
   ctkSignalMapperTest1.cpp
+  ctkSliderWidgetTest.cpp
   ctkSliderWidgetTest1.cpp
   ctkSliderWidgetTest2.cpp
   ctkThumbnailListWidgetTest1.cpp
@@ -178,6 +180,7 @@ set(Tests_MOC_CPP)
 QT4_WRAP_CPP(Tests_MOC_CPP ${Tests_MOC_SRCS})
 QT4_GENERATE_MOCS(
   ctkCoordinatesWidgetTest.cpp
+  ctkDoubleSpinBoxTest.cpp
   ctkFlatProxyModelTest.cpp
   ctkFontButtonTest.cpp
   ctkLanguageComboBoxTest.cpp
@@ -186,6 +189,7 @@ QT4_GENERATE_MOCS(
   ctkPathListWidgetWithButtonsTest.cpp
   ctkRangeSliderTest.cpp
   ctkSettingsPanelTest.cpp
+  ctkSliderWidgetTest.cpp
   )
 set(Tests_UI_CPP)
 if(TEST_UI_FORMS)
@@ -230,6 +234,7 @@ SIMPLE_TEST( ctkDoubleRangeSliderTest1 )
 SIMPLE_TEST( ctkDoubleRangeSliderTest2 )
 SIMPLE_TEST( ctkDoubleSliderTest1 )
 SIMPLE_TEST( ctkDoubleSliderTest2 )
+SIMPLE_TEST( ctkDoubleSpinBoxTest )
 SIMPLE_TEST( ctkDoubleSpinBoxTest1 )
 SIMPLE_TEST( ctkDynamicSpacerTest1 )
 SIMPLE_TEST( ctkDynamicSpacerTest2 )
@@ -274,6 +279,7 @@ SIMPLE_TEST( ctkSettingsPanelTest )
 SIMPLE_TEST( ctkSettingsPanelTest1 )
 SIMPLE_TEST( ctkSettingsTest1 )
 SIMPLE_TEST( ctkSignalMapperTest1 )
+SIMPLE_TEST( ctkSliderWidgetTest )
 SIMPLE_TEST( ctkSliderWidgetTest1 )
 SIMPLE_TEST( ctkSliderWidgetTest2 )
 SIMPLE_TEST( ctkThumbnailListWidgetTest1 )

+ 385 - 0
Libs/Widgets/Testing/Cpp/ctkDoubleSpinBoxTest.cpp

@@ -0,0 +1,385 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0.txt
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+// Qt includes
+#include <QApplication>
+#include <QDoubleSpinBox>
+#include <QLineEdit>
+#include <QString>
+#include <QStyle>
+#include <QStyleOptionSlider>
+#include <QTimer>
+
+// CTK includes
+#include "ctkDoubleSpinBox.h"
+#include "ctkTest.h"
+
+// ----------------------------------------------------------------------------
+class ctkDoubleSpinBoxTester: public QObject
+{
+  Q_OBJECT
+private slots:
+  void testUI();
+
+  void testToLocals();
+
+  void testDecimalsByKey();
+  void testDecimalsByKey_data();
+
+  void testPrefix();
+  void testPrefix_data();
+
+  void testDecimalsByValue();
+  void testDecimalsByValue_data();
+
+  void testDecimalPointAlwaysVisible();
+  void testDecimalPointAlwaysVisible_data();
+};
+
+// ----------------------------------------------------------------------------
+void ctkDoubleSpinBoxTester::testUI()
+{
+  ctkDoubleSpinBox spinBox;
+  spinBox.setMinimum(-100.);
+  spinBox.setMaximum(100.);
+  spinBox.setDecimalsOption( ctkDoubleSpinBox::DecimalsByValue |ctkDoubleSpinBox::DecimalsByShortcuts );
+  spinBox.setValue(26.2110001);
+  spinBox.setPrefix("A: ");
+  spinBox.setSetMode(ctkDoubleSpinBox::SetAlways);
+  spinBox.show();
+  QTest::qWaitForWindowShown(&spinBox);
+  QObject::connect(&spinBox, SIGNAL(valueChanged(double)),
+                   &spinBox, SLOT(setValue(double)), Qt::QueuedConnection);
+
+  QDoubleSpinBox doubleSpinBox;
+  doubleSpinBox.setMinimum(-100.);
+  doubleSpinBox.setMaximum(100.);
+  doubleSpinBox.setValue(2.);
+  //doubleSpinBox.show();
+  //QTest::qWaitForWindowShown(&doubleSpinBox);
+
+  //qApp->exec();
+}
+
+// ----------------------------------------------------------------------------
+void ctkDoubleSpinBoxTester::testToLocals()
+{
+  bool ok;
+  QLocale().toDouble("+.0", &ok);
+  qDebug() << "+.0" << ok;
+  QLocale().toDouble("0.0 1", &ok);
+  qDebug() << "0.0 1" << ok;
+}
+
+// ----------------------------------------------------------------------------
+void ctkDoubleSpinBoxTester::testDecimalsByKey()
+{
+  ctkDoubleSpinBox spinBox;
+  spinBox.setMinimum(-200.);
+  spinBox.setMaximum(200.);
+  spinBox.setValue(1.23);
+
+  QFETCH(int, decimalsOptions);
+  spinBox.setDecimalsOption( static_cast<ctkDoubleSpinBox::DecimalsOptions>(decimalsOptions) );
+  const int oldDecimals = spinBox.decimals();
+
+  QFETCH(int, cursorPosition);
+  QFETCH(int, key);
+
+  spinBox.lineEdit()->setCursorPosition(cursorPosition);
+  //spinBox.show();
+  //QTest::qWaitForWindowShown(&spinBox);
+  //qApp->exec();
+  QSignalSpy spy(&spinBox, SIGNAL(decimalsChanged(int)));
+  QTest::keyClick(spinBox.lineEdit(), static_cast<Qt::Key>(key));
+
+  QFETCH(QString, expectedText);
+  QFETCH(int, expectedDecimals);
+
+  QCOMPARE(spinBox.text(), expectedText);
+  QCOMPARE(spinBox.value(), expectedText.toDouble());
+  QCOMPARE(spinBox.decimals(), expectedDecimals);
+  QCOMPARE(spy.count(), spinBox.decimals() != oldDecimals ? 1 : 0);
+}
+
+// ----------------------------------------------------------------------------
+void ctkDoubleSpinBoxTester::testDecimalsByKey_data()
+{
+  QTest::addColumn<int>("decimalsOptions");
+  QTest::addColumn<int>("cursorPosition");
+  QTest::addColumn<int>("key");
+  QTest::addColumn<QString>("expectedText");
+  QTest::addColumn<int>("expectedDecimals");
+
+  QList<int> options;
+  // ctkDoubleSpinBox::DecimalsByKey
+  options << ctkDoubleSpinBox::DecimalsByKey;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 0:'1' -> 11.23") << options.last() << 0 << int(Qt::Key_1) << "11.23"<< 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 0:'del' -> .23") << options.last() << 0 << int(Qt::Key_Delete) << ".23" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 0:'backspace' -> 1.23") << options.last() << 0 << int(Qt::Key_Backspace) << "1.23" << 2;
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 1:'1' -> 11.23") << options.last() << 1 << int(Qt::Key_1) << "11.23" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 1:'del' -> 123") << options.last() << 1 << int(Qt::Key_Delete) << "123" << 0;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 1:'backspace' -> .23") << options.last() << 1 << int(Qt::Key_Backspace) << ".23" << 2;
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 2:'1' -> 1.12") << options.last() << 2 << int(Qt::Key_1) << "1.12" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 2:'del' -> 1.3") << options.last() << 2 << int(Qt::Key_Delete) << "1.3" << 1;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 2:'backspace' -> 123") << options.last() << 2 << int(Qt::Key_Backspace) << "123" << 0;
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 3:'1' -> 1.21") << options.last() << 3 << int(Qt::Key_1) << "1.21" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 3:'del' -> 1.2") << options.last() << 3 << int(Qt::Key_Delete) << "1.2" << 1;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 3:'backspace' -> 1.3") << options.last() << 3 << int(Qt::Key_Backspace) << "1.3" << 1;
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 4:'1' -> 1.231") << options.last() << 4 << int(Qt::Key_1) << "1.231" << 3;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 4:'del' -> 1.23") << options.last() << 4 << int(Qt::Key_Delete) << "1.23" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 4:'backspace' -> 1.2") << options.last() << 4 << int(Qt::Key_Backspace) << "1.2" << 1;
+
+  // ctkDoubleSpinBox::ReplaceDecimals
+  options << ctkDoubleSpinBox::ReplaceDecimals;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 0:'1' -> 11.23") << options.last() << 0 << int(Qt::Key_1) << "11.23"<< 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 0:'del' -> .23") << options.last() << 0 << int(Qt::Key_Delete) << ".23" << 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 0:'backspace' -> 1.23") << options.last() << 0 << int(Qt::Key_Backspace) << "1.23" << 2;
+
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 1:'1' -> 11.23") << options.last() << 1 << int(Qt::Key_1) << "11.23" << 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 1:'del' -> 123") << options.last() << 1 << int(Qt::Key_Delete) << "123" << 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 1:'backspace' -> .23") << options.last() << 1 << int(Qt::Key_Backspace) << ".23" << 2;
+
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 2:'1' -> 1.13") << options.last() << 2 << int(Qt::Key_1) << "1.13" << 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 2:'del' -> 1.3") << options.last() << 2 << int(Qt::Key_Delete) << "1.3" << 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 2:'backspace' -> 123") << options.last() << 2 << int(Qt::Key_Backspace) << "123" << 2;
+
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 3:'1' -> 1.21") << options.last() << 3 << int(Qt::Key_1) << "1.21" << 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 3:'del' -> 1.2") << options.last() << 3 << int(Qt::Key_Delete) << "1.2" << 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 3:'backspace' -> 1.3") << options.last() << 3 << int(Qt::Key_Backspace) << "1.3" << 2;
+
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 4:'1' -> 1.23") << options.last() << 4 << int(Qt::Key_1) << "1.23" << 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 4:'del' -> 1.23") << options.last() << 4 << int(Qt::Key_Delete) << "1.23" << 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 4:'backspace' -> 1.2") << options.last() << 4 << int(Qt::Key_Backspace) << "1.2" << 2;
+
+  // ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals
+  options << (ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals);
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 0:'1' -> 11.23")
+    << options.last() << 0 << int(Qt::Key_1) << "11.23"<< 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 0:'del' -> .23")
+    << options.last() << 0 << int(Qt::Key_Delete) << ".23" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 0:'backspace' -> 1.23")
+    << options.last() << 0 << int(Qt::Key_Backspace) << "1.23" << 2;
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 1:'1' -> 11.23")
+    << options.last() << 1 << int(Qt::Key_1) << "11.23" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 1:'del' -> 123")
+    << options.last() << 1 << int(Qt::Key_Delete) << "123" << 0;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 1:'backspace' -> .23")
+    << options.last() << 1 << int(Qt::Key_Backspace) << ".23" << 2;
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 2:'1' -> 1.13")
+    << options.last() << 2 << int(Qt::Key_1) << "1.13" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 2:'del' -> 1.3")
+    << options.last() << 2 << int(Qt::Key_Delete) << "1.3" << 1;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 2:'backspace' -> 123")
+    << options.last() << 2 << int(Qt::Key_Backspace) << "123" << 0;
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 3:'1' -> 1.21")
+    << options.last() << 3 << int(Qt::Key_1) << "1.21" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 3:'del' -> 1.2")
+    << options.last() << 3 << int(Qt::Key_Delete) << "1.2" << 1;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 3:'backspace' -> 1.3")
+    << options.last() << 3 << int(Qt::Key_Backspace) << "1.3" << 1;
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 4:'1' -> 1.231")
+    << options.last() << 4 << int(Qt::Key_1) << "1.231" << 3;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 4:'del' -> 1.23")
+    << options.last() << 4 << int(Qt::Key_Delete) << "1.23" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 4:'backspace' -> 1.23")
+    << options.last() << 4 << int(Qt::Key_Backspace) << "1.2" << 1;
+
+  // ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::InsertDecimals
+  options << (ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::InsertDecimals);
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::InsertDecimals 0:'1' -> 11.23")
+    << options.last() << 0 << int(Qt::Key_1) << "11.23"<< 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::InsertDecimals 0:'del' -> .23")
+    << options.last() << 0 << int(Qt::Key_Delete) << ".23" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::InsertDecimals 0:'backspace' -> 1.23")
+    << options.last() << 0 << int(Qt::Key_Backspace) << "1.23" << 2;
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::InsertDecimals 1:'1' -> 11.23")
+    << options.last() << 1 << int(Qt::Key_1) << "11.23" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::InsertDecimals 1:'del' -> 123")
+    << options.last() << 1 << int(Qt::Key_Delete) << "123" << 0;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::InsertDecimals 1:'backspace' -> .23")
+    << options.last() << 1 << int(Qt::Key_Backspace) << ".23" << 2;
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::InsertDecimals 2:'1' -> 1.123")
+    << options.last() << 2 << int(Qt::Key_1) << "1.123" << 3;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::InsertDecimals 2:'del' -> 1.3")
+    << options.last() << 2 << int(Qt::Key_Delete) << "1.3" << 1;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::InsertDecimals 2:'backspace' -> 123")
+    << options.last() << 2 << int(Qt::Key_Backspace) << "123" << 0;
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::InsertDecimals 3:'1' -> 1.213")
+    << options.last() << 3 << int(Qt::Key_1) << "1.213" << 3;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::InsertDecimals 3:'del' -> 1.2")
+    << options.last() << 3 << int(Qt::Key_Delete) << "1.2" << 1;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::InsertDecimals 3:'backspace' -> 1.3")
+    << options.last() << 3 << int(Qt::Key_Backspace) << "1.3" << 1;
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::InsertDecimals 4:'1' -> 1.231")
+    << options.last() << 4 << int(Qt::Key_1) << "1.231" << 3;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::InsertDecimals 4:'del' -> 1.23")
+    << options.last() << 4 << int(Qt::Key_Delete) << "1.23" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::InsertDecimals 4:'backspace' -> 1.23")
+    << options.last() << 4 << int(Qt::Key_Backspace) << "1.2" << 1;
+
+  foreach(int option, options)
+    {
+    // bad keys are always rejected
+    for (int i = 0; i < 5; ++i)
+      {
+      QTest::newRow(QString("%1 %2:'a' -> 1.23").arg(option).arg(i).toLatin1())
+        << option << i << int(Qt::Key_A) << "1.23" << 2;
+      }
+    // sign keys are only for the first digit
+    QTest::newRow(QString("%1 0:'+' -> 1.23").arg(option).toLatin1())
+      << option << 0 << int(Qt::Key_Plus) << "+1.23" << 2;
+    QTest::newRow(QString("%1 0:'-' -> -1.23").arg(option).toLatin1())
+      << option << 0 << int(Qt::Key_Minus) << "-1.23" << 2;
+    for (int i = 1; i < 5; ++i)
+      {
+      QTest::newRow(QString("%1 %2:'+' -> 1.23").arg(option).arg(i).toLatin1())
+        << option << i << int(Qt::Key_Plus) << "1.23" << 2;
+      QTest::newRow(QString("%1 %2:'-' -> 1.23").arg(option).arg(i).toLatin1())
+        << option << i << int(Qt::Key_Minus) << "1.23" << 2;
+      }
+    }
+}
+// ----------------------------------------------------------------------------
+void ctkDoubleSpinBoxTester::testPrefix()
+{
+  ctkDoubleSpinBox spinBox;
+  spinBox.setPrefix("A: ");
+  spinBox.setDecimalsOption( ctkDoubleSpinBox::FixedDecimals );
+
+  QFETCH(int, cursorPosition);
+  QFETCH(int, key);
+
+  spinBox.lineEdit()->setCursorPosition(cursorPosition);
+  QTest::keyClick(spinBox.lineEdit(), static_cast<Qt::Key>(key));
+
+  //spinBox.show();
+  //QTest::qWaitForWindowShown(&spinBox);
+  //qApp->exec();
+
+  QFETCH(double, expectedValue);
+  QFETCH(QString, expectedText);
+  QFETCH(int, expectedCursorPosition);
+
+  QCOMPARE(spinBox.text(), expectedText);
+  QCOMPARE(spinBox.value(), expectedValue);
+  QCOMPARE(spinBox.decimals(), 2);
+  QCOMPARE(spinBox.lineEdit()->cursorPosition(), expectedCursorPosition);
+}
+
+// ----------------------------------------------------------------------------
+void ctkDoubleSpinBoxTester::testPrefix_data()
+{
+  QTest::addColumn<int>("cursorPosition");
+  QTest::addColumn<int>("key");
+  QTest::addColumn<QString>("expectedText");
+  QTest::addColumn<double>("expectedValue");
+  QTest::addColumn<int>("expectedCursorPosition");
+
+  QTest::newRow("0:'1' -> 0.00") << 0 << int(Qt::Key_1) << "A: 0.00"<< 0.00 << 0;
+  QTest::newRow("1:'1' -> 10.00") << 1 << int(Qt::Key_1) << "A: 10.00"<< 10.00 << 4;
+  QTest::newRow("2:'1' -> 10.00") << 2 << int(Qt::Key_1) << "A: 10.00"<< 10.00 << 4;
+  QTest::newRow("3:'1' -> 10.00") << 3 << int(Qt::Key_1) << "A: 10.00"<< 10.00 << 4;
+  QTest::newRow("4:'1' -> 01.00") << 4 << int(Qt::Key_1) << "A: 01.00"<< 1.00 << 5;
+}
+
+// ----------------------------------------------------------------------------
+void ctkDoubleSpinBoxTester::testDecimalsByValue()
+{
+  ctkDoubleSpinBox spinBox;
+  spinBox.setMinimum(-100.);
+  spinBox.setMaximum(100.);
+  spinBox.setValue(1.23);
+  spinBox.setDecimalsOption( ctkDoubleSpinBox::DecimalsByValue );
+  QSignalSpy spy(&spinBox, SIGNAL(decimalsChanged(int)));
+  const int oldDecimals = spinBox.decimals();
+
+  QFETCH(double, value);
+  spinBox.setValue(value);
+
+  QFETCH(QString, expectedText);
+  QFETCH(int, expectedDecimals);
+
+  QCOMPARE(spinBox.text(), expectedText);
+  QCOMPARE(spinBox.value(), value);
+  QCOMPARE(spinBox.decimals(), expectedDecimals);
+  QCOMPARE(spy.count(), spinBox.decimals() != oldDecimals ? 1 : 0);
+}
+
+// ----------------------------------------------------------------------------
+void ctkDoubleSpinBoxTester::testDecimalsByValue_data()
+{
+  QTest::addColumn<double>("value");
+  QTest::addColumn<QString>("expectedText");
+  QTest::addColumn<int>("expectedDecimals");
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByValue 0") << 0. << "0"<< 0;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByValue 0.1") << 0.1 << "0.1" << 1;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByValue 0.02") << 0.02 << "0.02" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByValue 10.003") << 10.003 << "10.003" << 3;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByValue -0.0004") << -0.0004 << "-0.0004" << 4;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByValue 0.000056") << 0.000056 << "0.000056" << 6;
+  // internally represented as 123456.001109999997425
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByValue 5.00111") << 5.00111 << "5.00111" << 5;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByValue 0.1234567891013151") << 0.1234567891013151 << "0.1234567891013151" << 16;
+}
+
+// ----------------------------------------------------------------------------
+void ctkDoubleSpinBoxTester::testDecimalPointAlwaysVisible()
+{
+  ctkDoubleSpinBox spinBox;
+  spinBox.setDecimals(0);
+  spinBox.setDecimalsOption( ctkDoubleSpinBox::DecimalPointAlwaysVisible );
+
+  QFETCH(double, value);
+  spinBox.setValue(value);
+
+  QFETCH(QString, expectedText);
+  QCOMPARE(spinBox.text(), expectedText);
+}
+
+// ----------------------------------------------------------------------------
+void ctkDoubleSpinBoxTester::testDecimalPointAlwaysVisible_data()
+{
+  QTest::addColumn<double>("value");
+  QTest::addColumn<QString>("expectedText");
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalPointAlwaysVisible 0") << 0. << "0.";
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByValue 2") << 2. << "2.";
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByValue 1.01") << 1.01 << "1.";
+}
+
+// ----------------------------------------------------------------------------
+CTK_TEST_MAIN(ctkDoubleSpinBoxTest)
+#include "moc_ctkDoubleSpinBoxTest.cpp"
+
+

+ 118 - 0
Libs/Widgets/Testing/Cpp/ctkSliderWidgetTest.cpp

@@ -0,0 +1,118 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0.txt
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+// Qt includes
+#include <QApplication>
+#include <QDoubleSpinBox>
+#include <QLineEdit>
+#include <QString>
+#include <QStyle>
+#include <QStyleOptionSlider>
+#include <QTimer>
+
+// CTK includes
+#include "ctkDoubleSlider.h"
+#include "ctkDoubleSpinBox.h"
+#include "ctkSliderWidget.h"
+#include "ctkTest.h"
+
+// ----------------------------------------------------------------------------
+class ctkSliderWidgetTester: public QObject
+{
+  Q_OBJECT
+private slots:
+  void testUI();
+
+  void testDecimalsByValue();
+  /// This test makes sure the number of decimals increased with Ctrl+'+' does
+  /// not break the synchronization between the value of the slider and the
+  /// value of the spinbox.
+  void testDecimalsByShortcuts();
+
+};
+
+// ----------------------------------------------------------------------------
+void ctkSliderWidgetTester::testUI()
+{
+  ctkSliderWidget slider;
+  slider.setMinimum(-100.);
+  slider.setMaximum(100.);
+  slider.setValue(26.2110001);
+  slider.setPrefix("A: ");
+  slider.show();
+  QTest::qWaitForWindowShown(&slider);
+  QObject::connect(&slider, SIGNAL(valueChanged(double)),
+                   &slider, SLOT(setValue(double)), Qt::QueuedConnection);
+
+
+  //qApp->exec();
+}
+
+// ----------------------------------------------------------------------------
+void ctkSliderWidgetTester::testDecimalsByValue()
+{
+  ctkSliderWidget slider;
+  slider.spinBox()->setDecimalsOption(
+    ctkDoubleSpinBox::DecimalsByValue | ctkDoubleSpinBox::DecimalsByShortcuts );
+  slider.setValue(-12.4);
+
+  //slider.show();
+  //QTest::qWaitForWindowShown(&slider);
+  //qApp->exec();
+  //QSignalSpy spy(&slider, SIGNAL(decimalsChanged(int)));
+
+  slider.setSingleStep(1.3);
+  slider.setRange(-87.2949, 81.7045);
+  slider.setValue(-15);
+
+  slider.slider()->triggerAction(QAbstractSlider::SliderSingleStepSub);
+  slider.setSingleStep(1.3);
+}
+
+// ----------------------------------------------------------------------------
+void ctkSliderWidgetTester::testDecimalsByShortcuts()
+{
+  ctkSliderWidget slider;
+  slider.spinBox()->setDecimalsOption(ctkDoubleSpinBox::DecimalsByShortcuts);
+  slider.setSingleStep(1.299995422363281);
+  slider.setPageStep(1.299995422363281);
+  slider.setRange(-100., 100.);
+  slider.setValue( -2.145195007324205 );
+
+  slider.show();
+  QTest::qWaitForWindowShown(&slider);
+  //qApp->exec();
+  //QSignalSpy spy(&slider, SIGNAL(decimalsChanged(int)));
+
+  slider.slider()->triggerAction(QAbstractSlider::SliderSingleStepAdd);
+  slider.slider()->triggerAction(QAbstractSlider::SliderSingleStepAdd);
+  slider.slider()->triggerAction(QAbstractSlider::SliderSingleStepAdd);
+
+  QTest::keyClick(slider.spinBox(), Qt::Key_Plus, Qt::ControlModifier);
+  QTest::keyClick(slider.spinBox(), Qt::Key_Plus, Qt::ControlModifier);
+  QTest::keyClick(slider.spinBox(), Qt::Key_Plus, Qt::ControlModifier);
+  QCOMPARE(slider.decimals(), 5);
+}
+
+// ----------------------------------------------------------------------------
+CTK_TEST_MAIN(ctkSliderWidgetTest)
+#include "moc_ctkSliderWidgetTest.cpp"
+
+

+ 26 - 0
Libs/Widgets/ctkCoordinatesWidget.cpp

@@ -33,6 +33,8 @@
 ctkCoordinatesWidget::ctkCoordinatesWidget(QWidget* _parent) :QWidget(_parent)
 {
   this->Decimals = 3;
+  ctkDoubleSpinBox temp;
+  this->DecimalsOption = temp.decimalsOption();
   this->SingleStep = 1.;
   this->Minimum = -100000.;
   this->Maximum = 100000.;
@@ -58,6 +60,7 @@ void ctkCoordinatesWidget::addSpinBox()
 {
   ctkDoubleSpinBox* spinBox = new ctkDoubleSpinBox(this);
   spinBox->setDecimals(this->Decimals);
+  spinBox->setDecimalsOption(this->DecimalsOption);
   spinBox->setSingleStep(this->SingleStep);
   spinBox->setMinimum(this->Minimum);
   spinBox->setMaximum(this->Maximum);
@@ -204,6 +207,29 @@ int ctkCoordinatesWidget::decimals() const
   return this->Decimals;
 }
 
+// --------------------------------------------------------------------------
+ctkDoubleSpinBox::DecimalsOptions ctkCoordinatesWidget::decimalsOption()const
+{
+  return this->DecimalsOption;
+}
+
+// --------------------------------------------------------------------------
+void ctkCoordinatesWidget
+::setDecimalsOption(ctkDoubleSpinBox::DecimalsOptions newDecimalsOption)
+{
+  for (int i = 0; this->layout()->itemAt(i); ++i)
+    {
+    QLayoutItem* item = this->layout()->itemAt(i);
+    ctkDoubleSpinBox* spinBox = item ? qobject_cast<ctkDoubleSpinBox*>(
+      item->widget()) : 0;
+    if (spinBox)
+      {
+      spinBox->setDecimalsOption(newDecimalsOption);
+      }
+    }
+  this->DecimalsOption = newDecimalsOption;
+}
+
 //------------------------------------------------------------------------------
 void ctkCoordinatesWidget::setSingleStep(double step)
 {

+ 13 - 0
Libs/Widgets/ctkCoordinatesWidget.h

@@ -25,6 +25,7 @@
 #include <QWidget>
 
 // CTK includes
+#include "ctkDoubleSpinBox.h"
 #include "ctkWidgetsExport.h"
 
 /// \ingroup Widgets
@@ -44,6 +45,10 @@ class CTK_WIDGETS_EXPORT ctkCoordinatesWidget : public QWidget
   Q_PROPERTY(bool normalized READ isNormalized WRITE setNormalized)
 
   Q_PROPERTY(int decimals READ decimals WRITE setDecimals)
+  /// This property provides more controls over the decimals.
+  /// \sa ctkDoubleSpinBox::DecimalsOptions, decimals
+  Q_PROPERTY(ctkDoubleSpinBox::DecimalsOptions decimalsOption READ decimalsOption WRITE setDecimalsOption)
+
   Q_PROPERTY(double singleStep  READ singleStep WRITE setSingleStep STORED false)
   Q_PROPERTY(double minimum READ minimum WRITE setMinimum)
   Q_PROPERTY(double maximum READ maximum WRITE setMaximum)
@@ -63,6 +68,13 @@ public:
   /// The default number of decimals is 3.
   int decimals() const;
 
+  /// Return the decimalsOption property value
+  /// \sa decimalsOption
+  ctkDoubleSpinBox::DecimalsOptions decimalsOption()const;
+  /// Set the decimalsOption property value.
+  /// \sa decimalsOption
+  void setDecimalsOption(ctkDoubleSpinBox::DecimalsOptions option);
+
   /// Set/Get the single step of each coordinate spin box
   /// The default single step is 1.
   void setSingleStep(double step);
@@ -130,6 +142,7 @@ protected:
   static double squaredNorm(double* coordinates, int dimension);
 
   int     Decimals;
+  ctkDoubleSpinBox::DecimalsOptions DecimalsOption;
   double  SingleStep;
   double  Minimum;
   double  Maximum;

+ 537 - 8
Libs/Widgets/ctkDoubleSpinBox.cpp

@@ -29,18 +29,29 @@
 #include <QEvent>
 #include <QHBoxLayout>
 #include <QKeyEvent>
+#include <QLineEdit>
 #include <QShortcut>
 #include <QSizePolicy>
 #include <QVariant>
 
+//-----------------------------------------------------------------------------
+// ctkQDoubleSpinBox
 //----------------------------------------------------------------------------
-ctkQDoubleSpinBox::ctkQDoubleSpinBox(QWidget* spinBoxParent)
+ctkQDoubleSpinBox::ctkQDoubleSpinBox(ctkDoubleSpinBoxPrivate* pimpl,
+                                     QWidget* spinBoxParent)
   : QDoubleSpinBox(spinBoxParent)
+  , d_ptr(pimpl)
 {
   this->InvertedControls = false;
 }
 
 //----------------------------------------------------------------------------
+QLineEdit* ctkQDoubleSpinBox::lineEdit()const
+{
+  return this->QDoubleSpinBox::lineEdit();
+}
+
+//----------------------------------------------------------------------------
 void ctkQDoubleSpinBox::setInvertedControls(bool invertedControls)
 {
   this->InvertedControls = invertedControls;
@@ -94,6 +105,63 @@ QAbstractSpinBox::StepEnabled ctkQDoubleSpinBox::stepEnabled() const
 }
 
 //-----------------------------------------------------------------------------
+double ctkQDoubleSpinBox::valueFromText(const QString &text) const
+{
+  Q_D(const ctkDoubleSpinBox);
+
+  QString copy = text;
+  int pos = this->lineEdit()->cursorPosition();
+  QValidator::State state = QValidator::Acceptable;
+  int decimals = 0;
+  double value = d->validateAndInterpret(copy, pos, state, decimals);
+  return value;
+}
+
+//-----------------------------------------------------------------------------
+QString ctkQDoubleSpinBox::textFromValue(double value) const
+{
+  Q_D(const ctkDoubleSpinBox);
+  QString text = this->QDoubleSpinBox::textFromValue(value);
+  if (text.isEmpty())
+    {
+    text = "0";
+    }
+  // If there is no decimal, it does not mean there won't be any.
+  if (d->DOption & ctkDoubleSpinBox::DecimalPointAlwaysVisible &&
+      text.indexOf(this->locale().decimalPoint()) == -1)
+    {
+    text += this->locale().decimalPoint();
+    }
+  return text;
+}
+
+//-----------------------------------------------------------------------------
+int ctkQDoubleSpinBox::decimalsFromText(const QString &text) const
+{
+  Q_D(const ctkDoubleSpinBox);
+
+  QString copy = text;
+  int pos = this->lineEdit()->cursorPosition();
+  int decimals = 0;
+  QValidator::State state = QValidator::Acceptable;
+  d->validateAndInterpret(copy, pos, state, decimals);
+  return decimals;
+}
+
+//-----------------------------------------------------------------------------
+QValidator::State ctkQDoubleSpinBox::validate(QString &text, int &pos) const
+{
+  Q_D(const ctkDoubleSpinBox);
+
+  QValidator::State state = QValidator::Acceptable;
+  int decimals = 0;
+  d->validateAndInterpret(text, pos, state, decimals);
+  return state;
+}
+
+//-----------------------------------------------------------------------------
+// ctkDoubleSpinBoxPrivate
+//-----------------------------------------------------------------------------
 ctkDoubleSpinBoxPrivate::ctkDoubleSpinBoxPrivate(ctkDoubleSpinBox& object)
   : q_ptr(&object)
 {
@@ -102,7 +170,9 @@ ctkDoubleSpinBoxPrivate::ctkDoubleSpinBoxPrivate(ctkDoubleSpinBox& object)
   this->SpinBox = 0;
   this->Mode = ctkDoubleSpinBox::SetIfDifferent;
   this->DefaultDecimals = 2;
-  this->DOption = ctkDoubleSpinBox::UseShortcuts;
+  // InsertDecimals is not a great default, but it is QDoubleSpinBox's default.
+  this->DOption = ctkDoubleSpinBox::DecimalsByShortcuts
+    | ctkDoubleSpinBox::InsertDecimals;
   this->InvertedControls = false;
 }
 
@@ -110,8 +180,15 @@ ctkDoubleSpinBoxPrivate::ctkDoubleSpinBoxPrivate(ctkDoubleSpinBox& object)
 void ctkDoubleSpinBoxPrivate::init()
 {
   Q_Q(ctkDoubleSpinBox);
-  this->SpinBox = new ctkQDoubleSpinBox(q);
+  this->SpinBox = new ctkQDoubleSpinBox(this, q);
   this->SpinBox->setInvertedControls(this->InvertedControls);
+  // ctkQDoubleSpinBox needs to be first to receive textChanged() signals.
+  QLineEdit* lineEdit = new QLineEdit(q);
+  QObject::connect(lineEdit, SIGNAL(textChanged(QString)),
+                   this, SLOT(editorTextChanged(QString)));
+  this->SpinBox->setLineEdit(lineEdit);
+  lineEdit->setObjectName(QLatin1String("qt_spinbox_lineedit"));
+
   QObject::connect(this->SpinBox, SIGNAL(valueChanged(double)),
     q, SIGNAL(valueChanged(double)));
   QObject::connect(this->SpinBox, SIGNAL(valueChanged(const QString&)),
@@ -137,14 +214,444 @@ bool ctkDoubleSpinBoxPrivate::compare(double x1, double x2) const
 }
 
 //-----------------------------------------------------------------------------
+QString ctkDoubleSpinBoxPrivate::stripped(const QString& text, int* pos) const
+{
+  Q_Q(const ctkDoubleSpinBox);
+  QString strip(text);
+  if (strip.startsWith(q->prefix()))
+    {
+    strip.remove(0, q->prefix().size());
+    }
+  if (strip.endsWith(q->suffix()))
+    {
+    strip.chop(q->suffix().size());
+    }
+  strip = strip.trimmed();
+  if (pos)
+    {
+    int stripInText = text.indexOf(strip);
+    *pos = qBound(0, *pos - stripInText, strip.size());
+    }
+  return strip;
+}
+
+//-----------------------------------------------------------------------------
+int ctkDoubleSpinBoxPrivate::boundDecimals(int dec)const
+{
+  Q_Q(const ctkDoubleSpinBox);
+  if (dec == -1)
+    {
+    return q->decimals();
+    }
+  int min = (this->DOption & ctkDoubleSpinBox::DecimalsAsMin) ?
+    this->DefaultDecimals : 0;
+  int max = (this->DOption & ctkDoubleSpinBox::DecimalsAsMax) ?
+    this->DefaultDecimals : 323; // see QDoubleSpinBox::decimals doc
+  return qBound(min, dec, max);
+}
+
+//-----------------------------------------------------------------------------
+void ctkDoubleSpinBoxPrivate::setValue(double value, int dec)
+{
+  Q_Q(ctkDoubleSpinBox);
+  dec = this->boundDecimals(dec);
+  bool changeDecimals = dec != q->decimals();
+  if (changeDecimals)
+    {
+    // don't fire decimalsChanged signal yet, wait for the value to be
+    // up-to-date.
+    this->SpinBox->setDecimals(dec);
+    }
+  this->SpinBox->setValue(value); // re-do the text (calls textFromValue())
+  if (changeDecimals)
+    {
+    emit q->decimalsChanged(dec);
+    }
+}
+
+//-----------------------------------------------------------------------------
 void ctkDoubleSpinBoxPrivate::setDecimals(int dec)
 {
   Q_Q(ctkDoubleSpinBox);
+  dec = this->boundDecimals(dec);
   this->SpinBox->setDecimals(dec);
   emit q->decimalsChanged(dec);
 }
 
 //-----------------------------------------------------------------------------
+void ctkDoubleSpinBoxPrivate::editorTextChanged(const QString& text)
+{
+  if (this->SpinBox->keyboardTracking())
+    {
+    QString tmp = text;
+    int pos = this->SpinBox->lineEdit()->cursorPosition();
+    QValidator::State state = QValidator::Invalid;
+    int decimals = 0;
+    this->validateAndInterpret(tmp, pos, state, decimals);
+    if (state == QValidator::Acceptable)
+      {
+      double newValue = this->SpinBox->valueFromText(tmp);
+      int decimals = this->boundDecimals(this->SpinBox->decimalsFromText(tmp));
+      bool changeDecimals = this->DOption & ctkDoubleSpinBox::DecimalsByKey &&
+        decimals != this->SpinBox->decimals();
+      if (changeDecimals)
+        {
+        this->setValue(newValue, decimals);
+        }
+      // else, let QDoubleSpinBox process the validation.
+      }
+    }
+}
+
+//-----------------------------------------------------------------------------
+double ctkDoubleSpinBoxPrivate
+::validateAndInterpret(QString &input, int &pos,
+                       QValidator::State &state, int &decimals) const
+{
+  Q_Q(const ctkDoubleSpinBox);
+  //qDebug() << "input: " << input << "pos:" << pos;
+  if (this->CachedText == input)
+    {
+    state = this->CachedState;
+    decimals = this->CachedDecimals;
+    //qDebug() << "cachedText was '" << this->CachedText << "' state was "
+    //         << int(state) << " and value was " << this->CachedValue;
+    return this->CachedValue;
+    }
+  const double max = q->maximum();
+  const double min = q->minimum();
+
+  int posInValue = pos;
+  QString text = this->stripped(input, &posInValue);
+  // posInValue can change, track the offset.
+  const int oldPosInValue = posInValue;
+  //qDebug() << "text: " << text << pos;
+  state = QValidator::Acceptable;
+  decimals = 0;
+
+  double value = min;
+  const int dec = text.indexOf(q->locale().decimalPoint());
+
+  bool ok = false;
+  value = q->locale().toDouble(text, &ok);
+
+  // could be in an intermediate state
+  if (!ok  && state == QValidator::Acceptable)
+    {
+    if (text.isEmpty() ||
+        text == "." ||
+        text == "-" ||
+        text == "+" ||
+        text == "-." ||
+        text == "+.")
+      {
+      state = QValidator::Intermediate;
+      }
+    }
+  // could be because of group separators:
+  if (!ok && state == QValidator::Acceptable)
+    {
+    if (q->locale().groupSeparator().isPrint())
+      {
+      int start = (dec == -1 ? text.size() : dec)- 1;
+      int lastGroupSeparator = start;
+      for (int digit = start; digit >= 0; --digit)
+        {
+        if (text.at(digit) == q->locale().groupSeparator())
+          {
+          if (digit != lastGroupSeparator - 3)
+            {
+            state = QValidator::Invalid;
+            break;
+            }
+          text.remove(digit, 1);
+          lastGroupSeparator = digit;
+          }
+        }
+      }
+    // try again without the group separators
+    value = q->locale().toDouble(text, &ok);
+    }
+  // test the decimalPoint
+  if (!ok && state == QValidator::Acceptable)
+    {
+    // duplicate decimal points probably means the user typed another decimal points,
+    // move the cursor pos to the right then
+    if (dec + 1 < text.size() &&
+        text.at(dec + 1) == q->locale().decimalPoint() &&
+        posInValue == dec + 1)
+      {
+      text.remove(dec + 1, 1);
+      value = q->locale().toDouble(text, &ok);
+      }
+    }
+  if (ok && state != QValidator::Invalid)
+    {
+    if (dec != -1)
+      {
+      decimals = text.size() - (dec + 1);
+      if (decimals > q->decimals())
+        {
+        // With ReplaceDecimals on, key strokes replace decimal digits
+        if (posInValue > dec && posInValue < text.size())
+          {
+          const int extraDecimals = decimals - q->decimals();
+          if (this->DOption & ctkDoubleSpinBox::ReplaceDecimals)
+            {
+            text.remove(posInValue, extraDecimals);
+            decimals = q->decimals();
+            value = q->locale().toDouble(text, &ok);
+            }
+          else if (!(this->DOption & ctkDoubleSpinBox::InsertDecimals))
+            {
+            text.remove(text.size() - extraDecimals, extraDecimals);
+            decimals = q->decimals();
+            value = q->locale().toDouble(text, &ok);
+            }
+          }
+        }
+      // When DecimalsByKey is set, it is possible to extend the number of decimals
+      if (decimals > q->decimals() &&
+          !(this->DOption & ctkDoubleSpinBox::DecimalsByKey) )
+        {
+        state = QValidator::Invalid;
+        }
+      }
+    }
+  if (state == QValidator::Acceptable)
+    {
+    //qDebug() << "Acceptable: " << text << value << ok;
+    if (!ok)
+      {
+      state = QValidator::Invalid;
+      }
+    else if (value >= min && value <= max)
+      {
+      state = QValidator::Acceptable;
+      }
+    else if (max == min)
+      { // when max and min is the same the only non-Invalid input is max (or min)
+      state = QValidator::Invalid;
+      }
+    else if ((value >= 0 && value > max) || (value < 0 && value < min))
+      {
+      state = QValidator::Invalid;
+      }
+    else
+      {
+      state = QValidator::Intermediate;
+      }
+    }
+
+  if (state != QValidator::Acceptable)
+    {
+    value = max > 0 ? min : max;
+    }
+
+  pos += posInValue - oldPosInValue;
+  input = q->prefix() + text + q->suffix();
+  this->CachedText = input;
+  this->CachedState = state;
+  this->CachedValue = value;
+  this->CachedDecimals = decimals;
+  //qDebug() << "end: text is '" << this->CachedText << "' state is "
+  //         << int(state) << ", value is " << this->CachedValue
+  //         << " decimals is " << decimals
+  //         << " and pos is " << pos;
+  return value;
+}
+
+/*
+//-----------------------------------------------------------------------------
+double ctkDoubleSpinBoxPrivate
+::validateAndInterpret(QString &input, int &pos,
+                       QValidator::State &state, int &decimals) const
+{
+  Q_Q(const ctkDoubleSpinBox);
+  qDebug() << "input: " << input << "pos:" << pos;
+  if (this->CachedText == input && !input.isEmpty())
+    {
+    state = this->CachedState;
+    decimals = this->CachedDecimals;
+    qDebug() << "cachedText was '" << this->CachedText << "' state was "
+             << int(state) << " and value was " << this->CachedValue;
+    return this->CachedValue;
+    }
+  const double max = q->maximum();
+  const double min = q->minimum();
+
+  QString copy = this->stripped(input, &pos);
+  qDebug() << "copy: " << copy << pos;
+  int len = copy.size();
+  double num = min;
+  const bool plus = max >= 0;
+  const bool minus = min <= 0;
+  decimals = 0;
+
+
+  switch (len)
+    {
+    case 0:
+      state = max != min ? QValidator::Intermediate : QValidator::Invalid;
+      goto end;
+      break;
+    case 1:
+      if (copy.at(0) == q->locale().decimalPoint()
+          || (plus && copy.at(0) == QLatin1Char('+'))
+          || (minus && copy.at(0) == QLatin1Char('-')))
+        {
+        state = QValidator::Intermediate;
+        goto end;
+        }
+      break;
+    case 2:
+      if (copy.at(1) == q->locale().decimalPoint()
+          && ((plus && copy.at(0) == QLatin1Char('+')) || (minus && copy.at(0) == QLatin1Char('-'))))
+        {
+        state = QValidator::Intermediate;
+        goto end;
+        }
+      break;
+    default:
+      break;
+    }
+
+  if (copy.at(0) == q->locale().groupSeparator())
+    {
+    state = QValidator::Invalid;
+    goto end;
+    }
+  else if (len > 1)
+    {
+    const int dec = copy.indexOf(q->locale().decimalPoint());
+    if (dec != -1)
+      {
+      if (dec + 1 < copy.size() && copy.at(dec + 1) == q->locale().decimalPoint() && pos == dec + 1)
+        {
+        copy.remove(dec + 1, 1); // typing a delimiter when you are on the delimiter
+        } // should be treated as typing right arrow
+      // When DecimalsByKey is set, it is possible to extend the number of decimals
+      if (!(this->DOption & ctkDoubleSpinBox::DecimalsByKey) &&
+          (copy.size() - dec > q->decimals() + 1))
+        {
+        state = QValidator::Invalid;
+        goto end;
+        }
+      for (int i=dec + 1; i<copy.size(); ++i)
+        {
+        if (copy.at(i).isSpace() || copy.at(i) == q->locale().groupSeparator())
+          {
+          state = QValidator::Invalid;
+          goto end;
+          }
+        }
+      decimals = copy.size() - dec - 1;
+      }
+    else
+      {
+      /// Don't accept lack of decimal point.
+      /// It could change 1.00 into 100 in 1 key stroke (delete or backspace).
+      if (this->DOption & ctkDoubleSpinBox::DecimalsByKey)
+        {
+        state = QValidator::Invalid;
+        goto end;
+        }
+      const QChar &last = copy.at(len - 1);
+      const QChar &secondLast = copy.at(len - 2);
+      if ((last == q->locale().groupSeparator() || last.isSpace())
+          && (secondLast == q->locale().groupSeparator() || secondLast.isSpace()))
+        {
+        state = QValidator::Invalid;
+        goto end;
+        }
+      else if (last.isSpace() && (!q->locale().groupSeparator().isSpace() || secondLast.isSpace()))
+        {
+        state = QValidator::Invalid;
+        goto end;
+        }
+      }
+    }
+
+    {
+    bool ok = false;
+    num = q->locale().toDouble(copy, &ok);
+
+    if (!ok) {
+    if (q->locale().groupSeparator().isPrint())
+      {
+      if (max < 1000 && min > -1000 && copy.contains(q->locale().groupSeparator()))
+        {
+        state = QValidator::Invalid;
+        goto end;
+        }
+      const int len = copy.size();
+      for (int i=0; i<len- 1; ++i)
+        {
+        if (copy.at(i) == q->locale().groupSeparator() && copy.at(i + 1) == q->locale().groupSeparator())
+          {
+          state = QValidator::Invalid;
+          goto end;
+          }
+        }
+
+      QString copy2 = copy;
+      copy2.remove(q->locale().groupSeparator());
+      num = q->locale().toDouble(copy2, &ok);
+
+      if (!ok)
+        {
+        state = QValidator::Invalid;
+        goto end;
+        }
+      }
+    }
+
+    if (!ok)
+      {
+      state = QValidator::Invalid;
+      }
+    else if (num >= min && num <= max)
+      {
+      state = QValidator::Acceptable;
+      }
+    else if (max == min)
+      { // when max and min is the same the only non-Invalid input is max (or min)
+      state = QValidator::Invalid;
+      }
+    else
+      {
+      if ((num >= 0 && num > max) || (num < 0 && num < min))
+        {
+        state = QValidator::Invalid;
+        }
+      else
+        {
+        state = QValidator::Intermediate;
+        }
+      }
+    }
+end:
+    if (state != QValidator::Acceptable)
+      {
+      num = max > 0 ? min : max;
+      }
+
+    input = q->prefix() + copy + q->suffix();
+    this->CachedText = input;
+    this->CachedState = state;
+    this->CachedValue = num;
+    this->CachedDecimals = decimals;
+    qDebug() << "end: text is '" << this->CachedText << "' state is "
+             << int(state) << ", value is " << this->CachedValue
+             << " decimals is " << decimals
+             << " and pos is " << pos;
+
+    return num;
+}
+*/
+//-----------------------------------------------------------------------------
+// ctkDoubleSpinBox
+//-----------------------------------------------------------------------------
 ctkDoubleSpinBox::ctkDoubleSpinBox(QWidget* newParent)
   : QWidget(newParent)
   , d_ptr(new ctkDoubleSpinBoxPrivate(*this))
@@ -362,7 +869,6 @@ int ctkDoubleSpinBox::decimals() const
 void ctkDoubleSpinBox::setDecimals(int dec)
 {
   Q_D(ctkDoubleSpinBox);
-  dec = qMax(0, dec);
   if (d->Mode == ctkDoubleSpinBox::SetIfDifferent && dec == this->decimals())
     {
     return;
@@ -387,6 +893,13 @@ QDoubleSpinBox* ctkDoubleSpinBox::spinBox() const
 }
 
 //-----------------------------------------------------------------------------
+QLineEdit* ctkDoubleSpinBox::lineEdit() const
+{
+  Q_D(const ctkDoubleSpinBox);
+  return d->SpinBox->lineEdit();
+}
+
+//-----------------------------------------------------------------------------
 void ctkDoubleSpinBox::setValue(double value)
 {
   Q_D(ctkDoubleSpinBox);
@@ -406,15 +919,22 @@ void ctkDoubleSpinBox::setValueIfDifferent(double value)
   Q_D(ctkDoubleSpinBox);
   if (! d->compare(this->value(), value))
     {
-    d->SpinBox->setValue(value);
+    this->setValueAlways(value);
     }
 }
 
 //-----------------------------------------------------------------------------
 void ctkDoubleSpinBox::setValueAlways(double value)
 {
-  Q_D(const ctkDoubleSpinBox);
-  d->SpinBox->setValue(value);
+  Q_D(ctkDoubleSpinBox);
+  if (d->DOption & ctkDoubleSpinBox::DecimalsByValue)
+    {
+    d->setValue(value, ctk::significantDecimals(value));
+    }
+  else
+    {
+    d->setValue(value);
+    }
 }
 
 //-----------------------------------------------------------------------------
@@ -462,6 +982,7 @@ void ctkDoubleSpinBox::setDecimalsOption(ctkDoubleSpinBox::DecimalsOptions optio
     }
 
   d->DOption = option;
+  this->setValueAlways(this->value());
 }
 
 //----------------------------------------------------------------------------
@@ -480,10 +1001,18 @@ bool ctkDoubleSpinBox::invertedControls() const
 }
 
 //-----------------------------------------------------------------------------
+void ctkDoubleSpinBox::keyPressEvent(QKeyEvent* event)
+{
+  Q_D(ctkDoubleSpinBox);
+  const bool accept = this->eventFilter(d->SpinBox, event);
+  event->setAccepted(accept);
+}
+
+//-----------------------------------------------------------------------------
 bool ctkDoubleSpinBox::eventFilter(QObject* obj, QEvent* event)
 {
   Q_D(ctkDoubleSpinBox);
-  if (d->DOption & ctkDoubleSpinBox::UseShortcuts &&
+  if (d->DOption & ctkDoubleSpinBox::DecimalsByShortcuts &&
     obj == d->SpinBox && event->type() == QEvent::KeyPress)
     {
     QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);

+ 67 - 18
Libs/Widgets/ctkDoubleSpinBox.h

@@ -29,6 +29,7 @@
 class QDoubleSpinBox;
 class QEvent;
 class QKeyEvent;
+class QLineEdit;
 class QObject;
 
 // CTK includes
@@ -44,21 +45,37 @@ class CTK_WIDGETS_EXPORT ctkDoubleSpinBox : public QWidget
 {
   Q_OBJECT
   Q_ENUMS(SetMode)
-  Q_PROPERTY(SetMode setMode READ setMode WRITE setSetMode)
-
   Q_FLAGS(DecimalsOption DecimalsOptions)
-  Q_PROPERTY(DecimalsOptions decimalsOption READ decimalsOption WRITE setDecimalsOption)
 
   Q_PROPERTY(Qt::Alignment alignment READ alignment WRITE setAlignment)
   Q_PROPERTY(bool frame READ hasFrame WRITE setFrame)
   Q_PROPERTY(QString prefix READ prefix WRITE setPrefix)
   Q_PROPERTY(QString suffix READ suffix WRITE setSuffix)
   Q_PROPERTY(QString cleanText READ cleanText)
-  Q_PROPERTY(int decimals READ decimals WRITE setDecimals)
+  /// This property holds the precision of the spin box, in decimals.
+  /// Sets how many decimals the spinbox will use for displaying and
+  /// interpreting doubles.
+  /// If the flag DecimalsByShortcuts is set, decimals can be increased/decreased by
+  /// Ctrl+/Ctrl-, Ctrl0 restores the original decimals value.
+  /// If the flag DecimalsAsMax and/or DecimalsAsMin are set, decimals behave also
+  /// as the max and/or min number of decimals settable by DecimalsByShortcuts,
+  /// DecimalsByKey and DecimalsByValue.
+  /// 2 by default.
+  /// \sa decimalsOption, decimals(), setDecimals(), decimalsChanged
+  Q_PROPERTY(int decimals READ decimals WRITE setDecimals NOTIFY decimalsChanged)
+  /// This property provides more controls over the decimals.
+  /// The default (DecimalsByShortcuts|InsertDecimals) behaves as a QDoubleSpinbox
+  /// with an explicit control of decimals via shortcuts.
+  /// \sa DecimalsOptions, decimals
+  Q_PROPERTY(DecimalsOptions decimalsOption READ decimalsOption WRITE setDecimalsOption)
   Q_PROPERTY(double minimum READ minimum WRITE setMinimum)
   Q_PROPERTY(double maximum READ maximum WRITE setMaximum)
   Q_PROPERTY(double singleStep READ singleStep WRITE setSingleStep)
+  /// \sa setMode, decimals
   Q_PROPERTY(double value READ value WRITE setValue NOTIFY valueChanged USER true)
+  /// This property controls how setValue behaves.
+  /// \sa SetMode, setMode(), setSetMode(), value
+  Q_PROPERTY(SetMode setMode READ setMode WRITE setSetMode)
   /// This property controls whether decreasing the value by the mouse
   /// button or mouse wheel increases the value of the widget, and inverts the
   /// control similarly in the other way round or not. The property is switched off by
@@ -85,21 +102,46 @@ public:
     };
 
   /// DecimalsOption enums the input style of the spinbox decimals.
-  /// Fixed:
-  /// Behaves just like a QDoubleSpinBox. The maximum number of decimals
-  /// allowed is given by decimals().
-  /// UseShortcuts:
-  /// When the spinbox has focus, entering the shortcut "CTRL +"
-  /// adds decimals to the current number of decimals. "CTRL -" decreases the
-  /// number of decimals and "CTRL 0" returns it to its original decimals()
-  /// value.
-  ///
-  /// Default option is UseShortcuts.
+  /// Default option is DecimalsByShortcuts.
   /// \sa decimals(), currentDecimals()
   enum DecimalsOption
     {
-    Fixed =         0x0,
-    UseShortcuts =  0x01,
+    /// Behaves just like a QDoubleSpinBox. The maximum number of decimals
+    /// allowed is given by decimals().
+    FixedDecimals = 0x000,
+    /// When the spinbox has focus, entering the shortcut "CTRL +"
+    /// adds decimals to the current number of decimals. "CTRL -" decreases the
+    /// number of decimals and "CTRL 0" returns it to its original decimals()
+    /// value.
+    DecimalsByShortcuts = 0x001,
+    /// Allow the number of decimals to be controlled by adding/removing digits
+    /// with key strokes.
+    /// \sa InsertDecimals, ReplaceDecimals
+    DecimalsByKey = 0x002,
+    /// Allow the number of decimals to be controlled by the value set by
+    /// setValue().
+    DecimalsByValue = 0x004,
+    /// This flag controls whether inserted intermediate decimals increase the
+    /// number of decimals (on) or not (off).
+    /// It is incompatible with the ReplaceDecimals flag.
+    /// \sa DecimalsByKey.
+    InsertDecimals = 0x008,
+    /// This flag controls whether inserted intermediate decimals replace the
+    /// existing decimals (on) or not (off). This is similar to Insert but just
+    /// for decimal digits.
+    /// It is incompatible with the InsertDecimals flag.
+    /// \sa DecimalsByKey, InsertDecimals
+    ReplaceDecimals = 0x010,
+    /// Use the "decimals" property as a maximum limit for the number of
+    /// decimals.
+    DecimalsAsMax = 0x020,
+    /// Use the "decimals" property as a minimum limit for the number of
+    /// decimals.
+    DecimalsAsMin = 0x040,
+    /// Even if the number of decimals is 0, it enforces the decimal to be visible
+    /// (e.g. "0." )
+    /// \sa decimals
+    DecimalPointAlwaysVisible = 0x080
     };
   Q_DECLARE_FLAGS(DecimalsOptions, DecimalsOption)
 
@@ -173,9 +215,13 @@ public:
 
   /// Get a pointer on the spinbox used internally. It can be useful to
   /// change display properties for example. To use with caution.
-  /// \sa QDoubleSpinBox::QDoubleSpinBox
+  /// \sa QDoubleSpinBox, lineEdit()
   QDoubleSpinBox* spinBox() const;
 
+  /// Get a pointer on the line edit of the spinbox.
+  /// \sa QLineEdit, spinBox()
+  QLineEdit* lineEdit()const;
+
   /// Set the spinbox mode when using a set*() method.
   //// \sa round(), setValue(), setValueIfDifferent(), setValueAlways()
   ctkDoubleSpinBox::SetMode setMode() const;
@@ -228,7 +274,10 @@ Q_SIGNALS:
 protected:
   ctkDoubleSpinBoxPrivate* const d_ptr;
 
-  bool eventFilter(QObject *obj, QEvent *event);
+  /// Reimplemented to support shortcuts.
+  virtual void keyPressEvent(QKeyEvent* event);
+  /// Reimplemented to support shortcuts on the double spinbox.
+  virtual bool eventFilter(QObject *obj, QEvent *event);
 
 private:
   Q_DECLARE_PRIVATE(ctkDoubleSpinBox);

+ 44 - 6
Libs/Widgets/ctkDoubleSpinBox_p.h

@@ -23,7 +23,7 @@
 
 // Qt includes
 #include <QDoubleSpinBox>
-
+class ctkDoubleSpinBoxPrivate;
 
 //-----------------------------------------------------------------------------
 class ctkQDoubleSpinBox: public QDoubleSpinBox
@@ -37,7 +37,7 @@ class ctkQDoubleSpinBox: public QDoubleSpinBox
   Q_PROPERTY(bool invertedControls READ invertedControls WRITE setInvertedControls)
 public:
   typedef QDoubleSpinBox Superclass;
-  ctkQDoubleSpinBox(QWidget* widget);
+  ctkQDoubleSpinBox(ctkDoubleSpinBoxPrivate* pimpl, QWidget* widget);
   void setInvertedControls(bool invertedControls);
   bool invertedControls() const;
 
@@ -45,7 +45,18 @@ public:
   /// invertedControls property is true.
   virtual void stepBy(int steps);
 
+  /// Expose lineEdit() publicly.
+  /// \sa QAbstractSpinBox::lineEdit()
+  virtual QLineEdit* lineEdit()const;
+
+  virtual double valueFromText(const QString &text) const;
+  virtual QString textFromValue(double value) const;
+  virtual int decimalsFromText(const QString &text) const;
+  virtual QValidator::State	validate(QString& input, int& pos)const;
+
 protected:
+  ctkDoubleSpinBoxPrivate* const d_ptr;
+
   /// If the invertedControls property is false (by default) then this function
   /// behavesLike QDoubleSpinBox::stepEnabled(). If the property is true then
   /// stepping down is allowed if the value is less then the maximum, and
@@ -54,12 +65,14 @@ protected:
 
   bool InvertedControls;
 private:
+  Q_DECLARE_PRIVATE(ctkDoubleSpinBox);
   Q_DISABLE_COPY(ctkQDoubleSpinBox);
 };
 
 //-----------------------------------------------------------------------------
-class ctkDoubleSpinBoxPrivate
+class ctkDoubleSpinBoxPrivate: public QObject
 {
+  Q_OBJECT
   Q_DECLARE_PUBLIC(ctkDoubleSpinBox);
 protected:
   ctkDoubleSpinBox* const q_ptr;
@@ -69,15 +82,40 @@ public:
   ctkQDoubleSpinBox* SpinBox;
   ctkDoubleSpinBox::SetMode Mode;
   int DefaultDecimals;
+  int MinimumDecimals;
   ctkDoubleSpinBox::DecimalsOptions DOption;
   bool InvertedControls;
 
+  mutable QString CachedText;
+  mutable double CachedValue;
+  mutable QValidator::State CachedState;
+  mutable int CachedDecimals;
+
   void init();
-  // Compare two double previously rounded according to the number of decimals
+  /// Compare two double previously rounded according to the number of decimals
   bool compare(double x1, double x2) const;
 
-  // Set the number of decimals of the spinbox and emit the signal
-  // No check if they are the same.
+  /// Remove prefix and suffix
+  QString stripped(const QString& text, int* pos)const;
+
+  /// Return the number of decimals bounded by the allowed min and max number of
+  /// decimals.
+  /// If -1, returns the current number of decimals.
+  int boundDecimals(int decimals)const;
+  /// Set the number of decimals of the spinbox and emit the signal
+  /// No check if they are the same.
   void setDecimals(int dec);
+  /// Set value with a specific number of decimals. -1 means the number of
+  /// decimals stays the same.
+  void setValue(double value, int dec = -1);
+
+  /// Ensure the spinbox text is meaningful.
+  /// It is called multiple times when the spinbox line edit is modified,
+  /// therefore values are cached.
+  double validateAndInterpret(QString &input, int &pos,
+                              QValidator::State &state, int &decimals) const;
+
+public Q_SLOTS:
+  void editorTextChanged(const QString& text);
 
 };

+ 17 - 0
Libs/Widgets/ctkMatrixWidget.cpp

@@ -55,6 +55,7 @@ namespace
       this->setMinimum(matrixWidget->minimum());
       this->setMaximum(matrixWidget->maximum());
       this->setDecimals(matrixWidget->decimals());
+      this->setDecimalsOption(matrixWidget->decimalsOption());
       this->setSingleStep(matrixWidget->singleStep());
 
       this->connect(this, SIGNAL(decimalsChanged(int)),
@@ -83,6 +84,7 @@ public:
   double Minimum;
   double Maximum;
   int    Decimals;
+  ctkDoubleSpinBox::DecimalsOptions DecimalsOption;
   double SingleStep;
 };
 
@@ -349,6 +351,21 @@ void ctkMatrixWidget::setDecimals(int decimals)
 }
 
 // --------------------------------------------------------------------------
+ctkDoubleSpinBox::DecimalsOptions ctkMatrixWidget::decimalsOption()const
+{
+  Q_D(const ctkMatrixWidget);
+  return d->DecimalsOption;
+}
+
+// --------------------------------------------------------------------------
+void ctkMatrixWidget
+::setDecimalsOption(ctkDoubleSpinBox::DecimalsOptions newDecimalsOption)
+{
+  Q_D(ctkMatrixWidget);
+  d->DecimalsOption = newDecimalsOption;
+}
+
+// --------------------------------------------------------------------------
 QSize ctkMatrixWidget::minimumSizeHint() const
 {
   Q_D(const ctkMatrixWidget);

+ 11 - 0
Libs/Widgets/ctkMatrixWidget.h

@@ -26,6 +26,7 @@
 #include <QWidget>
 
 /// CTK includes
+#include "ctkDoubleSpinBox.h"
 #include "ctkPimpl.h"
 #include "ctkWidgetsExport.h"
 
@@ -43,6 +44,9 @@ class CTK_WIDGETS_EXPORT ctkMatrixWidget: public QWidget
   Q_PROPERTY(double minimum READ minimum WRITE setMinimum)
   Q_PROPERTY(double maximum READ maximum WRITE setMaximum)
   Q_PROPERTY(int decimals READ decimals WRITE setDecimals)
+  /// This property provides more controls over the decimals.
+  /// \sa ctkDoubleSpinBox::DecimalsOptions, decimals
+  Q_PROPERTY(ctkDoubleSpinBox::DecimalsOptions decimalsOption READ decimalsOption WRITE setDecimalsOption)
   Q_PROPERTY(double singleStep READ singleStep WRITE setSingleStep)
   Q_PROPERTY(QVector<double> values READ values WRITE setValues)
 
@@ -123,6 +127,13 @@ public:
   /// used to adjust the value of a matrix element.
   int decimals()const;
 
+  /// Return the decimalsOption property value
+  /// \sa decimalsOption
+  ctkDoubleSpinBox::DecimalsOptions decimalsOption()const;
+  /// Set the decimalsOption property value.
+  /// \sa decimalsOption
+  void setDecimalsOption(ctkDoubleSpinBox::DecimalsOptions option);
+
   ///
   /// Reimplemented from QAbstractScrollArea
   virtual QSize minimumSizeHint () const;

+ 13 - 0
Libs/Widgets/ctkRangeWidget.cpp

@@ -741,3 +741,16 @@ void ctkRangeWidget::setSlider(ctkDoubleRangeSlider* slider)
   d->relayout();
 }
 
+// -------------------------------------------------------------------------
+ctkDoubleSpinBox* ctkRangeWidget::minimumSpinBox()const
+{
+  Q_D(const ctkRangeWidget);
+  return d->MinimumSpinBox;
+}
+
+// -------------------------------------------------------------------------
+ctkDoubleSpinBox* ctkRangeWidget::maximumSpinBox()const
+{
+  Q_D(const ctkRangeWidget);
+  return d->MaximumSpinBox;
+}

+ 11 - 1
Libs/Widgets/ctkRangeWidget.h

@@ -30,6 +30,7 @@
 #include "ctkWidgetsExport.h"
 
 class ctkDoubleRangeSlider;
+class ctkDoubleSpinBox;
 class ctkRangeWidgetPrivate;
 
 /// \ingroup Widgets
@@ -179,6 +180,16 @@ public:
   bool symmetricMoves()const;
   void setSymmetricMoves(bool symmetry);
 
+  /// Return the slider of the range widget.
+  /// \sa minimumSpinBox(), maximumSpinBox()
+  ctkDoubleRangeSlider* slider()const;
+  /// Return the minimum spinbox.
+  /// \sa maximumSpinBox(), slider()
+  ctkDoubleSpinBox* minimumSpinBox()const;
+  /// Return the maximum spinbox.
+  /// \sa minimumSpinBox(), slider()
+  ctkDoubleSpinBox* maximumSpinBox()const;
+
 public Q_SLOTS:
   ///
   /// Reset the slider and spinbox to zero (value and position)
@@ -218,7 +229,6 @@ protected:
   virtual bool eventFilter(QObject *obj, QEvent *event);
 
   /// can be used to change the slider by a custom one
-  ctkDoubleRangeSlider* slider()const;
   void setSlider(ctkDoubleRangeSlider* slider);
 
 protected:

+ 43 - 18
Libs/Widgets/ctkSliderWidget.cpp

@@ -56,6 +56,7 @@ public:
   bool   Tracking;
   bool   Changing;
   double ValueBeforeChange;
+  bool   BlockSetSliderValue;
   ctkSliderWidget::SynchronizeSiblings SynchronizeMode;
   ctkPopupWidget* SliderPopup;
 };
@@ -69,6 +70,7 @@ ctkSliderWidgetPrivate::ctkSliderWidgetPrivate(ctkSliderWidget& object)
   this->Tracking = true;
   this->Changing = false;
   this->ValueBeforeChange = 0.;
+  this->BlockSetSliderValue = false;
   this->SynchronizeMode =
     ctkSliderWidget::SynchronizeWidth | ctkSliderWidget::SynchronizeDecimals;
   this->SliderPopup = 0;
@@ -166,13 +168,13 @@ ctkSliderWidget::ctkSliderWidget(QWidget* _parent) : Superclass(_parent)
   d->Slider->setMaximum(d->SpinBox->maximum());
   d->Slider->setMinimum(d->SpinBox->minimum());
 
-  this->connect(d->SpinBox, SIGNAL(valueChanged(double)), d->Slider, SLOT(setValue(double)));
+  this->connect(d->SpinBox, SIGNAL(valueChanged(double)), this, SLOT(setSliderValue(double)));
   this->connect(d->SpinBox, SIGNAL(decimalsChanged(int)), this, SLOT(setDecimals(int)));
 
-  //this->connect(d->Slider, SIGNAL(valueChanged(double)), SIGNAL(valueChanged(double)));
   this->connect(d->Slider, SIGNAL(sliderPressed()), this, SLOT(startChanging()));
   this->connect(d->Slider, SIGNAL(sliderReleased()), this, SLOT(stopChanging()));
-  this->connect(d->Slider, SIGNAL(valueChanged(double)), this, SLOT(changeValue(double)));
+  // setSpinBoxValue will fire the valueChanged signal.
+  this->connect(d->Slider, SIGNAL(valueChanged(double)), this, SLOT(setSpinBoxValue(double)));
   d->SpinBox->installEventFilter(this);
 }
 
@@ -201,9 +203,10 @@ double ctkSliderWidget::maximum()const
 void ctkSliderWidget::setMinimum(double min)
 {
   Q_D(ctkSliderWidget);
-  bool wasBlocked = d->SpinBox->blockSignals(true);
+  bool wasBlockSetSliderValue = d->BlockSetSliderValue;
+  d->BlockSetSliderValue = true;
   d->SpinBox->setMinimum(min);
-  d->SpinBox->blockSignals(wasBlocked);
+  d->BlockSetSliderValue = wasBlockSetSliderValue;
 
   // SpinBox can truncate min (depending on decimals).
   // use Spinbox's min to set Slider's min
@@ -218,9 +221,10 @@ void ctkSliderWidget::setMinimum(double min)
 void ctkSliderWidget::setMaximum(double max)
 {
   Q_D(ctkSliderWidget);
-  bool wasBlocked = d->SpinBox->blockSignals(true);
+  bool wasBlockSetSliderValue = d->BlockSetSliderValue;
+  d->BlockSetSliderValue = true;
   d->SpinBox->setMaximum(max);
-  d->SpinBox->blockSignals(wasBlocked);
+  d->BlockSetSliderValue = wasBlockSetSliderValue;
 
   // SpinBox can truncate max (depending on decimals).
   // use Spinbox's max to set Slider's max
@@ -236,9 +240,10 @@ void ctkSliderWidget::setRange(double min, double max)
 {
   Q_D(ctkSliderWidget);
   
-  bool wasBlocked = d->SpinBox->blockSignals(true);
+  bool wasBlockSetSliderValue = d->BlockSetSliderValue;
+  d->BlockSetSliderValue = true;
   d->SpinBox->setRange(min, max);
-  d->SpinBox->blockSignals(wasBlocked);
+  d->BlockSetSliderValue = wasBlockSetSliderValue;
   
   // SpinBox can truncate the range (depending on decimals).
   // use Spinbox's range to set Slider's range
@@ -274,6 +279,8 @@ double ctkSliderWidget::value()const
 {
   Q_D(const ctkSliderWidget);
   Q_ASSERT(d->equal(d->SpinBox->value(), d->Slider->value()));
+  // The slider is the most precise as it does not round the value with the
+  // decimals number.
   return d->Changing ? d->ValueBeforeChange : d->Slider->value();
 }
 
@@ -282,7 +289,7 @@ void ctkSliderWidget::setValue(double _value)
 {
   Q_D(ctkSliderWidget);
   // disable the tracking temporally to emit the
-  // signal valueChanged if changeValue() is called
+  // signal valueChanged if setSpinBoxValue() is called
   bool isChanging = d->Changing;
   d->Changing = false;
   d->SpinBox->setValue(_value);
@@ -324,22 +331,34 @@ void ctkSliderWidget::stopChanging()
 }
 
 // --------------------------------------------------------------------------
-void ctkSliderWidget::changeValue(double newValue)
+void ctkSliderWidget::setSliderValue(double spinBoxValue)
+{
+  Q_D(ctkSliderWidget);
+  if (d->BlockSetSliderValue)
+    {
+    return;
+    }
+  d->Slider->setValue(spinBoxValue);
+}
+
+// --------------------------------------------------------------------------
+void ctkSliderWidget::setSpinBoxValue(double sliderValue)
 {
   Q_D(ctkSliderWidget);
   
-  bool wasBlocked = d->SpinBox->blockSignals(true);
-  d->SpinBox->setValue(newValue);
-  d->SpinBox->blockSignals(wasBlocked);
+  bool wasBlockSetSliderValue = d->BlockSetSliderValue;
+  d->BlockSetSliderValue = true;
+  d->SpinBox->setValue(sliderValue);
+  d->BlockSetSliderValue = wasBlockSetSliderValue;
   Q_ASSERT(d->equal(d->SpinBox->value(), d->Slider->value()));
   
   if (!d->Tracking)
     {
-    emit this->valueIsChanging(newValue);
+    emit this->valueIsChanging(sliderValue);
     }
   if (!d->Changing)
     {
-    emit this->valueChanged(newValue);
+    emit this->valueChanged(sliderValue);
     }
 }
 
@@ -416,12 +435,18 @@ void ctkSliderWidget::setDecimals(int newDecimals)
   d->SpinBox->setDecimals(newDecimals);
   // The number of decimals can change the range values
   // i.e. 50.55 with 2 decimals -> 51 with 0 decimals
-  // As the SpinBox range change doesn't fire signals, 
+  // As the SpinBox range change doesn't fire signals,
   // we have to do the synchronization manually here
   d->Slider->setRange(d->SpinBox->minimum(), d->SpinBox->maximum());
   Q_ASSERT(d->equal(d->SpinBox->minimum(),d->Slider->minimum()));
-  Q_ASSERT(d->equal(d->SpinBox->value(),d->Slider->value()));
   Q_ASSERT(d->equal(d->SpinBox->maximum(),d->Slider->maximum()));
+  // Last time the value was set on the spinbox, the value might have been
+  // rounded by the previous number of decimals. The slider however never rounds
+  // the value. Now, if the number of decimals is higher, such rounding is lost
+  // precision. The "true" value must be set again to the spinbox to "recover"
+  // the precision.
+  this->setSpinBoxValue(d->Slider->value());
+  Q_ASSERT(d->equal(d->SpinBox->value(),d->Slider->value()));
   d->updateSpinBoxDecimals();
 }
 

+ 2 - 1
Libs/Widgets/ctkSliderWidget.h

@@ -279,7 +279,8 @@ protected Q_SLOTS:
   
   void startChanging();
   void stopChanging();
-  void changeValue(double value);
+  void setSpinBoxValue(double sliderValue);
+  void setSliderValue(double spinBoxValue);
 
 protected:
   virtual bool eventFilter(QObject *obj, QEvent *event);