Browse Source

Add value proxies

Value proxies are functions that can be set on widgets representing
numbers such as ctkDoubleSpinBox, ctkDoubleSlider... The proxies decouple
the values displayed from the input value.

For example, one might want to manipulate celsius in the code but to
display fahrenheits. In that case, a "celsius to fahrenheit" linear value
proxy can be added to the widget. The displayed values will be in
fahrenheit while the value given by the widget signals and accessors will
be give celsius.

Note, the minimum, maximum, single step and decimals apply on the spinbox
displayed value. If the spinbox limit is 200 and the proxy value a function
like f(x) = x - 30, then one can call setValue(212) on the widget. The
value displayed will be 182 and the signal valueChanged will give 212.
A setValue(256) however will be have a display value of 200 and the signals
valueChanged give 230.

The base class for value proxies is ctkValueProxy. Subclasses should
reimplement the methods valueFromProxyValue() and proxyValueFromValue().
Johan Andruejol 12 years ago
parent
commit
4a80c69db1

+ 6 - 0
Libs/Core/CMakeLists.txt

@@ -53,6 +53,8 @@ set(KIT_SRCS
   ctkException.cpp
   ctkException.cpp
   ctkException.h
   ctkException.h
   ctkHighPrecisionTimer.cpp
   ctkHighPrecisionTimer.cpp
+  ctkLinearValueProxy.cpp
+  ctkLinearValueProxy.h
   ctkLogger.cpp
   ctkLogger.cpp
   ctkLogger.h
   ctkLogger.h
   ctkHistogram.cpp
   ctkHistogram.cpp
@@ -69,6 +71,8 @@ set(KIT_SRCS
   ctkTransferFunctionRepresentation.h
   ctkTransferFunctionRepresentation.h
   ctkUtils.cpp
   ctkUtils.cpp
   ctkUtils.h
   ctkUtils.h
+  ctkValueProxy.cpp
+  ctkValueProxy.h
   ctkWorkflow.h
   ctkWorkflow.h
   ctkWorkflow.cpp
   ctkWorkflow.cpp
   ctkWorkflow_p.h
   ctkWorkflow_p.h
@@ -95,11 +99,13 @@ set(KIT_MOC_SRCS
   ctkCommandLineParser.h
   ctkCommandLineParser.h
   ctkErrorLogFDMessageHandler_p.h
   ctkErrorLogFDMessageHandler_p.h
   ctkErrorLogModel.h
   ctkErrorLogModel.h
+  ctkLinearValueProxy.h
   ctkLogger.h
   ctkLogger.h
   ctkHistogram.h
   ctkHistogram.h
   ctkModelTester.h
   ctkModelTester.h
   ctkTransferFunction.h
   ctkTransferFunction.h
   ctkTransferFunctionRepresentation.h
   ctkTransferFunctionRepresentation.h
+  ctkValueProxy.h
   ctkWorkflow.h
   ctkWorkflow.h
   ctkWorkflow_p.h
   ctkWorkflow_p.h
   ctkWorkflowStep_p.h
   ctkWorkflowStep_p.h

+ 3 - 0
Libs/Core/Testing/Cpp/CMakeLists.txt

@@ -38,6 +38,7 @@ set(KITTests_SRCS
   ctkExceptionTest.cpp
   ctkExceptionTest.cpp
   ctkHighPrecisionTimerTest.cpp
   ctkHighPrecisionTimerTest.cpp
   ctkHistogramTest1.cpp
   ctkHistogramTest1.cpp
+  ctkLinearValueProxyTest.cpp
   ctkLoggerTest1.cpp
   ctkLoggerTest1.cpp
   ctkModelTesterTest1.cpp
   ctkModelTesterTest1.cpp
   ctkModelTesterTest2.cpp
   ctkModelTesterTest2.cpp
@@ -103,6 +104,7 @@ set(Tests_Helpers_MOC_CPP)
 QT4_WRAP_CPP(Tests_Helpers_MOC_CPP ${Tests_Helpers_MOC_SRCS})
 QT4_WRAP_CPP(Tests_Helpers_MOC_CPP ${Tests_Helpers_MOC_SRCS})
 QT4_GENERATE_MOCS(
 QT4_GENERATE_MOCS(
   ctkBooleanMapperTest.cpp
   ctkBooleanMapperTest.cpp
+  ctkLinearValueProxyTest.cpp
   ctkUtilsTest.cpp
   ctkUtilsTest.cpp
   )
   )
 
 
@@ -154,6 +156,7 @@ SIMPLE_TEST( ctkErrorLogStreamMessageHandlerWithThreadsTest1 )
 SIMPLE_TEST( ctkExceptionTest )
 SIMPLE_TEST( ctkExceptionTest )
 SIMPLE_TEST( ctkHighPrecisionTimerTest )
 SIMPLE_TEST( ctkHighPrecisionTimerTest )
 SIMPLE_TEST( ctkHistogramTest1 )
 SIMPLE_TEST( ctkHistogramTest1 )
+SIMPLE_TEST( ctkLinearValueProxyTest )
 SIMPLE_TEST( ctkLoggerTest1 )
 SIMPLE_TEST( ctkLoggerTest1 )
 set_property(TEST ctkLoggerTest1 PROPERTY PASS_REGULAR_EXPRESSION "logger.debug\nlogger.info\nlogger.trace\nlogger.warn\nlogger.error\nlogger.fatal")
 set_property(TEST ctkLoggerTest1 PROPERTY PASS_REGULAR_EXPRESSION "logger.debug\nlogger.info\nlogger.trace\nlogger.warn\nlogger.error\nlogger.fatal")
 SIMPLE_TEST( ctkModelTesterTest1 )
 SIMPLE_TEST( ctkModelTesterTest1 )

+ 465 - 0
Libs/Core/Testing/Cpp/ctkLinearValueProxyTest.cpp

@@ -0,0 +1,465 @@
+/*=========================================================================
+
+  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 <QSignalSpy>
+
+// CTK includes
+#include "ctkLinearValueProxy.h"
+#include "ctkTest.h"
+
+// ----------------------------------------------------------------------------
+class ctkLinearValueProxyTester: public QObject
+{
+  Q_OBJECT
+private Q_SLOTS:
+  void testSimpleSetValue();
+  void testSimpleSetValue_data();
+
+  void testCoefficient();
+  void testCoefficient_data();
+
+  void testOffset();
+  void testOffset_data();
+
+  void testSetValue();
+  void testSetValue_data();
+
+  void testSetValueNullCoeff();
+  void testSetValueNullCoeff_data();
+
+  void testSetProxyValue();
+  void testSetProxyValue_data();
+
+  void testSetProxyValueNullCoeff();
+  void testSetProxyValueNullCoeff_data();
+};
+
+// ----------------------------------------------------------------------------
+void ctkLinearValueProxyTester::testSimpleSetValue()
+{
+  ctkLinearValueProxy proxy;
+  proxy.setValue(28.1358);
+
+  QSignalSpy valueSpy(&proxy, SIGNAL(valueChanged(double)));
+  QSignalSpy proxyValueSpy(&proxy, SIGNAL(proxyValueChanged(double)));
+
+  QFETCH(double, value);
+  proxy.setValue(value);
+
+  ctkTest::COMPARE(proxy.value(), value);
+  ctkTest::COMPARE(proxy.proxyValue(), value);
+
+  ctkTest::COMPARE(valueSpy.count(), 1);
+  double valueFromSpy = valueSpy.takeFirst().at(0).toDouble();
+  ctkTest::COMPARE(valueFromSpy, value);
+
+  ctkTest::COMPARE(proxyValueSpy.count(), 1);
+  double proxyValueFromSpy = proxyValueSpy.takeFirst().at(0).toDouble();
+  ctkTest::COMPARE(proxyValueFromSpy, value);
+}
+
+// ----------------------------------------------------------------------------
+void ctkLinearValueProxyTester::testSimpleSetValue_data()
+{
+  QTest::addColumn<double>("value");
+
+  QTest::newRow("Null value") << 0.0;
+  QTest::newRow("Very very small value") << 1e-26;
+  QTest::newRow("Not so small value") << 1e-6;
+  QTest::newRow("Max value") <<std::numeric_limits<double>::max();
+  QTest::newRow("Min value") <<std::numeric_limits<double>::min();
+  QTest::newRow("Infinity") <<std::numeric_limits<double>::infinity();
+  QTest::newRow(" - Infinity") << - std::numeric_limits<double>::infinity();
+  QTest::newRow("Nan") << - std::numeric_limits<double>::quiet_NaN();
+}
+
+// ----------------------------------------------------------------------------
+void ctkLinearValueProxyTester::testCoefficient()
+{
+  ctkLinearValueProxy proxy;
+  proxy.setValue(13.2);
+
+  QSignalSpy valueSpy(&proxy, SIGNAL(valueChanged(double)));
+  QSignalSpy proxyValueSpy(&proxy, SIGNAL(proxyValueChanged(double)));
+
+  QFETCH(double, coefficient);
+  proxy.setCoefficient(coefficient);
+
+  QFETCH(double, expectedProxyValue);
+  ctkTest::COMPARE(proxy.proxyValue(), expectedProxyValue);
+  ctkTest::COMPARE(proxy.value(), 13.2);
+
+  ctkTest::COMPARE(valueSpy.count(), 0);
+  ctkTest::COMPARE(proxyValueSpy.count(), proxy.proxyValue() != 13.2 ? 1 : 0);
+  if (proxyValueSpy.count())
+    {
+    double proxyValueFromSpy = proxyValueSpy.takeFirst().at(0).toDouble();
+    ctkTest::COMPARE(proxyValueFromSpy, expectedProxyValue);
+    }
+}
+
+// ----------------------------------------------------------------------------
+void ctkLinearValueProxyTester::testCoefficient_data()
+{
+  QTest::addColumn<double>("coefficient");
+  QTest::addColumn<double>("expectedProxyValue");
+
+  QTest::newRow("Null coeff") << 0.0 << 0.0;
+  QTest::newRow("Very very small coeff") << 1e-26 << 0.0;
+  QTest::newRow("Not so small coeff") << 1e-6 << 1.32e-5;
+  QTest::newRow("Normal coeff") << 2.0 << 26.4;
+  QTest::newRow("Negative coeff") << -2.0 << -26.4;
+  QTest::newRow("Large coeff") << 123456.0 << 1629619.2;
+  QTest::newRow("Very very large coeff") << 1e26 << 13.2 * 1e26;
+  QTest::newRow("unit coeff") << 1.0 << 13.2;
+  QTest::newRow("same coeff") << 1.0 << 13.2;
+  QTest::newRow("Max coeff") << std::numeric_limits<double>::max()
+    << std::numeric_limits<double>::infinity();
+  QTest::newRow("Min coeff") << - std::numeric_limits<double>::max()
+    << - std::numeric_limits<double>::infinity();
+  QTest::newRow("Infinity coeff") << std::numeric_limits<double>::infinity()
+    << std::numeric_limits<double>::infinity();
+  QTest::newRow(" - Infinity coeff")
+    << - std::numeric_limits<double>::infinity()
+    << - std::numeric_limits<double>::infinity();
+  QTest::newRow("Nan coeff") << std::numeric_limits<double>::quiet_NaN()
+    << std::numeric_limits<double>::quiet_NaN();
+}
+
+// ----------------------------------------------------------------------------
+void ctkLinearValueProxyTester::testOffset()
+{
+  ctkLinearValueProxy proxy;
+  proxy.setValue(13.2);
+
+  QSignalSpy valueSpy(&proxy, SIGNAL(valueChanged(double)));
+  QSignalSpy proxyValueSpy(&proxy, SIGNAL(proxyValueChanged(double)));
+
+  QFETCH(double, offset);
+  proxy.setOffset(offset);
+
+  QFETCH(double, expectedProxyValue);
+  ctkTest::COMPARE(proxy.proxyValue(), expectedProxyValue);
+  ctkTest::COMPARE(proxy.value(), 13.2);
+
+  ctkTest::COMPARE(valueSpy.count(), 0);
+  ctkTest::COMPARE(proxyValueSpy.count(), proxy.proxyValue() != 13.2 ? 1 : 0);
+  if (proxyValueSpy.count())
+    {
+    double proxyValueFromSpy = proxyValueSpy.takeFirst().at(0).toDouble();
+    ctkTest::COMPARE(proxyValueFromSpy, expectedProxyValue);
+    }
+}
+
+// ----------------------------------------------------------------------------
+void ctkLinearValueProxyTester::testOffset_data()
+{
+  QTest::addColumn<double>("offset");
+  QTest::addColumn<double>("expectedProxyValue");
+
+  QTest::newRow("Very very small offset") << 1 + 1e-26 << 14.2 + 1e-26;
+  QTest::newRow("Not so small offset") << 1e-5 << 13.20001;
+  QTest::newRow("Normal offset") << -2.0 << 11.2;
+  QTest::newRow("Null offset") << 0.0 << 13.2;
+  QTest::newRow("Null offset - again") << 0.0 << 13.2;
+  QTest::newRow("Max offset") << std::numeric_limits<double>::max()
+    << std::numeric_limits<double>::max() + 13.2;
+  QTest::newRow("Min offset") << - std::numeric_limits<double>::max()
+    << - std::numeric_limits<double>::max() + 13.2;
+  QTest::newRow("Infinity offset") << std::numeric_limits<double>::infinity()
+    << std::numeric_limits<double>::infinity();
+  QTest::newRow(" - Infinity offset") << - std::numeric_limits<double>::infinity()
+    << - std::numeric_limits<double>::infinity();
+  QTest::newRow("Nan offset") << std::numeric_limits<double>::quiet_NaN()
+    << std::numeric_limits<double>::quiet_NaN();
+}
+
+// ----------------------------------------------------------------------------
+void ctkLinearValueProxyTester::testSetValue()
+{
+  ctkLinearValueProxy proxy;
+  QFETCH(double, offset);
+  proxy.setCoefficient(3.1);
+  proxy.setOffset(offset);
+  proxy.setValue(13.2);
+
+  QSignalSpy valueSpy(&proxy, SIGNAL(valueChanged(double)));
+  QSignalSpy proxyValueSpy(&proxy, SIGNAL(proxyValueChanged(double)));
+
+  QFETCH(double, value);
+  proxy.setValue(value);
+
+  QFETCH(double, expectedProxyValue);
+  ctkTest::COMPARE(proxy.proxyValue(), expectedProxyValue);
+  ctkTest::COMPARE(proxy.value(), value);
+
+  ctkTest::COMPARE(valueSpy.count(), proxy.value() != 13.2 ? 1 : 0);
+  if (valueSpy.count())
+    {
+    double valueFromSpy = valueSpy.takeFirst().at(0).toDouble();
+    ctkTest::COMPARE(valueFromSpy, value);
+    }
+
+  ctkTest::COMPARE(proxyValueSpy.count(), proxy.proxyValue() != 0.0 ? 1 : 0);
+  if (proxyValueSpy.count())
+    {
+    double proxyValueFromSpy = proxyValueSpy.takeFirst().at(0).toDouble();
+    ctkTest::COMPARE(proxyValueFromSpy, expectedProxyValue);
+    }
+}
+
+// ----------------------------------------------------------------------------
+void ctkLinearValueProxyTester::testSetValue_data()
+{
+  QTest::addColumn<double>("offset");
+  QTest::addColumn<double>("value");
+  QTest::addColumn<double>("expectedProxyValue");
+
+  QTest::newRow("Linear transform: normal input") << -19.53 << 6.62 << 0.992;
+  QTest::newRow("Linear transform: max")
+    << -19.53 << std::numeric_limits<double>::max()
+    << 3.1*std::numeric_limits<double>::max() - 19.53;
+  QTest::newRow("Linear transform: min")
+    << -19.53 << - std::numeric_limits<double>::max()
+    << -3.1*std::numeric_limits<double>::max() - 19.53;
+  QTest::newRow("Linear transform: infinity")
+    << -19.53 << std::numeric_limits<double>::infinity()
+    << std::numeric_limits<double>::infinity();
+  QTest::newRow("Linear transform: - infinity")
+    << -19.53 << - std::numeric_limits<double>::infinity()
+    << - std::numeric_limits<double>::infinity();
+  QTest::newRow("Linear transform: Nan")
+    << -19.53 << std::numeric_limits<double>::quiet_NaN()
+    << std::numeric_limits<double>::quiet_NaN();
+
+  QTest::newRow("Null offset: normal input") << 0.0 << 6.62 << 20.522;
+  QTest::newRow("Null offset: max")
+    << 0.0 << std::numeric_limits<double>::max()
+    << 3.1*std::numeric_limits<double>::max();
+  QTest::newRow("Null offset: min")
+    << 0.0 << - std::numeric_limits<double>::max()
+    << -3.1*std::numeric_limits<double>::max();
+  QTest::newRow("Null offset: infinity")
+    << 0.0 << std::numeric_limits<double>::infinity()
+    << std::numeric_limits<double>::infinity();
+  QTest::newRow("Null offset: - infinity")
+    << 0.0 << - std::numeric_limits<double>::infinity()
+    << - std::numeric_limits<double>::infinity();
+  QTest::newRow("Null offset: Nan")
+    << 0.0 << std::numeric_limits<double>::quiet_NaN()
+    << std::numeric_limits<double>::quiet_NaN();
+}
+
+// ----------------------------------------------------------------------------
+void ctkLinearValueProxyTester::testSetValueNullCoeff()
+{
+  ctkLinearValueProxy proxy;
+  proxy.setCoefficient(0.0);
+  proxy.setOffset(-19.53);
+  proxy.setValue(13.2);
+
+  QSignalSpy valueSpy(&proxy, SIGNAL(valueChanged(double)));
+  QSignalSpy proxyValueSpy(&proxy, SIGNAL(proxyValueChanged(double)));
+
+  QFETCH(double, value);
+  proxy.setValue(value);
+
+  QFETCH(double, expectedProxyValue);
+  ctkTest::COMPARE(proxy.proxyValue(), expectedProxyValue);
+  ctkTest::COMPARE(proxy.value(), value);
+
+  ctkTest::COMPARE(valueSpy.count(), proxy.value() != 13.2 ? 1 : 0);
+  if (valueSpy.count())
+    {
+    double valueFromSpy = valueSpy.takeFirst().at(0).toDouble();
+    ctkTest::COMPARE(valueFromSpy, value);
+    }
+
+  QFETCH(bool, shouldExpectProxyValue);
+  ctkTest::COMPARE(proxyValueSpy.count(), shouldExpectProxyValue ? 1 : 0);
+  if (proxyValueSpy.count())
+    {
+    double proxyValueFromSpy = proxyValueSpy.takeFirst().at(0).toDouble();
+    ctkTest::COMPARE(proxyValueFromSpy, expectedProxyValue);
+    }
+}
+
+// ----------------------------------------------------------------------------
+void ctkLinearValueProxyTester::testSetValueNullCoeff_data()
+{
+  QTest::addColumn<double>("value");
+  QTest::addColumn<bool>("shouldExpectProxyValue");
+  QTest::addColumn<double>("expectedProxyValue");
+
+  QTest::newRow("Null coeff: normal input") << 6.62 << false << -19.53;
+  QTest::newRow("Null coeff: max")
+    << std::numeric_limits<double>::max() << false << -19.53;;
+  QTest::newRow("Null coeff: min")
+    << - std::numeric_limits<double>::max() << false << -19.53;
+  QTest::newRow("Null coeff: infinity")
+    << std::numeric_limits<double>::infinity() << true
+    << std::numeric_limits<double>::quiet_NaN();
+  QTest::newRow("Null coeff: - infinity")
+    << - std::numeric_limits<double>::infinity() << true
+    << std::numeric_limits<double>::quiet_NaN();
+  QTest::newRow("Null coeff: Nan") << std::numeric_limits<double>::quiet_NaN()
+    << true << std::numeric_limits<double>::quiet_NaN();
+}
+
+// ----------------------------------------------------------------------------
+void ctkLinearValueProxyTester::testSetProxyValue()
+{
+  ctkLinearValueProxy proxy;
+  QFETCH(double, offset);
+  proxy.setCoefficient(3.1);
+  proxy.setOffset(offset);
+  proxy.setProxyValue(13.2);
+  double oldValue = proxy.value();
+
+  QSignalSpy valueSpy(&proxy, SIGNAL(valueChanged(double)));
+  QSignalSpy proxyValueSpy(&proxy, SIGNAL(proxyValueChanged(double)));
+
+  QFETCH(double, proxyValue);
+  proxy.setProxyValue(proxyValue);
+
+  QFETCH(double, expectedValue);
+  ctkTest::COMPARE(proxy.proxyValue(), proxyValue);
+  ctkTest::COMPARE(proxy.value(), expectedValue);
+
+  ctkTest::COMPARE(valueSpy.count(), proxy.value() != oldValue ? 1 : 0);
+  if (valueSpy.count())
+    {
+    double valueFromSpy = valueSpy.takeFirst().at(0).toDouble();
+    ctkTest::COMPARE(valueFromSpy, expectedValue);
+    }
+
+  ctkTest::COMPARE(proxyValueSpy.count(), proxy.proxyValue() != 13.2 ? 1 : 0);
+  if (proxyValueSpy.count())
+    {
+    double proxyValueFromSpy = proxyValueSpy.takeFirst().at(0).toDouble();
+    ctkTest::COMPARE(proxyValueFromSpy, proxyValue);
+    }
+}
+
+// ----------------------------------------------------------------------------
+void ctkLinearValueProxyTester::testSetProxyValue_data()
+{
+  QTest::addColumn<double>("offset");
+  QTest::addColumn<double>("proxyValue");
+  QTest::addColumn<double>("expectedValue");
+
+  QTest::newRow("Linear transform: normal input") << -19.53 << 1.55 << 6.8;
+  QTest::newRow("Linear transform: max")
+    << -19.53 << std::numeric_limits<double>::max()
+    << (std::numeric_limits<double>::max() + 19.53) / 3.1;
+  QTest::newRow("Linear transform: min")
+    << -19.53 << - std::numeric_limits<double>::max()
+    << ( - std::numeric_limits<double>::max() + 19.53) / 3.1;
+  QTest::newRow("Linear transform: infinity")
+    << -19.53 << std::numeric_limits<double>::infinity()
+    << std::numeric_limits<double>::infinity();
+  QTest::newRow("Linear transform: - infinity")
+    << -19.53 << - std::numeric_limits<double>::infinity()
+    << - std::numeric_limits<double>::infinity();
+  QTest::newRow("Linear transform: Nan")
+    << -19.53 << std::numeric_limits<double>::quiet_NaN()
+    << std::numeric_limits<double>::quiet_NaN();
+
+  QTest::newRow("Null offset: normal input") << 0.0 << 232.5 << 75.0;
+  QTest::newRow("Null offset: max")
+    << 0.0 << std::numeric_limits<double>::max()
+    << std::numeric_limits<double>::max() / 3.1;
+  QTest::newRow("Null offset: min")
+    << 0.0 << - std::numeric_limits<double>::max()
+    << -std::numeric_limits<double>::max() / 3.1;
+  QTest::newRow("Null offset: infinity")
+    << 0.0 << std::numeric_limits<double>::infinity()
+    << std::numeric_limits<double>::infinity();
+  QTest::newRow("Null offset: - infinity")
+    << 0.0 << - std::numeric_limits<double>::infinity()
+    << - std::numeric_limits<double>::infinity();
+  QTest::newRow("Null offset: Nan")
+    << 0.0 << std::numeric_limits<double>::quiet_NaN()
+    << std::numeric_limits<double>::quiet_NaN();
+}
+
+// ----------------------------------------------------------------------------
+void ctkLinearValueProxyTester::testSetProxyValueNullCoeff()
+{
+  ctkLinearValueProxy proxy;
+  proxy.setCoefficient(0.0);
+  proxy.setOffset(-19.53);
+  proxy.setValue(13.2);
+
+  QSignalSpy valueSpy(&proxy, SIGNAL(valueChanged(double)));
+  QSignalSpy proxyValueSpy(&proxy, SIGNAL(proxyValueChanged(double)));
+
+  QFETCH(double, proxyValue);
+  proxy.setProxyValue(proxyValue);
+
+  QFETCH(double, expectedValue);
+  ctkTest::COMPARE(proxy.proxyValue(), proxyValue);
+  ctkTest::COMPARE(proxy.value(), expectedValue);
+
+  ctkTest::COMPARE(valueSpy.count(), proxy.value() != 13.2 ? 1 : 0);
+  if (valueSpy.count())
+    {
+    double valueFromSpy = valueSpy.takeFirst().at(0).toDouble();
+    ctkTest::COMPARE(valueFromSpy, expectedValue);
+    }
+
+  ctkTest::COMPARE(proxyValueSpy.count(), proxy.proxyValue() != 0.0 ? 1 : 0);
+  if (proxyValueSpy.count())
+    {
+    double proxyValueFromSpy = proxyValueSpy.takeFirst().at(0).toDouble();
+    ctkTest::COMPARE(proxyValueFromSpy, proxyValue);
+    }
+}
+
+// ----------------------------------------------------------------------------
+void ctkLinearValueProxyTester::testSetProxyValueNullCoeff_data()
+{
+  QTest::addColumn<double>("proxyValue");
+  QTest::addColumn<double>("expectedValue");
+
+  QTest::newRow("Null coeff: normal input") << 6.62
+    << std::numeric_limits<double>::infinity();
+  QTest::newRow("Null coeff: normal negative input") << -398.6
+    << - std::numeric_limits<double>::infinity();
+  QTest::newRow("Null coeff: max")
+    << std::numeric_limits<double>::max()
+    << std::numeric_limits<double>::infinity();
+  QTest::newRow("Null coeff: min")
+    << - std::numeric_limits<double>::max()
+    << - std::numeric_limits<double>::infinity();
+  QTest::newRow("Null coeff: infinity")
+    << std::numeric_limits<double>::infinity()
+    << std::numeric_limits<double>::infinity();
+  QTest::newRow("Null coeff: - infinity")
+    << - std::numeric_limits<double>::infinity()
+    << - std::numeric_limits<double>::infinity();
+  QTest::newRow("Null coeff: Nan") << std::numeric_limits<double>::quiet_NaN()
+    << std::numeric_limits<double>::quiet_NaN();
+}
+
+// ----------------------------------------------------------------------------
+CTK_TEST_MAIN(ctkLinearValueProxyTest)
+#include "moc_ctkLinearValueProxyTest.cpp"

+ 126 - 0
Libs/Core/ctkLinearValueProxy.cpp

@@ -0,0 +1,126 @@
+/*=========================================================================
+
+  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 <QDebug>
+
+// CTK includes
+#include "ctkLinearValueProxy.h"
+
+// STD includes
+#include <limits>
+
+// --------------------------------------------------------------------------
+// ctkLinearValueProxyPrivate
+
+class CTK_CORE_EXPORT ctkLinearValueProxyPrivate
+{
+  Q_DECLARE_PUBLIC(ctkLinearValueProxy);
+
+protected:
+  ctkLinearValueProxy* q_ptr;
+public:
+  ctkLinearValueProxyPrivate(ctkLinearValueProxy& object);
+  ~ctkLinearValueProxyPrivate();
+
+  bool isCoefficientValid() const;
+
+  double Coefficient;
+  double Offset;
+};
+
+// --------------------------------------------------------------------------
+// ctkLinearValueProxyPrivate methods
+
+// --------------------------------------------------------------------------
+ctkLinearValueProxyPrivate::ctkLinearValueProxyPrivate(ctkLinearValueProxy& object)
+  :q_ptr(&object)
+{
+  this->Coefficient = 1.0;
+  this->Offset = 0.0;
+}
+
+// --------------------------------------------------------------------------
+ctkLinearValueProxyPrivate::~ctkLinearValueProxyPrivate()
+{
+}
+
+// --------------------------------------------------------------------------
+bool ctkLinearValueProxyPrivate::isCoefficientValid() const
+{
+  return qAbs(this->Coefficient) > std::numeric_limits<double>::epsilon();
+}
+
+// --------------------------------------------------------------------------
+// ctkLinearValueProxy methods
+
+// --------------------------------------------------------------------------
+ctkLinearValueProxy::ctkLinearValueProxy(QObject* _parent)
+  : Superclass(_parent)
+  , d_ptr(new ctkLinearValueProxyPrivate(*this))
+{
+}
+
+// --------------------------------------------------------------------------
+ctkLinearValueProxy::~ctkLinearValueProxy()
+{
+}
+
+// --------------------------------------------------------------------------
+double ctkLinearValueProxy::proxyValueFromValue(double value) const
+{
+  return (this->coefficient() * value) + this->offset();
+}
+
+// --------------------------------------------------------------------------
+double ctkLinearValueProxy::valueFromProxyValue(double proxyValue) const
+{
+  return (proxyValue - this->offset()) / this->coefficient();
+}
+
+// --------------------------------------------------------------------------
+CTK_GET_CPP(ctkLinearValueProxy, double, coefficient, Coefficient);
+CTK_GET_CPP(ctkLinearValueProxy, double, offset, Offset);
+
+// --------------------------------------------------------------------------
+void ctkLinearValueProxy::setCoefficient(double newCoeff)
+{
+  Q_D(ctkLinearValueProxy);
+  if (d->Coefficient == newCoeff)
+    {
+    return;
+    }
+
+  d->Coefficient = newCoeff;
+  this->updateProxyValue();
+}
+
+// --------------------------------------------------------------------------
+void ctkLinearValueProxy::setOffset(double newOffset)
+{
+  Q_D(ctkLinearValueProxy);
+  if (d->Offset == newOffset)
+    {
+    return;
+    }
+
+  d->Offset = newOffset;
+  this->updateProxyValue();
+}

+ 70 - 0
Libs/Core/ctkLinearValueProxy.h

@@ -0,0 +1,70 @@
+/*=========================================================================
+
+  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.
+
+=========================================================================*/
+
+#ifndef __ctkLinearValueProxy_h
+#define __ctkLinearValueProxy_h
+
+// CTK includes
+#include "ctkCoreExport.h"
+#include "ctkValueProxy.h"
+#include "ctkPimpl.h"
+
+class ctkLinearValueProxyPrivate;
+
+/// \ingroup Core
+/// \brief Implementation of an affine value proxy.
+/// The ctkLinearValueProxy takes a coefficient and an offset,
+/// effectively implementing a value proxy such as:
+/// valueProxy = coefficient * value + offset
+/// Note: If the coefficient is null then the property value given
+/// by value = (valueProxy - offset) / coefficient can give bad results
+/// (+ or - infinity depending on the sign of valueProxy - offset).
+/// \sa ctkValueProxy
+class CTK_CORE_EXPORT ctkLinearValueProxy : public ctkValueProxy
+{
+  Q_OBJECT
+  Q_PROPERTY(double coefficient READ coefficient WRITE setCoefficient)
+  Q_PROPERTY(double offset READ offset WRITE setOffset)
+
+public:
+  typedef ctkValueProxy Superclass;
+  explicit ctkLinearValueProxy(QObject* parent = 0);
+  virtual ~ctkLinearValueProxy();
+
+  virtual double proxyValueFromValue(double value) const;
+
+  virtual double valueFromProxyValue(double proxyValue) const;
+
+  virtual double coefficient() const;
+  virtual double offset() const;
+
+public Q_SLOTS:
+  virtual void setCoefficient(double newCoeff);
+  virtual void setOffset(double newOffset);
+
+protected:
+  QScopedPointer<ctkLinearValueProxyPrivate> d_ptr;
+
+private:
+  Q_DECLARE_PRIVATE(ctkLinearValueProxy);
+  Q_DISABLE_COPY(ctkLinearValueProxy);
+};
+
+#endif

+ 140 - 0
Libs/Core/ctkValueProxy.cpp

@@ -0,0 +1,140 @@
+/*=========================================================================
+
+  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
+
+// CTK includes
+#include "ctkValueProxy.h"
+
+// --------------------------------------------------------------------------
+// ctkValueProxyPrivate
+
+class CTK_CORE_EXPORT ctkValueProxyPrivate
+{
+  Q_DECLARE_PUBLIC(ctkValueProxy);
+
+public:
+  ctkValueProxy* q_ptr;
+  ctkValueProxyPrivate(ctkValueProxy& object);
+  ~ctkValueProxyPrivate();
+
+  double Value;
+  double ProxyValue;
+};
+
+// --------------------------------------------------------------------------
+// ctkValueProxyPrivate methods
+
+// --------------------------------------------------------------------------
+ctkValueProxyPrivate::ctkValueProxyPrivate(ctkValueProxy& object)
+  : q_ptr(&object)
+{
+  this->Value = 0.0;
+  this->ProxyValue = 0.0;
+}
+
+// --------------------------------------------------------------------------
+ctkValueProxyPrivate::~ctkValueProxyPrivate()
+{
+}
+
+// --------------------------------------------------------------------------
+// ctkValueProxy methods
+
+// --------------------------------------------------------------------------
+ctkValueProxy::ctkValueProxy(QObject* _parent) : Superclass(_parent)
+  , d_ptr(new ctkValueProxyPrivate(*this))
+{
+}
+
+// --------------------------------------------------------------------------
+ctkValueProxy::~ctkValueProxy()
+{
+}
+
+// --------------------------------------------------------------------------
+double ctkValueProxy::value() const
+{
+  Q_D(const ctkValueProxy);
+  return d->Value;
+}
+
+// --------------------------------------------------------------------------
+void ctkValueProxy::setValue(double newValue)
+{
+  Q_D(ctkValueProxy);
+  if (d->Value == newValue)
+    {
+    return;
+    }
+
+  d->Value = newValue;
+  emit this->valueChanged(d->Value);
+  this->updateProxyValue();
+}
+
+// --------------------------------------------------------------------------
+double ctkValueProxy::proxyValue() const
+{
+  Q_D(const ctkValueProxy);
+  return d->ProxyValue;
+}
+
+// --------------------------------------------------------------------------
+void ctkValueProxy::setProxyValue(double newProxyValue)
+{
+  Q_D(ctkValueProxy);
+  if (d->ProxyValue == newProxyValue)
+    {
+    return;
+    }
+
+  d->ProxyValue = newProxyValue;
+  emit this->proxyValueChanged(d->ProxyValue);
+  this->updateValue();
+}
+
+// --------------------------------------------------------------------------
+void ctkValueProxy::updateProxyValue()
+{
+  Q_D(ctkValueProxy);
+  double newProxyValue = this->proxyValueFromValue(d->Value);
+  if (newProxyValue == d->ProxyValue)
+    {
+    return;
+    }
+
+  d->ProxyValue = newProxyValue;
+  emit this->proxyValueChanged(d->ProxyValue);
+}
+
+// --------------------------------------------------------------------------
+void ctkValueProxy::updateValue()
+{
+  Q_D(ctkValueProxy);
+  double newValue = this->valueFromProxyValue(d->ProxyValue);
+  if (newValue == d->Value)
+    {
+    return;
+    }
+
+  d->Value = newValue;
+  emit this->valueChanged(d->Value);
+}

+ 94 - 0
Libs/Core/ctkValueProxy.h

@@ -0,0 +1,94 @@
+/*=========================================================================
+
+  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.
+
+=========================================================================*/
+
+#ifndef __ctkValueProxy_h
+#define __ctkValueProxy_h
+
+// Qt includes
+#include <QObject>
+#include <QScopedPointer>
+
+// CTK includes
+#include "ctkCoreExport.h"
+
+class ctkValueProxyPrivate;
+
+/// \ingroup Core
+/// \brief Base class for value proxies.
+/// Value proxy allows to decouple the displayed value from the values
+/// accessed within the program. For example, one may want to display
+/// Fahrenheit while still working with Celsius.
+///
+/// A ctkValueProxy can be used by connecting signal/slots to the
+/// value and proxyValue properties or by using directly the
+/// valueFromProxyValue and proxyValueFromValue functions.
+///
+/// Subclasses should reimplement the function proxyValueFromValue()
+/// and valueFromProxyValue().
+/// \sa ctkLinearValueProxy
+class CTK_CORE_EXPORT ctkValueProxy : public QObject
+{
+  Q_OBJECT
+
+  /// The value holds the current value. If the value proxy is
+  /// considered as a function, then this function applied to the value is
+  /// the proxy value.
+  /// The value is updated if the proxy value is changed.
+  Q_PROPERTY(double value READ value WRITE setValue NOTIFY valueChanged)
+
+  /// The proxy value holds the value transformed. If the value proxy is
+  /// considered as a function, then the proxy value is the result of
+  /// this function applied to value.
+  /// The proxy value is updated if the value is changed.
+  Q_PROPERTY(double proxyValue READ proxyValue WRITE setProxyValue NOTIFY proxyValueChanged)
+
+public:
+  typedef QObject Superclass;
+  explicit ctkValueProxy(QObject* parent = 0);
+  virtual ~ctkValueProxy();
+
+  virtual double proxyValueFromValue(double value) const = 0;
+  virtual double valueFromProxyValue(double proxyValue) const = 0;
+
+  double value() const;
+  virtual double proxyValue() const;
+
+public Q_SLOTS:
+  void setValue(double newValue);
+  void setProxyValue(double newProxyValue);
+
+Q_SIGNALS:
+  void valueChanged(double);
+  void proxyValueChanged(double);
+
+protected:
+  QScopedPointer<ctkValueProxyPrivate> d_ptr;
+
+  /// Utilities function for subclasses.
+  /// Can be called to update the value/proxyValue from the proxyValue/value.
+  void updateProxyValue();
+  void updateValue();
+
+private:
+  Q_DECLARE_PRIVATE(ctkValueProxy);
+  Q_DISABLE_COPY(ctkValueProxy);
+};
+
+#endif

+ 5 - 0
Libs/Testing/ctkTest.h

@@ -122,6 +122,11 @@ inline void COMPARE(double v1, double v2)
     {
     {
     QVERIFY(v1 == std::numeric_limits<double>::infinity());
     QVERIFY(v1 == std::numeric_limits<double>::infinity());
     }
     }
+  // QCOMPARE doesn't like to compare zeroes
+  else if (v2 == 0.0)
+    {
+    QCOMPARE(v1 + 1.0, 1.0);
+    }
   else
   else
     {
     {
     QCOMPARE(v1, v2);
     QCOMPARE(v1, v2);

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

@@ -31,6 +31,7 @@ set(TEST_SOURCES
   ctkDoubleSliderTest1.cpp
   ctkDoubleSliderTest1.cpp
   ctkDoubleSpinBoxTest.cpp
   ctkDoubleSpinBoxTest.cpp
   ctkDoubleSpinBoxTest1.cpp
   ctkDoubleSpinBoxTest1.cpp
+  ctkDoubleSpinBoxValueProxyTest.cpp
   ctkDynamicSpacerTest1.cpp
   ctkDynamicSpacerTest1.cpp
   ctkDynamicSpacerTest2.cpp
   ctkDynamicSpacerTest2.cpp
   ctkErrorLogWidgetTest1.cpp
   ctkErrorLogWidgetTest1.cpp
@@ -183,6 +184,7 @@ QT4_GENERATE_MOCS(
   ctkCoordinatesWidgetTest.cpp
   ctkCoordinatesWidgetTest.cpp
   ctkDoubleSliderTest.cpp
   ctkDoubleSliderTest.cpp
   ctkDoubleSpinBoxTest.cpp
   ctkDoubleSpinBoxTest.cpp
+  ctkDoubleSpinBoxValueProxyTest.cpp
   ctkFlatProxyModelTest.cpp
   ctkFlatProxyModelTest.cpp
   ctkFontButtonTest.cpp
   ctkFontButtonTest.cpp
   ctkLanguageComboBoxTest.cpp
   ctkLanguageComboBoxTest.cpp
@@ -240,6 +242,7 @@ SIMPLE_TEST( ctkDoubleSliderTest )
 SIMPLE_TEST( ctkDoubleSliderTest1 )
 SIMPLE_TEST( ctkDoubleSliderTest1 )
 SIMPLE_TEST( ctkDoubleSpinBoxTest )
 SIMPLE_TEST( ctkDoubleSpinBoxTest )
 SIMPLE_TEST( ctkDoubleSpinBoxTest1 )
 SIMPLE_TEST( ctkDoubleSpinBoxTest1 )
+SIMPLE_TEST( ctkDoubleSpinBoxValueProxyTest )
 SIMPLE_TEST( ctkDynamicSpacerTest1 )
 SIMPLE_TEST( ctkDynamicSpacerTest1 )
 SIMPLE_TEST( ctkDynamicSpacerTest2 )
 SIMPLE_TEST( ctkDynamicSpacerTest2 )
 SIMPLE_TEST( ctkErrorLogWidgetTest1 )
 SIMPLE_TEST( ctkErrorLogWidgetTest1 )

+ 14 - 13
Libs/Widgets/Testing/Cpp/ctkDoubleSpinBoxTest1.cpp

@@ -79,20 +79,20 @@ int ctkDoubleSpinBoxTest1(int argc, char * argv [] )
 
 
   qDebug()<<"SingleStep:";
   qDebug()<<"SingleStep:";
   spinBox.setSingleStep(0.01);
   spinBox.setSingleStep(0.01);
-  if (spinBox.singleStep() != 0.01)
+  if (!qFuzzyCompare(spinBox.singleStep(), 0.01))
     {
     {
     qDebug()<<"singleStep does not correspond. Got: "<<spinBox.singleStep();
     qDebug()<<"singleStep does not correspond. Got: "<<spinBox.singleStep();
     return EXIT_FAILURE;
     return EXIT_FAILURE;
     }
     }
   spinBox.stepUp();
   spinBox.stepUp();
-  if (spinBox.value() != 8.70)
+  if (!qFuzzyCompare(spinBox.value(), 8.70))
     {
     {
     qDebug()<<"Value does not correspond. Got: "<<spinBox.value();
     qDebug()<<"Value does not correspond. Got: "<<spinBox.value();
     return EXIT_FAILURE;
     return EXIT_FAILURE;
     }
     }
   spinBox.setSingleStep(1.0);
   spinBox.setSingleStep(1.0);
   spinBox.stepDown();
   spinBox.stepDown();
-  if (spinBox.displayedValue() != 7.7)
+  if (!qFuzzyCompare(spinBox.displayedValue(), 7.7))
     {
     {
     qDebug()<<"Value does not correspond. Got: "<<spinBox.value();
     qDebug()<<"Value does not correspond. Got: "<<spinBox.value();
     return EXIT_FAILURE;
     return EXIT_FAILURE;
@@ -100,12 +100,12 @@ int ctkDoubleSpinBoxTest1(int argc, char * argv [] )
 
 
   qDebug()<<"Minimum:";
   qDebug()<<"Minimum:";
   spinBox.setMinimum(9.1);
   spinBox.setMinimum(9.1);
-  if (spinBox.minimum() != 9.1)
+  if (!qFuzzyCompare(spinBox.minimum(), 9.1))
     {
     {
     qDebug()<<"minimum does not correspond. Got: "<<spinBox.minimum();
     qDebug()<<"minimum does not correspond. Got: "<<spinBox.minimum();
     return EXIT_FAILURE;
     return EXIT_FAILURE;
     }
     }
-  if (spinBox.value() != 9.1)
+  if (!qFuzzyCompare(spinBox.value(), 9.1))
     {
     {
     qDebug()<<"Value does not correspond. Got: "<<spinBox.value();
     qDebug()<<"Value does not correspond. Got: "<<spinBox.value();
     return EXIT_FAILURE;
     return EXIT_FAILURE;
@@ -114,12 +114,12 @@ int ctkDoubleSpinBoxTest1(int argc, char * argv [] )
   qDebug()<<"Maximum:";
   qDebug()<<"Maximum:";
   spinBox.setValue(18.34);
   spinBox.setValue(18.34);
   spinBox.setMaximum(15);
   spinBox.setMaximum(15);
-  if (spinBox.maximum() != 15)
+  if (!qFuzzyCompare(spinBox.maximum(), 15.0))
     {
     {
     qDebug()<<"maximum does not correspond. Got: "<<spinBox.maximum();
     qDebug()<<"maximum does not correspond. Got: "<<spinBox.maximum();
     return EXIT_FAILURE;
     return EXIT_FAILURE;
     }
     }
-  if (spinBox.value() != 15)
+  if (!qFuzzyCompare(spinBox.value(), 15.0))
     {
     {
     qDebug()<<"Value does not correspond. Got: "<<spinBox.value();
     qDebug()<<"Value does not correspond. Got: "<<spinBox.value();
     return EXIT_FAILURE;
     return EXIT_FAILURE;
@@ -127,7 +127,8 @@ int ctkDoubleSpinBoxTest1(int argc, char * argv [] )
 
 
   qDebug()<<"Range:";
   qDebug()<<"Range:";
   spinBox.setRange(-3.6, 42);
   spinBox.setRange(-3.6, 42);
-  if (spinBox.maximum() != 42 || spinBox.minimum() != -3.6)
+  if (!qFuzzyCompare(spinBox.maximum(), 42)
+    || !qFuzzyCompare(spinBox.minimum(), -3.6))
     {
     {
     qDebug()<<"Range does not correspond. Got: "
     qDebug()<<"Range does not correspond. Got: "
       <<spinBox.minimum()<<" : "<<spinBox.minimum();
       <<spinBox.minimum()<<" : "<<spinBox.minimum();
@@ -150,7 +151,7 @@ int ctkDoubleSpinBoxTest1(int argc, char * argv [] )
   qDebug()<<"Round:";
   qDebug()<<"Round:";
   spinBox.setDecimals(3);
   spinBox.setDecimals(3);
   double roundedValue = spinBox.round(6.67899156);
   double roundedValue = spinBox.round(6.67899156);
-  if (roundedValue != 6.679)
+  if (!qFuzzyCompare(roundedValue, 6.679))
     {
     {
     qDebug()<<"Round does not correspond. Got: "<<roundedValue;
     qDebug()<<"Round does not correspond. Got: "<<roundedValue;
     return EXIT_FAILURE;
     return EXIT_FAILURE;
@@ -160,14 +161,14 @@ int ctkDoubleSpinBoxTest1(int argc, char * argv [] )
 
 
   qDebug()<<"SetValue:";
   qDebug()<<"SetValue:";
   spinBox.setValue(28.36);
   spinBox.setValue(28.36);
-  if (spinBox.value() != 28.36)
+  if (!qFuzzyCompare(spinBox.value(), 28.36))
     {
     {
     qDebug()<<"setValue does not correspond. Got: "<<spinBox.value() ;
     qDebug()<<"setValue does not correspond. Got: "<<spinBox.value() ;
     return EXIT_FAILURE;
     return EXIT_FAILURE;
     }
     }
 
 
   spinBox.setValue(28.366917352);
   spinBox.setValue(28.366917352);
-  if (spinBox.value() != 28.367)
+  if (!qFuzzyCompare(spinBox.value(), 28.367))
     {
     {
     qDebug()<<"setValue does not correspond. Got: "<<spinBox.value() ;
     qDebug()<<"setValue does not correspond. Got: "<<spinBox.value() ;
     return EXIT_FAILURE;
     return EXIT_FAILURE;
@@ -175,14 +176,14 @@ int ctkDoubleSpinBoxTest1(int argc, char * argv [] )
 
 
   qDebug()<<"SetValueIfDifferent:";
   qDebug()<<"SetValueIfDifferent:";
   spinBox.setValueIfDifferent(33.312587);
   spinBox.setValueIfDifferent(33.312587);
-  if (spinBox.value() != 33.313)
+  if (!qFuzzyCompare(spinBox.value(), 33.313))
     {
     {
     qDebug()<<"setValueIfDifferent does not correspond. Got: "<<spinBox.value() ;
     qDebug()<<"setValueIfDifferent does not correspond. Got: "<<spinBox.value() ;
     return EXIT_FAILURE;
     return EXIT_FAILURE;
     }
     }
 
 
   spinBox.setValueIfDifferent(33.312960134);
   spinBox.setValueIfDifferent(33.312960134);
-  if (spinBox.value() != 33.313)
+  if (!qFuzzyCompare(spinBox.value(), 33.313))
     {
     {
     qDebug()<<"setValueIfDifferent does not correspond. Got: "<<spinBox.value() ;
     qDebug()<<"setValueIfDifferent does not correspond. Got: "<<spinBox.value() ;
     return EXIT_FAILURE;
     return EXIT_FAILURE;

+ 337 - 0
Libs/Widgets/Testing/Cpp/ctkDoubleSpinBoxValueProxyTest.cpp

@@ -0,0 +1,337 @@
+/*=========================================================================
+
+  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 <QDebug>
+#include <QString>
+
+// CTK includes
+#include "ctkDoubleSpinBox.h"
+#include "ctkLinearValueProxy.h"
+#include "ctkTest.h"
+#include "ctkValueProxy.h"
+
+// STD includes
+#include <limits>
+
+namespace
+{
+
+//-----------------------------------------------------------------------------
+void getSpyReport(QSignalSpy& spy, double expectedValue)
+{
+  QCOMPARE(spy.count(), 1);
+
+  QList<QVariant> arguments = spy.takeFirst(); // take the first signal
+  ctkTest::COMPARE(arguments.at(0).toDouble(), expectedValue);
+}
+
+//-----------------------------------------------------------------------------
+void getSpyReport(QSignalSpy& spy, QString expectedValue)
+{
+  QCOMPARE(spy.count(), 1);
+
+  QList<QVariant> arguments = spy.takeFirst(); // take the first signal
+  QCOMPARE(arguments.at(0).toString(), expectedValue);
+}
+
+} // end namespace
+
+// ----------------------------------------------------------------------------
+class ctkDoubleSpinBoxValueProxyTester: public QObject
+{
+  Q_OBJECT
+private slots:
+
+  void testSetValue();
+  void testSetValue_data();
+
+  void testSetDisplayedValue();
+  void testSetDisplayedValue_data();
+};
+
+//-----------------------------------------------------------------------------
+void ctkDoubleSpinBoxValueProxyTester::testSetValue()
+{
+  // Setup
+  ctkDoubleSpinBox spinBox;
+  spinBox.setMinimum(-200);
+  spinBox.setMaximum(200);
+  spinBox.setValue(-32.6);
+
+  QFETCH(double, coefficient);
+  QFETCH(double, offset);
+  ctkLinearValueProxy proxy;
+  proxy.setCoefficient(coefficient);
+  proxy.setOffset(offset);
+  spinBox.setValueProxy(&proxy);
+
+  // Spies
+  QSignalSpy valueSpy(&spinBox, SIGNAL(valueChanged(double)));
+  QSignalSpy valueStringSpy(&spinBox, SIGNAL(valueChanged(QString)));
+
+  // Test
+  QFETCH(double, value);
+  spinBox.setValue(value);
+
+  QFETCH(double, expectedValue);
+  QFETCH(QString, expectedStringValue);
+  getSpyReport(valueSpy, expectedValue);
+  getSpyReport(valueStringSpy, expectedStringValue);
+  ctkTest::COMPARE(spinBox.value(), expectedValue);
+}
+
+//-----------------------------------------------------------------------------
+void ctkDoubleSpinBoxValueProxyTester::testSetValue_data()
+{
+  QTest::addColumn<double>("coefficient");
+  QTest::addColumn<double>("offset");
+  QTest::addColumn<double>("value");
+  QTest::addColumn<double>("expectedValue");
+  QTest::addColumn<QString>("expectedStringValue");
+
+  //---------------------------------------------------------------------------
+  // Offset
+  QTest::newRow("Offset only") << 1.0 << 42.19176 << 0.1 << 0.1 << "0.10";
+
+  QTest::newRow("Offset only: less than min")
+    << 1.0 << 42.19176 << -510.0 << -242.19 << "-242.19";
+  QTest::newRow("Offset only: less than min but ok with offset")
+    << 1.0 << 42.19176 << -230.0 << -230.0 << "-230.00";
+  QTest::newRow("Offset only: less than min with offset")
+    << 1.0 << -42.1976 << -190.0 << -157.8 << "-157.80";
+
+  QTest::newRow("Offset only: more than max with offset")
+    << 1.0 << 42.19176 << 160.0 << 157.81 << "157.81";
+  QTest::newRow("Offset only: more than max")
+    << 1.0 << -42.1976 << 65010.0 << 242.2 << "242.20";
+  QTest::newRow("Offset only: less than max but ok with offset")
+    << 1.0 << -42.1976 << 229.1 << 229.1 << "229.10";
+
+  QTest::newRow("Offset only: max")
+    << 1.0 << 42.19176 << std::numeric_limits<double>::max()
+    << 157.81 << "157.81";
+  QTest::newRow("Offset only:  min")
+    << 1.0 << 42.19176 << -std::numeric_limits<double>::max()
+    << -242.19 << "-242.19";
+  QTest::newRow("Offset only: infinity")
+    << 1.0 << 42.19176 << std::numeric_limits<double>::infinity()
+    << 157.81 << "157.81";
+  QTest::newRow("Offset only:  - infinity")
+    << 1.0 << 42.19176 << -std::numeric_limits<double>::infinity()
+    << -242.19 << "-242.19";
+  QTest::newRow("Offset only: Nan")
+    << 1.0 << 42.19176 << std::numeric_limits<double>::quiet_NaN()
+    << 157.81 << "157.81";
+
+  // coeff // offset // value // expectedValue // expectedStringValue
+  //---------------------------------------------------------------------------
+  // Coefficient
+  QTest::newRow("Coeff only") << 5.0 << 0.0 << 0.1 << 0.1 << "0.10";
+
+  QTest::newRow("Coeff only: less than min")
+    << 5.0 << 0.0 << -510.0 << -40.0 << "-40.00";
+  QTest::newRow("Coeff only: less than min but ok with coeff")
+    << 0.5 << 0.0 << -230.0 << -230.0 << "-230.00";
+  QTest::newRow("Coeff only: less than min with coeff")
+    << 5.0 << 0.0 << -190.0 << -40.0 << "-40.00";
+
+  QTest::newRow("Coeff only: more than max with coeff")
+    << 5.0 << 0.0 << 160.0 << 40.0 << "40.00";
+  QTest::newRow("Coeff only: more than max")
+    << 5.0 << 0.0 << 65010.0 << 40.0 << "40.00";
+  QTest::newRow("Offset only: less than max but ok with coeff")
+    << 0.5 << 0.0 << 229.2 << 229.2 << "229.20";
+
+  QTest::newRow("Coeff only: max")
+    << 5.0 << 0.0 << std::numeric_limits<double>::max() << 40.0 << "40.00";
+  QTest::newRow("Coeff only:  min")
+    << 5.0 << 0.0 << -std::numeric_limits<double>::max() << -40.0 << "-40.00";
+  QTest::newRow("Coeff only: infinity")
+    << 5.0 << 0.0 << std::numeric_limits<double>::infinity()
+    << 40.0 << "40.00";
+  QTest::newRow("Coeff only:  - infinity")
+    << 5.0 << 0.0 << -std::numeric_limits<double>::infinity()
+    << -40.0 << "-40.00";
+  QTest::newRow("Coeff only: Nan")
+    << 5.0 << 0.0 << std::numeric_limits<double>::quiet_NaN()
+    << 40.0 << "40.00";
+
+
+  // coeff // offset // value // expectedValue // expectedStringValue
+  //---------------------------------------------------------------------------
+  // Linear
+  QTest::newRow("Linear") << 5.0 << 0.0 << 0.1 << 0.1 << "0.10";
+
+  QTest::newRow("Linear: less than min")
+    << 5.0 << 12.0 << -510.0 << -42.4 << "-42.40";
+  QTest::newRow("Linear: less than min but ok with function")
+    << 0.5 << 12.0 << -230.0 << -230.0 << "-230.00";
+  QTest::newRow("Linear: less than min with function")
+    << 5.0 << 12.0 << -61.5 << -42.4 << "-42.40";
+
+  QTest::newRow("Linear: more than max with function")
+    << 5.0 << 12.0 << 160.0 << 37.6 << "37.60";
+  QTest::newRow("Linear: more than max")
+    << 5.0 << 12.0 << 65010.0 << 37.6 << "37.60";
+  QTest::newRow("Offset only: less than max but ok with function")
+    << 0.5 << 12.0 << 229.2 << 229.2 << "229.20";
+
+  QTest::newRow("Linear: max")
+    << 5.0 << 12.0 << std::numeric_limits<double>::max() << 37.6 << "37.60";
+  QTest::newRow("Linear:  min")
+    << 5.0 << 12.0 << -std::numeric_limits<double>::max() << -42.4 << "-42.40";
+  QTest::newRow("Linear: infinity")
+    << 5.0 << 12.0 << std::numeric_limits<double>::infinity()
+    << 37.6 << "37.60";
+  QTest::newRow("Linear:  - infinity")
+    << 5.0 << 12.0 << -std::numeric_limits<double>::infinity()
+    << -42.4 << "-42.40";
+  QTest::newRow("Linear: Nan")
+    << 5.0 << 12.0 << std::numeric_limits<double>::quiet_NaN()
+    << 37.6 << "37.60";
+}
+
+//-----------------------------------------------------------------------------
+void ctkDoubleSpinBoxValueProxyTester::testSetDisplayedValue()
+{
+  // Setup
+  ctkDoubleSpinBox spinBox;
+  spinBox.setMinimum(-200);
+  spinBox.setMaximum(200);
+  spinBox.setValue(-32.6);
+
+  QFETCH(double, coefficient);
+  QFETCH(double, offset);
+  ctkLinearValueProxy proxy;
+  proxy.setCoefficient(coefficient);
+  proxy.setOffset(offset);
+  spinBox.setValueProxy(&proxy);
+
+  // Spies
+  QSignalSpy valueSpy(&spinBox, SIGNAL(valueChanged(double)));
+  QSignalSpy valueStringSpy(&spinBox, SIGNAL(valueChanged(QString)));
+
+  // Test
+  QFETCH(double, displayValue);
+  spinBox.setDisplayedValue(displayValue);
+
+  QFETCH(double, expectedValue);
+  QFETCH(QString, expectedStringValue);
+  QFETCH(double, expectedDisplayValue);
+  getSpyReport(valueSpy, expectedValue);
+  getSpyReport(valueStringSpy, expectedStringValue);
+  ctkTest::COMPARE(spinBox.value(), expectedValue);
+  QCOMPARE(spinBox.displayedValue(), expectedDisplayValue);
+}
+
+//-----------------------------------------------------------------------------
+void ctkDoubleSpinBoxValueProxyTester::testSetDisplayedValue_data()
+{
+  QTest::addColumn<double>("coefficient");
+  QTest::addColumn<double>("offset");
+  QTest::addColumn<double>("displayValue");
+  QTest::addColumn<double>("expectedValue");
+  QTest::addColumn<QString>("expectedStringValue");
+  QTest::addColumn<double>("expectedDisplayValue");
+
+  //---------------------------------------------------------------------------
+  // Offset
+  QTest::newRow("Offset only")
+    << 1.0 << 42.19176 << 0.1 << -42.09 << "-42.09" << 0.1;
+
+  QTest::newRow("Offset only: less than min")
+    << 1.0 << 42.19176 << -510.0 << -242.19 << "-242.19" << -200.0;
+  QTest::newRow("Offset only: more than max")
+    << 1.0 << -42.1976 << 65010.0 << 242.2 << "242.20" << 200.0;
+
+  QTest::newRow("Offset only: max")
+    << 1.0 << 42.19176 << std::numeric_limits<double>::max()
+    << 157.81 << "157.81" << 200.0;
+  QTest::newRow("Offset only:  min")
+    << 1.0 << 42.19176 << -std::numeric_limits<double>::max()
+    << -242.19 << "-242.19" << -200.0;
+  QTest::newRow("Offset only: infinity")
+    << 1.0 << 42.19176 << std::numeric_limits<double>::infinity()
+    << 157.81 << "157.81" << 200.0;
+  QTest::newRow("Offset only:  - infinity")
+    << 1.0 << 42.19176 << -std::numeric_limits<double>::infinity()
+    << -242.19 << "-242.19" << -200.0;
+  QTest::newRow("Offset only: Nan")
+    << 1.0 << 42.19176 << std::numeric_limits<double>::quiet_NaN()
+    << 157.81 << "157.81" << 200.0;
+
+  //---------------------------------------------------------------------------
+  // Coefficient
+  QTest::newRow("Coeff only")
+    << 5.0 << 0.0 << 5.0 << 1.0 << "1.00" << 5.0;
+
+  QTest::newRow("Coeff only: less than min")
+    << 5.0 << 0.0 << -1010.0 << -40.0 << "-40.00" << -200.0;
+  QTest::newRow("Coeff only: more than max")
+    << 5.0 << 0.0 << 65010.0 << 40.0 << "40.00" << 200.0;
+
+  QTest::newRow("Coeff only: max")
+    << 5.0 << 0.0 << std::numeric_limits<double>::max()
+    << 40.0 << "40.00" << 200.0;
+  QTest::newRow("Coeff only:  min")
+    << 5.0 << 0.0 << -std::numeric_limits<double>::max()
+    << -40.0 << "-40.00" << -200.0;
+  QTest::newRow("Coeff only: infinity")
+    << 5.0 << 0.0 << std::numeric_limits<double>::infinity()
+    << 40.0 << "40.00" << 200.0;
+  QTest::newRow("Coeff only:  - infinity")
+    << 5.0 << 0.0 << -std::numeric_limits<double>::infinity()
+    << -40.0 << "-40.00" << -200.0;
+  QTest::newRow("Coeff only: Nan")
+    << 5.0 << 0.0 << std::numeric_limits<double>::quiet_NaN()
+    << 40.0 << "40.00" << 200.0;
+
+  //---------------------------------------------------------------------------
+  // Linear
+  QTest::newRow("Linear") << 5.0 << 12.0 << 42.0 << 6.0 << "6.00" << 42.0;
+
+  QTest::newRow("Linear: less than min")
+    << 5.0 << 12.0 << -5010.0 << -42.4 << "-42.40" << -200.0;
+
+  QTest::newRow("Linear: more than max")
+    << 5.0 << 12.0 << 65010.0 << 37.6 << "37.60" << 200.0;
+
+  QTest::newRow("Linear: max")
+    << 5.0 << 12.0 << std::numeric_limits<double>::max()
+    << 37.6 << "37.60" << 200.0;
+  QTest::newRow("Linear:  min")
+    << 5.0 << 12.0 << -std::numeric_limits<double>::max()
+    << -42.4 << "-42.40" << -200.0;
+  QTest::newRow("Linear: infinity")
+    << 5.0 << 12.0 << std::numeric_limits<double>::infinity()
+    << 37.6 << "37.60" << 200.0;
+  QTest::newRow("Linear:  - infinity")
+    << 5.0 << 12.0 << -std::numeric_limits<double>::infinity()
+    << -42.4 << "-42.40" << -200.0;
+  QTest::newRow("Linear: Nan")
+    << 5.0 << 12.0 << std::numeric_limits<double>::quiet_NaN()
+    << 37.6 << "37.60" << 200.0;
+}
+
+// ----------------------------------------------------------------------------
+CTK_TEST_MAIN(ctkDoubleSpinBoxValueProxyTest)
+#include "moc_ctkDoubleSpinBoxValueProxyTest.cpp"

+ 66 - 14
Libs/Widgets/ctkDoubleSpinBox.cpp

@@ -21,10 +21,9 @@
 // CTK includes
 // CTK includes
 #include "ctkDoubleSpinBox_p.h"
 #include "ctkDoubleSpinBox_p.h"
 #include "ctkUtils.h"
 #include "ctkUtils.h"
+#include "ctkValueProxy.h"
 #include "ctkPimpl.h"
 #include "ctkPimpl.h"
 
 
-#include <QDebug>
-
 // Qt includes
 // Qt includes
 #include <QEvent>
 #include <QEvent>
 #include <QHBoxLayout>
 #include <QHBoxLayout>
@@ -190,9 +189,7 @@ void ctkDoubleSpinBoxPrivate::init()
   lineEdit->setObjectName(QLatin1String("qt_spinbox_lineedit"));
   lineEdit->setObjectName(QLatin1String("qt_spinbox_lineedit"));
 
 
   QObject::connect(this->SpinBox, SIGNAL(valueChanged(double)),
   QObject::connect(this->SpinBox, SIGNAL(valueChanged(double)),
-    q, SIGNAL(valueChanged(double)));
-  QObject::connect(this->SpinBox, SIGNAL(valueChanged(const QString&)),
-    q, SIGNAL(valueChanged(const QString &)));
+    this, SLOT(onValueChanged()));
   QObject::connect(this->SpinBox, SIGNAL(editingFinished()),
   QObject::connect(this->SpinBox, SIGNAL(editingFinished()),
     q, SIGNAL(editingFinished()));
     q, SIGNAL(editingFinished()));
 
 
@@ -655,6 +652,16 @@ end:
     return num;
     return num;
 }
 }
 */
 */
+
+//-----------------------------------------------------------------------------
+void ctkDoubleSpinBoxPrivate::onValueChanged()
+{
+  Q_Q(ctkDoubleSpinBox);
+  double value = q->value();
+  emit q->valueChanged(value);
+  emit q->valueChanged(QString::number(value, 'f', this->SpinBox->decimals()));
+}
+
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
 // ctkDoubleSpinBox
 // ctkDoubleSpinBox
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
@@ -680,13 +687,26 @@ ctkDoubleSpinBox::ctkDoubleSpinBox(ctkDoubleSpinBox::SetMode mode, QWidget* newP
 double ctkDoubleSpinBox::value() const
 double ctkDoubleSpinBox::value() const
 {
 {
   Q_D(const ctkDoubleSpinBox);
   Q_D(const ctkDoubleSpinBox);
-  return d->SpinBox->value();
+  double val = d->SpinBox->value();
+  if (d->Proxy)
+    {
+    val = this->round(d->Proxy.data()->valueFromProxyValue(val));
+    }
+  return val;
 }
 }
 
 
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
 double ctkDoubleSpinBox::displayedValue() const
 double ctkDoubleSpinBox::displayedValue() const
 {
 {
-  return this->round(this->value());
+  Q_D(const ctkDoubleSpinBox);
+  return d->SpinBox->value();
+}
+
+//----------------------------------------------------------------------------
+void ctkDoubleSpinBox::setDisplayedValue(double value)
+{
+  Q_D(ctkDoubleSpinBox);
+  d->SpinBox->setValue(value);
 }
 }
 
 
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
@@ -926,18 +946,27 @@ void ctkDoubleSpinBox::setValueIfDifferent(double newValue)
 {
 {
   Q_D(ctkDoubleSpinBox);
   Q_D(ctkDoubleSpinBox);
   bool set = false;
   bool set = false;
+  double proxyValue = newValue;
+  if (d->Proxy)
+    {
+    proxyValue = d->Proxy.data()->proxyValueFromValue(proxyValue);
+    }
+
   if (d->DOption & ctkDoubleSpinBox::DecimalsByValue)
   if (d->DOption & ctkDoubleSpinBox::DecimalsByValue)
     {
     {
-    int newValueDecimals = ctk::significantDecimals(newValue, d->DefaultDecimals);
-    set = this->value() != d->round(newValue, newValueDecimals)
+    int newValueDecimals =
+      ctk::significantDecimals(proxyValue, d->DefaultDecimals);
+    set = this->value() != d->round(proxyValue, newValueDecimals)
       || d->SpinBox->decimals() != newValueDecimals;
       || d->SpinBox->decimals() != newValueDecimals;
     }
     }
   else
   else
     {
     {
-    set = !d->compare(this->value(), newValue);
+    set = !d->compare(this->value(), proxyValue);
     }
     }
   if (set)
   if (set)
     {
     {
+    // Pass newValue and not proxyValue as setValueAlways also computes the
+    // proxyValue from the given value
     this->setValueAlways(newValue);
     this->setValueAlways(newValue);
     }
     }
 }
 }
@@ -946,14 +975,18 @@ void ctkDoubleSpinBox::setValueIfDifferent(double newValue)
 void ctkDoubleSpinBox::setValueAlways(double value)
 void ctkDoubleSpinBox::setValueAlways(double value)
 {
 {
   Q_D(ctkDoubleSpinBox);
   Q_D(ctkDoubleSpinBox);
-  if (d->DOption & ctkDoubleSpinBox::DecimalsByValue)
+  if (d->Proxy)
     {
     {
-    d->setValue(value, ctk::significantDecimals(value, d->DefaultDecimals));
+    value = d->Proxy.data()->proxyValueFromValue(value);
     }
     }
-  else
+
+  int decimals = -1;
+  if (d->DOption & ctkDoubleSpinBox::DecimalsByValue)
     {
     {
-    d->setValue(value);
+    decimals = ctk::significantDecimals(value, d->DefaultDecimals);
     }
     }
+
+  d->setValue(value, decimals);
 }
 }
 
 
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
@@ -1019,6 +1052,25 @@ bool ctkDoubleSpinBox::invertedControls() const
   return d->InvertedControls;
   return d->InvertedControls;
 }
 }
 
 
+//----------------------------------------------------------------------------
+void ctkDoubleSpinBox::setValueProxy(ctkValueProxy* proxy)
+{
+  Q_D(ctkDoubleSpinBox);
+  if (d->Proxy.data() == proxy)
+    {
+    return;
+    }
+
+  d->Proxy = proxy;
+}
+
+//----------------------------------------------------------------------------
+ctkValueProxy* ctkDoubleSpinBox::valueProxy() const
+{
+  Q_D(const ctkDoubleSpinBox);
+  return d->Proxy.data();
+}
+
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
 void ctkDoubleSpinBox::keyPressEvent(QKeyEvent* event)
 void ctkDoubleSpinBox::keyPressEvent(QKeyEvent* event)
 {
 {

+ 20 - 1
Libs/Widgets/ctkDoubleSpinBox.h

@@ -36,6 +36,7 @@ class QObject;
 #include "ctkWidgetsExport.h"
 #include "ctkWidgetsExport.h"
 
 
 class ctkDoubleSpinBoxPrivate;
 class ctkDoubleSpinBoxPrivate;
+class ctkValueProxy;
 
 
 /// \brief Custom SpinBox
 /// \brief Custom SpinBox
 /// The ctkDoubleSpinBox internaly uses a QDoubleSpinBox while it retain controls
 /// The ctkDoubleSpinBox internaly uses a QDoubleSpinBox while it retain controls
@@ -157,9 +158,16 @@ public:
   double value() const;
   double value() const;
 
 
   /// Get the spinbox current displayed value
   /// Get the spinbox current displayed value
-  /// \sa value(), cleanText()
+  /// \sa value(), cleanText(), setValue(), displayedValue()
   double displayedValue() const;
   double displayedValue() const;
 
 
+  /// Set the displayed value if there is no proxy installed.
+  /// If there is a proxy installed, then it allows to modify the proxy value.
+  /// If there is no value proxy installed, then it's just a setter to the
+  /// value property.
+  /// \sa displayedValue(), setValue(), value(), setValueProxy()
+  void setDisplayedValue(double displayValue);
+
   /// Get the spinbox current text. This includes any prefix or suffix.
   /// Get the spinbox current text. This includes any prefix or suffix.
   /// \sa prefix(), suffix()
   /// \sa prefix(), suffix()
   QString text() const;
   QString text() const;
@@ -239,6 +247,17 @@ public:
   void setInvertedControls(bool invertedControls);
   void setInvertedControls(bool invertedControls);
   bool invertedControls() const;
   bool invertedControls() const;
 
 
+  /// Install or remove a value proxy filter. The value proxy decouples the
+  /// displayed value from the value retrieved by the value property.
+  /// For example, the value proxy can allow one to display celsius in the
+  /// spinbox while the value retrieved from the value property and signals
+  /// are in farenheit.
+  /// To remove the proxy, simply install a new empty proxy. The proxy
+  /// installation/removal is silent.
+  /// \sa installValueProxy(), valueProxy()
+  void setValueProxy(ctkValueProxy* proxy);
+  ctkValueProxy* valueProxy() const;
+
 public Q_SLOTS:
 public Q_SLOTS:
   /// Set the value of the spinbox following the current mode.
   /// Set the value of the spinbox following the current mode.
   /// \sa setMode(), value(), setValueIfDifferent(), setValueAlways()
   /// \sa setMode(), value(), setValueIfDifferent(), setValueAlways()

+ 8 - 1
Libs/Widgets/ctkDoubleSpinBox_p.h

@@ -23,7 +23,9 @@
 
 
 // Qt includes
 // Qt includes
 #include <QDoubleSpinBox>
 #include <QDoubleSpinBox>
+#include <QWeakPointer>
 class ctkDoubleSpinBoxPrivate;
 class ctkDoubleSpinBoxPrivate;
+class ctkValueProxy;
 
 
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
 class ctkQDoubleSpinBox: public QDoubleSpinBox
 class ctkQDoubleSpinBox: public QDoubleSpinBox
@@ -91,6 +93,8 @@ public:
   mutable QValidator::State CachedState;
   mutable QValidator::State CachedState;
   mutable int CachedDecimals;
   mutable int CachedDecimals;
 
 
+  QWeakPointer<ctkValueProxy> Proxy;
+
   void init();
   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;
   bool compare(double x1, double x2) const;
@@ -117,7 +121,10 @@ public:
   double validateAndInterpret(QString &input, int &pos,
   double validateAndInterpret(QString &input, int &pos,
                               QValidator::State &state, int &decimals) const;
                               QValidator::State &state, int &decimals) const;
 
 
+  void connectSpinBoxValueChanged();
+  void disconnectSpinBoxValueChanged();
+
 public Q_SLOTS:
 public Q_SLOTS:
   void editorTextChanged(const QString& text);
   void editorTextChanged(const QString& text);
-
+  void onValueChanged();
 };
 };