Compare commits

..

12 Commits

Author SHA1 Message Date
Talley Lambert
17fd211740 test: drop old napari test (#296) 2025-07-16 18:14:27 -04:00
pre-commit-ci[bot]
3b83a8a1e2 ci: [pre-commit.ci] autoupdate (#299)
* ci: [pre-commit.ci] autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.12 → v0.12.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.12...v0.12.2)
- [github.com/pre-commit/mirrors-mypy: v1.16.0 → v1.16.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.16.0...v1.16.1)

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

* pin pytestqt

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Talley Lambert <talley.lambert@gmail.com>
2025-07-16 18:14:01 -04:00
Talley Lambert
13e033e4a2 chore: changelog v0.7.5 2025-06-17 20:24:57 -04:00
Grzegorz Bokota
55b66393c3 Use scientific notation for big values in labeled slider (#226)
* initial implementation

* fix formating labels

* add minimum number of decimals

* fix typo in function name

* add `decimals` method

* fix after napari src migration

* use --import-mode=importlib

* allow enforce decimals

* fix seting 0

* flexible set range for range labels

* better set range

* fix seting mode

* fix max calculation

---------

Co-authored-by: Talley Lambert <talley.lambert@gmail.com>
2025-06-17 20:20:30 -04:00
Talley Lambert
b495c70206 chore: changelog v0.7.4 2025-06-10 10:23:25 -04:00
Lorenzo Gaifas
a9fa720577 Allow setting label position on labeled slider (#294) 2025-06-03 05:24:58 -04:00
pre-commit-ci[bot]
257d97ae0f ci: [pre-commit.ci] autoupdate (#297) 2025-06-02 18:32:50 -04:00
Gabriel Selzer
7193480796 fix: Set SliderProxy range params to Any (#290)
* fix: Set SliderProxy range params to Any

When the types are ints, this raises mypy errors when e.g. setting the
range of a QDoubleSlider to float values.

This change aligns with the parameter typing of _SliderProxy.setValue

* Update src/superqt/sliders/_labeled.py

---------

Co-authored-by: Talley Lambert <talley.lambert@gmail.com>
2025-06-02 16:35:13 -04:00
pre-commit-ci[bot]
788d0f0325 ci: [pre-commit.ci] autoupdate (#289)
* ci: [pre-commit.ci] autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.9 → v0.11.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.9...v0.11.8)
- [github.com/abravalheri/validate-pyproject: v0.23 → v0.24.1](https://github.com/abravalheri/validate-pyproject/compare/v0.23...v0.24.1)

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-06-02 16:27:12 -04:00
Lorenzo Gaifas
935025eacc Fix napari test (#295)
* move napari to src

* use importlib import for new test
2025-06-02 16:25:36 -04:00
Sandro
358d041c0d Make qimage_to_array() work on big endian (#288)
* Make qimage_to_array() work on big endian

Make sure the returned ndarray is ordered the same as on little
endian systems.

Solves #287

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

* Update src/superqt/utils/_img_utils.py

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Grzegorz Bokota <bokota+github@gmail.com>
2025-04-06 23:44:07 -04:00
Talley Lambert
49a8114843 docs: document QToggleSwitch (#286)
* wip

* add toggleswitch

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-03-28 15:30:58 -04:00
20 changed files with 253 additions and 97 deletions

View File

@@ -94,14 +94,14 @@ jobs:
dependency-ref: ${{ matrix.napari-version }}
dependency-extras: "testing"
qt: ${{ matrix.qt }}
pytest-args: 'napari/_qt -k "not async and not qt_dims_2 and not qt_viewer_console_focus and not keybinding_editor and not preferences_dialog_not_dismissed"'
pytest-args: 'src/napari/_qt --import-mode=importlib -k "not async and not qt_dims_2 and not qt_viewer_console_focus and not keybinding_editor and not preferences_dialog_not_dismissed"'
python-version: "3.10"
post-install-cmd: "pip install lxml_html_clean"
strategy:
fail-fast: false
matrix:
napari-version: ["", "v0.4.19.post1"]
qt: ["pyqt5", "pyside2"]
napari-version: [ "" ]
qt: [ "pyqt5", "pyside2" ]
check-manifest:
name: Check Manifest

View File

@@ -5,19 +5,19 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.9
rev: v0.12.3
hooks:
- id: ruff
args: [--fix, --unsafe-fixes]
- id: ruff-format
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.23
rev: v0.24.1
hooks:
- id: validate-pyproject
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.15.0
rev: v1.17.0
hooks:
- id: mypy
exclude: tests|examples

View File

@@ -1,5 +1,34 @@
# Changelog
## [v0.7.5](https://github.com/pyapp-kit/superqt/tree/v0.7.5) (2025-06-18)
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.7.4...v0.7.5)
**Implemented enhancements:**
- feat: Use scientific notation for big values in labeled slider [\#226](https://github.com/pyapp-kit/superqt/pull/226) ([Czaki](https://github.com/Czaki))
## [v0.7.4](https://github.com/pyapp-kit/superqt/tree/v0.7.4) (2025-06-10)
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.7.3...v0.7.4)
**Implemented enhancements:**
- feat: Allow setting label position on labeled slider [\#294](https://github.com/pyapp-kit/superqt/pull/294) ([brisvag](https://github.com/brisvag))
**Fixed bugs:**
- fix: Set SliderProxy range params to Any [\#290](https://github.com/pyapp-kit/superqt/pull/290) ([gselzer](https://github.com/gselzer))
- Make qimage\_to\_array\(\) work on big endian [\#288](https://github.com/pyapp-kit/superqt/pull/288) ([penguinpee](https://github.com/penguinpee))
**Documentation updates:**
- docs: document QToggleSwitch [\#286](https://github.com/pyapp-kit/superqt/pull/286) ([tlambert03](https://github.com/tlambert03))
**Tests & CI:**
- Fix napari test [\#295](https://github.com/pyapp-kit/superqt/pull/295) ([brisvag](https://github.com/brisvag))
## [v0.7.3](https://github.com/pyapp-kit/superqt/tree/v0.7.3) (2025-03-28)
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.7.2...v0.7.3)
@@ -537,21 +566,13 @@
## [v0.2.1](https://github.com/pyapp-kit/superqt/tree/v0.2.1) (2021-07-10)
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.2.0rc1...v0.2.1)
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.2.0...v0.2.1)
**Fixed bugs:**
- Fix QLabeledRangeSlider API \(fix slider proxy\) [\#10](https://github.com/pyapp-kit/superqt/pull/10) ([tlambert03](https://github.com/tlambert03))
- Fix range slider with negative min range [\#9](https://github.com/pyapp-kit/superqt/pull/9) ([tlambert03](https://github.com/tlambert03))
## [v0.2.0rc1](https://github.com/pyapp-kit/superqt/tree/v0.2.0rc1) (2021-06-26)
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.2.0rc0...v0.2.0rc1)
## [v0.2.0rc0](https://github.com/pyapp-kit/superqt/tree/v0.2.0rc0) (2021-06-26)
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.2.0...v0.2.0rc0)
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*

View File

@@ -35,7 +35,7 @@ def define_env(env: "MacrosPlugin"):
src = src.replace(
"QApplication([])", "QApplication.instance() or QApplication([])"
)
src = src.replace("app.exec_()", "")
src = src.replace("app.exec_()", "app.processEvents()")
exec(src)
_grab(dest, width)
@@ -127,7 +127,6 @@ def define_env(env: "MacrosPlugin"):
def _grab(dest: str | Path, width) -> list[Path]:
"""Grab the top widgets of the application."""
from qtpy.QtCore import QTimer
from qtpy.QtWidgets import QApplication
w = QApplication.topLevelWidgets()[-1]
@@ -135,12 +134,3 @@ def _grab(dest: str | Path, width) -> list[Path]:
w.activateWindow()
w.setMinimumHeight(40)
w.grab().save(str(dest))
# hack to make sure the object is truly closed and deleted
while True:
QTimer.singleShot(10, w.deleteLater)
QApplication.processEvents()
try:
w.parent()
except RuntimeError:
return

View File

@@ -11,7 +11,7 @@ running in the desired thread:
`ensure_object_thread` ensures that a decorated bound method of a `QObject` runs
in the thread in which the instance lives ([see qt documentation for
details](https://doc.qt.io/qt-5/threads-qobject.html#accessing-qobject-subclasses-from-other-threads)).
details](https://doc.qt.io/qt-6/threads-qobject.html#accessing-qobject-subclasses-from-other-threads)).
## Usage

View File

@@ -27,6 +27,7 @@ The following are QWidget subclasses:
| [`QSearchableTreeWidget`](./qsearchabletreewidget.md) | `QTreeWidget` variant with search field that filters available options |
| [`QColorComboBox`](./qcolorcombobox.md) | `QComboBox` to select from a specified set of colors |
| [`QColormapComboBox`](./qcolormap.md) | `QComboBox` to select from a specified set of colormaps. |
| [`QToggleSwitch`](./qtoggleswitch.md) | `QAbstractButton` that represents a boolean value with a toggle switch. |
## Frames and containers

View File

@@ -1,7 +1,7 @@
# QEnumComboBox
`QEnumComboBox` is a variant of
[`QComboBox`](https://doc.qt.io/qt-5/qcombobox.html) that populates the items in
[`QComboBox`](https://doc.qt.io/qt-6/qcombobox.html) that populates the items in
the combobox based on a python `Enum` class. In addition to all the methods
provided by `QComboBox`, this subclass adds the methods
`enumClass`/`setEnumClass` to get/set the current `Enum` class represented by

View File

@@ -20,7 +20,7 @@ app.exec_()
{{ show_widget() }}
- `QRangeSlider` inherits from [`QSlider`](https://doc.qt.io/qt-5/qslider.html)
- `QRangeSlider` inherits from [`QSlider`](https://doc.qt.io/qt-6/qslider.html)
and attempts to match the Qt API as closely as possible
- It uses platform-specific styles (for handle, groove, & ticks) but also supports
QSS style sheets.
@@ -28,9 +28,9 @@ app.exec_()
- Supports more than 2 handles (e.g. `slider.setValue([0, 10, 60, 80])`)
As `QRangeSlider` inherits from
[`QtWidgets.QSlider`](https://doc.qt.io/qt-5/qslider.html), you can use all of
[`QtWidgets.QSlider`](https://doc.qt.io/qt-6/qslider.html), you can use all of
the same methods available in the [QSlider
API](https://doc.qt.io/qt-5/qslider.html). The major difference is that `value()`
API](https://doc.qt.io/qt-6/qslider.html). The major difference is that `value()`
and `sliderPosition()` are reimplemented as `tuples` of `int` (where the length of
the tuple is equal to the number of handles in the slider.)

View File

@@ -1,7 +1,7 @@
# QSearchableComboBox
`QSearchableComboBox` is a variant of
[`QComboBox`](https://doc.qt.io/qt-5/qcombobox.html) that allow to filter list
[`QComboBox`](https://doc.qt.io/qt-6/qcombobox.html) that allow to filter list
of options by enter part of text. It could be drop in replacement for
`QComboBox`.

View File

@@ -1,13 +1,13 @@
# QSearchableListWidget
`QSearchableListWidget` is a variant of
[`QListWidget`](https://doc.qt.io/qt-5/qlistwidget.html) that add text entry
[`QListWidget`](https://doc.qt.io/qt-6/qlistwidget.html) that add text entry
above list widget that allow to filter list of available options.
Due to implementation details, this widget it does not inherit directly from
[`QListWidget`](https://doc.qt.io/qt-5/qlistwidget.html) but it does fully
[`QListWidget`](https://doc.qt.io/qt-6/qlistwidget.html) but it does fully
satisfy its api. The only limitation is that it cannot be used as argument of
[`QListWidgetItem`](https://doc.qt.io/qt-5/qlistwidgetitem.html) constructor.
[`QListWidgetItem`](https://doc.qt.io/qt-6/qlistwidgetitem.html) constructor.
```python
from qtpy.QtWidgets import QApplication

View File

@@ -0,0 +1,24 @@
# QToggleSwitch
`QToggleSwitch` is a
[`QAbstractButton`](https://doc.qt.io/qt-6/qabstractbutton.html) subclass
that represents a boolean value as a toggle switch. The API is similar to
[`QCheckBox`](https://doc.qt.io/qt-6/qcheckbox.html) but with a different
visual representation.
```python
from qtpy.QtWidgets import QApplication
from superqt import QToggleSwitch
app = QApplication([])
switch = QToggleSwitch()
switch.show()
app.exec_()
```
{{ show_widget(80) }}
{{ show_members('superqt.QToggleSwitch') }}

View File

@@ -27,11 +27,12 @@ qlds.setValue(0.5)
qlds.setSingleStep(0.1)
qlrs = QLabeledRangeSlider(ORIENTATION)
qlrs.valueChanged.connect(lambda e: print("QLabeledRangeSlider valueChanged", e))
qlrs.setValue((20, 60))
qlrs.valueChanged.connect(lambda e: print("qlrs valueChanged", e))
qlrs.setRange(0, 10**11)
qlrs.setValue((20, 60 * 10**9))
qldrs = QLabeledDoubleRangeSlider(ORIENTATION)
qldrs.valueChanged.connect(lambda e: print("qlrs valueChanged", e))
qldrs.valueChanged.connect(lambda e: print("qldrs valueChanged", e))
qldrs.setRange(0, 1)
qldrs.setSingleStep(0.01)
qldrs.setValue((0.2, 0.7))

View File

@@ -51,7 +51,7 @@ test = [
"pint",
"pytest",
"pytest-cov",
"pytest-qt",
"pytest-qt==4.4.0",
"numpy",
"cmap",
"pyconify",

View File

@@ -70,7 +70,7 @@ class _GenericEliding:
text = self._wrappedText()
last_line = fm.elidedText("".join(text[nlines:]), self._elide_mode, width)
# join them
return "".join(text[:nlines] + [last_line])
return "".join([*text[:nlines], last_line])
def _wrappedText(self) -> list[str]:
return _GenericEliding.wrapText(self._text, self.width(), self.font())

View File

@@ -132,7 +132,7 @@ class IconOpts:
def dict(self) -> IconOptionDict:
# not using asdict due to pickle errors on animation
d = {k: v for k, v in vars(self).items() if v is not _Unset}
return cast(IconOptionDict, d)
return cast("IconOptionDict", d)
@dataclass
@@ -151,7 +151,7 @@ class _IconOptions:
def dict(self) -> IconOptionDict:
# not using asdict due to pickle errors on animation
return cast(IconOptionDict, vars(self))
return cast("IconOptionDict", vars(self))
class _QFontIconEngine(QIconEngine):
@@ -167,7 +167,7 @@ class _QFontIconEngine(QIconEngine):
@property
def _default_opts(self) -> _IconOptions:
return cast(_IconOptions, self._opts[QIcon.State.Off][QIcon.Mode.Normal])
return cast("_IconOptions", self._opts[QIcon.State.Off][QIcon.Mode.Normal])
def _add_opts(self, state: QIcon.State, mode: QIcon.Mode, opts: IconOpts) -> None:
self._opts[state][mode] = self._default_opts._update(opts)
@@ -358,7 +358,7 @@ class QFontIconStore(QObject):
def __init__(self, parent: QObject | None = None) -> None:
super().__init__(parent=parent)
if tuple(cast(str, QT_VERSION).split(".")) < ("6", "0"):
if tuple(cast("str", QT_VERSION).split(".")) < ("6", "0"):
# QT6 drops this
QApplication.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps)
@@ -480,7 +480,7 @@ class QFontIconStore(QObject):
# in Qt6, everything becomes a static member
QFd: QFontDatabase | type[QFontDatabase] = (
QFontDatabase()
if tuple(cast(str, QT_VERSION).split(".")) < ("6", "0")
if tuple(cast("str", QT_VERSION).split(".")) < ("6", "0")
else QFontDatabase
)

View File

@@ -17,7 +17,7 @@ class QSearchableTreeWidget(QWidget):
into the `filter` line edit. An item is only shown if its, any of its ancestors',
or any of its descendants' keys or values match this pattern.
The regular expression follows the conventions described by the Qt docs:
https://doc.qt.io/qt-5/qregularexpression.html#details
https://doc.qt.io/qt-6/qregularexpression.html#details
Attributes
----------

View File

@@ -1,20 +1,18 @@
from __future__ import annotations
import contextlib
from enum import IntEnum, IntFlag, auto
from functools import partial
from typing import TYPE_CHECKING, Any, overload
from qtpy import QtGui
from qtpy.QtCore import Property, QPoint, QSize, Qt, Signal
from qtpy.QtGui import QFontMetrics, QValidator
from qtpy.QtGui import QDoubleValidator, QFontMetrics, QValidator
from qtpy.QtWidgets import (
QAbstractSlider,
QBoxLayout,
QDoubleSpinBox,
QHBoxLayout,
QLineEdit,
QSlider,
QSpinBox,
QStyle,
QStyleOptionSpinBox,
QVBoxLayout,
@@ -83,7 +81,7 @@ class _SliderProxy:
def setPageStep(self, step: int) -> None:
self._slider.setPageStep(step)
def setRange(self, min: int, max: int) -> None:
def setRange(self, min: float, max: float) -> None:
self._slider.setRange(min, max)
def tickInterval(self) -> int:
@@ -185,6 +183,7 @@ class QLabeledSlider(_SliderProxy, QAbstractSlider):
self._slider = self._slider_class(parent=self)
self._label = SliderLabel(self._slider, connect=self._setValue, parent=self)
self._edge_label_mode: EdgeLabelMode = EdgeLabelMode.LabelIsValue
self._edge_label_position: LabelPosition = LabelPosition.LabelsRight
self._rename_signals()
self._slider.actionTriggered.connect(self.actionTriggered.emit)
@@ -205,18 +204,29 @@ class QLabeledSlider(_SliderProxy, QAbstractSlider):
marg = (0, 0, 0, 0)
if orientation == Qt.Orientation.Vertical:
layout = QVBoxLayout()
layout.addWidget(self._slider, alignment=Qt.AlignmentFlag.AlignHCenter)
layout.addWidget(self._label, alignment=Qt.AlignmentFlag.AlignHCenter)
if not self._edge_label_position:
layout.addWidget(self._slider, alignment=Qt.AlignmentFlag.AlignHCenter)
elif self._edge_label_position == LabelPosition.LabelsBelow:
layout.addWidget(self._slider, alignment=Qt.AlignmentFlag.AlignHCenter)
layout.addWidget(self._label, alignment=Qt.AlignmentFlag.AlignHCenter)
else:
layout.addWidget(self._label, alignment=Qt.AlignmentFlag.AlignHCenter)
layout.addWidget(self._slider, alignment=Qt.AlignmentFlag.AlignHCenter)
self._label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.setSpacing(1)
else:
if self._edge_label_mode == EdgeLabelMode.NoLabel:
marg = (0, 0, 5, 0)
layout = QHBoxLayout() # type: ignore
layout.addWidget(self._slider)
layout.addWidget(self._label)
self._label.setAlignment(Qt.AlignmentFlag.AlignRight)
if not self._edge_label_position:
layout.addWidget(self._slider)
elif self._edge_label_position == LabelPosition.LabelsRight:
layout.addWidget(self._slider)
layout.addWidget(self._label)
self._label.setAlignment(Qt.AlignmentFlag.AlignRight)
else:
layout.addWidget(self._label)
layout.addWidget(self._slider)
self._label.setAlignment(Qt.AlignmentFlag.AlignLeft)
marg = (0, 0, 5, 0)
layout.setSpacing(6)
old_layout = self.layout()
@@ -249,27 +259,46 @@ class QLabeledSlider(_SliderProxy, QAbstractSlider):
)
self._edge_label_mode = opt
self._on_slider_range_changed(self.minimum(), self.maximum())
if not self._edge_label_mode:
self._label.hide()
w = 5 if self.orientation() == Qt.Orientation.Horizontal else 0
self.layout().setContentsMargins(0, 0, w, 0)
if self._edge_label_position == LabelPosition.LabelsRight:
self.layout().setContentsMargins(0, 0, w, 0)
elif self._edge_label_position == LabelPosition.LabelsLeft:
self.layout().setContentsMargins(0, 0, 0, w)
if opt & EdgeLabelMode.LabelIsValue:
if self.isVisible():
self._label.show()
self._label.setMode(opt)
self._label.setValue(self._slider.value())
self.layout().setContentsMargins(0, 0, 0, 0)
self._on_slider_range_changed(self.minimum(), self.maximum())
def edgeLabelPosition(self) -> LabelPosition:
"""Return where/whether a label is shown at the edge of the slider."""
return self._edge_label_position
def setEdgeLabelPosition(self, opt: LabelPosition) -> None:
"""Set where/whether a label is shown at the edge of the slider."""
if opt is LabelPosition.LabelsOnHandle:
raise ValueError("position cannot be 'LabelPosition.LabelsOnHandle'")
self._edge_label_position = opt
self._label.setVisible(bool(opt))
# TODO: make double clickable to edit
self.setOrientation(self.orientation())
# putting this after labelMode methods for the sake of mypy
EdgeLabelMode = EdgeLabelMode
LabelPosition = LabelPosition
# --------------------- private api --------------------
def _on_slider_range_changed(self, min_: int, max_: int) -> None:
slash = " / " if self._edge_label_mode & EdgeLabelMode.LabelIsValue else ""
if self._edge_label_mode & EdgeLabelMode.LabelIsRange:
self._label.setSuffix(f"{slash}{max_}")
self._label.setSuffix(f" / {max_}")
else:
self._label.setSuffix("")
self.rangeChanged.emit(min_, max_)
def _on_slider_value_changed(self, v: Any) -> None:
@@ -629,7 +658,7 @@ class QLabeledDoubleRangeSlider(QLabeledRangeSlider):
"""The color of the bar between the first and last handle."""
class SliderLabel(QDoubleSpinBox):
class SliderLabel(QLineEdit):
def __init__(
self,
slider: QSlider,
@@ -639,52 +668,139 @@ class SliderLabel(QDoubleSpinBox):
) -> None:
super().__init__(parent=parent)
self._slider = slider
self._prefix = ""
self._suffix = ""
self._min = slider.minimum()
self._max = slider.maximum()
self._value = self._min
self._callback = connect
self._decimals = -1
self.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
self.setMode(EdgeLabelMode.LabelIsValue)
self.setDecimals(0)
self.setText(str(self._value))
validator = QDoubleValidator(self)
validator.setNotation(QDoubleValidator.Notation.ScientificNotation)
self.setValidator(validator)
self.setRange(slider.minimum(), slider.maximum())
slider.rangeChanged.connect(self._update_size)
self.setAlignment(alignment)
self.setButtonSymbols(QSpinBox.ButtonSymbols.NoButtons)
self.setStyleSheet("background:transparent; border: 0;")
if connect is not None:
self.editingFinished.connect(lambda: connect(self.value()))
self.editingFinished.connect(self._editing_finished)
self.editingFinished.connect(self._silent_clear_focus)
self._update_size()
def _editing_finished(self):
self._silent_clear_focus()
self.setValue(float(self.text()))
if self._callback:
self._callback(self.value())
def setRange(self, min_: float, max_: float) -> None:
if self._mode == EdgeLabelMode.LabelIsRange:
max_val = max(abs(min_), abs(max_))
n_digits = max(len(str(int(max_val))), 7)
upper_bound = int("9" * n_digits)
self._min = -upper_bound
self._max = upper_bound
self._update_size()
else:
max_ = max(max_, min_)
self._min = min_
self._max = max_
def setDecimals(self, prec: int) -> None:
super().setDecimals(prec)
# super().setDecimals(prec)
self._decimals = prec
self._update_size()
def decimals(self) -> int:
"""Return the number of decimals used in the label."""
return self._decimals
def value(self) -> float:
return self._value
def setValue(self, val: Any) -> None:
super().setValue(val)
if val < self._min:
val = self._min
elif val > self._max:
val = self._max
self._value = val
self.updateText()
def updateText(self) -> None:
val = float(self._value)
use_scientific = (abs(val) < 0.0001 or abs(val) > 9999999.0) and val != 0.0
font_metrics = QFontMetrics(self.font())
eight_len = _fm_width(font_metrics, "8")
available_chars = self.width() // eight_len
total, _fraction = f"{val:.<f}".split(".")
if len(total) > available_chars:
use_scientific = True
if self._decimals < 0:
if use_scientific:
mantissa, exponent = f"{val:.{available_chars}e}".split("e")
mantissa = mantissa.rstrip("0").rstrip(".")
if len(mantissa) + len(exponent) + 1 < available_chars:
text = f"{mantissa}e{exponent}"
else:
decimals = max(available_chars - len(exponent) - 3, 2)
text = f"{val:.{decimals}e}"
else:
decimals = max(available_chars - len(total) - 1, 2)
text = f"{val:.{decimals}f}"
text = text.rstrip("0").rstrip(".")
else:
if use_scientific:
mantissa, exponent = f"{val:.{self._decimals}e}".split("e")
mantissa = mantissa.rstrip("0").rstrip(".")
text = f"{mantissa}e{exponent}"
else:
text = f"{val:.{self._decimals}f}"
if text == "":
text = "0"
self.setText(text)
if self._mode == EdgeLabelMode.LabelIsRange:
self._update_size()
def setMaximum(self, max: float) -> None:
super().setMaximum(max)
if self._mode == EdgeLabelMode.LabelIsValue:
self._update_size()
def minimum(self):
return self._min
def setMinimum(self, min: float) -> None:
super().setMinimum(min)
if self._mode == EdgeLabelMode.LabelIsValue:
self._update_size()
def setMaximum(self, max_: float) -> None:
self.setRange(self._min, max_)
def maximum(self):
return self._max
def setMinimum(self, min_: float) -> None:
self.setRange(min_, self._max)
def setMode(self, opt: EdgeLabelMode) -> None:
# when the edge labels are controlling slider range,
# we want them to have a big range, but not have a huge label
self._mode = opt
if opt == EdgeLabelMode.LabelIsRange:
self.setMinimum(-9999999)
self.setMaximum(9999999)
with contextlib.suppress(Exception):
self._slider.rangeChanged.disconnect(self.setRange)
else:
self.setMinimum(self._slider.minimum())
self.setMaximum(self._slider.maximum())
self._slider.rangeChanged.connect(self.setRange)
self.setRange(self._slider.minimum(), self._slider.maximum())
self._update_size()
def prefix(self) -> str:
return self._prefix
def setPrefix(self, prefix: str) -> None:
self._prefix = prefix
self._update_size()
def suffix(self) -> str:
return self._suffix
def setSuffix(self, suffix: str) -> None:
self._suffix = suffix
self._update_size()
# --------------- private ----------------
@@ -701,21 +817,19 @@ class SliderLabel(QDoubleSpinBox):
if self._mode & EdgeLabelMode.LabelIsValue:
# determine width based on min/max/specialValue
mintext = self.textFromValue(self.minimum())[:18]
maxtext = self.textFromValue(self.maximum())[:18]
mintext = str(self.minimum())[:18]
maxtext = str(self.maximum())[:18]
w = max(0, _fm_width(fm, mintext + fixed_content))
w = max(w, _fm_width(fm, maxtext + fixed_content))
if self.specialValueText():
w = max(w, _fm_width(fm, self.specialValueText()))
if self._mode & EdgeLabelMode.LabelIsRange:
w += 8 # it seems as thought suffix() is not enough
else:
w = max(0, _fm_width(fm, self.textFromValue(self.value()))) + 3
w = max(0, _fm_width(fm, str(self.value()))) + 3
w += 3 # cursor blinking space
# get the final size hint
opt = QStyleOptionSpinBox()
self.initStyleOption(opt)
# self.initStyleOption(opt)
size = self.style().sizeFromContents(
QStyle.ContentsType.CT_SpinBox, opt, QSize(w, h), self
)

View File

@@ -10,7 +10,7 @@ class _IntMixin:
self._singleStep = 1
def _type_cast(self, value) -> int:
return int(round(value))
return round(value)
class _FloatMixin:

View File

@@ -1,3 +1,4 @@
import sys
from typing import TYPE_CHECKING
from qtpy.QtGui import QImage
@@ -37,4 +38,8 @@ def qimage_to_array(img: QImage) -> "np.ndarray":
arr = np.frombuffer(b, np.uint8).reshape(h, w, c)
# reverse channel colors for numpy
return arr.take([2, 1, 0, 3], axis=2)
# On big endian we need to specify a different order
if sys.byteorder == "big":
return arr.take([1, 2, 3, 0], axis=2) # pragma: no cover
else:
return arr.take([2, 1, 0, 3], axis=2)

View File

@@ -766,7 +766,7 @@ def thread_worker(
############################################################################
# This is a variant on the above pattern, it uses QThread instead of Qrunnable
# see https://doc.qt.io/qt-5/threads-technologies.html#comparison-of-solutions
# see https://doc.qt.io/qt-6/threads-technologies.html#comparison-of-solutions
# (it appears from that table that QRunnable cannot emit or receive signals,
# but we circumvent that here with our WorkerBase class that also inherits from
# QObject... providing signals/slots).
@@ -777,7 +777,7 @@ def thread_worker(
#
# However, a disadvantage is that you have no access to (and therefore less
# control over) the QThread itself. See for example all of the methods
# provided on the QThread object: https://doc.qt.io/qt-5/qthread.html
# provided on the QThread object: https://doc.qt.io/qt-6/qthread.html
if TYPE_CHECKING:
@@ -808,7 +808,7 @@ def new_worker_qthread(
standard "single-threaded" signals & slots, note that inter-thread
signals and slots (automatically) use an event-based QueuedConnection, while
intra-thread signals use a DirectConnection. See [Signals and Slots Across
Threads](https://doc.qt.io/qt-5/threads-qobject.html#signals-and-slots-across-threads>)
Threads](https://doc.qt.io/qt-6/threads-qobject.html#signals-and-slots-across-threads>)
Parameters
----------