mirror of
https://github.com/pyapp-kit/superqt.git
synced 2025-07-21 12:11:07 +02:00
Compare commits
20 Commits
53732c0bc9
...
6038291bd6
Author | SHA1 | Date | |
---|---|---|---|
|
6038291bd6 | ||
|
0ec5cd3a2f | ||
|
8f62b0b00d | ||
|
4a0aaca2e9 | ||
|
2d49e77c3d | ||
|
ba495a5e72 | ||
|
12f10be8da | ||
|
9ca0bbf858 | ||
|
0ab6758972 | ||
|
d2bc3d898c | ||
|
1bb1a58a73 | ||
|
1288250597 | ||
|
34a776e8d0 | ||
|
146644e105 | ||
|
e7873ad93d | ||
|
0396d465e2 | ||
|
4bf73c37f1 | ||
|
d407af2089 | ||
|
16f9ef9d3d | ||
|
56f65ff123 |
37
.github/workflows/test_and_deploy.yml
vendored
37
.github/workflows/test_and_deploy.yml
vendored
@@ -16,17 +16,17 @@ 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' }}
|
||||
coverage-upload: artifact
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ubuntu-latest, windows-latest, macos-latest]
|
||||
platform: [ubuntu-latest, windows-latest, macos-13]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||
backend: [pyqt5, pyside2, pyqt6]
|
||||
exclude:
|
||||
@@ -36,25 +36,23 @@ jobs:
|
||||
# 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"
|
||||
platform: macos-latest
|
||||
backend: pyside6
|
||||
backend: "'pyside6!=6.6.2'"
|
||||
- python-version: "3.11"
|
||||
platform: macos-latest
|
||||
backend: pyside6
|
||||
backend: "'pyside6!=6.6.2'"
|
||||
- python-version: "3.10"
|
||||
platform: windows-latest
|
||||
backend: pyside6
|
||||
backend: "'pyside6!=6.6.2'"
|
||||
- python-version: "3.11"
|
||||
platform: windows-latest
|
||||
backend: pyside6
|
||||
|
||||
backend: "'pyside6!=6.6.2'"
|
||||
- python-version: "3.12"
|
||||
platform: macos-latest
|
||||
backend: pyqt6
|
||||
|
||||
# legacy Qt
|
||||
- python-version: 3.8
|
||||
platform: ubuntu-latest
|
||||
@@ -67,25 +65,34 @@ jobs:
|
||||
backend: "pyqt5==5.14.*"
|
||||
|
||||
test-qt-minreqs:
|
||||
uses: pyapp-kit/workflows/.github/workflows/test-pyrepo.yml@v1
|
||||
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:
|
||||
@@ -124,6 +131,6 @@ jobs:
|
||||
twine check dist/*
|
||||
twine upload dist/*
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
generate_release_notes: true
|
||||
|
@@ -4,31 +4,20 @@ ci:
|
||||
autoupdate_commit_msg: "ci: [pre-commit.ci] autoupdate"
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: check-docstring-first
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.12.1
|
||||
hooks:
|
||||
- id: black
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.1.9
|
||||
rev: v0.4.3
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: ["--fix"]
|
||||
args: [--fix, --unsafe-fixes]
|
||||
- id: ruff-format
|
||||
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.15
|
||||
rev: v0.16
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.8.0
|
||||
rev: v1.10.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: tests|examples
|
||||
|
80
CHANGELOG.md
80
CHANGELOG.md
@@ -1,5 +1,79 @@
|
||||
# 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)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- fix: fix inverted appearance [\#240](https://github.com/pyapp-kit/superqt/pull/240) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- ci: \[pre-commit.ci\] autoupdate [\#238](https://github.com/pyapp-kit/superqt/pull/238) ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci))
|
||||
|
||||
## [v0.6.3](https://github.com/pyapp-kit/superqt/tree/v0.6.3) (2024-03-27)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.6.2...v0.6.3)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- fix: fix sliderReleased, sliderPressed signals, and setTracking [\#237](https://github.com/pyapp-kit/superqt/pull/237) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- ci\(dependabot\): bump softprops/action-gh-release from 1 to 2 [\#236](https://github.com/pyapp-kit/superqt/pull/236) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
|
||||
## [v0.6.2](https://github.com/pyapp-kit/superqt/tree/v0.6.2) (2024-03-06)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.6.1...v0.6.2)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- feat: make toggle button public in QCollapsible [\#232](https://github.com/pyapp-kit/superqt/pull/232) ([tlambert03](https://github.com/tlambert03))
|
||||
- feat: add addKey method to QIconifyIcon [\#218](https://github.com/pyapp-kit/superqt/pull/218) ([tlambert03](https://github.com/tlambert03))
|
||||
- feat: Add QIconifyIcon.name\(\) method [\#213](https://github.com/pyapp-kit/superqt/pull/213) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- fix: don't use AbstractContextManager for exceptions\_as\_dialog [\#234](https://github.com/pyapp-kit/superqt/pull/234) ([tlambert03](https://github.com/tlambert03))
|
||||
- fix: Check min max versus current value [\#221](https://github.com/pyapp-kit/superqt/pull/221) ([psobolewskiPhD](https://github.com/psobolewskiPhD))
|
||||
- fix: better default size policy for qcollapsible [\#217](https://github.com/pyapp-kit/superqt/pull/217) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- style: use ruff format instead of black, update pre-commit, restrict pyside6 tests [\#235](https://github.com/pyapp-kit/superqt/pull/235) ([tlambert03](https://github.com/tlambert03))
|
||||
- ci: \[pre-commit.ci\] autoupdate [\#228](https://github.com/pyapp-kit/superqt/pull/228) ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci))
|
||||
- ci\(dependabot\): bump actions/setup-python from 4 to 5 [\#225](https://github.com/pyapp-kit/superqt/pull/225) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- ci: \[pre-commit.ci\] autoupdate [\#223](https://github.com/pyapp-kit/superqt/pull/223) ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci))
|
||||
- ci: \[pre-commit.ci\] autoupdate [\#216](https://github.com/pyapp-kit/superqt/pull/216) ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci))
|
||||
- ci: use reusable test workflow [\#215](https://github.com/pyapp-kit/superqt/pull/215) ([tlambert03](https://github.com/tlambert03))
|
||||
- build: remove packaging dep [\#212](https://github.com/pyapp-kit/superqt/pull/212) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
## [v0.6.1](https://github.com/pyapp-kit/superqt/tree/v0.6.1) (2023-10-10)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.6.0...v0.6.1)
|
||||
@@ -382,17 +456,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.0rc0...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.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)*
|
||||
|
@@ -37,7 +37,7 @@ def define_env(env: "MacrosPlugin"):
|
||||
)
|
||||
src = src.replace("app.exec_()", "")
|
||||
|
||||
exec(src) # noqa: S102
|
||||
exec(src)
|
||||
_grab(dest, width)
|
||||
return (
|
||||
f""
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Example for QCollapsible."""
|
||||
|
||||
from qtpy.QtWidgets import QApplication, QLabel, QPushButton
|
||||
|
||||
from superqt import QCollapsible
|
||||
|
@@ -49,7 +49,6 @@ dependencies = [
|
||||
[project.optional-dependencies]
|
||||
test = ["pint", "pytest", "pytest-cov", "pytest-qt", "numpy", "cmap", "pyconify"]
|
||||
dev = [
|
||||
"black",
|
||||
"ipython",
|
||||
"ruff",
|
||||
"mypy",
|
||||
@@ -66,9 +65,10 @@ pyside2 = ["pyside2"]
|
||||
# see issues surrounding usage of Generics in pyside6.5.x
|
||||
# https://github.com/pyapp-kit/superqt/pull/177
|
||||
# https://github.com/pyapp-kit/superqt/pull/164
|
||||
pyside6 = ["pyside6 !=6.5.0,!=6.5.1"]
|
||||
# https://bugreports.qt.io/browse/PYSIDE-2627
|
||||
pyside6 = ["pyside6 !=6.5.0,!=6.5.1,!=6.6.2"]
|
||||
pyqt5 = ["pyqt5"]
|
||||
pyqt6 = ["pyqt6"]
|
||||
pyqt6 = ["pyqt6<6.7"]
|
||||
font-fa5 = ["fonticon-fontawesome5"]
|
||||
font-fa6 = ["fonticon-fontawesome6"]
|
||||
font-mi6 = ["fonticon-materialdesignicons6"]
|
||||
@@ -111,49 +111,44 @@ matrix.qt.extra-dependencies = [
|
||||
{value = "pyqt5==5.12", if = ["pyqt5.12"]},
|
||||
]
|
||||
|
||||
# https://pycqa.github.io/isort/docs/configuration/options.html
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
src_paths = ["src/superqt", "tests"]
|
||||
|
||||
# https://github.com/charliermarsh/ruff
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
target-version = "py38"
|
||||
src = ["src", "tests"]
|
||||
|
||||
# https://docs.astral.sh/ruff/rules
|
||||
[tool.ruff.lint]
|
||||
pydocstyle = { convention = "numpy" }
|
||||
select = [
|
||||
"E", # style errors
|
||||
"W", # style warnings
|
||||
"F", # flakes
|
||||
"W", # flakes
|
||||
"D", # pydocstyle
|
||||
"D417", # Missing argument descriptions in Docstrings
|
||||
"I", # isort
|
||||
"UP", # pyupgrade
|
||||
"S", # bandit
|
||||
"C4", # flake8-comprehensions
|
||||
"B", # flake8-bugbear
|
||||
"A001", # flake8-builtins
|
||||
"RUF", # ruff-specific rules
|
||||
"TID", # tidy imports
|
||||
"TCH", # flake8-type-checking
|
||||
"TID", # flake8-tidy-imports
|
||||
]
|
||||
ignore = [
|
||||
"D100", # Missing docstring in public module
|
||||
"D101", # Missing docstring in public class
|
||||
"D104", # Missing docstring in public package
|
||||
"D107", # Missing docstring in __init__
|
||||
"D203", # 1 blank line required before class docstring
|
||||
"D212", # Multi-line docstring summary should start at the first line
|
||||
"D213", # Multi-line docstring summary should start at the second line
|
||||
"D401", # First line should be in imperative mood
|
||||
"D413", # Missing blank line after last section
|
||||
"D416", # Section name should end with a colon
|
||||
"D401", # First line should be in imperative mood (remove to opt in)
|
||||
]
|
||||
|
||||
|
||||
[tool.ruff.per-file-ignores]
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/*.py" = ["D", "S101"]
|
||||
"examples/demo_widget.py" = ["E501"]
|
||||
"examples/*.py" = ["B", "D"]
|
||||
|
||||
# https://docs.astral.sh/ruff/formatter/
|
||||
[tool.ruff.format]
|
||||
docstring-code-format = true
|
||||
|
||||
# https://docs.pytest.org/en/6.2.x/customize.html
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "6.0"
|
||||
@@ -189,6 +184,7 @@ allow_redefinition = true
|
||||
source = ["superqt"]
|
||||
|
||||
[tool.coverage.report]
|
||||
show_missing = true
|
||||
exclude_lines = [
|
||||
"pragma: no cover",
|
||||
"if TYPE_CHECKING:",
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""superqt is a collection of Qt components for python."""
|
||||
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
@@ -50,8 +51,8 @@ __all__ = [
|
||||
]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .combobox import QColormapComboBox
|
||||
from .spinbox._quantity import QQuantity
|
||||
from .combobox import QColormapComboBox # noqa: TCH004
|
||||
from .spinbox._quantity import QQuantity # noqa: TCH004
|
||||
|
||||
|
||||
def __getattr__(name: str) -> Any:
|
||||
|
@@ -4,7 +4,6 @@ from typing import TYPE_CHECKING, Container
|
||||
|
||||
from cmap import Colormap
|
||||
from qtpy.QtCore import Qt, Signal
|
||||
from qtpy.QtGui import QKeyEvent
|
||||
from qtpy.QtWidgets import QComboBox, QCompleter, QWidget
|
||||
|
||||
from ._cmap_item_delegate import QColormapItemDelegate
|
||||
@@ -13,6 +12,7 @@ from ._cmap_utils import try_cast_colormap
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cmap._catalog import Category, Interpolation
|
||||
from qtpy.QtGui import QKeyEvent
|
||||
|
||||
|
||||
class CmapCatalogComboBox(QComboBox):
|
||||
|
@@ -1,14 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import cast
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from cmap import Colormap
|
||||
from qtpy.QtCore import QModelIndex, QObject, QPersistentModelIndex, QRect, QSize, Qt
|
||||
from qtpy.QtGui import QColor, QPainter
|
||||
from qtpy.QtWidgets import QStyle, QStyledItemDelegate, QStyleOptionViewItem
|
||||
|
||||
from ._cmap_utils import CMAP_ROLE, draw_colormap, pick_font_color, try_cast_colormap
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cmap import Colormap
|
||||
|
||||
DEFAULT_SIZE = QSize(80, 22)
|
||||
DEFAULT_BORDER_COLOR = QColor(Qt.GlobalColor.transparent)
|
||||
|
||||
|
@@ -1,12 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from cmap import Colormap
|
||||
from qtpy.QtCore import Qt
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from qtpy.QtCore import QRect, Qt
|
||||
from qtpy.QtGui import QIcon, QPainter, QPaintEvent, QPalette
|
||||
from qtpy.QtWidgets import QApplication, QLineEdit, QStyle, QWidget
|
||||
|
||||
from ._cmap_utils import draw_colormap, pick_font_color, try_cast_colormap
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cmap import Colormap
|
||||
|
||||
MISSING = QStyle.StandardPixmap.SP_TitleBarContextHelpButton
|
||||
|
||||
|
||||
@@ -99,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
|
||||
@@ -108,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
|
||||
|
@@ -56,12 +56,14 @@ def draw_colormap(
|
||||
from qtpy.QtWidgets import QWidget
|
||||
from superqt.utils import draw_colormap
|
||||
|
||||
viridis = 'viridis' # or cmap.Colormap('viridis')
|
||||
viridis = "viridis" # or cmap.Colormap('viridis')
|
||||
|
||||
|
||||
class W(QWidget):
|
||||
def paintEvent(self, event) -> None:
|
||||
draw_colormap(self, viridis, event.rect())
|
||||
|
||||
|
||||
# or draw onto a QPixmap
|
||||
pm = QPixmap(200, 200)
|
||||
draw_colormap(pm, viridis)
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""A collapsible widget to hide and unhide child widgets."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from qtpy.QtCore import (
|
||||
@@ -66,6 +67,10 @@ class QCollapsible(QFrame):
|
||||
_content.layout().setContentsMargins(QMargins(5, 0, 0, 0))
|
||||
self.setContent(_content)
|
||||
|
||||
def toggleButton(self) -> QPushButton:
|
||||
"""Return the toggle button."""
|
||||
return self._toggle_btn
|
||||
|
||||
def setText(self, text: str) -> None:
|
||||
"""Set the text of the toggle button."""
|
||||
self._toggle_btn.setText(text)
|
||||
|
@@ -13,7 +13,7 @@ __all__ = (
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from superqt.cmap import QColormapComboBox
|
||||
from superqt.cmap import QColormapComboBox # noqa: TCH004
|
||||
|
||||
|
||||
def __getattr__(name: str) -> Any: # pragma: no cover
|
||||
|
@@ -104,7 +104,7 @@ def icon(
|
||||
plugin is installed)
|
||||
|
||||
>>> btn = QPushButton()
|
||||
>>> btn.setIcon(icon('fa5s.smile'))
|
||||
>>> btn.setIcon(icon("fa5s.smile"))
|
||||
|
||||
can also directly import from fonticon_fa5
|
||||
>>> from fonticon_fa5 import FA5S
|
||||
@@ -130,7 +130,7 @@ def icon(
|
||||
... "disabled": {
|
||||
... "color": "green",
|
||||
... "scale_factor": 0.8,
|
||||
... "animation": spin(btn)
|
||||
... "animation": spin(btn),
|
||||
... },
|
||||
... },
|
||||
... )
|
||||
|
@@ -4,7 +4,7 @@ import warnings
|
||||
from collections import abc, defaultdict
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import ClassVar, DefaultDict, Sequence, Tuple, Union, cast
|
||||
from typing import TYPE_CHECKING, ClassVar, DefaultDict, Sequence, Tuple, Union, cast
|
||||
|
||||
from qtpy import QT_VERSION
|
||||
from qtpy.QtCore import QObject, QPoint, QRect, QSize, Qt
|
||||
@@ -25,7 +25,8 @@ from typing_extensions import TypedDict
|
||||
|
||||
from superqt.utils import QMessageHandler
|
||||
|
||||
from ._animations import Animation
|
||||
if TYPE_CHECKING:
|
||||
from ._animations import Animation
|
||||
|
||||
|
||||
class Unset:
|
||||
@@ -157,9 +158,9 @@ class _QFontIconEngine(QIconEngine):
|
||||
|
||||
def __init__(self, options: _IconOptions):
|
||||
super().__init__()
|
||||
self._opts: defaultdict[
|
||||
QIcon.State, dict[QIcon.Mode, _IconOptions | None]
|
||||
] = DefaultDict(dict)
|
||||
self._opts: defaultdict[QIcon.State, dict[QIcon.Mode, _IconOptions | None]] = (
|
||||
DefaultDict(dict)
|
||||
)
|
||||
self._opts[QIcon.State.Off][QIcon.Mode.Normal] = options
|
||||
self.update_hash()
|
||||
|
||||
|
@@ -13,6 +13,8 @@ warnings.warn(
|
||||
|
||||
# forward any requests for superqt.qtcompat.* to qtpy.*
|
||||
class SuperQtImporter(abc.MetaPathFinder):
|
||||
"""Pseudo-importer to forward superqt.qtcompat.* to qtpy.*."""
|
||||
|
||||
def find_spec(self, fullname: str, path, target=None): # type: ignore
|
||||
"""Forward any requests for superqt.qtcompat.* to qtpy.*."""
|
||||
if fullname.startswith(__name__):
|
||||
|
@@ -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
|
||||
|
@@ -19,6 +19,7 @@ So that's what `_GenericSlider` is below.
|
||||
scalar (with one handle per item), and it forms the basis of
|
||||
QRangeSlider.
|
||||
"""
|
||||
|
||||
import os
|
||||
import platform
|
||||
from typing import TypeVar
|
||||
@@ -73,6 +74,7 @@ class _GenericSlider(QSlider):
|
||||
self._position: _T = 0.0
|
||||
self._singleStep = 1.0
|
||||
self._offsetAccumulated = 0.0
|
||||
self._inverted_appearance = False
|
||||
self._blocktracking = False
|
||||
self._tickInterval = 0.0
|
||||
self._pressedControl = SC_NONE
|
||||
@@ -97,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.
|
||||
@@ -173,6 +175,13 @@ class _GenericSlider(QSlider):
|
||||
self._tickInterval = max(0.0, ts)
|
||||
self.update()
|
||||
|
||||
def invertedAppearance(self) -> bool:
|
||||
return self._inverted_appearance
|
||||
|
||||
def setInvertedAppearance(self, inverted: bool) -> None:
|
||||
self._inverted_appearance = inverted
|
||||
self.update()
|
||||
|
||||
def triggerAction(self, action: QSlider.SliderAction) -> None:
|
||||
self._blocktracking = True
|
||||
# other actions here
|
||||
@@ -192,9 +201,8 @@ class _GenericSlider(QSlider):
|
||||
if self.orientation() == Qt.Orientation.Horizontal
|
||||
else not self.invertedAppearance()
|
||||
)
|
||||
option.direction = (
|
||||
Qt.LayoutDirection.LeftToRight
|
||||
) # we use the upsideDown option instead
|
||||
# we use the upsideDown option instead
|
||||
option.direction = Qt.LayoutDirection.LeftToRight
|
||||
# option.sliderValue = self._value # type: ignore
|
||||
# option.singleStep = self._singleStep # type: ignore
|
||||
if self.orientation() == Qt.Orientation.Horizontal:
|
||||
@@ -334,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()
|
||||
|
@@ -3,13 +3,13 @@ from __future__ import annotations
|
||||
import contextlib
|
||||
from enum import IntEnum, IntFlag, auto
|
||||
from functools import partial
|
||||
from typing import Any, overload
|
||||
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:
|
||||
@@ -94,6 +95,36 @@ class _SliderProxy:
|
||||
def setTickPosition(self, pos: QSlider.TickPosition) -> None:
|
||||
self._slider.setTickPosition(pos)
|
||||
|
||||
def triggerAction(self, action: QAbstractSlider.SliderAction) -> None:
|
||||
return self._slider.triggerAction(action)
|
||||
|
||||
def invertedControls(self) -> bool:
|
||||
return self._slider.invertedControls()
|
||||
|
||||
def setInvertedControls(self, a0: bool) -> None:
|
||||
return self._slider.setInvertedControls(a0)
|
||||
|
||||
def invertedAppearance(self) -> bool:
|
||||
return self._slider.invertedAppearance()
|
||||
|
||||
def setInvertedAppearance(self, a0: bool) -> None:
|
||||
return self._slider.setInvertedAppearance(a0)
|
||||
|
||||
def isSliderDown(self) -> bool:
|
||||
return self._slider.isSliderDown()
|
||||
|
||||
def setSliderDown(self, a0: bool) -> None:
|
||||
return self._slider.setSliderDown(a0)
|
||||
|
||||
def hasTracking(self) -> bool:
|
||||
return self._slider.hasTracking()
|
||||
|
||||
def setTracking(self, enable: bool) -> None:
|
||||
return self._slider.setTracking(enable)
|
||||
|
||||
def orientation(self) -> Qt.Orientation:
|
||||
return self._slider.orientation()
|
||||
|
||||
def __getattr__(self, name: Any) -> Any:
|
||||
return getattr(self._slider, name)
|
||||
|
||||
@@ -128,19 +159,20 @@ 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
|
||||
|
||||
@overload
|
||||
def __init__(self, parent: QWidget | None = ...) -> None:
|
||||
...
|
||||
def __init__(self, parent: QWidget | None = ...) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self, orientation: Qt.Orientation, parent: QWidget | None = ...
|
||||
) -> None:
|
||||
...
|
||||
) -> None: ...
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
parent, orientation = _handle_overloaded_slider_sig(args, kwargs)
|
||||
@@ -229,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
|
||||
|
||||
@@ -251,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):
|
||||
@@ -263,14 +294,12 @@ class QLabeledDoubleSlider(QLabeledSlider):
|
||||
_frangeChanged = Signal(float, float)
|
||||
|
||||
@overload
|
||||
def __init__(self, parent: QWidget | None = ...) -> None:
|
||||
...
|
||||
def __init__(self, parent: QWidget | None = ...) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self, orientation: Qt.Orientation, parent: QWidget | None = ...
|
||||
) -> None:
|
||||
...
|
||||
) -> None: ...
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -294,20 +323,20 @@ class QLabeledDoubleSlider(QLabeledSlider):
|
||||
|
||||
class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
|
||||
_valueChanged = Signal(tuple)
|
||||
_sliderPressed = Signal()
|
||||
_sliderReleased = Signal()
|
||||
editingFinished = Signal()
|
||||
|
||||
_slider_class = QRangeSlider
|
||||
_slider: QRangeSlider
|
||||
|
||||
@overload
|
||||
def __init__(self, parent: QWidget | None = ...) -> None:
|
||||
...
|
||||
def __init__(self, parent: QWidget | None = ...) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self, orientation: Qt.Orientation, parent: QWidget | None = ...
|
||||
) -> None:
|
||||
...
|
||||
) -> None: ...
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
parent, orientation = _handle_overloaded_slider_sig(args, kwargs)
|
||||
@@ -315,7 +344,7 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
|
||||
self._rename_signals()
|
||||
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_ShowWithoutActivating)
|
||||
self._handle_labels = []
|
||||
self._handle_labels: list[SliderLabel] = []
|
||||
self._handle_label_position: LabelPosition = LabelPosition.LabelsAbove
|
||||
|
||||
# for fine tuning label position
|
||||
@@ -324,6 +353,8 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
|
||||
|
||||
self._slider = self._slider_class()
|
||||
self._slider.valueChanged.connect(self.valueChanged.emit)
|
||||
self._slider.sliderPressed.connect(self.sliderPressed.emit)
|
||||
self._slider.sliderReleased.connect(self.sliderReleased.emit)
|
||||
self._slider.rangeChanged.connect(self.rangeChanged.emit)
|
||||
self.sliderMoved = self._slider._slidersMoved
|
||||
|
||||
@@ -358,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:
|
||||
@@ -387,27 +418,33 @@ 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:
|
||||
self._on_range_changed(min, max)
|
||||
|
||||
def _add_labels(self, layout: QBoxLayout, inverted: bool = False) -> None:
|
||||
if inverted:
|
||||
first, second = self._max_label, self._min_label
|
||||
else:
|
||||
first, second = self._min_label, self._max_label
|
||||
layout.addWidget(first)
|
||||
layout.addWidget(self._slider)
|
||||
layout.addWidget(second)
|
||||
|
||||
def setOrientation(self, orientation: Qt.Orientation) -> None:
|
||||
"""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)
|
||||
layout.addWidget(self._max_label)
|
||||
layout.addWidget(self._slider)
|
||||
layout.addWidget(self._min_label)
|
||||
self._add_labels(layout, inverted=not inverted)
|
||||
# TODO: set margins based on label width
|
||||
if self._handle_label_position == LabelPosition.LabelsLeft:
|
||||
marg = (30, 0, 0, 0)
|
||||
elif self._handle_label_position == LabelPosition.NoLabel:
|
||||
marg = (0, 0, 0, 0)
|
||||
else:
|
||||
elif self._handle_label_position == LabelPosition.LabelsRight:
|
||||
marg = (0, 0, 20, 0)
|
||||
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
else:
|
||||
@@ -415,13 +452,9 @@ 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)
|
||||
layout.addWidget(self._min_label)
|
||||
layout.addWidget(self._slider)
|
||||
layout.addWidget(self._max_label)
|
||||
self._add_labels(layout, inverted=inverted)
|
||||
|
||||
# remove old layout
|
||||
old_layout = self.layout()
|
||||
@@ -431,10 +464,13 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
|
||||
self.setLayout(layout)
|
||||
layout.setContentsMargins(*marg)
|
||||
super().setOrientation(orientation)
|
||||
QApplication.processEvents()
|
||||
self._reposition_labels()
|
||||
|
||||
def resizeEvent(self, a0) -> None:
|
||||
def setInvertedAppearance(self, a0: bool) -> None:
|
||||
self._slider.setInvertedAppearance(a0)
|
||||
self.setOrientation(self._slider.orientation())
|
||||
|
||||
def resizeEvent(self, a0: Any) -> None:
|
||||
super().resizeEvent(a0)
|
||||
self._reposition_labels()
|
||||
|
||||
@@ -442,9 +478,20 @@ 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
|
||||
self.sliderReleased = self._sliderReleased
|
||||
self.sliderPressed = self._sliderPressed
|
||||
|
||||
def _reposition_labels(self) -> None:
|
||||
if (
|
||||
@@ -455,17 +502,26 @@ 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
|
||||
for i, label in enumerate(self._handle_labels):
|
||||
labels: Iterable[tuple[int, SliderLabel]] = enumerate(self._handle_labels)
|
||||
if self._slider.invertedAppearance():
|
||||
labels = reversed(list(labels))
|
||||
for i, label in labels:
|
||||
rect = self._slider._handleRect(i)
|
||||
dx = -label.width() / 2
|
||||
dx = (-label.width() / 2) + 2
|
||||
dy = -label.height() / 2
|
||||
if labels_above:
|
||||
if labels_above: # or on the right
|
||||
if horizontal:
|
||||
dy *= 3
|
||||
else:
|
||||
dx *= -1
|
||||
elif labels_on_handle:
|
||||
if horizontal:
|
||||
dy += 0.5
|
||||
else:
|
||||
dx += 0.5
|
||||
else:
|
||||
if horizontal:
|
||||
dy *= -1
|
||||
@@ -482,6 +538,7 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
|
||||
label.move(pos)
|
||||
last_edge = pos
|
||||
label.clearFocus()
|
||||
label.raise_()
|
||||
label.show()
|
||||
self.update()
|
||||
|
||||
@@ -545,14 +602,12 @@ class QLabeledDoubleRangeSlider(QLabeledRangeSlider):
|
||||
_frangeChanged = Signal(float, float)
|
||||
|
||||
@overload
|
||||
def __init__(self, parent: QWidget | None = ...) -> None:
|
||||
...
|
||||
def __init__(self, parent: QWidget | None = ...) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self, orientation: Qt.Orientation, parent: QWidget | None = ...
|
||||
) -> None:
|
||||
...
|
||||
) -> None: ...
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -571,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
|
||||
|
@@ -1,7 +1,7 @@
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from superqt.cmap import draw_colormap
|
||||
from superqt.cmap import draw_colormap # noqa: TCH004
|
||||
|
||||
__all__ = (
|
||||
"CodeSyntaxHighlight",
|
||||
|
@@ -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
|
||||
@@ -68,8 +70,6 @@ def ensure_main_thread(
|
||||
timeout: int = 1000,
|
||||
) -> Callable[P, Future[R]]: ...
|
||||
# fmt: on
|
||||
|
||||
|
||||
def ensure_main_thread(
|
||||
func: Callable | None = None, await_return: bool = False, timeout: int = 1000
|
||||
):
|
||||
@@ -132,8 +132,6 @@ def ensure_object_thread(
|
||||
timeout: int = 1000,
|
||||
) -> Callable[P, Future[R]]: ...
|
||||
# fmt: on
|
||||
|
||||
|
||||
def ensure_object_thread(
|
||||
func: Callable | None = None, await_return: bool = False, timeout: int = 1000
|
||||
):
|
||||
|
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import traceback
|
||||
from contextlib import AbstractContextManager
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from qtpy.QtCore import Qt
|
||||
@@ -14,7 +13,7 @@ if TYPE_CHECKING:
|
||||
_DEFAULT_FLAGS = Qt.WindowType.Dialog | Qt.WindowType.MSWindowsFixedSizeDialogHint
|
||||
|
||||
|
||||
class exceptions_as_dialog(AbstractContextManager):
|
||||
class exceptions_as_dialog:
|
||||
"""Context manager that shows a dialog when an exception is raised.
|
||||
|
||||
See examples below for common usage patterns.
|
||||
@@ -66,8 +65,8 @@ class exceptions_as_dialog(AbstractContextManager):
|
||||
exception : BaseException | None
|
||||
Will hold the exception instance if an exception was raised and caught.
|
||||
|
||||
Examplez
|
||||
-------
|
||||
Examples
|
||||
--------
|
||||
```python
|
||||
from qtpy.QtWidgets import QApplication
|
||||
from superqt.utils import exceptions_as_dialog
|
||||
|
@@ -38,7 +38,7 @@ class QMessageHandler:
|
||||
|
||||
>>> logger = logging.getLogger(__name__)
|
||||
>>> with QMessageHandler(logger): # re-reoute Qt messages to a python logger.
|
||||
... ...
|
||||
... ...
|
||||
"""
|
||||
|
||||
_qt2loggertype: ClassVar[dict[QtMsgType, int]] = {
|
||||
|
@@ -23,16 +23,13 @@ if TYPE_CHECKING:
|
||||
|
||||
class SigInst(Generic[_T]):
|
||||
@staticmethod
|
||||
def connect(slot: Callable[[_T], Any], type: type | None = ...) -> None:
|
||||
...
|
||||
def connect(slot: Callable[[_T], Any], type: type | None = ...) -> None: ...
|
||||
|
||||
@staticmethod
|
||||
def disconnect(slot: Callable[[_T], Any] = ...) -> None:
|
||||
...
|
||||
def disconnect(slot: Callable[[_T], Any] = ...) -> None: ...
|
||||
|
||||
@staticmethod
|
||||
def emit(*args: _T) -> None:
|
||||
...
|
||||
def emit(*args: _T) -> None: ...
|
||||
|
||||
from typing_extensions import Literal, ParamSpec
|
||||
|
||||
@@ -52,7 +49,7 @@ _R = TypeVar("_R")
|
||||
|
||||
|
||||
def as_generator_function(
|
||||
func: Callable[_P, _R]
|
||||
func: Callable[_P, _R],
|
||||
) -> Callable[_P, Generator[None, None, _R]]:
|
||||
"""Turns a regular function (single return) into a generator function."""
|
||||
|
||||
@@ -211,7 +208,6 @@ class WorkerBase(QRunnable, Generic[_R]):
|
||||
--------
|
||||
```python
|
||||
class MyWorker(WorkerBase):
|
||||
|
||||
def work(self):
|
||||
i = 0
|
||||
while True:
|
||||
@@ -499,8 +495,7 @@ def create_worker(
|
||||
_worker_class: type[GeneratorWorker] | type[FunctionWorker] | None = None,
|
||||
_ignore_errors: bool = False,
|
||||
**kwargs,
|
||||
) -> GeneratorWorker[_Y, _S, _R]:
|
||||
...
|
||||
) -> GeneratorWorker[_Y, _S, _R]: ...
|
||||
|
||||
|
||||
@overload
|
||||
@@ -512,8 +507,7 @@ def create_worker(
|
||||
_worker_class: type[GeneratorWorker] | type[FunctionWorker] | None = None,
|
||||
_ignore_errors: bool = False,
|
||||
**kwargs,
|
||||
) -> FunctionWorker[_R]:
|
||||
...
|
||||
) -> FunctionWorker[_R]: ...
|
||||
|
||||
|
||||
def create_worker(
|
||||
@@ -574,8 +568,10 @@ def create_worker(
|
||||
```python
|
||||
def long_function(duration):
|
||||
import time
|
||||
|
||||
time.sleep(duration)
|
||||
|
||||
|
||||
worker = create_worker(long_function, 10)
|
||||
```
|
||||
"""
|
||||
@@ -630,8 +626,7 @@ def thread_worker(
|
||||
connect: dict[str, Callable | Sequence[Callable]] | None = None,
|
||||
worker_class: type[WorkerBase] | None = None,
|
||||
ignore_errors: bool = False,
|
||||
) -> Callable[_P, GeneratorWorker[_Y, _S, _R]]:
|
||||
...
|
||||
) -> Callable[_P, GeneratorWorker[_Y, _S, _R]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
@@ -641,8 +636,7 @@ def thread_worker(
|
||||
connect: dict[str, Callable | Sequence[Callable]] | None = None,
|
||||
worker_class: type[WorkerBase] | None = None,
|
||||
ignore_errors: bool = False,
|
||||
) -> Callable[_P, FunctionWorker[_R]]:
|
||||
...
|
||||
) -> Callable[_P, FunctionWorker[_R]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
@@ -652,8 +646,7 @@ def thread_worker(
|
||||
connect: dict[str, Callable | Sequence[Callable]] | None = None,
|
||||
worker_class: type[WorkerBase] | None = None,
|
||||
ignore_errors: bool = False,
|
||||
) -> Callable[[Callable], Callable[_P, FunctionWorker | GeneratorWorker]]:
|
||||
...
|
||||
) -> Callable[[Callable], Callable[_P, FunctionWorker | GeneratorWorker]]: ...
|
||||
|
||||
|
||||
def thread_worker(
|
||||
@@ -737,7 +730,8 @@ def thread_worker(
|
||||
yield i
|
||||
|
||||
# do teardown
|
||||
return 'anything'
|
||||
return "anything"
|
||||
|
||||
|
||||
# call the function to start running in another thread.
|
||||
worker = long_function()
|
||||
@@ -790,8 +784,7 @@ if TYPE_CHECKING:
|
||||
class WorkerProtocol(QObject):
|
||||
finished: Signal
|
||||
|
||||
def work(self) -> None:
|
||||
...
|
||||
def work(self) -> None: ...
|
||||
|
||||
|
||||
def new_worker_qthread(
|
||||
@@ -846,9 +839,7 @@ def new_worker_qthread(
|
||||
Create some QObject that has a long-running work method:
|
||||
|
||||
```python
|
||||
|
||||
class Worker(QObject):
|
||||
|
||||
finished = Signal()
|
||||
increment = Signal(int)
|
||||
|
||||
@@ -860,16 +851,18 @@ def new_worker_qthread(
|
||||
def work(self):
|
||||
# some long running task...
|
||||
import time
|
||||
|
||||
for i in range(10):
|
||||
time.sleep(1)
|
||||
self.increment.emit(i)
|
||||
self.finished.emit()
|
||||
|
||||
|
||||
worker, thread = new_worker_qthread(
|
||||
Worker,
|
||||
'argument',
|
||||
"argument",
|
||||
_start_thread=True,
|
||||
_connect={'increment': print},
|
||||
_connect={"increment": print},
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
@@ -26,6 +26,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from concurrent.futures import Future
|
||||
@@ -301,8 +302,7 @@ def qthrottled(
|
||||
leading: bool = True,
|
||||
timer_type: Qt.TimerType = Qt.TimerType.PreciseTimer,
|
||||
parent: QObject | None = None,
|
||||
) -> ThrottledCallable[P, R]:
|
||||
...
|
||||
) -> ThrottledCallable[P, R]: ...
|
||||
|
||||
|
||||
@overload
|
||||
@@ -312,8 +312,7 @@ def qthrottled(
|
||||
leading: bool = True,
|
||||
timer_type: Qt.TimerType = Qt.TimerType.PreciseTimer,
|
||||
parent: QObject | None = None,
|
||||
) -> Callable[[Callable[P, R]], ThrottledCallable[P, R]]:
|
||||
...
|
||||
) -> Callable[[Callable[P, R]], ThrottledCallable[P, R]]: ...
|
||||
|
||||
|
||||
def qthrottled(
|
||||
@@ -364,8 +363,7 @@ def qdebounced(
|
||||
leading: bool = False,
|
||||
timer_type: Qt.TimerType = Qt.TimerType.PreciseTimer,
|
||||
parent: QObject | None = None,
|
||||
) -> ThrottledCallable[P, R]:
|
||||
...
|
||||
) -> ThrottledCallable[P, R]: ...
|
||||
|
||||
|
||||
@overload
|
||||
@@ -375,8 +373,7 @@ def qdebounced(
|
||||
leading: bool = False,
|
||||
timer_type: Qt.TimerType = Qt.TimerType.PreciseTimer,
|
||||
parent: QObject | None = None,
|
||||
) -> Callable[[Callable[P, R]], ThrottledCallable[P, R]]:
|
||||
...
|
||||
) -> Callable[[Callable[P, R]], ThrottledCallable[P, R]]: ...
|
||||
|
||||
|
||||
def qdebounced(
|
||||
|
Reference in New Issue
Block a user