Compare commits

...

29 Commits

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

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

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

---------

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

* merge in fixes

* remove comment

* fix hint

* fix napari

* change pyqt6

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

* add lxml_html_clean

* fix napari version name

* breakout coverage

* use main

* cump

* bump again

* bump

* bump

* skip more napari tests

* add always

* back to v1

* try editabel

* use main again

* remove editable

* editable again

* bump

* bump

* bump

* use v2
2024-05-06 15:29:43 -04:00
Talley Lambert
9ca0bbf858 chore: changelog v0.6.4 2024-04-25 15:56:57 -04:00
Talley Lambert
0ab6758972 fix: fix inverted appearance (#240)
* fix: fix inverted appearance

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

* pass codecov token

* inherit secrets

* explicitly pass token

* pin "'PyQt6<6.7'"

* pin upper pyqt6

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-25 15:55:02 -04:00
Talley Lambert
d2bc3d898c drop to macos-13 2024-04-25 13:54:44 -04:00
Talley Lambert
1bb1a58a73 inherit secret 2024-04-22 14:08:53 -04:00
Talley Lambert
1288250597 add secret 2024-04-22 13:51:54 -04:00
pre-commit-ci[bot]
34a776e8d0 ci: [pre-commit.ci] autoupdate (#238)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.0 → v0.3.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.0...v0.3.5)
- [github.com/pre-commit/mirrors-mypy: v1.8.0 → v1.9.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.8.0...v1.9.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-22 13:15:40 -04:00
Talley Lambert
146644e105 chore: changelog v0.6.3 2024-03-27 17:34:31 -04:00
Talley Lambert
e7873ad93d fix: fix sliderReleased, sliderPressed signals, and setTracking (#237) 2024-03-27 17:32:25 -04:00
dependabot[bot]
0396d465e2 ci(dependabot): bump softprops/action-gh-release from 1 to 2 (#236)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 1 to 2.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v1...v2)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 15:56:10 -04:00
Talley Lambert
4bf73c37f1 chore: changelog v0.6.2 2024-03-06 15:48:43 -05:00
Talley Lambert
d407af2089 fix: don't use AbstractContextManager for exceptions_as_dialog (#234)
* fix: don't use AbstractContextManager

* fix docs
2024-03-06 15:46:28 -05:00
Talley Lambert
16f9ef9d3d style: use ruff format instead of black, update pre-commit, restrict pyside6 tests (#235)
* style: use ruff format

* fix import

* disallow pyside 6.6.2

* pin in tests too

* pyside6.4 on windows

* fix greedy imports

* double quote

* run sliders last

* try 6.6.1 again
2024-03-06 15:42:51 -05:00
Talley Lambert
56f65ff123 feat: make toggle button public in QCollapsible (#232) 2024-03-06 15:27:56 -05:00
pre-commit-ci[bot]
60188de52e ci: [pre-commit.ci] autoupdate (#228)
updates:
- [github.com/psf/black: 23.11.0 → 23.12.1](https://github.com/psf/black/compare/23.11.0...23.12.1)
- [github.com/astral-sh/ruff-pre-commit: v0.1.6 → v0.1.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.6...v0.1.9)
- [github.com/pre-commit/mirrors-mypy: v1.7.1 → v1.8.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.7.1...v1.8.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-01-09 10:09:23 -05:00
pre-commit-ci[bot]
b4d3a4f9b7 ci: [pre-commit.ci] autoupdate (#223)
updates:
- [github.com/psf/black: 23.10.1 → 23.11.0](https://github.com/psf/black/compare/23.10.1...23.11.0)
- [github.com/astral-sh/ruff-pre-commit: v0.1.4 → v0.1.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.4...v0.1.6)
- [github.com/pre-commit/mirrors-mypy: v1.6.1 → v1.7.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.6.1...v1.7.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-12-28 12:03:21 -05:00
dependabot[bot]
95b1178647 ci(dependabot): bump actions/setup-python from 4 to 5 (#225)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-22 17:15:47 -05:00
Peter Sobolewski
ef87685626 Bugfix: Check min max versus current value (#221)
* Check min max vs value

* add test

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

* test min too

* check that max > min per Qt

* update test

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-25 13:29:48 -05:00
Talley Lambert
b927159f49 feat: add addKey method to QIconifyIcon (#218)
* feat: addKey method to Iconify

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

* remove breakpoint

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-08 12:52:41 -05:00
Talley Lambert
61e7409b1c fix: better default size policy for qcollapsible (#217)
* fix: better default size policy for qcollapsible

* fix: fix annotations
2023-11-07 07:44:19 -05:00
Talley Lambert
c9103e3dd8 ci: use reusable test workflow (#215)
* ci: try resuable

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

* remove x

* fix cov

* update

* update

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-06 18:36:36 -05:00
pre-commit-ci[bot]
570c261368 ci: [pre-commit.ci] autoupdate (#216)
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.4.0 → v4.5.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.4.0...v4.5.0)
- [github.com/psf/black: 23.9.1 → 23.10.1](https://github.com/psf/black/compare/23.9.1...23.10.1)
- [github.com/astral-sh/ruff-pre-commit: v0.0.292 → v0.1.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.292...v0.1.4)
- [github.com/abravalheri/validate-pyproject: v0.14 → v0.15](https://github.com/abravalheri/validate-pyproject/compare/v0.14...v0.15)
- [github.com/pre-commit/mirrors-mypy: v1.5.1 → v1.6.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.5.1...v1.6.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-06 16:36:08 -05:00
Talley Lambert
bd6899133f feat: icon.name() (#213) 2023-10-23 11:20:59 -04:00
Talley Lambert
3efafd7aa8 fix: remove old dep (#212) 2023-10-10 16:52:08 -04:00
40 changed files with 492 additions and 303 deletions

View File

@@ -6,23 +6,27 @@ concurrency:
on:
push:
branches:
- main
tags:
- "v*" # Push events to matching v*, i.e. v1.0, v20.15.10
branches: [main]
tags: [v*]
pull_request:
branches:
- main
workflow_dispatch:
schedule:
- cron: "0 0 * * 0" # run weekly
jobs:
test:
name: ${{ matrix.platform }} py${{ matrix.python-version }} ${{ matrix.backend }}
runs-on: ${{ matrix.platform }}
name: Test
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' }}
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:
@@ -32,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
@@ -62,96 +64,43 @@ jobs:
platform: ubuntu-latest
backend: "pyqt5==5.14.*"
steps:
- uses: actions/checkout@v4
test-qt-minreqs:
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
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- uses: tlambert03/setup-qt-libs@v1.4
- name: Linux opengl
if: runner.os == 'Linux' && ( startsWith(matrix.backend, 'pyside6') || startsWith(matrix.backend, 'pyqt6') )
run: sudo apt-get install -y libopengl0 libegl1-mesa libxcb-xinput0
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -e .[test]
python -m pip install ${{ matrix.backend }}
- name: Test
uses: aganders3/headless-gui@v1.2
with:
run: python -m pytest --color=yes --cov=superqt --cov-report=xml
- name: Coverage
uses: codecov/codecov-action@v3
test_old_qtpy:
name: qtpy minreq
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: tlambert03/setup-qt-libs@v1.4
- uses: actions/setup-python@v4
with:
python-version: "3.8"
- name: install
run: |
python -m pip install -U pip
python -m pip install -e .[test,pyqt5]
python -m pip install qtpy==1.1.0 typing-extensions==3.7.4.3
- name: Test
uses: aganders3/headless-gui@v1.2
with:
run: python -m pytest --color=yes
upload_coverage:
if: always()
needs: [test, test-qt-minreqs]
uses: pyapp-kit/workflows/.github/workflows/upload-coverage.yml@v2
secrets: inherit
test_napari:
name: napari tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
path: superqt
- uses: actions/checkout@v4
with:
repository: napari/napari
path: napari-repo
fetch-depth: 2
- uses: tlambert03/setup-qt-libs@v1
- uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: install
run: |
python -m pip install -U pip
python -m pip install ./superqt
python -m pip install ./napari-repo[testing,pyqt5]
- name: Test napari
uses: aganders3/headless-gui@v1.2
with:
working-directory: napari-repo
run: python -m pytest --color=yes napari/_qt
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 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.19.post1"]
qt: ["pyqt5", "pyside2"]
check-manifest:
name: Check Manifest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: "3.x"
- run: pip install check-manifest && check-manifest
- run: pipx run check-manifest
deploy:
# this will run when you have tagged a commit, starting with "v*"
@@ -165,7 +114,7 @@ jobs:
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install dependencies
@@ -182,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

View File

@@ -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.4.0
hooks:
- id: check-docstring-first
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 23.9.1
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.292
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.14
rev: v0.16
hooks:
- id: validate-pyproject
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.5.1
rev: v1.10.0
hooks:
- id: mypy
exclude: tests|examples

View File

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

View File

@@ -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"![{page.title}](../{dest.parent.name}/{dest.name})"

View File

@@ -1,4 +1,4 @@
from PyQt5.QtGui import QColor, QPalette
from qtpy.QtGui import QColor, QPalette
from qtpy.QtWidgets import QApplication, QTextEdit
from superqt.utils import CodeSyntaxHighlight

View File

@@ -88,7 +88,7 @@ class IconPreviewArea(QtWidgets.QWidget):
self.updatePixmapLabels()
def createHeaderLabel(self, text):
label = QtWidgets.QLabel("<b>%s</b>" % text)
label = QtWidgets.QLabel(f"<b>{text}</b>")
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
return label

View File

@@ -1,4 +1,5 @@
"""Example for QCollapsible."""
from qtpy.QtWidgets import QApplication, QLabel, QPushButton
from superqt import QCollapsible

View File

@@ -39,7 +39,6 @@ classifiers = [
]
dynamic = ["version"]
dependencies = [
"packaging",
"pygments>=2.4.0",
"qtpy>=1.1.0",
"typing-extensions >=3.7.4.3,!=3.10.0.0",
@@ -50,7 +49,6 @@ dependencies = [
[project.optional-dependencies]
test = ["pint", "pytest", "pytest-cov", "pytest-qt", "numpy", "cmap", "pyconify"]
dev = [
"black",
"ipython",
"ruff",
"mypy",
@@ -67,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"]
@@ -112,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"
@@ -187,15 +181,17 @@ allow_redefinition = true
# https://coverage.readthedocs.io/en/6.4/config.html
[tool.coverage.run]
source = ["src/superqt"]
source = ["superqt"]
[tool.coverage.report]
show_missing = true
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"@overload",
"except ImportError",
"\\.\\.\\."
"\\.\\.\\.",
"pass"
]
# https://github.com/mgedmin/check-manifest#configuration

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
"""A collapsible widget to hide and unhide child widgets."""
from typing import Optional, Union
from __future__ import annotations
from qtpy.QtCore import (
QEasingCurve,
@@ -12,7 +13,7 @@ from qtpy.QtCore import (
Signal,
)
from qtpy.QtGui import QIcon, QPainter, QPalette, QPixmap
from qtpy.QtWidgets import QFrame, QPushButton, QVBoxLayout, QWidget
from qtpy.QtWidgets import QFrame, QPushButton, QSizePolicy, QVBoxLayout, QWidget
class QCollapsible(QFrame):
@@ -28,9 +29,9 @@ class QCollapsible(QFrame):
def __init__(
self,
title: str = "",
parent: Optional[QWidget] = None,
expandedIcon: Optional[Union[QIcon, str]] = "",
collapsedIcon: Optional[Union[QIcon, str]] = "",
parent: QWidget | None = None,
expandedIcon: QIcon | str | None = "",
collapsedIcon: QIcon | str | None = "",
):
super().__init__(parent)
self._locked = False
@@ -41,13 +42,15 @@ class QCollapsible(QFrame):
self._toggle_btn.setCheckable(True)
self.setCollapsedIcon(icon=collapsedIcon)
self.setExpandedIcon(icon=expandedIcon)
self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum)
self._toggle_btn.setStyleSheet("text-align: left; border: none; outline: none;")
self._toggle_btn.toggled.connect(self._toggle)
# frame layout
self.setLayout(QVBoxLayout())
self.layout().setAlignment(Qt.AlignmentFlag.AlignTop)
self.layout().addWidget(self._toggle_btn)
layout = QVBoxLayout(self)
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
layout.addWidget(self._toggle_btn)
# Create animators
self._animation = QPropertyAnimation(self)
@@ -64,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)
@@ -98,7 +105,7 @@ class QCollapsible(QFrame):
"""Returns the icon used when the widget is expanded."""
return self._expanded_icon
def setExpandedIcon(self, icon: Optional[Union[QIcon, str]] = None) -> None:
def setExpandedIcon(self, icon: QIcon | str | None = None) -> None:
"""Set the icon on the toggle button when the widget is expanded."""
if icon and isinstance(icon, QIcon):
self._expanded_icon = icon
@@ -112,7 +119,7 @@ class QCollapsible(QFrame):
"""Returns the icon used when the widget is collapsed."""
return self._collapsed_icon
def setCollapsedIcon(self, icon: Optional[Union[QIcon, str]] = None) -> None:
def setCollapsedIcon(self, icon: QIcon | str | None = None) -> None:
"""Set the icon on the toggle button when the widget is collapsed."""
if icon and isinstance(icon, QIcon):
self._collapsed_icon = icon
@@ -126,7 +133,7 @@ class QCollapsible(QFrame):
"""Set duration of the collapse/expand animation."""
self._animation.setDuration(msecs)
def setEasingCurve(self, easing: QEasingCurve) -> None:
def setEasingCurve(self, easing: QEasingCurve | QEasingCurve.Type) -> None:
"""Set the easing curve for the collapse/expand animation."""
self._animation.setEasingCurve(easing)

View File

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

View File

@@ -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),
... },
... },
... )

View File

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

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from qtpy.QtCore import QSize
from qtpy.QtGui import QIcon
if TYPE_CHECKING:
@@ -10,6 +11,11 @@ if TYPE_CHECKING:
Flip = Literal["horizontal", "vertical", "horizontal,vertical"]
Rotation = Literal["90", "180", "270", 90, 180, 270, "-90", 1, 2, 3]
try:
from pyconify import svg_path
except ModuleNotFoundError: # pragma: no cover
svg_path = None
class QIconifyIcon(QIcon):
"""QIcon backed by an iconify icon.
@@ -27,6 +33,9 @@ class QIconifyIcon(QIcon):
SVGs are cached to disk, and persist across sessions (until `pyconify.clear_cache()`
is called).
Parameters are the same as `QIconifyIcon.addKey`, which can be used to add
additional icons for various modes and states to the same QIcon.
Parameters
----------
*key: str
@@ -63,13 +72,55 @@ class QIconifyIcon(QIcon):
rotate: Rotation | None = None,
dir: str | None = None,
):
try:
from pyconify import svg_path
except ModuleNotFoundError as e: # pragma: no cover
raise ImportError(
if svg_path is None: # pragma: no cover
raise ModuleNotFoundError(
"pyconify is required to use QIconifyIcon. "
"Please install it with `pip install pyconify` or use the "
"`pip install superqt[iconify]` extra."
) from e
self.path = svg_path(*key, color=color, flip=flip, rotate=rotate, dir=dir)
super().__init__(str(self.path))
)
super().__init__()
self.addKey(*key, color=color, flip=flip, rotate=rotate, dir=dir)
def addKey(
self,
*key: str,
color: str | None = None,
flip: Flip | None = None,
rotate: Rotation | None = None,
dir: str | None = None,
size: QSize | None = None,
mode: QIcon.Mode = QIcon.Mode.Normal,
state: QIcon.State = QIcon.State.Off,
) -> None:
"""Add an icon to this QIcon.
This is a variant of `QIcon.addFile` that uses an iconify icon keys and
arguments instead of a file path.
Parameters
----------
*key: str
Icon set prefix and name. May be passed as a single string in the format
`"prefix:name"` or as two separate strings: `'prefix', 'name'`.
color : str, optional
Icon color. If not provided, the icon will appear black (the icon fill color
will be set to the string "currentColor").
flip : str, optional
Flip icon. Must be one of "horizontal", "vertical", "horizontal,vertical"
rotate : str | int, optional
Rotate icon. Must be one of 0, 90, 180, 270, or 0, 1, 2, 3 (equivalent to 0,
90, 180, 270, respectively)
dir : str, optional
If 'dir' is not None, the file will be created in that directory, otherwise
a default
[directory](https://docs.python.org/3/library/tempfile.html#tempfile.mkstemp)
is used.
size : QSize, optional
Size specified for the icon, passed to `QIcon.addFile`.
mode : QIcon.Mode, optional
Mode specified for the icon, passed to `QIcon.addFile`.
state : QIcon.State, optional
State specified for the icon, passed to `QIcon.addFile`.
"""
path = svg_path(*key, color=color, flip=flip, rotate=rotate, dir=dir)
self.addFile(str(path), size or QSize(), mode, state)

View File

@@ -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__):

View File

@@ -103,7 +103,7 @@ class _GenericRangeSlider(_GenericSlider):
"""Show the bar between the first and last handle."""
self.setBarVisible(True)
def applyMacStylePatch(self) -> str:
def applyMacStylePatch(self) -> None:
"""Apply a QSS patch to fix sliders on macos>=12 with QT < 6.
see [FAQ](../faq.md#sliders-not-dragging-properly-on-macos-12) for more details.
@@ -124,11 +124,27 @@ class _GenericRangeSlider(_GenericSlider):
"""
return tuple(float(i) for i in self._position)
def setSliderPosition(self, pos: Union[float, Sequence[float]], index=None) -> None:
def setSliderPosition( # type: ignore
self,
pos: Union[float, Sequence[float]],
index: Optional[int] = None,
*,
reversed: bool = False,
) -> None:
"""Set current position of the handles with a sequence of integers.
If `pos` is a sequence, it must have the same length as `value()`.
If it is a scalar, index will be
Parameters
----------
pos : Union[float, Sequence[float]]
The new position of the slider handle(s). If a sequence, it must have the
same length as `value()`. If it is a scalar, index will be used to set the
position of the handle at that index.
index : int | None
The index of the handle to set the position of. If None, the "pressedIndex"
will be used.
reversed : bool
Order in which to set the positions. Can be useful when setting multiple
positions, to avoid intermediate overlapping values.
"""
if isinstance(pos, (list, tuple)):
val_len = len(self.value())
@@ -139,6 +155,9 @@ class _GenericRangeSlider(_GenericSlider):
else:
pairs = [(self._pressedIndex if index is None else index, pos)]
if reversed:
pairs = pairs[::-1]
for idx, position in pairs:
self._position[idx] = self._bound(position, idx)
@@ -222,7 +241,7 @@ class _GenericRangeSlider(_GenericSlider):
offset = self.maximum() - ref[-1]
elif ref[0] + offset < self.minimum():
offset = self.minimum() - ref[0]
self.setSliderPosition([i + offset for i in ref])
self.setSliderPosition([i + offset for i in ref], reversed=offset > 0)
def _fixStyleOption(self, option):
pass

View File

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

View File

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

View File

@@ -5,7 +5,6 @@ import re
from dataclasses import dataclass, replace
from typing import TYPE_CHECKING
from qtpy import QT_VERSION
from qtpy.QtCore import Qt
from qtpy.QtGui import (
QBrush,
@@ -140,8 +139,9 @@ CATALINA_STYLE = replace(
tick_offset=4,
)
if QT_VERSION and int(QT_VERSION.split(".")[0]) == 6:
CATALINA_STYLE = replace(CATALINA_STYLE, tick_offset=2)
# I can no longer reproduce the cases in which this was necessary
# if QT_VERSION and int(QT_VERSION.split(".")[0]) == 6:
# CATALINA_STYLE = replace(CATALINA_STYLE, tick_offset=2)
BIG_SUR_STYLE = replace(
CATALINA_STYLE,
@@ -155,8 +155,9 @@ BIG_SUR_STYLE = replace(
tick_bar_alpha=0.2,
)
if QT_VERSION and int(QT_VERSION.split(".")[0]) == 6:
BIG_SUR_STYLE = replace(BIG_SUR_STYLE, tick_offset=-3)
# I can no longer reproduce the cases in which this was necessary
# if QT_VERSION and int(QT_VERSION.split(".")[0]) == 6:
# BIG_SUR_STYLE = replace(BIG_SUR_STYLE, tick_offset=-3)
WINDOWS_STYLE = replace(
BASE_STYLE,
@@ -229,7 +230,7 @@ rgba_pattern = re.compile(
)
def parse_color(color: str, default_attr) -> QColor | QGradient:
def parse_color(color: str, default_attr: str) -> QColor | QGradient:
qc = QColor(color)
if qc.isValid():
return qc
@@ -241,6 +242,7 @@ def parse_color(color: str, default_attr) -> QColor | QGradient:
# try linear gradient:
match = qlineargrad_pattern.search(color)
grad: QGradient
if match:
grad = QLinearGradient(*(float(i) for i in match.groups()[:4]))
grad.setColorAt(0, QColor(match.groupdict()["stop0"]))
@@ -259,11 +261,11 @@ def parse_color(color: str, default_attr) -> QColor | QGradient:
return QColor(getattr(SYSTEM_STYLE, default_attr))
def update_styles_from_stylesheet(obj: _GenericRangeSlider):
def update_styles_from_stylesheet(obj: _GenericRangeSlider) -> None:
qss: str = obj.styleSheet()
parent = obj.parent()
while parent is not None:
while parent and hasattr(parent, "styleSheet"):
qss = parent.styleSheet() + qss
parent = parent.parent()
qss = QApplication.instance().styleSheet() + qss

View File

@@ -65,14 +65,20 @@ class QLargeIntSpinBox(QAbstractSpinBox):
def setMinimum(self, min):
self._minimum = int(min)
if self._minimum > self._value:
self.setValue(self._minimum)
def maximum(self):
return self._maximum
def setMaximum(self, max):
self._maximum = int(max)
if self._maximum < self._value:
self.setValue(self._maximum)
def setRange(self, minimum, maximum):
if maximum < minimum:
maximum = minimum
self.setMinimum(minimum)
self.setMaximum(maximum)

View File

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

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from concurrent.futures import Future
from contextlib import suppress
from functools import wraps
from typing import TYPE_CHECKING, Any, Callable, ClassVar, overload
@@ -41,7 +42,8 @@ class CallCallable(QObject):
def call(self):
CallCallable.instances.remove(self)
res = self._callable(*self._args, **self._kwargs)
self.finished.emit(res)
with suppress(RuntimeError):
self.finished.emit(res)
# fmt: off
@@ -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
):

View File

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

View File

@@ -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]] = {

View File

@@ -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},
)
```
"""

View File

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

View File

@@ -1,6 +1,7 @@
from typing import TYPE_CHECKING
import pytest
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import QPushButton
from superqt import QIconifyIcon
@@ -13,8 +14,8 @@ def test_qiconify(qtbot: "QtBot", monkeypatch: "pytest.MonkeyPatch") -> None:
monkeypatch.setenv("PYCONIFY_CACHE", "0")
pytest.importorskip("pyconify")
icon = QIconifyIcon("bi:alarm-fill", color="red", rotate=90)
assert icon.path.name.endswith(".svg")
icon = QIconifyIcon("bi:alarm-fill", color="red", flip="vertical")
icon.addKey("bi:alarm", color="blue", rotate=90, state=QIcon.State.On)
btn = QPushButton()
qtbot.addWidget(btn)

View File

@@ -22,6 +22,24 @@ def test_large_spinbox(qtbot):
assert sb.value() == -(10**e)
def test_large_spinbox_range(qtbot):
sb = QLargeIntSpinBox()
qtbot.addWidget(sb)
sb.setRange(-100, 100)
sb.setValue(50)
sb.setRange(-10, 10)
assert sb.value() == 10
sb.setRange(100, 1000)
assert sb.value() == 100
sb.setRange(50, 0)
assert sb.minimum() == 50
assert sb.maximum() == 50
assert sb.value() == 50
def test_large_spinbox_type(qtbot):
sb = QLargeIntSpinBox()
qtbot.addWidget(sb)