Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fix a number of issues with Labeled and Range Sliders, add LabelsOnHandle mode. #242

Merged
merged 8 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@
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

Check warning on line 349 in src/superqt/sliders/_generic_slider.py

View check run for this annotation

Codecov / codecov/patch

src/superqt/sliders/_generic_slider.py#L349

Added line #L349 was not covered by tests
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 @@
LabelsBelow = auto()
LabelsRight = LabelsAbove
LabelsLeft = LabelsBelow
LabelsOnHandle = auto()


class EdgeLabelMode(IntFlag):
Expand All @@ -43,10 +44,10 @@
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 @@

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 @@
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 @@
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 @@
"""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

Check warning on line 393 in src/superqt/sliders/_labeled.py

View check run for this annotation

Codecov / codecov/patch

src/superqt/sliders/_labeled.py#L392-L393

Added lines #L392 - L393 were not covered by tests
# TODO: make double clickable to edit
lbl.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, trans)

Check warning on line 395 in src/superqt/sliders/_labeled.py

View check run for this annotation

Codecov / codecov/patch

src/superqt/sliders/_labeled.py#L395

Added line #L395 was not covered by tests
self.setOrientation(self.orientation())

def edgeLabelMode(self) -> EdgeLabelMode:
Expand All @@ -415,7 +418,6 @@
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 @@
"""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 @@
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)

Check warning on line 482 in src/superqt/sliders/_labeled.py

View check run for this annotation

Codecov / codecov/patch

src/superqt/sliders/_labeled.py#L482

Added line #L482 was not covered by tests

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

Check warning on line 485 in src/superqt/sliders/_labeled.py

View check run for this annotation

Codecov / codecov/patch

src/superqt/sliders/_labeled.py#L485

Added line #L485 was not covered by tests

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 @@

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

Check warning on line 522 in src/superqt/sliders/_labeled.py

View check run for this annotation

Codecov / codecov/patch

src/superqt/sliders/_labeled.py#L520-L522

Added lines #L520 - L522 were not covered by tests
else:
dx += 0.5

Check warning on line 524 in src/superqt/sliders/_labeled.py

View check run for this annotation

Codecov / codecov/patch

src/superqt/sliders/_labeled.py#L524

Added line #L524 was not covered by tests
else:
if horizontal:
dy *= -1
Expand All @@ -525,6 +538,7 @@
label.move(pos)
last_edge = pos
label.clearFocus()
label.raise_()
label.show()
self.update()

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

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

Check warning on line 630 in src/superqt/sliders/_labeled.py

View check run for this annotation

Codecov / codecov/patch

src/superqt/sliders/_labeled.py#L630

Added line #L630 was not covered by tests

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

Check warning on line 633 in src/superqt/sliders/_labeled.py

View check run for this annotation

Codecov / codecov/patch

src/superqt/sliders/_labeled.py#L633

Added line #L633 was not covered by tests

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