From fb1faad89f6e24a3d688f19cac5418aa92b21480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 29 Jul 2025 10:36:38 -0400 Subject: [PATCH 1/5] Add an option to enable or not the 'precise' mode --- qtapputils/widgets/range.py | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/qtapputils/widgets/range.py b/qtapputils/widgets/range.py index ce4122f..26b9ae2 100644 --- a/qtapputils/widgets/range.py +++ b/qtapputils/widgets/range.py @@ -73,40 +73,56 @@ def textFromValue(self, value: float): class PreciseSpinBox(DoubleSpinBox): """ - QDoubleSpinBox that preserves full float64 precision when values are set - programmatically, while still displaying only as many decimals as + QDoubleSpinBox that optionally preserves full float64 precision when values + are set programmatically, while still displaying only as many decimals as configured. + + Parameters + ---------- + precise : bool, optional + If True (default), the spinbox preserves full float64 precision for + programmatically set values. If False, it behaves like a standard + QDoubleSpinBox without storing an internal precise value. """ sig_value_changed = Signal(float) - def __init__(self, *args, **kwargs): + def __init__(self, *args, precise: bool = True, **kwargs): super().__init__(*args, **kwargs) self.setKeyboardTracking(False) + self._precise = precise self._true_value = super().value() self.valueChanged.connect(self._handle_user_change) def _handle_user_change(self, value: float): """Update internal value when the user edits the widget.""" - self._true_value = value + if self._precise: + self._true_value = value self.sig_value_changed.emit(value) # ---- QDoubleSpinBox Interface def value(self) -> float: - """Return the precise stored value.""" - return self._true_value + """ + Return the precise stored value if enabled, + otherwise the standard UI value. + """ + if self._precise: + return self._true_value + return super().value() def setValue(self, new_value: float): """Set the value without losing precision due to display rounding.""" + old_value = self.value() new_value = max(min(new_value, self.maximum()), self.minimum()) - if new_value == self._true_value: - return - self._true_value = float(new_value) self.blockSignals(True) super().setValue(new_value) self.blockSignals(False) - self.sig_value_changed.emit(self._true_value) + if self._precise: + self._true_value = float(new_value) + + if old_value != self.value(): + self.sig_value_changed.emit(self.value()) class RangeSpinBox(DoubleSpinBox): From b4409f08b36cc227ff15e59d3429aa78a16b0a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 29 Jul 2025 10:36:53 -0400 Subject: [PATCH 2/5] Update test_range.py --- qtapputils/widgets/tests/test_range.py | 35 +++++++++++++++++--------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/qtapputils/widgets/tests/test_range.py b/qtapputils/widgets/tests/test_range.py index 22940c6..a6dccc7 100644 --- a/qtapputils/widgets/tests/test_range.py +++ b/qtapputils/widgets/tests/test_range.py @@ -63,12 +63,13 @@ def range_widget(qtbot): # ============================================================================= # Tests # ============================================================================= -def test_precise_dspinbox(qtbot): +@pytest.mark.parametrize("precise_mode", [True, False]) +def test_precise_dspinbox(qtbot, precise_mode): """ - Test that PreciseSpinBox preserves full precision internally - while displaying rounded values. + Test that PreciseSpinBox behaves correctly in both precise + and normal modes. """ - spin = PreciseSpinBox() + spin = PreciseSpinBox(precise=precise_mode) spin.setDecimals(2) spin.setRange(-1e9, 1e9) qtbot.addWidget(spin) @@ -79,19 +80,26 @@ def test_precise_dspinbox(qtbot): spin.sig_value_changed.connect(lambda v: emitted_values.append(v)) # Set a value with high precision - precise_value = 3.141592653589793 - spin.setValue(precise_value) + spin.setValue(3.141592653589793) - # Internal value should match the full float64 precision. - assert spin.value() == precise_value - assert emitted_values == [precise_value] + # Internal value should match the full float64 precision in + # precise mode. + if precise_mode: + assert spin.value() == 3.141592653589793 + assert emitted_values == [3.141592653589793] + else: + assert spin.value() == 3.14 + assert emitted_values == [3.14] # The displayed value in the UI should be rounded to 2 decimals assert spin.text() == "3.14" # Setting the same value again should NOT emit the signal - spin.setValue(precise_value) - assert emitted_values == [precise_value] + spin.setValue(3.141592653589793) + if precise_mode: + assert emitted_values == [3.141592653589793] + else: + assert emitted_values == [3.14] # Changing the value from the GUI should update the internal value. spin.clear() @@ -99,7 +107,10 @@ def test_precise_dspinbox(qtbot): qtbot.keyClick(spin, Qt.Key_Enter) assert spin.value() == 4.12 - assert emitted_values == [precise_value, 4.12] + if precise_mode: + assert emitted_values == [3.141592653589793, 4.12] + else: + assert emitted_values == [3.14, 4.12] def test_range_spinbox(range_spinbox, qtbot): From 4e0b9bf7ad9bc28ceb873174490c81d67d1cfcea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 29 Jul 2025 11:33:36 -0400 Subject: [PATCH 3/5] Update test_range.py --- qtapputils/widgets/tests/test_range.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/qtapputils/widgets/tests/test_range.py b/qtapputils/widgets/tests/test_range.py index a6dccc7..3c02c03 100644 --- a/qtapputils/widgets/tests/test_range.py +++ b/qtapputils/widgets/tests/test_range.py @@ -117,35 +117,44 @@ def test_range_spinbox(range_spinbox, qtbot): """ Test that the RangeSpinBox is working as expected. """ + # Connect a flag to check signal emission + emitted_values = [] + range_spinbox.sig_value_changed.connect(lambda v: emitted_values.append(v)) + # Test entering a value above the maximum. range_spinbox.clear() qtbot.keyClicks(range_spinbox, '120') qtbot.keyClick(range_spinbox, Qt.Key_Enter) assert range_spinbox.value() == 101 + assert emitted_values == [101] # Test entering a value below the minimum. range_spinbox.clear() qtbot.keyClicks(range_spinbox, '-12') qtbot.keyClick(range_spinbox, Qt.Key_Enter) assert range_spinbox.value() == 3 + assert emitted_values == [101, 3] # Test entering a valid value. range_spinbox.clear() qtbot.keyClicks(range_spinbox, '45.3') qtbot.keyClick(range_spinbox, Qt.Key_Enter) assert range_spinbox.value() == 45.3 + assert emitted_values == [101, 3, 45.3] # Test entering an intermediate value. range_spinbox.clear() qtbot.keyClicks(range_spinbox, '-') qtbot.keyClick(range_spinbox, Qt.Key_Enter) assert range_spinbox.value() == 45.3 + assert emitted_values == [101, 3, 45.3] # Test entering invalid values. range_spinbox.clear() qtbot.keyClicks(range_spinbox, '23..a-45') qtbot.keyClick(range_spinbox, Qt.Key_Enter) assert range_spinbox.value() == 23.45 + assert emitted_values == [101, 3, 45.3, 23.45] def test_range_widget(range_widget, qtbot): From 05d2bcb3618b357ae982dd834eea6125fb1bc5a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 29 Jul 2025 11:34:25 -0400 Subject: [PATCH 4/5] PreciseSpinBox: make setValue more robust --- qtapputils/widgets/range.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qtapputils/widgets/range.py b/qtapputils/widgets/range.py index 26b9ae2..4636dd2 100644 --- a/qtapputils/widgets/range.py +++ b/qtapputils/widgets/range.py @@ -114,9 +114,10 @@ def setValue(self, new_value: float): old_value = self.value() new_value = max(min(new_value, self.maximum()), self.minimum()) + was_blocked = self.signalsBlocked() self.blockSignals(True) super().setValue(new_value) - self.blockSignals(False) + self.blockSignals(was_blocked) if self._precise: self._true_value = float(new_value) From 2b767cbb1c767f121f4bac1807de882f26fe6048 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 29 Jul 2025 11:35:05 -0400 Subject: [PATCH 5/5] RangeSpinBox: inherit from PreciseSpinBox --- qtapputils/widgets/range.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/qtapputils/widgets/range.py b/qtapputils/widgets/range.py index 4636dd2..64bfb49 100644 --- a/qtapputils/widgets/range.py +++ b/qtapputils/widgets/range.py @@ -126,7 +126,7 @@ def setValue(self, new_value: float): self.sig_value_changed.emit(self.value()) -class RangeSpinBox(DoubleSpinBox): +class RangeSpinBox(PreciseSpinBox): """ A spinbox that allow to enter values that are lower or higher than the minimum and maximum value of the spinbox. @@ -138,9 +138,9 @@ class RangeSpinBox(DoubleSpinBox): def __init__(self, parent: QWidget = None, maximum: float = None, minimum: float = None, singlestep: float = None, - decimals: int = None, value: float = None): - super().__init__(parent) - self.setKeyboardTracking(False) + decimals: int = None, value: float = None, + precise: bool = False): + super().__init__(parent=parent, precise=precise) if minimum is not None: self.setMinimum(minimum) if maximum is not None: @@ -211,17 +211,15 @@ def __init__(self, parent: QWidget = None, maximum: float = 99.99, self.spinbox_start = RangeSpinBox( minimum=minimum, singlestep=singlestep, decimals=decimals, value=minimum) - self.spinbox_start.valueChanged.connect( - lambda: self._handle_value_changed()) - self.spinbox_start.editingFinished.connect( + + self.spinbox_start.sig_value_changed.connect( lambda: self._handle_value_changed()) self.spinbox_end = RangeSpinBox( maximum=maximum, singlestep=singlestep, decimals=decimals, value=maximum) - self.spinbox_end.valueChanged.connect( - lambda: self._handle_value_changed()) - self.spinbox_end.editingFinished.connect( + + self.spinbox_end.sig_value_changed.connect( lambda: self._handle_value_changed()) self._update_spinbox_range() @@ -241,10 +239,14 @@ def set_range(self, start: float, end: float): step = 0 if self.null_range_ok else 10**-self.decimals self._block_spinboxes_signals(True) + + # Reset the spin boxes range. self.spinbox_start.setMaximum(self.spinbox_end.maximum() - step) self.spinbox_end.setMinimum(self.spinbox_start.minimum() + step) + self.spinbox_start.setValue(start) self.spinbox_end.setValue(end) + self._block_spinboxes_signals(False) if old_start != self.start() or old_end != self.end(): @@ -284,9 +286,11 @@ def _update_spinbox_range(self): mutually exclusive """ self._block_spinboxes_signals(True) + step = 0 if self.null_range_ok else 10**-self.decimals self.spinbox_start.setMaximum(self.spinbox_end.value() - step) self.spinbox_end.setMinimum(self.spinbox_start.value() + step) + self._block_spinboxes_signals(False) def _handle_value_changed(self, silent: bool = False):