Skip to content

Commit

Permalink
fix: fix a number of issues with Labeled and Range Sliders, add Label…
Browse files Browse the repository at this point in the history
…sOnHandle mode. (#242)

* fix: remove processEvents

* merge in fixes

* remove comment

* fix hint

* fix napari

* change pyqt6

* fix: fix range slider styles
  • Loading branch information
tlambert03 authored May 6, 2024
1 parent 12f10be commit ba495a5
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 41 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/test_and_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
exclude:
# Abort (core dumped) on linux pyqt6, unknown reason
- platform: ubuntu-latest
backend: "'PyQt6<6.6'"
backend: pyqt6
# lack of wheels for pyside2/py3.11
- python-version: "3.11"
backend: pyside2
Expand All @@ -52,7 +52,7 @@ jobs:
backend: "'pyside6!=6.6.2'"
- python-version: "3.12"
platform: macos-latest
backend: "'PyQt6<6.6'"
backend: pyqt6
# legacy Qt
- python-version: 3.8
platform: ubuntu-latest
Expand Down
29 changes: 24 additions & 5 deletions src/superqt/sliders/_generic_range_slider.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def showBar(self) -> None:
"""Show the bar between the first and last handle."""
self.setBarVisible(True)

def applyMacStylePatch(self) -> str:
def applyMacStylePatch(self) -> None:
"""Apply a QSS patch to fix sliders on macos>=12 with QT < 6.
see [FAQ](../faq.md#sliders-not-dragging-properly-on-macos-12) for more details.
Expand All @@ -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: Optional[int] = 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())
Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions src/superqt/sliders/_generic_slider.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def __init__(self, *args, **kwargs) -> None:
if USE_MAC_SLIDER_PATCH:
self.applyMacStylePatch()

def applyMacStylePatch(self) -> str:
def applyMacStylePatch(self) -> None:
"""Apply a QSS patch to fix sliders on macos>=12 with QT < 6.
see [FAQ](../faq.md#sliders-not-dragging-properly-on-macos-12) for more details.
Expand Down Expand Up @@ -342,8 +342,12 @@ def _fixStyleOption(self, option):
option.sliderValue = self._to_qinteger_space(self._value - self._minimum)

def _to_qinteger_space(self, val, _max=None):
"""Converts a value to the internal integer space."""
_max = _max or self.MAX_DISPLAY
return int(min(QOVERFLOW, val / (self._maximum - self._minimum) * _max))
range_ = self._maximum - self._minimum
if range_ == 0:
return self._minimum
return int(min(QOVERFLOW, val / range_ * _max))

def _pick(self, pt: QPoint) -> int:
return pt.x() if self.orientation() == Qt.Orientation.Horizontal else pt.y()
Expand Down
69 changes: 46 additions & 23 deletions src/superqt/sliders/_labeled.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
from functools import partial
from typing import Any, Iterable, overload

from qtpy.QtCore import QPoint, QSize, Qt, Signal
from qtpy import QtGui
from qtpy.QtCore import Property, QPoint, QSize, Qt, Signal
from qtpy.QtGui import QFontMetrics, QValidator
from qtpy.QtWidgets import (
QAbstractSlider,
QApplication,
QBoxLayout,
QDoubleSpinBox,
QHBoxLayout,
Expand All @@ -32,6 +32,7 @@ class LabelPosition(IntEnum):
LabelsBelow = auto()
LabelsRight = LabelsAbove
LabelsLeft = LabelsBelow
LabelsOnHandle = auto()


class EdgeLabelMode(IntFlag):
Expand All @@ -43,10 +44,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:
Expand Down Expand Up @@ -158,6 +159,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
Expand Down Expand Up @@ -257,8 +261,6 @@ def setEdgeLabelMode(self, opt: EdgeLabelMode) -> None:
self.layout().setContentsMargins(0, 0, 0, 0)
self._on_slider_range_changed(self.minimum(), self.maximum())

QApplication.processEvents()

# putting this after labelMode methods for the sake of mypy
EdgeLabelMode = EdgeLabelMode

Expand All @@ -279,8 +281,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):
Expand Down Expand Up @@ -386,10 +389,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:
Expand All @@ -415,7 +418,6 @@ def setEdgeLabelMode(self, opt: EdgeLabelMode) -> None:
elif opt == EdgeLabelMode.LabelIsRange:
self._min_label.setValue(self._slider.minimum())
self._max_label.setValue(self._slider.maximum())
QApplication.processEvents()
self._reposition_labels()

def setRange(self, min: int, max: int) -> None:
Expand All @@ -434,26 +436,23 @@ 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)
self._add_labels(layout, inverted=not inverted)
# 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:
layout = QHBoxLayout()
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)

Expand All @@ -465,21 +464,29 @@ 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:
self._slider.setInvertedAppearance(a0)
self.setOrientation(self._slider.orientation())

