mirror of
https://github.com/pyapp-kit/superqt.git
synced 2025-07-21 12:11:07 +02:00
Compare commits
3 Commits
12f10be8da
...
4a0aaca2e9
Author | SHA1 | Date | |
---|---|---|---|
|
4a0aaca2e9 | ||
|
2d49e77c3d | ||
|
ba495a5e72 |
4
.github/workflows/test_and_deploy.yml
vendored
4
.github/workflows/test_and_deploy.yml
vendored
@@ -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
|
||||
@@ -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
|
||||
|
@@ -5,7 +5,7 @@ ci:
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.3.5
|
||||
rev: v0.4.3
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --unsafe-fixes]
|
||||
@@ -17,7 +17,7 @@ repos:
|
||||
- id: validate-pyproject
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.9.0
|
||||
rev: v1.10.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: tests|examples
|
||||
|
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,5 +1,17 @@
|
||||
# Changelog
|
||||
|
||||
## [v0.6.5](https://github.com/pyapp-kit/superqt/tree/v0.6.5) (2024-05-06)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.6.4...v0.6.5)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- fix: fix a number of issues with Labeled and Range Sliders, add LabelsOnHandle mode. [\#242](https://github.com/pyapp-kit/superqt/pull/242) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- ci: trying to fix tests on various platforms [\#243](https://github.com/pyapp-kit/superqt/pull/243) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
## [v0.6.4](https://github.com/pyapp-kit/superqt/tree/v0.6.4) (2024-04-25)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.6.3...v0.6.4)
|
||||
|
@@ -88,7 +88,7 @@ class IconPreviewArea(QtWidgets.QWidget):
|
||||
self.updatePixmapLabels()
|
||||
|
||||
def createHeaderLabel(self, text):
|
||||
label = QtWidgets.QLabel("<b>%s</b>" % text)
|
||||
label = QtWidgets.QLabel(f"<b>{text}</b>")
|
||||
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
return label
|
||||
|
||||
|
@@ -103,7 +103,7 @@ class _GenericRangeSlider(_GenericSlider):
|
||||
"""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.
|
||||
@@ -124,11 +124,27 @@ class _GenericRangeSlider(_GenericSlider):
|
||||
"""
|
||||
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())
|
||||
@@ -139,6 +155,9 @@ class _GenericRangeSlider(_GenericSlider):
|
||||
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 @@ class _GenericRangeSlider(_GenericSlider):
|
||||
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
|
||||
|
@@ -99,7 +99,7 @@ class _GenericSlider(QSlider):
|
||||
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.
|
||||
@@ -342,8 +342,12 @@ class _GenericSlider(QSlider):
|
||||
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()
|
||||
|
@@ -5,11 +5,11 @@ from enum import IntEnum, IntFlag, auto
|
||||
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,
|
||||
@@ -32,6 +32,7 @@ class LabelPosition(IntEnum):
|
||||
LabelsBelow = auto()
|
||||
LabelsRight = LabelsAbove
|
||||
LabelsLeft = LabelsBelow
|
||||
LabelsOnHandle = auto()
|
||||
|
||||
|
||||
class EdgeLabelMode(IntFlag):
|
||||
@@ -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:
|
||||
@@ -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
|
||||
@@ -257,8 +261,6 @@ class QLabeledSlider(_SliderProxy, QAbstractSlider):
|
||||
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
|
||||
|
||||
@@ -279,8 +281,9 @@ class QLabeledSlider(_SliderProxy, QAbstractSlider):
|
||||
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):
|
||||
@@ -386,10 +389,10 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
|
||||
"""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:
|
||||
@@ -415,7 +418,6 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
|
||||
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:
|
||||
@@ -434,6 +436,7 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
|
||||
"""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)
|
||||
@@ -441,9 +444,7 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
|
||||
# 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:
|
||||
@@ -451,9 +452,7 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
|
||||
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)
|
||||
|
||||
@@ -465,14 +464,13 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
|
||||
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()
|
||||
|
||||
@@ -480,6 +478,15 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
|
||||
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
|
||||
@@ -495,6 +502,7 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
|
||||
|
||||
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)
|
||||
@@ -502,13 +510,18 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
|
||||
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
|
||||
@@ -525,6 +538,7 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
|
||||
label.move(pos)
|
||||
last_edge = pos
|
||||
label.clearFocus()
|
||||
label.raise_()
|
||||
label.show()
|
||||
self.update()
|
||||
|
||||
@@ -612,6 +626,15 @@ class QLabeledDoubleRangeSlider(QLabeledRangeSlider):
|
||||
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__(
|
||||
|
@@ -5,7 +5,6 @@ import re
|
||||
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,
|
||||
@@ -140,8 +139,9 @@ CATALINA_STYLE = replace(
|
||||
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,
|
||||
@@ -155,8 +155,9 @@ BIG_SUR_STYLE = replace(
|
||||
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,
|
||||
@@ -229,7 +230,7 @@ rgba_pattern = re.compile(
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
@@ -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"]))
|
||||
@@ -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
|
||||
|
@@ -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 @@ class CallCallable(QObject):
|
||||
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
|
||||
|
Reference in New Issue
Block a user