Compare commits

...

6 Commits

Author SHA1 Message Date
Talley Lambert
0ec5cd3a2f chore: changelog v0.6.6 2024-05-12 11:11:56 -04:00
Talley Lambert
8f62b0b00d perf: improve paint time for QColormapLineEdit (#245) 2024-05-12 10:32:59 -04:00
pre-commit-ci[bot]
4a0aaca2e9 ci: [pre-commit.ci] autoupdate (#244)
* ci: [pre-commit.ci] autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.5 → v0.4.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.5...v0.4.3)
- [github.com/pre-commit/mirrors-mypy: v1.9.0 → v1.10.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.9.0...v1.10.0)

* style: [pre-commit.ci] auto fixes [...]

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-05-06 19:18:03 -04:00
Talley Lambert
2d49e77c3d chore: changelog v0.6.5 2024-05-06 17:45:31 -04:00
Talley Lambert
ba495a5e72 fix: fix a number of issues with Labeled and Range Sliders, add LabelsOnHandle mode. (#242)
* fix: remove processEvents

* merge in fixes

* remove comment

* fix hint

* fix napari

* change pyqt6

* fix: fix range slider styles
2024-05-06 17:43:53 -04:00
Talley Lambert
12f10be8da ci: trying to fix tests on various platforms (#243)
* ci: attempt1

* add lxml_html_clean

* fix napari version name

* breakout coverage

* use main

* cump

* bump again

* bump

* bump

* skip more napari tests

* add always

* back to v1

* try editabel

* use main again

* remove editable

* editable again

* bump

* bump

* bump

* use v2
2024-05-06 15:29:43 -04:00
10 changed files with 150 additions and 68 deletions

View File

@@ -16,29 +16,26 @@ on:
jobs:
test:
name: Test
uses: pyapp-kit/workflows/.github/workflows/test-pyrepo.yml@v1
uses: pyapp-kit/workflows/.github/workflows/test-pyrepo.yml@v2
with:
os: ${{ matrix.platform }}
python-version: ${{ matrix.python-version }}
qt: ${{ matrix.backend }}
pip-install-pre-release: ${{ github.event_name == 'schedule' }}
report-failures: ${{ github.event_name == 'schedule' }}
secrets:
codecov-token: ${{ secrets.CODECOV_TOKEN }}
coverage-upload: artifact
strategy:
fail-fast: false
matrix:
platform: [ubuntu-latest, windows-latest, macos-13]
python-version: ["3.8", "3.9", "3.10", "3.11"]
backend: [pyqt5, pyside2, "'PyQt6<6.6'"]
backend: [pyqt5, pyside2, pyqt6]
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
include:
# https://bugreports.qt.io/browse/PYSIDE-2627
- python-version: "3.10"
@@ -53,11 +50,9 @@ jobs:
- python-version: "3.11"
platform: windows-latest
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
@@ -70,26 +65,34 @@ jobs:
backend: "pyqt5==5.14.*"
test-qt-minreqs:
uses: pyapp-kit/workflows/.github/workflows/test-pyrepo.yml@v1
secrets: inherit
uses: pyapp-kit/workflows/.github/workflows/test-pyrepo.yml@v2
with:
python-version: "3.8"
qt: pyqt5
pip-post-installs: 'qtpy==1.1.0 typing-extensions==3.7.4.3'
pip-install-flags: -e
coverage-upload: artifact
upload_coverage:
if: always()
needs: [test, test-qt-minreqs]
uses: pyapp-kit/workflows/.github/workflows/upload-coverage.yml@v2
secrets: inherit
test_napari:
uses: pyapp-kit/workflows/.github/workflows/test-dependents.yml@v1
uses: pyapp-kit/workflows/.github/workflows/test-dependents.yml@v2
with:
dependency-repo: napari/napari
dependency-ref: ${{ matrix.napari-version }}
dependency-extras: 'testing'
qt: ${{ matrix.qt }}
pytest-args: 'napari/_qt -k "not async and not qt_dims_2"'
pytest-args: 'napari/_qt -k "not async and not qt_dims_2 and not qt_viewer_console_focus and not keybinding_editor"'
python-version: "3.10"
post-install-cmd: 'pip install lxml_html_clean'
strategy:
fail-fast: false
matrix:
napari-version: ["", "v0.4.18"]
napari-version: ["", "v0.4.19.post1"]
qt: ["pyqt5", "pyside2"]
check-manifest:

View File

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

View File

@@ -1,5 +1,29 @@
# Changelog
## [v0.6.6](https://github.com/pyapp-kit/superqt/tree/v0.6.6) (2024-05-12)
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.6.5...v0.6.6)
**Refactors:**
- perf: improve paint time for QColormapLineEdit [\#245](https://github.com/pyapp-kit/superqt/pull/245) ([tlambert03](https://github.com/tlambert03))
**Merged pull requests:**
- ci: \[pre-commit.ci\] autoupdate [\#244](https://github.com/pyapp-kit/superqt/pull/244) ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci))
## [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)
**Implemented enhancements:**
- 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)

View File

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

View File

@@ -1,8 +1,8 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
from qtpy.QtCore import Qt
from qtpy.QtCore import QRect, Qt
from qtpy.QtGui import QIcon, QPainter, QPaintEvent, QPalette
from qtpy.QtWidgets import QApplication, QLineEdit, QStyle, QWidget
@@ -103,6 +103,19 @@ class QColormapLineEdit(QLineEdit):
def _cmap_is_full_width(self):
return self._colormap_fraction >= 0.75
def _cmap_rect(self) -> QRect:
cmap_rect = self.rect().adjusted(2, 0, 0, 0)
cmap_rect.setWidth(int(cmap_rect.width() * self._colormap_fraction))
return cmap_rect
def resizeEvent(self, e: Any) -> None:
left_margin = 6
if not self._cmap_is_full_width():
# leave room for the colormap
left_margin += self._cmap_rect().width()
self.setTextMargins(left_margin, 2, 0, 0)
super().resizeEvent(e)
def paintEvent(self, e: QPaintEvent) -> None:
# don't draw the background
# otherwise it will cover the colormap during super().paintEvent
@@ -112,15 +125,7 @@ class QColormapLineEdit(QLineEdit):
palette.setColor(palette.ColorRole.Base, Qt.GlobalColor.transparent)
self.setPalette(palette)
cmap_rect = self.rect().adjusted(2, 0, 0, 0)
cmap_rect.setWidth(int(cmap_rect.width() * self._colormap_fraction))
left_margin = 6
if not self._cmap_is_full_width():
# leave room for the colormap
left_margin += cmap_rect.width()
self.setTextMargins(left_margin, 2, 0, 0)
cmap_rect = self._cmap_rect()
if self._cmap:
draw_colormap(
self, self._cmap, cmap_rect, checkerboard_size=self._checkerboard_size

View File

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

View File

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

View File

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

View File

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

View File

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