mirror of
https://github.com/pyapp-kit/superqt.git
synced 2025-07-21 04:01:07 +02:00
Compare commits
12 Commits
v0.7.3
...
17fd211740
Author | SHA1 | Date | |
---|---|---|---|
|
17fd211740 | ||
|
3b83a8a1e2 | ||
|
13e033e4a2 | ||
|
55b66393c3 | ||
|
b495c70206 | ||
|
a9fa720577 | ||
|
257d97ae0f | ||
|
7193480796 | ||
|
788d0f0325 | ||
|
935025eacc | ||
|
358d041c0d | ||
|
49a8114843 |
6
.github/workflows/test_and_deploy.yml
vendored
6
.github/workflows/test_and_deploy.yml
vendored
@@ -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
|
||||
|
@@ -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
|
||||
|
39
CHANGELOG.md
39
CHANGELOG.md
@@ -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)*
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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.)
|
||||
|
||||
|
@@ -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`.
|
||||
|
||||
|
@@ -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
|
||||
|
24
docs/widgets/qtoggleswitch.md
Normal file
24
docs/widgets/qtoggleswitch.md
Normal 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') }}
|
@@ -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))
|
||||
|
@@ -51,7 +51,7 @@ test = [
|
||||
"pint",
|
||||
"pytest",
|
||||
"pytest-cov",
|
||||
"pytest-qt",
|
||||
"pytest-qt==4.4.0",
|
||||
"numpy",
|
||||
"cmap",
|
||||
"pyconify",
|
||||
|
@@ -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())
|
||||
|
@@ -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
|
||||
)
|
||||
|
||||
|
@@ -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
|
||||
----------
|
||||
|
@@ -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
|
||||
)
|
||||
|
@@ -10,7 +10,7 @@ class _IntMixin:
|
||||
self._singleStep = 1
|
||||
|
||||
def _type_cast(self, value) -> int:
|
||||
return int(round(value))
|
||||
return round(value)
|
||||
|
||||
|
||||
class _FloatMixin:
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
----------
|
||||
|
Reference in New Issue
Block a user