diff --git a/src/superqt/sliders/_generic_range_slider.py b/src/superqt/sliders/_generic_range_slider.py index 883571b3..a8e4b148 100644 --- a/src/superqt/sliders/_generic_range_slider.py +++ b/src/superqt/sliders/_generic_range_slider.py @@ -124,11 +124,27 @@ def sliderPosition(self): """ return tuple(float(i) for i in self._position) - def setSliderPosition(self, pos: Union[float, Sequence[float]], index=None) -> None: + def setSliderPosition( # type: ignore + self, + pos: Union[float, Sequence[float]], + index: int | None = None, + *, + reversed: bool = False, + ) -> None: """Set current position of the handles with a sequence of integers. - If `pos` is a sequence, it must have the same length as `value()`. - If it is a scalar, index will be + Parameters + ---------- + pos : Union[float, Sequence[float]] + The new position of the slider handle(s). If a sequence, it must have the + same length as `value()`. If it is a scalar, index will be used to set the + position of the handle at that index. + index : int | None + The index of the handle to set the position of. If None, the "pressedIndex" + will be used. + reversed : bool + Order in which to set the positions. Can be useful when setting multiple + positions, to avoid intermediate overlapping values. """ if isinstance(pos, (list, tuple)): val_len = len(self.value()) @@ -139,6 +155,9 @@ def setSliderPosition(self, pos: Union[float, Sequence[float]], index=None) -> N else: pairs = [(self._pressedIndex if index is None else index, pos)] + if reversed: + pairs = pairs[::-1] + for idx, position in pairs: self._position[idx] = self._bound(position, idx) @@ -222,7 +241,7 @@ def _offsetAllPositions(self, offset: float, ref=None) -> None: offset = self.maximum() - ref[-1] elif ref[0] + offset < self.minimum(): offset = self.minimum() - ref[0] - self.setSliderPosition([i + offset for i in ref]) + self.setSliderPosition([i + offset for i in ref], reversed=offset > 0) def _fixStyleOption(self, option): pass diff --git a/src/superqt/sliders/_labeled.py b/src/superqt/sliders/_labeled.py index 2cc05538..6f702477 100644 --- a/src/superqt/sliders/_labeled.py +++ b/src/superqt/sliders/_labeled.py @@ -31,6 +31,7 @@ class LabelPosition(IntEnum): LabelsBelow = auto() LabelsRight = LabelsAbove LabelsLeft = LabelsBelow + LabelsOnHandle = auto() class EdgeLabelMode(IntFlag): @@ -42,10 +43,10 @@ class EdgeLabelMode(IntFlag): class _SliderProxy: _slider: QSlider - def value(self) -> int: + def value(self) -> Any: return self._slider.value() - def setValue(self, value: int) -> None: + def setValue(self, value: Any) -> None: self._slider.setValue(value) def sliderPosition(self) -> int: @@ -157,6 +158,9 @@ def _handle_overloaded_slider_sig( class QLabeledSlider(_SliderProxy, QAbstractSlider): editingFinished = Signal() + _ivalueChanged = Signal(int) + _isliderMoved = Signal(int) + _irangeChanged = Signal(int, int) _slider_class = QSlider _slider: QSlider @@ -276,8 +280,9 @@ def _setValue(self, value: float) -> None: self._slider.setValue(int(value)) def _rename_signals(self) -> None: - # for subclasses - pass + self.valueChanged = self._ivalueChanged + self.sliderMoved = self._isliderMoved + self.rangeChanged = self._irangeChanged class QLabeledDoubleSlider(QLabeledSlider): @@ -383,10 +388,10 @@ def setHandleLabelPosition(self, opt: LabelPosition) -> None: """Set where/whether labels are shown adjacent to slider handles.""" self._handle_label_position = opt for lbl in self._handle_labels: - if not opt: - lbl.hide() - else: - lbl.show() + lbl.setVisible(bool(opt)) + trans = opt == LabelPosition.LabelsOnHandle + # TODO: make double clickable to edit + lbl.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, trans) self.setOrientation(self.orientation()) def edgeLabelMode(self) -> EdgeLabelMode: @@ -430,6 +435,7 @@ def setOrientation(self, orientation: Qt.Orientation) -> None: """Set orientation, value will be 'horizontal' or 'vertical'.""" self._slider.setOrientation(orientation) inverted = self._slider.invertedAppearance() + marg = (0, 0, 0, 0) if orientation == Qt.Orientation.Vertical: layout: QBoxLayout = QVBoxLayout() layout.setSpacing(1) @@ -437,9 +443,7 @@ def setOrientation(self, orientation: Qt.Orientation) -> None: # TODO: set margins based on label width if self._handle_label_position == LabelPosition.LabelsLeft: marg = (30, 0, 0, 0) - elif self._handle_label_position == LabelPosition.NoLabel: - marg = (0, 0, 0, 0) - else: + elif self._handle_label_position == LabelPosition.LabelsRight: marg = (0, 0, 20, 0) layout.setAlignment(Qt.AlignmentFlag.AlignCenter) else: @@ -447,9 +451,7 @@ def setOrientation(self, orientation: Qt.Orientation) -> None: layout.setSpacing(7) if self._handle_label_position == LabelPosition.LabelsBelow: marg = (0, 0, 0, 25) - elif self._handle_label_position == LabelPosition.NoLabel: - marg = (0, 0, 0, 0) - else: + elif self._handle_label_position == LabelPosition.LabelsAbove: marg = (0, 25, 0, 0) self._add_labels(layout, inverted=inverted) @@ -461,6 +463,7 @@ def setOrientation(self, orientation: Qt.Orientation) -> None: self.setLayout(layout) layout.setContentsMargins(*marg) super().setOrientation(orientation) + # QApplication.processEvents() self._reposition_labels() def setInvertedAppearance(self, a0: bool) -> None: @@ -490,6 +493,7 @@ def _reposition_labels(self) -> None: horizontal = self.orientation() == Qt.Orientation.Horizontal labels_above = self._handle_label_position == LabelPosition.LabelsAbove + labels_on_handle = self._handle_label_position == LabelPosition.LabelsOnHandle last_edge = None labels: Iterable[tuple[int, SliderLabel]] = enumerate(self._handle_labels) @@ -497,13 +501,16 @@ def _reposition_labels(self) -> None: labels = reversed(list(labels)) for i, label in labels: rect = self._slider._handleRect(i) - dx = -label.width() / 2 - dy = -label.height() / 2 - if labels_above: + dx = (-label.width() / 2) + 1.5 + dy = (-label.height() / 2) + 0.5 + if labels_above: # or on the right if horizontal: dy *= 3 else: dx *= -1 + elif labels_on_handle: + if not horizontal: + dx *= -1 else: if horizontal: dy *= -1 @@ -520,6 +527,7 @@ def _reposition_labels(self) -> None: label.move(pos) last_edge = pos label.clearFocus() + label.raise_() label.show() self.update() diff --git a/src/superqt/utils/_ensure_thread.py b/src/superqt/utils/_ensure_thread.py index d98bd5d9..602c877d 100644 --- a/src/superqt/utils/_ensure_thread.py +++ b/src/superqt/utils/_ensure_thread.py @@ -2,6 +2,7 @@ from __future__ import annotations from concurrent.futures import Future +from contextlib import suppress from functools import wraps from typing import TYPE_CHECKING, Any, Callable, ClassVar, overload @@ -41,7 +42,8 @@ def __init__(self, callable: Callable, args: tuple, kwargs: dict): def call(self): CallCallable.instances.remove(self) res = self._callable(*self._args, **self._kwargs) - self.finished.emit(res) + with suppress(RuntimeError): + self.finished.emit(res) # fmt: off