def resizeEvent(self, a0) -> None:
def resizeEvent(self, a0: Any) -> None:
super().resizeEvent(a0)
self._reposition_labels()

# putting this after methods above for the sake of mypy
LabelPosition = LabelPosition
EdgeLabelMode = EdgeLabelMode

def _getBarColor(self) -> QtGui.QBrush:
return self._slider._style.brush(self._slider._styleOption)

def _setBarColor(self, color: str) -> None:
self._slider._style.brush_active = color

barColor = Property(QtGui.QBrush, _getBarColor, _setBarColor)
"""The color of the bar between the first and last handle."""

# ------------- private methods ----------------
def _rename_signals(self) -> None:
self.valueChanged = self._valueChanged
Expand All @@ -495,20 +502,26 @@ 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)
if self._slider.invertedAppearance():
labels = reversed(list(labels))
for i, label in labels:
rect = self._slider._handleRect(i)
dx = -label.width() / 2
dx = (-label.width() / 2) + 2
dy = -label.height() / 2
if labels_above:
if labels_above: # or on the right
if horizontal:
dy *= 3
else:
dx *= -1
elif labels_on_handle:
if horizontal:
dy += 0.5
else:
dx += 0.5
else:
if horizontal:
dy *= -1
Expand All @@ -525,6 +538,7 @@ def _reposition_labels(self) -> None:
label.move(pos)
last_edge = pos
label.clearFocus()
label.raise_()
label.show()
self.update()

Expand Down Expand Up @@ -612,6 +626,15 @@ def setDecimals(self, prec: int) -> None:
for lbl in self._handle_labels:
lbl.setDecimals(prec)

def _getBarColor(self) -> QtGui.QBrush:
return self._slider._style.brush(self._slider._styleOption)

def _setBarColor(self, color: str) -> None:
self._slider._style.brush_active = color

barColor = Property(QtGui.QBrush, _getBarColor, _setBarColor)
"""The color of the bar between the first and last handle."""


class SliderLabel(QDoubleSpinBox):
def __init__(
Expand Down
18 changes: 10 additions & 8 deletions src/superqt/sliders/_range_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from dataclasses import dataclass, replace
from typing import TYPE_CHECKING

from qtpy import QT_VERSION
from qtpy.QtCore import Qt
from qtpy.QtGui import (
QBrush,
Expand Down Expand Up @@ -140,8 +139,9 @@ def thickness(self, opt: QStyleOptionSlider) -> float:
tick_offset=4,
)

if QT_VERSION and int(QT_VERSION.split(".")[0]) == 6:
CATALINA_STYLE = replace(CATALINA_STYLE, tick_offset=2)
# I can no longer reproduce the cases in which this was necessary
# if QT_VERSION and int(QT_VERSION.split(".")[0]) == 6:
# CATALINA_STYLE = replace(CATALINA_STYLE, tick_offset=2)

BIG_SUR_STYLE = replace(
CATALINA_STYLE,
Expand All @@ -155,8 +155,9 @@ def thickness(self, opt: QStyleOptionSlider) -> float:
tick_bar_alpha=0.2,
)

if QT_VERSION and int(QT_VERSION.split(".")[0]) == 6:
BIG_SUR_STYLE = replace(BIG_SUR_STYLE, tick_offset=-3)
# I can no longer reproduce the cases in which this was necessary
# if QT_VERSION and int(QT_VERSION.split(".")[0]) == 6:
# BIG_SUR_STYLE = replace(BIG_SUR_STYLE, tick_offset=-3)

WINDOWS_STYLE = replace(
BASE_STYLE,
Expand Down Expand Up @@ -229,7 +230,7 @@ def thickness(self, opt: QStyleOptionSlider) -> float:
)


def parse_color(color: str, default_attr) -> QColor | QGradient:
def parse_color(color: str, default_attr: str) -> QColor | QGradient:
qc = QColor(color)
if qc.isValid():
return qc
Expand All @@ -241,6 +242,7 @@ def parse_color(color: str, default_attr) -> QColor | QGradient:

# try linear gradient:
match = qlineargrad_pattern.search(color)
grad: QGradient
if match:
grad = QLinearGradient(*(float(i) for i in match.groups()[:4]))
grad.setColorAt(0, QColor(match.groupdict()["stop0"]))
Expand All @@ -259,11 +261,11 @@ def parse_color(color: str, default_attr) -> QColor | QGradient:
return QColor(getattr(SYSTEM_STYLE, default_attr))


def update_styles_from_stylesheet(obj: _GenericRangeSlider):
def update_styles_from_stylesheet(obj: _GenericRangeSlider) -> None:
qss: str = obj.styleSheet()

parent = obj.parent()
while parent is not None:
while parent and hasattr(parent, "styleSheet"):
qss = parent.styleSheet() + qss
parent = parent.parent()
qss = QApplication.instance().styleSheet() + qss
Expand Down
4 changes: 3 additions & 1 deletion src/superqt/utils/_ensure_thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit ba495a5

Please sign in to comment.