Browse Source

Add ctkDoubleSpinBox::DecimalsByKey, DecimalsByValue and ReplaceDecimals

Julien Finet 12 years ago
parent
commit
45cefa15a9

+ 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>

+ 3 - 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
@@ -178,6 +179,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
@@ -230,6 +232,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 )

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

@@ -0,0 +1,291 @@
+/*=========================================================================
+
+  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 testDecimalsByValue();
+  void testDecimalsByValue_data();
+};
+
+// ----------------------------------------------------------------------------
+void ctkDoubleSpinBoxTester::testUI()
+{
+  ctkDoubleSpinBox spinBox;
+  spinBox.setMinimum(-100.);
+  spinBox.setMaximum(100.);
+  spinBox.setValue(1.);
+  spinBox.setDecimalsOption( ctkDoubleSpinBox::DecimalsByKey );
+  spinBox.show();
+
+  QDoubleSpinBox doubleSpinBox;
+  doubleSpinBox.setMinimum(-100.);
+  doubleSpinBox.setMaximum(100.);
+  doubleSpinBox.setValue(2.);
+  doubleSpinBox.show();
+
+  QTest::qWaitForWindowShown(&spinBox);
+  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(-100.);
+  spinBox.setMaximum(100.);
+  spinBox.setValue(1.);
+
+  QFETCH(int, decimalsOptions);
+  spinBox.setDecimalsOption( static_cast<ctkDoubleSpinBox::DecimalsOptions>(decimalsOptions) );
+
+  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() != 2 ? 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.00") << options.last() << 0 << int(Qt::Key_1) << "11.00"<< 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 0:'-' -> -1.00") << options.last() << 0 << int(Qt::Key_Minus) << "-1.00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 0:'del' -> .00") << options.last() << 0 << int(Qt::Key_Delete) << ".00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 0:'backspace' -> 1.00") << options.last() << 0 << int(Qt::Key_Backspace) << "1.00" << 2;
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 1:'1' -> 11.00") << options.last() << 1 << int(Qt::Key_1) << "11.00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 1:'-' -> 1.00") << options.last() << 1 << int(Qt::Key_Minus) << "1.00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 1:'del' -> 100.") << options.last() << 1 << int(Qt::Key_Delete) << "100." << 0;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 1:'backspace' -> .00") << options.last() << 1 << int(Qt::Key_Backspace) << ".00" << 2;
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 2:'1' -> 1.100") << options.last() << 2 << int(Qt::Key_1) << "1.100" << 3;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 2:'-' -> 1.00") << options.last() << 2 << int(Qt::Key_Minus) << "1.00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 2:'del' -> 1.0") << options.last() << 2 << int(Qt::Key_Delete) << "1.0" << 1;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 2:'backspace' -> 100.") << options.last() << 2 << int(Qt::Key_Backspace) << "100." << 0;
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 3:'1' -> 1.010") << options.last() << 3 << int(Qt::Key_1) << "1.010" << 3;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 3:'-' -> 1.00") << options.last() << 3 << int(Qt::Key_Minus) << "1.00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 3:'del' -> 1.0") << options.last() << 3 << int(Qt::Key_Delete) << "1.0" << 1;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 3:'backspace' -> 1.0") << options.last() << 3 << int(Qt::Key_Backspace) << "1.0" << 1;
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 4:'1' -> 1.001") << options.last() << 4 << int(Qt::Key_1) << "1.001" << 3;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 4:'-' -> 1.00") << options.last() << 4 << int(Qt::Key_Minus) << "1.00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 4:'del' -> 1.00") << options.last() << 4 << int(Qt::Key_Delete) << "1.00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey 4:'backspace' -> 1.0") << options.last() << 4 << int(Qt::Key_Backspace) << "1.0" << 1;
+
+  // ctkDoubleSpinBox::ReplaceDecimals
+  options << ctkDoubleSpinBox::ReplaceDecimals;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 0:'1' -> 11.00") << options.last() << 0 << int(Qt::Key_1) << "11.00"<< 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 0:'-' -> -1.00") << options.last() << 0 << int(Qt::Key_Minus) << "-1.00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 0:'del' -> .00") << options.last() << 0 << int(Qt::Key_Delete) << ".00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 0:'backspace' -> 1.00") << options.last() << 0 << int(Qt::Key_Backspace) << "1.00" << 2;
+
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 1:'1' -> 11.00") << options.last() << 1 << int(Qt::Key_1) << "11.00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 1:'-' -> 1.00") << options.last() << 1 << int(Qt::Key_Minus) << "1.00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 1:'del' -> 100") << options.last() << 1 << int(Qt::Key_Delete) << "100" << 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 1:'backspace' -> .00") << options.last() << 1 << int(Qt::Key_Backspace) << ".00" << 2;
+
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 2:'1' -> 1.10") << options.last() << 2 << int(Qt::Key_1) << "1.10" << 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 2:'-' -> 1.00") << options.last() << 2 << int(Qt::Key_Minus) << "1.00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 2:'del' -> 1.00") << options.last() << 2 << int(Qt::Key_Delete) << "1.0" << 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 2:'backspace' -> 100") << options.last() << 2 << int(Qt::Key_Backspace) << "100" << 2;
+
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 3:'1' -> 1.01") << options.last() << 3 << int(Qt::Key_1) << "1.01" << 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 3:'-' -> 1.00") << options.last() << 3 << int(Qt::Key_Minus) << "1.00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 3:'del' -> 1.0") << options.last() << 3 << int(Qt::Key_Delete) << "1.0" << 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 3:'backspace' -> 1.00") << options.last() << 3 << int(Qt::Key_Backspace) << "1.0" << 2;
+
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 4:'1' -> 1.00") << options.last() << 4 << int(Qt::Key_1) << "1.00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 4:'-' -> 1.00") << options.last() << 4 << int(Qt::Key_Minus) << "1.00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 4:'del' -> 1.00") << options.last() << 4 << int(Qt::Key_Delete) << "1.00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::ReplaceDecimals 4:'backspace' -> 1.00") << options.last() << 4 << int(Qt::Key_Backspace) << "1.0" << 2;
+
+  // ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals
+  options << (ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals);
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 0:'1' -> 11.00")
+    << options.last() << 0 << int(Qt::Key_1) << "11.00"<< 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 0:'-' -> -1.00")
+    << options.last() << 0 << int(Qt::Key_Minus) << "-1.00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 0:'del' -> .00")
+    << options.last() << 0 << int(Qt::Key_Delete) << ".00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 0:'backspace' -> 1.00")
+    << options.last() << 0 << int(Qt::Key_Backspace) << "1.00" << 2;
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 1:'1' -> 11.00")
+    << options.last() << 1 << int(Qt::Key_1) << "11.00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 1:'-' -> 1.00")
+    << options.last() << 1 << int(Qt::Key_Minus) << "1.00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 1:'del' -> 100.")
+    << options.last() << 1 << int(Qt::Key_Delete) << "100." << 0;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 1:'backspace' -> .00")
+    << options.last() << 1 << int(Qt::Key_Backspace) << ".00" << 2;
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 2:'1' -> 1.10")
+    << options.last() << 2 << int(Qt::Key_1) << "1.10" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 2:'-' -> 1.00")
+    << options.last() << 2 << int(Qt::Key_Minus) << "1.00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 2:'del' -> 1.0")
+    << options.last() << 2 << int(Qt::Key_Delete) << "1.0" << 1;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 2:'backspace' -> 100.")
+    << options.last() << 2 << int(Qt::Key_Backspace) << "100." << 0;
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 3:'1' -> 1.01")
+    << options.last() << 3 << int(Qt::Key_1) << "1.01" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 3:'-' -> 1.00")
+    << options.last() << 3 << int(Qt::Key_Minus) << "1.00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 3:'del' -> 1.0")
+    << options.last() << 3 << int(Qt::Key_Delete) << "1.0" << 1;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 3:'backspace' -> 1.0")
+    << options.last() << 3 << int(Qt::Key_Backspace) << "1.0" << 1;
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 4:'1' -> 1.001")
+    << options.last() << 4 << int(Qt::Key_1) << "1.001" << 3;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 4:'-' -> 1.00")
+    << options.last() << 4 << int(Qt::Key_Minus) << "1.00" << 2;
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 4:'del' -> 1.00")
+    << options.last() << 4 << int(Qt::Key_Delete) << "1.00" << 2;
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByKey|ctkDoubleSpinBox::ReplaceDecimals 4:'backspace' -> 1.00")
+    << options.last() << 4 << int(Qt::Key_Backspace) << "1.0" << 1;
+
+  foreach(int option, options)
+    {
+    // bad keys are always rejected
+    for (int i = 0; i < 5; ++i)
+      {
+      QTest::newRow(QString("%1 %2:'a' -> 1.00").arg(option).arg(i).toLatin1())
+        << option << i << int(Qt::Key_A) << "1.00" << 2;
+      }
+    // sign keys are only for the first digit
+    QTest::newRow(QString("%1 0:'+' -> 1.00").arg(option).toLatin1())
+      << option << 0 << int(Qt::Key_Plus) << "+1.00" << 2;
+    QTest::newRow(QString("%1 0:'-' -> -1.00").arg(option).toLatin1())
+      << option << 0 << int(Qt::Key_Minus) << "-1.00" << 2;
+    for (int i = 1; i < 5; ++i)
+      {
+      QTest::newRow(QString("%1 %2:'+' -> 1.00").arg(option).arg(i).toLatin1())
+        << option << i << int(Qt::Key_Plus) << "1.00" << 2;
+      QTest::newRow(QString("%1 %2:'-' -> 1.00").arg(option).arg(i).toLatin1())
+        << option << i << int(Qt::Key_Minus) << "1.00" << 2;
+      }
+    }
+}
+
+// ----------------------------------------------------------------------------
+void ctkDoubleSpinBoxTester::testDecimalsByValue()
+{
+  ctkDoubleSpinBox spinBox;
+  spinBox.setMinimum(-100.);
+  spinBox.setMaximum(100.);
+  spinBox.setValue(1.);
+  spinBox.setDecimalsOption( ctkDoubleSpinBox::DecimalsByValue );
+  QSignalSpy spy(&spinBox, SIGNAL(decimalsChanged(int)));
+
+  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() != 2 ? 1 : 0);
+}
+
+// ----------------------------------------------------------------------------
+void ctkDoubleSpinBoxTester::testDecimalsByValue_data()
+{
+  QTest::addColumn<double>("value");
+  QTest::addColumn<QString>("expectedText");
+  QTest::addColumn<int>("expectedDecimals");
+
+  QTest::newRow("ctkDoubleSpinBox::DecimalsByValue 0.00") << 0.00 << "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;
+}
+
+// ----------------------------------------------------------------------------
+CTK_TEST_MAIN(ctkDoubleSpinBoxTest)
+#include "moc_ctkDoubleSpinBoxTest.cpp"
+
+

+ 515 - 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,65 @@ 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 (text.indexOf(this->locale().decimalPoint()) == -1 &&
+      ((d->DOption & ctkDoubleSpinBox::DecimalsByKey) ||
+       (d->DOption & ctkDoubleSpinBox::DecimalsByValue)))
+    {
+    text += this->locale().decimalPoint();
+    }
+  //qDebug() << "textFromValue" << text;
+  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 +172,7 @@ ctkDoubleSpinBoxPrivate::ctkDoubleSpinBoxPrivate(ctkDoubleSpinBox& object)
   this->SpinBox = 0;
   this->Mode = ctkDoubleSpinBox::SetIfDifferent;
   this->DefaultDecimals = 2;
-  this->DOption = ctkDoubleSpinBox::UseShortcuts;
+  this->DOption = ctkDoubleSpinBox::DecimalsByShortcuts;
   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,431 @@ 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();
+
+  QString text = this->stripped(input, &pos);
+  //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() &&
+        pos == 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 (this->DOption & ctkDoubleSpinBox::ReplaceDecimals &&
+            pos > dec && pos < text.size())
+          {
+          text.remove(pos, decimals - q->decimals());
+          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;
+    }
+
+  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 +856,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 +880,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 +906,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);
+    }
 }
 
 //-----------------------------------------------------------------------------
@@ -483,7 +990,7 @@ bool ctkDoubleSpinBox::invertedControls() const
 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);

+ 49 - 17
Libs/Widgets/ctkDoubleSpinBox.h

@@ -29,6 +29,7 @@
 class QDoubleSpinBox;
 class QEvent;
 class QKeyEvent;
+class QLineEdit;
 class QObject;
 
 // CTK includes
@@ -44,21 +45,35 @@ 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.
+  /// \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 +100,34 @@ 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.
+    DecimalsByKey = 0x002,
+    /// Allow the number of decimals to be controlled by the value set by
+    /// setValue().
+    DecimalsByValue = 0x004,
+    /// This flag controls whether intermediate decimals are replaced or added
+    /// with key strokes. This is similar to NumLock but just for decimal
+    /// digits.
+    ReplaceDecimals = 0x008,
+    /// Use the "decimals" property as a maximum limit for the number of
+    /// decimals.
+    DecimalsAsMax = 0x010,
+    /// Use the "decimals" property as a minimum limit for the number of
+    /// decimals.
+    DecimalsAsMin = 0x020
     };
   Q_DECLARE_FLAGS(DecimalsOptions, DecimalsOption)
 
@@ -173,9 +201,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;

+ 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);
 
 };