Repository navigation
Add feature to allow editing a slider's value outside of its current range - #353
jules-vanaret wants to merge 2 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #353 +/- ##
==========================================
- Coverage 84.90% 84.77% -0.14%
==========================================
Files 49 49
Lines 3928 3940 +12
==========================================
+ Hits 3335 3340 +5
- Misses 593 600 +7 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@Czaki @brisvag @psobolewskiPhD would you have input on this ? |
brisvag
left a comment
There was a problem hiding this comment.
Makes sense to me, and I like that this is settable and false by default 👍
| def setValue(self, val: Any, clamp_values: bool = True) -> None: | ||
| if clamp_values: | ||
| if val < self._min: | ||
| val = self._min | ||
| elif val > self._max: | ||
| val = self._max |
There was a problem hiding this comment.
This change is not needed right? The range should be expanded before the value is set.
There was a problem hiding this comment.
You're probably right, I put this more as a fail-safe because I did not properly take the time to see where this function was used elsewhere in the codebase 😅
tlambert03
left a comment
There was a problem hiding this comment.
thanks for the PR @jules-vanaret ... still some issues to iron out before we can include this though
| @@ -608,6 +622,12 @@ def _on_value_changed(self, v: tuple[int, ...]) -> None: | |||
|
|
|||
| def _on_slider_label_edited(self, pos: float) -> None: | |||
There was a problem hiding this comment.
thanks for this @jules-vanaret. there's one weird thing still:
if I initialize the slider with range (0,100), value (20,80), and expand on, then enter 150 into the lower handle. What I end up with is this:
as before this PR, the lower handle doesn't go past the upper handle (at 80) ... but the range of the upper handle has now been extended to 150. It's subtle... but I would expect that the range should only grow when the handle can actually end up at the typed value?
that is: expand the range only on the side the handle is actually allowed to move to.
def _on_slider_label_edited(self, pos: float) -> None:
idx = getattr(self.sender(), "_index", 0)
if self._expand_range_on_handle_edit:
min_, max_ = self._slider.minimum(), self._slider.maximum()
if idx == 0 and pos < min_:
self._slider.setRange(pos, max_)
elif idx == len(self._slider.value()) - 1 and pos > max_:
self._slider.setRange(min_, pos)
self._slider.setSliderPosition(pos, idx)| value = float(self.text()) | ||
| self.setValue(value) | ||
| self.valueEdited.emit(value) |
There was a problem hiding this comment.
i think this introduces a buggy change in behavior?
Before (main):
self.setValue(float(self.text())) # label clamps to its _min/_max
self.valueEdited.emit(self.value()) # emits the clamped value
Type 500 into a 0–100 slider: the label clamps it to 100 and emits 100.0.
After (PR):
value = float(self.text())
self.setValue(value) # label still clamps its display
self.valueEdited.emit(value) # emits the raw 500.0The display still shows the clamped 100, but the signal now carries 500.0.
... and more importantly: that change hits every labeled slider, whether or not the new flag is on. So this PR isn't entirely opt-in: plain sliders are affected. Here's a test that passes on main but fails here:
from __future__ import annotations
import pytest
from superqt import QLabeledDoubleSlider, QLabeledRangeSlider, QLabeledSlider
@pytest.mark.parametrize("cls", [QLabeledSlider, QLabeledDoubleSlider])
def test_label_value_edited_is_clamped(cls, qtbot):
slider = cls()
qtbot.addWidget(slider)
slider.setRange(0, 100)
with qtbot.waitSignal(slider._label.valueEdited) as blocker:
slider._label.setText("500")
slider._label.editingFinished.emit()
assert blocker.args == [100.0]
def test_range_handle_label_value_edited_is_clamped(qtbot):
slider = QLabeledRangeSlider()
qtbot.addWidget(slider)
slider.setRange(0, 100)
slider.setValue((20, 80))
label = slider._handle_labels[-1]
with qtbot.waitSignal(label.valueEdited) as blocker:
label.setText("500")
label.editingFinished.emit()
assert blocker.args == [100.0]moreover, the raw self.valueEdited.emit(value) risks overflow errors: on a plain QLabeledSlider (the int version) with range 0–100 and the new flag off, type 2147483648 into the label and press Enter. you'll see OverflowError: argument 1 overflowed
so, we need to keep _editing_finished as it is on main, and only stop clamping for handle labels when the expand flag is on.
PR description
This is a small UX tweak for labeled range sliders.
The problem addressed is that if you edit a slider value label (the text box just above the slider knob) and type a value outside the current slider range, that value gets clamped at the slider bounds. This could be cumbersome for things like contrast-limit controls, where users might expect to be able to type a new endpoint directly instead of first adjusting the min/max edge labels (see napari/napari#9310 ).
Changes
SliderLabel.setValue()normally clamps values to the slider bounds when it refreshes labels. It now accepts aclamp_valuesparameter, which defaults toTrueto retain its existing bounds-clamping behavior for normal label updates.SliderLabel._editing_finished()passesclamp_values=Falsebefore emittingvalueEdited, allowing the owning range slider to receive the raw out-of-range value and decide whether to expand its bounds.I added an opt-in flag to the range slider itself to trigger the new behaviour:
expandRangeOnHandleEdit()setExpandRangeOnHandleEdit(bool)How to use it in practice
With that enabled, typing 110 into the upper handle label will expand the max to 110 and set the handle there instead of being stuck at the old bound.
Reproducer
The small example below illustrates simple- and double-sliders with and without the new range-expansion feature triggered. Try to modify e.g the value of one of the slider above 100 and observe the behavior.