mirror of
https://github.com/pyapp-kit/superqt.git
synced 2025-07-21 12:11:07 +02:00
Compare commits
103 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
41ea4e8907 | ||
|
39b6a0596f | ||
|
9ff01e757b | ||
|
dd9af3bfed | ||
|
7b964beb89 | ||
|
0407fdc4bd | ||
|
9119336de5 | ||
|
6318675a8c | ||
|
efa2757111 | ||
|
402d237bc4 | ||
|
dc255bdeac | ||
|
ae186df2ae | ||
|
0002d5ee37 | ||
|
f990fea78c | ||
|
1fb46854d4 | ||
|
ca4a1ecb20 | ||
|
c22b7d6f07 | ||
|
bb43cd7fad | ||
|
09c76a0bfa | ||
|
183899c4e7 | ||
|
a39b467563 | ||
|
6ce87d44a6 | ||
|
2cebc868a8 | ||
|
6abd3a21a6 | ||
|
7b2d8bfb2d | ||
|
ad2f05d908 | ||
|
3df7f49706 | ||
|
e98936e8d8 | ||
|
532d3bf89c | ||
|
16b383e783 | ||
|
38d15d1b3b | ||
|
8f09c38074 | ||
|
3c8b5bcf98 | ||
|
3ece7a27b1 | ||
|
e0bb2ea871 | ||
|
78997fe155 | ||
|
021f164419 | ||
|
7f50e69e28 | ||
|
2c747c5a4f | ||
|
b79c8e95b7 | ||
|
b393c6d039 | ||
|
61b8ab30ab | ||
|
abf544cf0e | ||
|
9f9dab6f3b | ||
|
97bb814451 | ||
|
d1c056886f | ||
|
a73e56bb83 | ||
|
6f71e46914 | ||
|
fbc67a745c | ||
|
77bd737e13 | ||
|
ba626e8786 | ||
|
04efa95511 | ||
|
f401d6d59c | ||
|
a3bd0d0edf | ||
|
e7e8dfc44c | ||
|
a556f16745 | ||
|
2864058974 | ||
|
463332f4fc | ||
|
f08e2d1720 | ||
|
39c10aa238 | ||
|
d5d40a35f3 | ||
|
5b92a19b82 | ||
|
a3b0f1b115 | ||
|
b1e6d55957 | ||
|
55535b7600 | ||
|
31c834053c | ||
|
69219c846d | ||
|
2edb3c287e | ||
|
218a7b4034 | ||
|
9ab24dbcf6 | ||
|
35acbbf5e6 | ||
|
0ae3350c57 | ||
|
c7f8780900 | ||
|
cc25733ce8 | ||
|
accb87021f | ||
|
ccad397838 | ||
|
68248c920c | ||
|
f8ac85aaf6 | ||
|
bd6fba96ad | ||
|
7d31812858 | ||
|
f27377ab1b | ||
|
2052fb8310 | ||
|
40d3e20bff | ||
|
f4d9881b0c | ||
|
ba1ae92bcc | ||
|
8217a1cc71 | ||
|
96de1a261a | ||
|
d8a8328793 | ||
|
2a9f47816a | ||
|
e06ab4d081 | ||
|
13e092e381 | ||
|
b2c485bcea | ||
|
d0d67da377 | ||
|
bc98f15ba1 | ||
|
49bd078012 | ||
|
d379611491 | ||
|
329eaaa9a0 | ||
|
d25f4c1cf7 | ||
|
a07ee64f8b | ||
|
bbd60eebaf | ||
|
9c55c6c657 | ||
|
3c217026af | ||
|
0681f7138a |
10
.github/dependabot.yml
vendored
Normal file
10
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: "ci(dependabot):"
|
212
.github/workflows/test_and_deploy.yml
vendored
212
.github/workflows/test_and_deploy.yml
vendored
@@ -1,15 +1,17 @@
|
||||
name: Test
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
tags:
|
||||
- "v*" # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -17,157 +19,149 @@ jobs:
|
||||
test:
|
||||
name: ${{ matrix.platform }} py${{ matrix.python-version }} ${{ matrix.backend }}
|
||||
runs-on: ${{ matrix.platform }}
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ubuntu-latest, windows-latest, macos-latest]
|
||||
python-version: [3.7, 3.8, 3.9]
|
||||
backend: [pyqt5, pyside2]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||
backend: [pyqt5, pyside2, pyqt6]
|
||||
exclude:
|
||||
# Abort (core dumped) on linux pyqt6, unknown reason
|
||||
- platform: ubuntu-latest
|
||||
backend: pyqt6
|
||||
# lack of wheels for pyside2/py3.11
|
||||
- python-version: "3.11"
|
||||
backend: pyside2
|
||||
|
||||
include:
|
||||
# pyqt6 and pyside6 on latest platforms
|
||||
- python-version: 3.9
|
||||
platform: ubuntu-latest
|
||||
- python-version: "3.10"
|
||||
platform: macos-latest
|
||||
backend: pyside6
|
||||
screenshot: 1
|
||||
- python-version: 3.9
|
||||
- python-version: "3.11"
|
||||
platform: macos-latest
|
||||
backend: pyside6
|
||||
- python-version: "3.10"
|
||||
platform: windows-latest
|
||||
backend: pyside6
|
||||
screenshot: 1
|
||||
- python-version: 3.9
|
||||
platform: macos-11.0
|
||||
backend: pyside6
|
||||
screenshot: 1
|
||||
- python-version: 3.9
|
||||
platform: ubuntu-latest
|
||||
backend: pyqt6
|
||||
- python-version: 3.9
|
||||
- python-version: "3.11"
|
||||
platform: windows-latest
|
||||
backend: pyqt6
|
||||
- python-version: 3.9
|
||||
platform: macos-11.0
|
||||
backend: pyqt6
|
||||
# py3.10
|
||||
- python-version: "3.10"
|
||||
platform: ubuntu-latest
|
||||
backend: pyside6
|
||||
- python-version: "3.10"
|
||||
platform: ubuntu-latest
|
||||
backend: pyqt5
|
||||
- python-version: "3.10"
|
||||
platform: ubuntu-latest
|
||||
backend: pyqt6
|
||||
|
||||
# big sur, 3.9
|
||||
- python-version: 3.9
|
||||
platform: macos-11.0
|
||||
backend: pyside2
|
||||
- python-version: 3.9
|
||||
platform: macos-11.0
|
||||
backend: pyqt5
|
||||
|
||||
# legacy OS
|
||||
- python-version: 3.8
|
||||
platform: ubuntu-18.04
|
||||
backend: pyside2
|
||||
- python-version: 3.6
|
||||
platform: windows-2016
|
||||
backend: pyqt5
|
||||
|
||||
# legacy Qt
|
||||
- python-version: 3.7
|
||||
- python-version: 3.8
|
||||
platform: ubuntu-latest
|
||||
backend: pyside511
|
||||
- python-version: 3.7
|
||||
backend: "pyqt5==5.12.*"
|
||||
- python-version: 3.8
|
||||
platform: ubuntu-latest
|
||||
backend: pyqt511
|
||||
- python-version: 3.7
|
||||
backend: "pyqt5==5.13.*"
|
||||
- python-version: 3.8
|
||||
platform: ubuntu-latest
|
||||
backend: pyqt512
|
||||
- python-version: 3.7
|
||||
platform: ubuntu-latest
|
||||
backend: pyqt513
|
||||
- python-version: 3.7
|
||||
platform: ubuntu-latest
|
||||
backend: pyqt514
|
||||
|
||||
|
||||
backend: "pyqt5==5.14.*"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- uses: tlambert03/setup-qt-libs@v1
|
||||
- uses: tlambert03/setup-qt-libs@v1.4
|
||||
|
||||
- name: Linux opengl
|
||||
if: runner.os == 'Linux' && ( matrix.backend == 'pyside6' || matrix.backend == 'pyqt6' )
|
||||
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
|
||||
pip install setuptools tox tox-gh-actions
|
||||
python -m pip install -e .[test]
|
||||
python -m pip install ${{ matrix.backend }}
|
||||
|
||||
- name: Test with tox
|
||||
uses: GabrielBB/xvfb-action@v1
|
||||
timeout-minutes: 3
|
||||
- name: Test
|
||||
uses: aganders3/headless-gui@v1.2
|
||||
with:
|
||||
run: tox
|
||||
env:
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
BACKEND: ${{ matrix.backend }}
|
||||
run: python -m pytest --color=yes --cov=superqt --cov-report=xml
|
||||
|
||||
- name: Coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
uses: codecov/codecov-action@v3
|
||||
|
||||
- name: Install for screenshots
|
||||
if: matrix.screenshot
|
||||
run: pip install . ${{ matrix.backend }}
|
||||
|
||||
- name: Screenshots (Linux)
|
||||
if: runner.os == 'Linux' && matrix.screenshot
|
||||
uses: GabrielBB/xvfb-action@v1
|
||||
with:
|
||||
run: python examples/demo_widget.py -snap
|
||||
|
||||
- name: Screenshots (macOS/Win)
|
||||
if: runner.os != 'Linux' && matrix.screenshot
|
||||
run: python examples/demo_widget.py -snap
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: matrix.screenshot
|
||||
with:
|
||||
name: screenshots ${{ runner.os }}
|
||||
path: screenshots
|
||||
|
||||
check_manifest:
|
||||
test_old_qtpy:
|
||||
name: qtpy minreq
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/checkout@v3
|
||||
- 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.10.0.0
|
||||
|
||||
- name: Test
|
||||
uses: aganders3/headless-gui@v1.2
|
||||
with:
|
||||
run: python -m pytest --color=yes
|
||||
|
||||
test_napari:
|
||||
name: napari tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
path: superqt
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
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
|
||||
|
||||
check-manifest:
|
||||
name: Check Manifest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Check manifest
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install check-manifest
|
||||
check-manifest
|
||||
- run: pip install check-manifest && check-manifest
|
||||
|
||||
deploy:
|
||||
# this will run when you have tagged a commit, starting with "v*"
|
||||
# and requires that you have put your twine API key in your
|
||||
# github secrets (see readme for details)
|
||||
needs: [test, check_manifest]
|
||||
if: ${{ github.repository == 'napari/superqt' && contains(github.ref, 'tags') }}
|
||||
needs: [test, check-manifest]
|
||||
if: ${{ github.repository == 'pyapp-kit/superqt' && contains(github.ref, 'tags') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Install dependencies
|
||||
@@ -183,3 +177,7 @@ jobs:
|
||||
python -m build
|
||||
twine check dist/*
|
||||
twine upload dist/*
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
generate_release_notes: true
|
||||
|
@@ -1,8 +1,9 @@
|
||||
# run this with:
|
||||
# export CHANGELOG_GITHUB_TOKEN=......
|
||||
# github_changelog_generator --future-release vX.Y.Z
|
||||
user=napari
|
||||
user=pyapp-kit
|
||||
project=superqt
|
||||
issues=false
|
||||
since-tag=v0.2.0
|
||||
add-sections={"documentation":{"prefix":"**Documentation updates:**","labels":["documentation"]}}
|
||||
exclude-labels=duplicate,question,invalid,wontfix,hide
|
||||
add-sections={"documentation":{"prefix":"**Documentation updates:**","labels":["documentation"]},"tests":{"prefix":"**Tests & CI:**","labels":["tests"]},"refactor":{"prefix":"**Refactors:**","labels":["refactor"]}}
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -45,7 +45,6 @@ nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
.hypothesis/
|
||||
.napari_cache
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
@@ -82,3 +81,4 @@ src/superqt/_version.py
|
||||
screenshots
|
||||
|
||||
.mypy_cache
|
||||
docs/_auto_images/
|
||||
|
@@ -1,40 +1,38 @@
|
||||
ci:
|
||||
autoupdate_schedule: monthly
|
||||
autofix_commit_msg: "style: [pre-commit.ci] auto fixes [...]"
|
||||
autoupdate_commit_msg: "ci: [pre-commit.ci] autoupdate"
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.0.1
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: check-docstring-first
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v1.20.0
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 4.0.1
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [flake8-typing-imports==1.7.0]
|
||||
exclude: examples
|
||||
- repo: https://github.com/myint/autoflake
|
||||
rev: v1.4
|
||||
hooks:
|
||||
- id: autoflake
|
||||
args: ["--in-place", "--remove-all-unused-imports"]
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.10.1
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.29.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py37-plus, --keep-runtime-typing]
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 21.11b1
|
||||
rev: 23.7.0
|
||||
hooks:
|
||||
- id: black
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.0.281
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: ["--fix"]
|
||||
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.13
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v0.910-1
|
||||
rev: v1.4.1
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: examples
|
||||
stages: [manual]
|
||||
exclude: tests|examples
|
||||
additional_dependencies:
|
||||
- types-Pygments
|
||||
stages:
|
||||
- manual
|
||||
|
329
CHANGELOG.md
329
CHANGELOG.md
@@ -1,76 +1,297 @@
|
||||
# Changelog
|
||||
|
||||
## [v0.2.5](https://github.com/napari/superqt/tree/v0.2.5) (2021-11-22)
|
||||
## [v0.5.0](https://github.com/pyapp-kit/superqt/tree/v0.5.0) (2023-08-06)
|
||||
|
||||
[Full Changelog](https://github.com/napari/superqt/compare/v0.2.4...v0.2.5)
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.4.1...v0.5.0)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- add support for python 3.10 [\#42](https://github.com/napari/superqt/pull/42) ([tlambert03](https://github.com/tlambert03))
|
||||
- QCollapsible for Collapsible Section Control [\#37](https://github.com/napari/superqt/pull/37) ([MosGeo](https://github.com/MosGeo))
|
||||
- Threadworker [\#31](https://github.com/napari/superqt/pull/31) ([tlambert03](https://github.com/tlambert03))
|
||||
- Add font icons [\#24](https://github.com/napari/superqt/pull/24) ([tlambert03](https://github.com/tlambert03))
|
||||
- feat: add stepType to largeInt spinbox [\#179](https://github.com/pyapp-kit/superqt/pull/179) ([tlambert03](https://github.com/tlambert03))
|
||||
- Searchable tree widget from a mapping [\#158](https://github.com/pyapp-kit/superqt/pull/158) ([andy-sweet](https://github.com/andy-sweet))
|
||||
- Add `QElidingLineEdit` class for elidable `QLineEdit`s [\#154](https://github.com/pyapp-kit/superqt/pull/154) ([dalthviz](https://github.com/dalthviz))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Fix some small linting issues. [\#41](https://github.com/napari/superqt/pull/41) ([tlambert03](https://github.com/tlambert03))
|
||||
- Use functools.wraps insterad of \_\_wraped\_\_ and manual proxing \_\_name\_\_ [\#29](https://github.com/napari/superqt/pull/29) ([Czaki](https://github.com/Czaki))
|
||||
- Propagate function name in `ensure_main_thread` and `ensure_object_thread` [\#28](https://github.com/napari/superqt/pull/28) ([Czaki](https://github.com/Czaki))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- reskip test\_object\_thread\_return on ci [\#43](https://github.com/napari/superqt/pull/43) ([tlambert03](https://github.com/tlambert03))
|
||||
- refactoring qtcompat [\#34](https://github.com/napari/superqt/pull/34) ([tlambert03](https://github.com/tlambert03))
|
||||
- update deploy [\#33](https://github.com/napari/superqt/pull/33) ([tlambert03](https://github.com/tlambert03))
|
||||
- move to src layout [\#32](https://github.com/napari/superqt/pull/32) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
## [v0.2.4](https://github.com/napari/superqt/tree/v0.2.4) (2021-09-13)
|
||||
|
||||
[Full Changelog](https://github.com/napari/superqt/compare/v0.2.3...v0.2.4)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Add type stubs for ensure\_thread decorator [\#23](https://github.com/napari/superqt/pull/23) ([tlambert03](https://github.com/tlambert03))
|
||||
- Add `ensure_main_tread` and `ensure_object_thread` [\#22](https://github.com/napari/superqt/pull/22) ([Czaki](https://github.com/Czaki))
|
||||
- Add QMessageHandler context manager [\#21](https://github.com/napari/superqt/pull/21) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- add changelog for 0.2.4 [\#25](https://github.com/napari/superqt/pull/25) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
## [v0.2.3](https://github.com/napari/superqt/tree/v0.2.3) (2021-08-25)
|
||||
|
||||
[Full Changelog](https://github.com/napari/superqt/compare/v0.2.2...v0.2.3)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Fix warnings on eliding label for 5.12, test more qt versions [\#19](https://github.com/napari/superqt/pull/19) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
## [v0.2.2](https://github.com/napari/superqt/tree/v0.2.2) (2021-08-17)
|
||||
|
||||
[Full Changelog](https://github.com/napari/superqt/compare/v0.2.1...v0.2.2)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Add QElidingLabel [\#16](https://github.com/napari/superqt/pull/16) ([tlambert03](https://github.com/tlambert03))
|
||||
- Enum ComboBox implementation [\#13](https://github.com/napari/superqt/pull/13) ([Czaki](https://github.com/Czaki))
|
||||
- fix: focus events on QLabeledSlider [\#175](https://github.com/pyapp-kit/superqt/pull/175) ([tlambert03](https://github.com/tlambert03))
|
||||
- Set parent of timer in throttler [\#171](https://github.com/pyapp-kit/superqt/pull/171) ([Czaki](https://github.com/Czaki))
|
||||
- fix: fix double slider label editing [\#168](https://github.com/pyapp-kit/superqt/pull/168) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Documentation updates:**
|
||||
|
||||
- fix broken link [\#18](https://github.com/napari/superqt/pull/18) ([haesleinhuepf](https://github.com/haesleinhuepf))
|
||||
- Fix typos [\#147](https://github.com/pyapp-kit/superqt/pull/147) ([kianmeng](https://github.com/kianmeng))
|
||||
|
||||
## [v0.2.1](https://github.com/napari/superqt/tree/v0.2.1) (2021-07-10)
|
||||
**Tests & CI:**
|
||||
|
||||
[Full Changelog](https://github.com/napari/superqt/compare/v0.2.0rc1...v0.2.1)
|
||||
- tests: add qtbot to test to fix windows segfault [\#165](https://github.com/pyapp-kit/superqt/pull/165) ([tlambert03](https://github.com/tlambert03))
|
||||
- test: fixing tests \[wip\] [\#164](https://github.com/pyapp-kit/superqt/pull/164) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- build: unpin pyside6.5 [\#178](https://github.com/pyapp-kit/superqt/pull/178) ([tlambert03](https://github.com/tlambert03))
|
||||
- build: pin pyside6 to \<6.5.1 [\#169](https://github.com/pyapp-kit/superqt/pull/169) ([tlambert03](https://github.com/tlambert03))
|
||||
- pin pyside6\<6.5 [\#160](https://github.com/pyapp-kit/superqt/pull/160) ([tlambert03](https://github.com/tlambert03))
|
||||
- ci: \[pre-commit.ci\] autoupdate [\#146](https://github.com/pyapp-kit/superqt/pull/146) ([pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci))
|
||||
|
||||
## [v0.4.1](https://github.com/pyapp-kit/superqt/tree/v0.4.1) (2022-12-01)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.4.0...v0.4.1)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- feat: Add signal to QCollapsible [\#142](https://github.com/pyapp-kit/superqt/pull/142) ([ppwadhwa](https://github.com/ppwadhwa))
|
||||
- feat: Change icon used in Collapsible widget [\#140](https://github.com/pyapp-kit/superqt/pull/140) ([ppwadhwa](https://github.com/ppwadhwa))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Fix QLabeledRangeSlider API \(fix slider proxy\) [\#10](https://github.com/napari/superqt/pull/10) ([tlambert03](https://github.com/tlambert03))
|
||||
- Fix range slider with negative min range [\#9](https://github.com/napari/superqt/pull/9) ([tlambert03](https://github.com/tlambert03))
|
||||
- Move QCollapsible toggle signal emit [\#144](https://github.com/pyapp-kit/superqt/pull/144) ([ppwadhwa](https://github.com/ppwadhwa))
|
||||
|
||||
## [v0.2.0rc1](https://github.com/napari/superqt/tree/v0.2.0rc1) (2021-06-26)
|
||||
**Merged pull requests:**
|
||||
|
||||
[Full Changelog](https://github.com/napari/superqt/compare/v0.2.0...v0.2.0rc1)
|
||||
- build: use hatch for build backend, and use ruff for linting [\#139](https://github.com/pyapp-kit/superqt/pull/139) ([tlambert03](https://github.com/tlambert03))
|
||||
- chore: rename napari org to pyapp-kit [\#137](https://github.com/pyapp-kit/superqt/pull/137) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
## [v0.4.0](https://github.com/pyapp-kit/superqt/tree/v0.4.0) (2022-11-09)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.3.8...v0.4.0)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- fix: fix quantity set value and add test [\#131](https://github.com/pyapp-kit/superqt/pull/131) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Refactors:**
|
||||
|
||||
- refactor: update pyproject and ci, add py3.11 test [\#132](https://github.com/pyapp-kit/superqt/pull/132) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- chore: changelog v0.4.0 [\#136](https://github.com/pyapp-kit/superqt/pull/136) ([tlambert03](https://github.com/tlambert03))
|
||||
- ci\(dependabot\): bump actions/upload-artifact from 2 to 3 [\#135](https://github.com/pyapp-kit/superqt/pull/135) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- ci\(dependabot\): bump codecov/codecov-action from 2 to 3 [\#134](https://github.com/pyapp-kit/superqt/pull/134) ([dependabot[bot]](https://github.com/apps/dependabot))
|
||||
- build: unpin pyside6 [\#133](https://github.com/pyapp-kit/superqt/pull/133) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
## [v0.3.8](https://github.com/pyapp-kit/superqt/tree/v0.3.8) (2022-10-10)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.3.7...v0.3.8)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- fix: allow submodule imports [\#128](https://github.com/pyapp-kit/superqt/pull/128) ([kne42](https://github.com/kne42))
|
||||
|
||||
## [v0.3.7](https://github.com/pyapp-kit/superqt/tree/v0.3.7) (2022-10-10)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.3.6...v0.3.7)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- feat: add Quantity widget \(using pint\) [\#126](https://github.com/pyapp-kit/superqt/pull/126) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
## [v0.3.6](https://github.com/pyapp-kit/superqt/tree/v0.3.6) (2022-10-05)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.3.6rc0...v0.3.6)
|
||||
|
||||
**Documentation updates:**
|
||||
|
||||
- minor fix to readme [\#125](https://github.com/pyapp-kit/superqt/pull/125) ([tlambert03](https://github.com/tlambert03))
|
||||
- Docs [\#124](https://github.com/pyapp-kit/superqt/pull/124) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
## [v0.3.6rc0](https://github.com/pyapp-kit/superqt/tree/v0.3.6rc0) (2022-10-03)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.3.5...v0.3.6rc0)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- feat: add editing finished signal to LabeledSliders [\#122](https://github.com/pyapp-kit/superqt/pull/122) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- fix: fix missing labels after setValue [\#123](https://github.com/pyapp-kit/superqt/pull/123) ([tlambert03](https://github.com/tlambert03))
|
||||
- fix: Fix TypeError on slider rangeChanged signal [\#121](https://github.com/pyapp-kit/superqt/pull/121) ([tlambert03](https://github.com/tlambert03))
|
||||
- Simple workaround for pyside 6 [\#119](https://github.com/pyapp-kit/superqt/pull/119) ([Czaki](https://github.com/Czaki))
|
||||
- fix: Offer patch for \(unstyled\) QSliders on macos 12 and Qt \<6 [\#117](https://github.com/pyapp-kit/superqt/pull/117) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
## [v0.3.5](https://github.com/pyapp-kit/superqt/tree/v0.3.5) (2022-08-17)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.3.4...v0.3.5)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- fix range slider drag crash on PyQt6 [\#108](https://github.com/pyapp-kit/superqt/pull/108) ([sfhbarnett](https://github.com/sfhbarnett))
|
||||
- Fix float value error in pyqt configuration [\#106](https://github.com/pyapp-kit/superqt/pull/106) ([mstabrin](https://github.com/mstabrin))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- chore: changelog v0.3.5 [\#110](https://github.com/pyapp-kit/superqt/pull/110) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
## [v0.3.4](https://github.com/pyapp-kit/superqt/tree/v0.3.4) (2022-07-24)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.3.3...v0.3.4)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- fix: relax runtime typing extensions requirement [\#101](https://github.com/pyapp-kit/superqt/pull/101) ([tlambert03](https://github.com/tlambert03))
|
||||
- fix: catch qpixmap deprecation [\#99](https://github.com/pyapp-kit/superqt/pull/99) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
## [v0.3.3](https://github.com/pyapp-kit/superqt/tree/v0.3.3) (2022-07-10)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.3.2...v0.3.3)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Add code syntax highlight utils [\#88](https://github.com/pyapp-kit/superqt/pull/88) ([Czaki](https://github.com/Czaki))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- fix: fix deprecation warning on fonticon plugin discovery on python 3.10 [\#95](https://github.com/pyapp-kit/superqt/pull/95) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
## [v0.3.2](https://github.com/pyapp-kit/superqt/tree/v0.3.2) (2022-05-03)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.3.1...v0.3.2)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Add QSearchableListWidget and QSearchableComboBox widgets [\#80](https://github.com/pyapp-kit/superqt/pull/80) ([Czaki](https://github.com/Czaki))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Fix crazy animation loop on Qcollapsible [\#84](https://github.com/pyapp-kit/superqt/pull/84) ([tlambert03](https://github.com/tlambert03))
|
||||
- Reorder label update signal [\#83](https://github.com/pyapp-kit/superqt/pull/83) ([tlambert03](https://github.com/tlambert03))
|
||||
- Fix height of expanded QCollapsible when child changes size [\#72](https://github.com/pyapp-kit/superqt/pull/72) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Tests & CI:**
|
||||
|
||||
- Fix deprecation warnings in tests [\#82](https://github.com/pyapp-kit/superqt/pull/82) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Add changelog for v0.3.2 [\#86](https://github.com/pyapp-kit/superqt/pull/86) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
## [v0.3.1](https://github.com/pyapp-kit/superqt/tree/v0.3.1) (2022-03-02)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.3.0...v0.3.1)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Add `signals_blocked` util [\#69](https://github.com/pyapp-kit/superqt/pull/69) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- put SignalInstance in TYPE\_CHECKING clause, check min requirements [\#70](https://github.com/pyapp-kit/superqt/pull/70) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Add changelog for v0.3.1 [\#71](https://github.com/pyapp-kit/superqt/pull/71) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
## [v0.3.0](https://github.com/pyapp-kit/superqt/tree/v0.3.0) (2022-02-16)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.2.5-1...v0.3.0)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Qthrottler and debouncer [\#62](https://github.com/pyapp-kit/superqt/pull/62) ([tlambert03](https://github.com/tlambert03))
|
||||
- add edgeLabelMode option to QLabeledSlider [\#59](https://github.com/pyapp-kit/superqt/pull/59) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Fix nested threadworker not starting [\#63](https://github.com/pyapp-kit/superqt/pull/63) ([tlambert03](https://github.com/tlambert03))
|
||||
- Add missing signals on proxy sliders [\#54](https://github.com/pyapp-kit/superqt/pull/54) ([tlambert03](https://github.com/tlambert03))
|
||||
- Ugly but functional workaround for pyside6.2.1 breakages [\#51](https://github.com/pyapp-kit/superqt/pull/51) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Tests & CI:**
|
||||
|
||||
- add napari test to CI [\#67](https://github.com/pyapp-kit/superqt/pull/67) ([tlambert03](https://github.com/tlambert03))
|
||||
- add gh-release action [\#65](https://github.com/pyapp-kit/superqt/pull/65) ([tlambert03](https://github.com/tlambert03))
|
||||
- fix xvfb tests [\#61](https://github.com/pyapp-kit/superqt/pull/61) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Refactors:**
|
||||
|
||||
- Use qtpy, deprecate superqt.qtcompat, drop support for Qt \<5.12 [\#39](https://github.com/pyapp-kit/superqt/pull/39) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Add changelog for v0.3.0 [\#68](https://github.com/pyapp-kit/superqt/pull/68) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
## [v0.2.5-1](https://github.com/pyapp-kit/superqt/tree/v0.2.5-1) (2021-11-23)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.2.5...v0.2.5-1)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- typing-extensions version pinning [\#46](https://github.com/pyapp-kit/superqt/pull/46) ([AhmetCanSolak](https://github.com/AhmetCanSolak))
|
||||
|
||||
## [v0.2.5](https://github.com/pyapp-kit/superqt/tree/v0.2.5) (2021-11-22)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.2.4...v0.2.5)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- add support for python 3.10 [\#42](https://github.com/pyapp-kit/superqt/pull/42) ([tlambert03](https://github.com/tlambert03))
|
||||
- QCollapsible for Collapsible Section Control [\#37](https://github.com/pyapp-kit/superqt/pull/37) ([MosGeo](https://github.com/MosGeo))
|
||||
- Threadworker [\#31](https://github.com/pyapp-kit/superqt/pull/31) ([tlambert03](https://github.com/tlambert03))
|
||||
- Add font icons [\#24](https://github.com/pyapp-kit/superqt/pull/24) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Fix some small linting issues. [\#41](https://github.com/pyapp-kit/superqt/pull/41) ([tlambert03](https://github.com/tlambert03))
|
||||
- Use functools.wraps insterad of \_\_wraped\_\_ and manual proxing \_\_name\_\_ [\#29](https://github.com/pyapp-kit/superqt/pull/29) ([Czaki](https://github.com/Czaki))
|
||||
- Propagate function name in `ensure_main_thread` and `ensure_object_thread` [\#28](https://github.com/pyapp-kit/superqt/pull/28) ([Czaki](https://github.com/Czaki))
|
||||
|
||||
**Tests & CI:**
|
||||
|
||||
- reskip test\_object\_thread\_return on ci [\#43](https://github.com/pyapp-kit/superqt/pull/43) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Refactors:**
|
||||
|
||||
- refactoring qtcompat [\#34](https://github.com/pyapp-kit/superqt/pull/34) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Fix-manifest, move font tests [\#44](https://github.com/pyapp-kit/superqt/pull/44) ([tlambert03](https://github.com/tlambert03))
|
||||
- update deploy [\#33](https://github.com/pyapp-kit/superqt/pull/33) ([tlambert03](https://github.com/tlambert03))
|
||||
- move to src layout [\#32](https://github.com/pyapp-kit/superqt/pull/32) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
## [v0.2.4](https://github.com/pyapp-kit/superqt/tree/v0.2.4) (2021-09-13)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.2.3...v0.2.4)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Add type stubs for ensure\_thread decorator [\#23](https://github.com/pyapp-kit/superqt/pull/23) ([tlambert03](https://github.com/tlambert03))
|
||||
- Add `ensure_main_tread` and `ensure_object_thread` [\#22](https://github.com/pyapp-kit/superqt/pull/22) ([Czaki](https://github.com/Czaki))
|
||||
- Add QMessageHandler context manager [\#21](https://github.com/pyapp-kit/superqt/pull/21) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- add changelog for 0.2.4 [\#25](https://github.com/pyapp-kit/superqt/pull/25) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
## [v0.2.3](https://github.com/pyapp-kit/superqt/tree/v0.2.3) (2021-08-25)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.2.2...v0.2.3)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Fix warnings on eliding label for 5.12, test more qt versions [\#19](https://github.com/pyapp-kit/superqt/pull/19) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
## [v0.2.2](https://github.com/pyapp-kit/superqt/tree/v0.2.2) (2021-08-17)
|
||||
|
||||
[Full Changelog](https://github.com/pyapp-kit/superqt/compare/v0.2.1...v0.2.2)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Add QElidingLabel [\#16](https://github.com/pyapp-kit/superqt/pull/16) ([tlambert03](https://github.com/tlambert03))
|
||||
- Enum ComboBox implementation [\#13](https://github.com/pyapp-kit/superqt/pull/13) ([Czaki](https://github.com/Czaki))
|
||||
|
||||
**Documentation updates:**
|
||||
|
||||
- fix broken link [\#18](https://github.com/pyapp-kit/superqt/pull/18) ([haesleinhuepf](https://github.com/haesleinhuepf))
|
||||
|
||||
## [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.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))
|
||||
|
||||
|
||||
|
||||
|
@@ -26,13 +26,11 @@ pytest
|
||||
|
||||
All widgets must be well-tested, and should work on:
|
||||
|
||||
- Python 3.7 and above
|
||||
- Python 3.8 and above
|
||||
- PyQt5 (5.11 and above) & PyQt6
|
||||
- PySide2 (5.11 and above) & PySide6
|
||||
- macOS, Windows, & Linux
|
||||
|
||||
Until [qtpy](https://github.com/spyder-ide/qtpy) supports PyQt6/PySide6, imports
|
||||
should use (and modify if necessary) `superqt.qtcompat`.
|
||||
|
||||
## Style Guide
|
||||
|
||||
@@ -50,5 +48,4 @@ All widgets should try to match the native Qt API as much as possible:
|
||||
|
||||
## Testing
|
||||
|
||||
Tests can be run in the current environment with `pytest`. Or, to run tests
|
||||
against all supported python & Qt versions, run `tox`.
|
||||
Tests can be run in the current environment with `pytest`.
|
||||
|
17
MANIFEST.in
17
MANIFEST.in
@@ -1,17 +0,0 @@
|
||||
include LICENSE
|
||||
include README.md
|
||||
include CHANGELOG.md
|
||||
include src/superqt/py.typed
|
||||
recursive-include src/superqt *.py
|
||||
recursive-include src/superqt *.pyi
|
||||
|
||||
recursive-exclude * __pycache__
|
||||
recursive-exclude * *.py[co]
|
||||
recursive-exclude docs *
|
||||
recursive-exclude examples *
|
||||
recursive-exclude tests *
|
||||
exclude tox.ini
|
||||
exclude CONTRIBUTING.md
|
||||
exclude codecov.yml
|
||||
exclude .github_changelog_generator
|
||||
exclude .pre-commit-config.yaml
|
34
README.md
34
README.md
@@ -1,12 +1,11 @@
|
||||
#  superqt!
|
||||
|
||||
|
||||
[](https://github.com/napari/superqt/raw/master/LICENSE)
|
||||
[](https://github.com/pyapp-kit/superqt/raw/master/LICENSE)
|
||||
[](https://pypi.org/project/superqt)
|
||||
[](https://python.org)
|
||||
[](https://github.com/napari/superqt/actions/workflows/test_and_deploy.yml)
|
||||
[](https://codecov.io/gh/napari/superqt)
|
||||
[](https://github.com/pyapp-kit/superqt/actions/workflows/test_and_deploy.yml)
|
||||
[](https://codecov.io/gh/pyapp-kit/superqt)
|
||||
|
||||
### "missing" widgets and components for PyQt/PySide
|
||||
|
||||
@@ -16,29 +15,36 @@ that are not provided in the native QtWidgets module.
|
||||
Components are tested on:
|
||||
|
||||
- macOS, Windows, & Linux
|
||||
- Python 3.7 and above
|
||||
- Python 3.8 and above
|
||||
- PyQt5 (5.11 and above) & PyQt6
|
||||
- PySide2 (5.11 and above) & PySide6
|
||||
|
||||
## Documentation
|
||||
|
||||
Documentation is available at https://pyapp-kit.github.io/superqt/
|
||||
|
||||
## Widgets
|
||||
|
||||
Widgets include:
|
||||
superqt provides a variety of widgets that are not included in the native QtWidgets module, including multihandle (range) sliders, comboboxes, and more.
|
||||
|
||||
- [Float Slider](docs/sliders.md#float-slider)
|
||||
See the [widgets documentation](https://pyapp-kit.github.io/superqt/widgets) for a full list of widgets.
|
||||
|
||||
- [Range Slider](docs/sliders.md#range-slider) (multi-handle slider)
|
||||
- [Range Slider](https://pyapp-kit.github.io/superqt/widgets/qrangeslider/) (multi-handle slider)
|
||||
|
||||
<img src="https://raw.githubusercontent.com/napari/superqt/main/docs/images/demo_darwin10.png" alt="range sliders" width=680>
|
||||
<img src="https://raw.githubusercontent.com/pyapp-kit/superqt/main/docs/images/demo_darwin10.png" alt="range sliders" width=680>
|
||||
|
||||
<img src="https://raw.githubusercontent.com/pyapp-kit/superqt/main/docs/images/labeled_qslider.png" alt="range sliders" width=680>
|
||||
|
||||
- [Labeled Sliders](docs/sliders.md#labeled-sliders) (sliders with linked
|
||||
spinboxes)
|
||||
<img src="https://raw.githubusercontent.com/pyapp-kit/superqt/main/docs/images/labeled_range.png" alt="range sliders" width=680>
|
||||
|
||||
<img src="https://raw.githubusercontent.com/napari/superqt/main/docs/images/labeled_qslider.png" alt="range sliders" width=680>
|
||||
## Utilities
|
||||
|
||||
<img src="https://raw.githubusercontent.com/napari/superqt/main/docs/images/labeled_range.png" alt="range sliders" width=680>
|
||||
superqt includes a number of utilities for working with Qt, including:
|
||||
|
||||
- Unbound Integer SpinBox (backed by python `int`)
|
||||
- tools and decorators for working with threads in qt.
|
||||
- `superqt.fonticon` for generating icons from font files (such as [Material Design Icons](https://materialdesignicons.com/) and [Font Awesome](https://fontawesome.com/))
|
||||
|
||||
See the [utilities documentation](https://pyapp-kit.github.io/superqt/utilities/) for a full list of utilities.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
@@ -1,6 +1,5 @@
|
||||
ignore:
|
||||
- superqt/_version.py
|
||||
- superqt/qtcompat/*
|
||||
- '*_tests*'
|
||||
coverage:
|
||||
status:
|
||||
|
146
docs/_macros.py
Normal file
146
docs/_macros.py
Normal file
@@ -0,0 +1,146 @@
|
||||
import sys
|
||||
from enum import EnumMeta
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from jinja2 import pass_context
|
||||
from qtpy.QtCore import QObject, Signal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mkdocs_macros.plugin import MacrosPlugin
|
||||
|
||||
EXAMPLES = Path(__file__).parent.parent / "examples"
|
||||
IMAGES = Path(__file__).parent / "_auto_images"
|
||||
IMAGES.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
|
||||
def define_env(env: "MacrosPlugin"):
|
||||
@env.macro
|
||||
@pass_context
|
||||
def show_widget(context, width: int = 500) -> list[Path]:
|
||||
# extract all fenced code blocks starting with "python"
|
||||
page = context["page"]
|
||||
dest = IMAGES / f"{page.title}.png"
|
||||
if "build" in sys.argv:
|
||||
dest.unlink(missing_ok=True)
|
||||
|
||||
codeblocks = [
|
||||
b[6:].strip()
|
||||
for b in page.markdown.split("```")
|
||||
if b.startswith("python")
|
||||
]
|
||||
src = codeblocks[0].strip()
|
||||
src = src.replace(
|
||||
"QApplication([])", "QApplication.instance() or QApplication([])"
|
||||
)
|
||||
src = src.replace("app.exec_()", "")
|
||||
|
||||
exec(src) # noqa: S102
|
||||
_grab(dest, width)
|
||||
return (
|
||||
f""
|
||||
f"{{ loading=lazy; width={width} }}\n\n"
|
||||
)
|
||||
|
||||
@env.macro
|
||||
def show_members(cls: str):
|
||||
# import class
|
||||
module, name = cls.rsplit(".", 1)
|
||||
_cls = getattr(import_module(module), name)
|
||||
|
||||
first_q = next(
|
||||
(
|
||||
b.__name__
|
||||
for b in _cls.__mro__
|
||||
if issubclass(b, QObject) and ".Qt" in b.__module__
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
inherited_members = set()
|
||||
for base in _cls.__mro__:
|
||||
if issubclass(base, QObject) and ".Qt" in base.__module__:
|
||||
inherited_members.update(
|
||||
{k for k in dir(base) if not k.startswith("_")}
|
||||
)
|
||||
|
||||
new_signals = {
|
||||
k
|
||||
for k, v in vars(_cls).items()
|
||||
if not k.startswith("_") and isinstance(v, Signal)
|
||||
}
|
||||
|
||||
self_members = {
|
||||
k
|
||||
for k in dir(_cls)
|
||||
if not k.startswith("_") and k not in inherited_members | new_signals
|
||||
}
|
||||
|
||||
enums = []
|
||||
for m in list(self_members):
|
||||
if isinstance(getattr(_cls, m), EnumMeta):
|
||||
self_members.remove(m)
|
||||
enums.append(m)
|
||||
|
||||
out = ""
|
||||
if first_q:
|
||||
url = f"https://doc.qt.io/qt-6/{first_q.lower()}.html"
|
||||
out += f"## Qt Class\n\n<a href='{url}'>`{first_q}`</a>\n\n"
|
||||
|
||||
out += ""
|
||||
|
||||
if new_signals:
|
||||
out += "## Signals\n\n"
|
||||
for sig in new_signals:
|
||||
out += f"### `{sig}`\n\n"
|
||||
|
||||
if enums:
|
||||
out += "## Enums\n\n"
|
||||
for e in enums:
|
||||
out += f"### `{_cls.__name__}.{e}`\n\n"
|
||||
for m in getattr(_cls, e):
|
||||
out += f"- `{m.name}`\n\n"
|
||||
|
||||
if self_members:
|
||||
out += dedent(
|
||||
f"""
|
||||
## Methods
|
||||
|
||||
::: {cls}
|
||||
options:
|
||||
heading_level: 3
|
||||
show_source: False
|
||||
show_inherited_members: false
|
||||
show_signature_annotations: True
|
||||
members: {sorted(self_members)}
|
||||
docstring_style: numpy
|
||||
show_bases: False
|
||||
show_root_toc_entry: False
|
||||
show_root_heading: False
|
||||
"""
|
||||
)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def _grab(dest: str | Path, width) -> list[Path]:
|
||||
"""Grab the top widgets of the application."""
|
||||
from qtpy.QtCore import QTimer
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
w = QApplication.topLevelWidgets()[-1]
|
||||
w.setFixedWidth(width)
|
||||
w.activateWindow()
|
||||
w.setMinimumHeight(40)
|
||||
w.grab().save(str(dest))
|
||||
|
||||
# hack to make sure the object is truly closed and deleted
|
||||
while True:
|
||||
QTimer.singleShot(10, w.deleteLater)
|
||||
QApplication.processEvents()
|
||||
try:
|
||||
w.parent()
|
||||
except RuntimeError:
|
||||
return
|
@@ -1,63 +0,0 @@
|
||||
# ComboBox
|
||||
|
||||
|
||||
## Enum Combo Box
|
||||
|
||||
`QEnumComboBox` is a variant of [`QComboBox`](https://doc.qt.io/qt-5/qcombobox.html)
|
||||
that populates the items in the combobox based on a python `Enum` class. In addition to all
|
||||
the methods provided by `QComboBox`, this subclass adds the methods
|
||||
`enumClass`/`setEnumClass` to get/set the current `Enum` class represented by the combobox,
|
||||
and `currentEnum`/`setCurrentEnum` to get/set the current `Enum` member in the combobox.
|
||||
There is also a new signal `currentEnumChanged(enum)` analogous to `currentIndexChanged` and `currentTextChanged`.
|
||||
|
||||
Method like `insertItem` and `addItem` are blocked and try of its usage will end with `RuntimeError`
|
||||
|
||||
```python
|
||||
from enum import Enum
|
||||
|
||||
from superqt import QEnumComboBox
|
||||
|
||||
class SampleEnum(Enum):
|
||||
first = 1
|
||||
second = 2
|
||||
third = 3
|
||||
|
||||
# as usual:
|
||||
# you must create a QApplication before create a widget.
|
||||
|
||||
combo = QEnumComboBox()
|
||||
combo.setEnumClass(SampleEnum)
|
||||
```
|
||||
|
||||
other option is to use optional `enum_class` argument of constructor and change
|
||||
```python
|
||||
combo = QEnumComboBox()
|
||||
combo.setEnumClass(SampleEnum)
|
||||
```
|
||||
to
|
||||
```python
|
||||
combo = QEnumComboBox(enum_class=SampleEnum)
|
||||
```
|
||||
|
||||
|
||||
### Allow `None`
|
||||
`QEnumComboBox` allow using Optional type annotation:
|
||||
|
||||
```python
|
||||
from enum import Enum
|
||||
|
||||
from superqt import QEnumComboBox
|
||||
|
||||
class SampleEnum(Enum):
|
||||
first = 1
|
||||
second = 2
|
||||
third = 3
|
||||
|
||||
# as usual:
|
||||
# you must create a QApplication before create a widget.
|
||||
|
||||
combo = QEnumComboBox()
|
||||
combo.setEnumClass(SampleEnum, allow_none=True)
|
||||
```
|
||||
|
||||
In this case there is added option `----` and `currentEnum` will return `None` for it.
|
26
docs/faq.md
Normal file
26
docs/faq.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# FAQ
|
||||
|
||||
## Sliders not dragging properly on MacOS 12+
|
||||
|
||||
??? details
|
||||
On MacOS Monterey, with Qt5, there is a bug that causes all sliders
|
||||
(including native Qt sliders) to not respond properly to drag events. See:
|
||||
|
||||
- [https://bugreports.qt.io/browse/QTBUG-98093](https://bugreports.qt.io/browse/QTBUG-98093)
|
||||
- [https://github.com/pyapp-kit/superqt/issues/74](https://github.com/pyapp-kit/superqt/issues/74)
|
||||
|
||||
Superqt includes a workaround for this issue, but it is not perfect, and it requires using a custom stylesheet (which may interfere with your own styles). Note that you
|
||||
may not see this issue if you're already using custom stylesheets.
|
||||
|
||||
To opt in to the workaround, do any of the following:
|
||||
|
||||
- set the environment variable `USE_MAC_SLIDER_PATCH=1` before importing superqt
|
||||
(note: this is safe to use even if you're targeting more than just MacOS 12, it will only be applied when needed)
|
||||
- call the `applyMacStylePatch()` method on any of the superqt slider subclasses (note, this will override your slider styles)
|
||||
- apply the stylesheet manually:
|
||||
|
||||
```python
|
||||
from superqt.sliders import MONTEREY_SLIDER_STYLES_FIX
|
||||
|
||||
slider.setStyleSheet(MONTEREY_SLIDER_STYLES_FIX)
|
||||
```
|
29
docs/index.md
Normal file
29
docs/index.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# superqt
|
||||
|
||||
##  "missing" widgets and components for PyQt/PySide
|
||||
|
||||
This repository aims to provide high-quality community-contributed Qt widgets
|
||||
and components for [PyQt](https://riverbankcomputing.com/software/pyqt/) &
|
||||
[PySide](https://www.qt.io/qt-for-python) that are not provided in the native
|
||||
QtWidgets module.
|
||||
|
||||
Components are tested on:
|
||||
|
||||
- macOS, Windows, & Linux
|
||||
- Python 3.8 and above
|
||||
- PyQt5 (5.11 and above) & PyQt6
|
||||
- PySide2 (5.11 and above) & PySide6
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install superqt
|
||||
```
|
||||
|
||||
```bash
|
||||
conda install -c conda-forge superqt
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
See the [Widgets](./widgets/) and [Utilities](./utilities/) pages for features offered by superqt.
|
237
docs/sliders.md
237
docs/sliders.md
@@ -1,237 +0,0 @@
|
||||
# Sliders
|
||||
|
||||
|
||||

|
||||
|
||||
- `QRangeSlider` inherits from [`QSlider`](https://doc.qt.io/qt-5/qslider.html)
|
||||
and attempts to match the Qt API as closely as possible
|
||||
- Uses platform-specific styles (for handle, groove, & ticks) but also supports
|
||||
QSS style sheets.
|
||||
- Supports mouse wheel and keypress (soon) events
|
||||
- Supports more than 2 handles (e.g. `slider.setValue([0, 10, 60, 80])`)
|
||||
|
||||
------
|
||||
|
||||
## Range Slider
|
||||
|
||||
```python
|
||||
from superqt import QRangeSlider
|
||||
|
||||
# as usual:
|
||||
# you must create a QApplication before create a widget.
|
||||
range_slider = QRangeSlider()
|
||||
```
|
||||
|
||||
As `QRangeSlider` inherits from `QtWidgets.QSlider`, you can use all of the
|
||||
same methods available in the [QSlider API](https://doc.qt.io/qt-5/qslider.html). The major difference is that `value` and `sliderPosition` are reimplemented as `tuples` of `int` (where the length of the tuple is equal to the number of handles in the slider.)
|
||||
|
||||
### `value: Tuple[int, ...]`
|
||||
|
||||
This property holds the current value of all handles in the slider.
|
||||
|
||||
The slider forces all values to be within the legal range:
|
||||
`minimum <= value <= maximum`.
|
||||
|
||||
Changing the value also changes the sliderPosition.
|
||||
|
||||
##### Access Functions:
|
||||
|
||||
```python
|
||||
range_slider.value() -> Tuple[int, ...]
|
||||
```
|
||||
|
||||
```python
|
||||
range_slider.setValue(val: Sequence[int]) -> None
|
||||
```
|
||||
|
||||
##### Notifier Signal:
|
||||
|
||||
```python
|
||||
valueChanged(Tuple[int, ...])
|
||||
```
|
||||
|
||||
### `sliderPosition: Tuple[int, ...]`
|
||||
|
||||
This property holds the current slider positions. It is a `tuple` with length equal to the number of handles.
|
||||
|
||||
If [tracking](https://doc.qt.io/qt-5/qabstractslider.html#tracking-prop) is enabled (the default), this is identical to [`value`](#value--tupleint-).
|
||||
|
||||
##### Access Functions:
|
||||
|
||||
```python
|
||||
range_slider.sliderPosition() -> Tuple[int, ...]
|
||||
```
|
||||
|
||||
```python
|
||||
range_slider.setSliderPosition(val: Sequence[int]) -> None
|
||||
```
|
||||
|
||||
##### Notifier Signal:
|
||||
|
||||
```python
|
||||
sliderMoved(Tuple[int, ...])
|
||||
```
|
||||
|
||||
### Additional properties
|
||||
|
||||
These options are in addition to the Qt QSlider API, and control the behavior of the bar between handles.
|
||||
|
||||
| getter | setter | type | default | description |
|
||||
| -------------------- | ------------------------------------------- | ------ | ------- | ------------------------------------------------------------------------------------------------ |
|
||||
| `barIsVisible` | `setBarIsVisible` <br>`hideBar` / `showBar` | `bool` | `True` | <small>Whether the bar between handles is visible.</small> |
|
||||
| `barMovesAllHandles` | `setBarMovesAllHandles` | `bool` | `True` | <small>Whether clicking on the bar moves all handles or just the nearest</small> |
|
||||
| `barIsRigid` | `setBarIsRigid` | `bool` | `True` | <small>Whether bar length is constant or "elastic" when dragging the bar beyond min/max.</small> |
|
||||
------
|
||||
|
||||
### Examples
|
||||
|
||||
These screenshots show `QRangeSlider` (multiple handles) next to the native `QSlider`
|
||||
(single handle). With no styles applied, `QRangeSlider` will match the native OS
|
||||
style of `QSlider` – with or without tick marks. When styles have been applied
|
||||
using [Qt Style Sheets](https://doc.qt.io/qt-5/stylesheet-reference.html), then
|
||||
`QRangeSlider` will inherit any styles applied to `QSlider` (since it inherits
|
||||
from QSlider). If you'd like to style `QRangeSlider` differently than `QSlider`,
|
||||
then you can also target it directly in your style sheet. The one "special"
|
||||
property for QRangeSlider is `qproperty-barColor`, which sets the color of the
|
||||
bar between the handles.
|
||||
|
||||
> The code for these example widgets is [here](../examples/demo_widget.py)
|
||||
<details>
|
||||
|
||||
<summary><em>See style sheet used for this example</em></summary>
|
||||
|
||||
```css
|
||||
/*
|
||||
Because QRangeSlider inherits from QSlider, it will also inherit styles
|
||||
*/
|
||||
QSlider {
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
QSlider::groove:horizontal {
|
||||
border: 0px;
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
|
||||
stop:0 #777, stop:1 #aaa);
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
QSlider::handle {
|
||||
background: qradialgradient(cx:0, cy:0, radius: 1.2, fx:0.5,
|
||||
fy:0.5, stop:0 #eef, stop:1 #000);
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/*
|
||||
"QSlider::sub-page" is the one exception ...
|
||||
(it styles the area to the left of the QSlider handle)
|
||||
*/
|
||||
QSlider::sub-page:horizontal {
|
||||
background: #447;
|
||||
border-top-left-radius: 10px;
|
||||
border-bottom-left-radius: 10px;
|
||||
}
|
||||
|
||||
/*
|
||||
for QRangeSlider: use "qproperty-barColor". "sub-page" will not work.
|
||||
*/
|
||||
QRangeSlider {
|
||||
qproperty-barColor: #447;
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### macOS
|
||||
|
||||
##### Catalina
|
||||
|
||||

|
||||
|
||||
##### Big Sur
|
||||
|
||||

|
||||
|
||||
#### Windows
|
||||
|
||||

|
||||
|
||||
#### Linux
|
||||
|
||||

|
||||
|
||||
## Labeled Sliders
|
||||
|
||||
This package also includes two "labeled" slider variants. One for `QRangeSlider`, and one for the native `QSlider`:
|
||||
|
||||
### `QLabeledRangeSlider`
|
||||
|
||||

|
||||
|
||||
```python
|
||||
from superqt import QLabeledRangeSlider
|
||||
```
|
||||
|
||||
This has the same API as `QRangeSlider` with the following additional options:
|
||||
|
||||
#### `handleLabelPosition`/`setHandleLabelPosition`
|
||||
|
||||
Where/whether labels are shown adjacent to slider handles.
|
||||
|
||||
**type:** `QLabeledRangeSlider.LabelPosition`
|
||||
|
||||
**default:** `LabelPosition.LabelsAbove`
|
||||
|
||||
*options:*
|
||||
|
||||
- `LabelPosition.NoLabel` (no labels shown adjacent to handles)
|
||||
- `LabelPosition.LabelsAbove`
|
||||
- `LabelPosition.LabelsBelow`
|
||||
- `LabelPosition.LabelsRight` (alias for `LabelPosition.LabelsAbove`)
|
||||
- `LabelPosition.LabelsLeft` (alias for `LabelPosition.LabelsBelow`)
|
||||
|
||||
#### `edgeLabelMode`/`setEdgeLabelMode`
|
||||
|
||||
**type:** `QLabeledRangeSlider.EdgeLabelMode`
|
||||
|
||||
**default:** `EdgeLabelMode.LabelIsRange`
|
||||
|
||||
*options:*
|
||||
|
||||
- `EdgeLabelMode.NoLabel`: no labels shown at slider extremes
|
||||
- `EdgeLabelMode.LabelIsRange`: edge labels shown the min/max values
|
||||
- `EdgeLabelMode.LabelIsValue`: edge labels shown the slider range
|
||||
|
||||
#### fine tuning position of labels:
|
||||
|
||||
If you find that you need to fine tune the position of the handle labels:
|
||||
|
||||
- `QLabeledRangeSlider.label_shift_x`: adjust horizontal label position
|
||||
- `QLabeledRangeSlider.label_shift_y`: adjust vertical label position
|
||||
|
||||
### `QLabeledSlider`
|
||||
|
||||

|
||||
|
||||
```python
|
||||
from superqt import QLabeledSlider
|
||||
```
|
||||
|
||||
(no additional options at this point)
|
||||
|
||||
## Issues
|
||||
|
||||
If you encounter any problems, please [file an issue] along with a detailed
|
||||
description.
|
||||
|
||||
[file an issue]: https://github.com/napari/superqt/issues
|
||||
|
||||
## Float Slider
|
||||
|
||||
just like QSlider, but supports float values
|
||||
|
||||
```python
|
||||
from superqt import QDoubleSlider
|
||||
```
|
52
docs/utilities/code_syntax_highlight.md
Normal file
52
docs/utilities/code_syntax_highlight.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# CodeSyntaxHighlight
|
||||
|
||||
A code highlighter subclass of `QSyntaxHighlighter`
|
||||
that can be used to highlight code in a QTextEdit.
|
||||
|
||||
Code lexer and available styles are from [`pygments`](https://pygments.org/) python library
|
||||
|
||||
List of available languages are available [here](https://pygments.org/languages/).
|
||||
|
||||
List of available styles are available [here](https://pygments.org/styles/).
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from qtpy.QtGui import QColor, QPalette
|
||||
from qtpy.QtWidgets import QApplication, QTextEdit
|
||||
|
||||
from superqt.utils import CodeSyntaxHighlight
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
text_area = QTextEdit()
|
||||
|
||||
highlight = CodeSyntaxHighlight(text_area.document(), "python", "monokai")
|
||||
|
||||
palette = text_area.palette()
|
||||
palette.setColor(QPalette.Base, QColor(highlight.background_color))
|
||||
text_area.setPalette(palette)
|
||||
text_area.setText(
|
||||
"""from argparse import ArgumentParser
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument("name", help="Your name")
|
||||
args = parser.parse_args()
|
||||
print(f"Hello {args.name}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
"""
|
||||
)
|
||||
|
||||
text_area.show()
|
||||
text_area.resize(400, 200)
|
||||
|
||||
app.exec_()
|
||||
```
|
||||
|
||||
{{ show_widget() }}
|
||||
|
||||
{{ show_members('superqt.utils.CodeSyntaxHighlight') }}
|
101
docs/utilities/fonticon.md
Normal file
101
docs/utilities/fonticon.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Font icons
|
||||
|
||||
The `superqt.fonticon` module provides a set of utilities for working with font
|
||||
icons such as [Font Awesome](https://fontawesome.com/) or [Material Design
|
||||
Icons](https://materialdesignicons.com/).
|
||||
|
||||
## Basic Example
|
||||
|
||||
```python
|
||||
from fonticon_fa5 import FA5S
|
||||
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtWidgets import QApplication, QPushButton
|
||||
|
||||
from superqt.fonticon import icon, pulse
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
btn2 = QPushButton()
|
||||
btn2.setIcon(icon(FA5S.smile, color="blue"))
|
||||
btn2.setIconSize(QSize(225, 225))
|
||||
btn2.show()
|
||||
|
||||
app.exec()
|
||||
```
|
||||
|
||||
{{ show_widget(225) }}
|
||||
|
||||
## Font Icon plugins
|
||||
|
||||
Ready-made fonticon packs are available as plugins:
|
||||
|
||||
### [Font Awesome 5](https://fontawesome.com/v5/search)
|
||||
|
||||
```bash
|
||||
pip install fonticon-fontawesome5
|
||||
```
|
||||
|
||||
### [Font Awesome 6](https://fontawesome.com/v6/search)
|
||||
|
||||
```bash
|
||||
pip install fonticon-fontawesome6
|
||||
```
|
||||
|
||||
### [Material Design Icons](https://materialdesignicons.com/)
|
||||
|
||||
```bash
|
||||
pip install fonticon-materialdesignicons6
|
||||
```
|
||||
|
||||
### See also
|
||||
|
||||
- <https://github.com/tlambert03/fonticon-bootstrapicons>
|
||||
- <https://github.com/tlambert03/fonticon-linearicons>
|
||||
- <https://github.com/tlambert03/fonticon-feather>
|
||||
|
||||
`superqt.fonticon` is a pluggable system, and font icon packs may use the `"superqt.fonticon"`
|
||||
entry point to register themselves with superqt. See [`fonticon-cookiecutter`](https://github.com/tlambert03/fonticon-cookiecutter) for a template, or look through the following repos for examples:
|
||||
|
||||
- <https://github.com/tlambert03/fonticon-fontawesome6>
|
||||
- <https://github.com/tlambert03/fonticon-fontawesome5>
|
||||
- <https://github.com/tlambert03/fonticon-materialdesignicons6>
|
||||
|
||||
## API
|
||||
|
||||
::: superqt.fonticon.icon
|
||||
options:
|
||||
heading_level: 3
|
||||
|
||||
::: superqt.fonticon.setTextIcon
|
||||
options:
|
||||
heading_level: 3
|
||||
|
||||
::: superqt.fonticon.font
|
||||
options:
|
||||
heading_level: 3
|
||||
|
||||
::: superqt.fonticon.IconOpts
|
||||
options:
|
||||
heading_level: 3
|
||||
|
||||
::: superqt.fonticon.addFont
|
||||
options:
|
||||
heading_level: 3
|
||||
|
||||
## Animations
|
||||
|
||||
the `animation` parameter to `icon()` accepts a subclass of
|
||||
`Animation` that will be
|
||||
|
||||
::: superqt.fonticon.Animation
|
||||
options:
|
||||
heading_level: 3
|
||||
|
||||
::: superqt.fonticon.pulse
|
||||
options:
|
||||
heading_level: 3
|
||||
|
||||
::: superqt.fonticon.spin
|
||||
options:
|
||||
heading_level: 3
|
31
docs/utilities/index.md
Normal file
31
docs/utilities/index.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Utilities
|
||||
|
||||
## Font Icons
|
||||
|
||||
| Object | Description |
|
||||
| ----------- | --------------------- |
|
||||
| [`addFont`](./fonticon.md#superqt.fonticon.addFont) | Add an `OTF/TTF` file at to the font registry. |
|
||||
| [`font`](./fonticon.md#superqt.fonticon.font) | Create `QFont` for a given font-icon font family key |
|
||||
| [`icon`](./fonticon.md#superqt.fonticon.icon) | Create a `QIcon` for font-con glyph key |
|
||||
| [`setTextIcon`](./fonticon.md#superqt.fonticon.setTextIcon) | Set text on a `QWidget` to a specific font & glyph. |
|
||||
| [`IconFont`](./fonticon.md#superqt.fonticon.IconFont) | Helper class that provides a standard way to create an `IconFont`. |
|
||||
| [`IconOpts`](./fonticon.md#superqt.fonticon.IconOpts) | Options for rendering an icon |
|
||||
| [`Animation`](./fonticon.md#superqt.fonticon.Animation) | Base class for adding animations to a font-icon. |
|
||||
|
||||
## Threading tools
|
||||
|
||||
| Object | Description |
|
||||
| ----------- | --------------------- |
|
||||
| [`ensure_main_thread`](./thread_decorators.md#ensure_main_thread) | Decorator that ensures a function is called in the main `QApplication` thread. |
|
||||
| [`ensure_object_thread`](./thread_decorators.md#ensure_object_thread) | Decorator that ensures a `QObject` method is called in the object's thread. |
|
||||
| [`FunctionWorker`](./threading.md#superqt.utils.FunctionWorker) | `QRunnable` with signals that wraps a simple long-running function. |
|
||||
| [`GeneratorWorker`](./threading.md#superqt.utils.GeneratorWorker) | `QRunnable` with signals that wraps a long-running generator. |
|
||||
| [`create_worker`](./threading.md#superqt.utils.create_worker) | Create a worker to run a target function in another thread. |
|
||||
| [`thread_worker`](./threading.md#superqt.utils.thread_worker) | Decorator for `create_worker`, turn a function into a worker. |
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
| Object | Description |
|
||||
| ----------- | --------------------- |
|
||||
| [`QMessageHandler`](./qmessagehandler.md) | A context manager to intercept messages from Qt. |
|
||||
| [`CodeSyntaxHighlight`](./code_syntax_highlight.md) | A `QSyntaxHighlighter` for code syntax highlighting. |
|
8
docs/utilities/qmessagehandler.md
Normal file
8
docs/utilities/qmessagehandler.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# QMessageHandler
|
||||
|
||||
::: superqt.utils.QMessageHandler
|
||||
options:
|
||||
heading_level: 3
|
||||
show_signature_annotations: True
|
||||
docstring_style: numpy
|
||||
show_bases: False
|
3
docs/utilities/signal_utils.md
Normal file
3
docs/utilities/signal_utils.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Signal Utilities
|
||||
|
||||
::: superqt.utils.signals_blocked
|
@@ -1,22 +1,28 @@
|
||||
# Decorators
|
||||
|
||||
## Move to thread decorators
|
||||
# Threading decorators
|
||||
|
||||
`superqt` provides two decorators that help to ensure that given function is
|
||||
running in the desired thread:
|
||||
|
||||
* `ensure_main_thread` - ensures that the decorated function/method runs in the main thread
|
||||
* `ensure_object_thread` - ensures that a decorated bound method of a `QObject` runs in the
|
||||
thread in which the instance lives ([qt
|
||||
documentation](https://doc.qt.io/qt-5/threads-qobject.html#accessing-qobject-subclasses-from-other-threads)).
|
||||
## `ensure_main_thread`
|
||||
|
||||
`ensure_main_thread` ensures that the decorated function/method runs in the main thread
|
||||
|
||||
## `ensure_object_thread`
|
||||
|
||||
`ensure_object_thread` ensures that a decorated bound method of a `QObject` runs
|
||||
in the thread in which the instance lives ([see qt documentation for
|
||||
details](https://doc.qt.io/qt-5/threads-qobject.html#accessing-qobject-subclasses-from-other-threads)).
|
||||
|
||||
## Usage
|
||||
|
||||
By default, functions are executed asynchronously (they return immediately with
|
||||
an instance of
|
||||
[`concurrent.futures.Future`](https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Future)).
|
||||
|
||||
To block and wait for the result, see [Synchronous mode](#synchronous-mode)
|
||||
|
||||
```python
|
||||
from superqt.qtcompat.QtCore import QObject
|
||||
from qtpy.QtCore import QObject
|
||||
from superqt import ensure_main_thread, ensure_object_thread
|
||||
|
||||
@ensure_main_thread
|
||||
@@ -57,12 +63,14 @@ As can be seen in this example these decorators can also be used for setters.
|
||||
These decorators should not be used as replacement of Qt Signals but rather to
|
||||
interact with Qt objects from non Qt code.
|
||||
|
||||
### Synchronous mode
|
||||
## Synchronous mode
|
||||
|
||||
If you'd like for the program to block and wait for the result of your function
|
||||
call, use the `await_return=True` parameter, and optionally specify a timeout.
|
||||
|
||||
> *Note: Using synchronous mode may significantly impact performance.*
|
||||
!!! important
|
||||
|
||||
Using synchronous mode may significantly impact performance.
|
||||
|
||||
```python
|
||||
from superqt import ensure_main_thread
|
36
docs/utilities/threading.md
Normal file
36
docs/utilities/threading.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Thread workers
|
||||
|
||||
The objects in this module provide utilities for running tasks in a separate
|
||||
thread. In general (with the exception of `new_worker_qthread`), everything
|
||||
here wraps Qt's [QRunnable API](https://doc.qt.io/qt-6/qrunnable.html).
|
||||
|
||||
The highest level object is the
|
||||
[`@thread_worker`][superqt.utils.thread_worker] decorator. It was originally
|
||||
written for `napari`, and was later extracted into `superqt`. You may also be
|
||||
interested in reading the [napari
|
||||
documentation](https://napari.org/stable/guides/threading.html#threading-in-napari-with-thread-worker) on this feature,
|
||||
which provides a more in-depth/introductory usage guide.
|
||||
|
||||
For additional control, you can create your own
|
||||
[`FunctionWorker`][superqt.utils.FunctionWorker] or
|
||||
[`GeneratorWorker`][superqt.utils.GeneratorWorker] objects.
|
||||
|
||||
::: superqt.utils.WorkerBase
|
||||
|
||||
::: superqt.utils.FunctionWorker
|
||||
|
||||
::: superqt.utils.GeneratorWorker
|
||||
|
||||
## Convenience functions
|
||||
|
||||
::: superqt.utils.thread_worker
|
||||
options:
|
||||
heading_level: 3
|
||||
|
||||
::: superqt.utils.create_worker
|
||||
options:
|
||||
heading_level: 3
|
||||
|
||||
::: superqt.utils.new_worker_qthread
|
||||
options:
|
||||
heading_level: 3
|
46
docs/utilities/throttling.md
Normal file
46
docs/utilities/throttling.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Throttling & Debouncing
|
||||
|
||||
These utilities allow you to throttle or debounce a function. This is useful
|
||||
when you have a function that is called multiple times in a short period of
|
||||
time, and you want to make sure it is only "actually" called once (or at least
|
||||
no more than a certain frequency).
|
||||
|
||||
For background on throttling and debouncing, see:
|
||||
|
||||
- <https://blog.openreplay.com/forever-functional-debouncing-and-throttling-for-performance>
|
||||
- <https://css-tricks.com/debouncing-throttling-explained-examples/>
|
||||
|
||||
::: superqt.utils.qdebounced
|
||||
options:
|
||||
show_source: false
|
||||
docstring_style: numpy
|
||||
show_root_toc_entry: True
|
||||
show_root_heading: True
|
||||
|
||||
::: superqt.utils.qthrottled
|
||||
options:
|
||||
show_source: false
|
||||
docstring_style: numpy
|
||||
show_root_toc_entry: True
|
||||
show_root_heading: True
|
||||
|
||||
::: superqt.utils.QSignalDebouncer
|
||||
options:
|
||||
show_source: false
|
||||
docstring_style: numpy
|
||||
show_root_toc_entry: True
|
||||
show_root_heading: True
|
||||
|
||||
::: superqt.utils.QSignalThrottler
|
||||
options:
|
||||
show_source: false
|
||||
docstring_style: numpy
|
||||
show_root_toc_entry: True
|
||||
show_root_heading: True
|
||||
|
||||
::: superqt.utils._throttler.GenericSignalThrottler
|
||||
options:
|
||||
show_source: false
|
||||
docstring_style: numpy
|
||||
show_root_toc_entry: True
|
||||
show_root_heading: True
|
32
docs/widgets/index.md
Normal file
32
docs/widgets/index.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Widgets
|
||||
|
||||
The following are QWidget subclasses:
|
||||
|
||||
## Sliders and Numerical Inputs
|
||||
|
||||
| Widget | Description |
|
||||
| ----------- | --------------------- |
|
||||
| [`QDoubleRangeSlider`](./qdoublerangeslider.md) | Multi-handle slider for float values |
|
||||
| [`QDoubleSlider`](./qdoubleslider.md) | Slider for float values |
|
||||
| [`QLabeledDoubleRangeSlider`](./qlabeleddoublerangeslider.md) | `QDoubleRangeSlider` variant with editable labels for each handle |
|
||||
| [`QLabeledDoubleSlider`](./qlabeleddoubleslider.md) | `QSlider` for float values with editable `QSpinBox` with the current value |
|
||||
| [`QLabeledRangeSlider`](./qlabeledrangeslider.md) | `QRangeSlider` variant, with editable labels for each handle |
|
||||
| [`QLabeledSlider`](./qlabeledslider.md) | `QSlider` with editable `QSpinBox` that shows the current value |
|
||||
| [`QLargeIntSpinBox`](./qlargeintspinbox.md) | `QSpinbox` that accepts arbitrarily large integers |
|
||||
| [`QRangeSlider`](./qrangeslider.md) | Multi-handle slider |
|
||||
| [`QQuantity`](./qquantity.md) | Pint-backed quantity widget (magnitude combined with unit dropdown) |
|
||||
|
||||
## Labels and categorical inputs
|
||||
|
||||
| Widget | Description |
|
||||
| ----------- | --------------------- |
|
||||
| [`QElidingLabel`](./qelidinglabel.md) | A `QLabel` variant that will elide text (add `…`) to fit width. |
|
||||
| [`QEnumComboBox`](./qenumcombobox.md) | `QComboBox` that populates the combobox from a python `Enum` |
|
||||
| [`QSearchableComboBox`](./qsearchablecombobox.md) | `QComboBox` variant that filters available options based on text input |
|
||||
| [`QSearchableListWidget`](./qsearchablelistwidget.md) | `QListWidget` variant with search field that filters available options |
|
||||
|
||||
## Frames and containers
|
||||
|
||||
| Widget | Description |
|
||||
| ----------- | --------------------- |
|
||||
| [`QCollapsible`](./qcollapsible.md) | A collapsible widget to hide and unhide child widgets. |
|
24
docs/widgets/qcollapsible.md
Normal file
24
docs/widgets/qcollapsible.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# QCollapsible
|
||||
|
||||
Collapsible `QFrame` that can be expanded or collapsed by clicking on the header.
|
||||
|
||||
```python
|
||||
from qtpy.QtWidgets import QApplication, QLabel, QPushButton
|
||||
|
||||
from superqt import QCollapsible
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
collapsible = QCollapsible("Advanced analysis")
|
||||
collapsible.addWidget(QLabel("This is the inside of the collapsible frame"))
|
||||
for i in range(10):
|
||||
collapsible.addWidget(QPushButton(f"Content button {i + 1}"))
|
||||
|
||||
collapsible.expand(animate=False)
|
||||
collapsible.show()
|
||||
app.exec_()
|
||||
```
|
||||
|
||||
{{ show_widget(350) }}
|
||||
|
||||
{{ show_members('superqt.QCollapsible') }}
|
23
docs/widgets/qdoublerangeslider.md
Normal file
23
docs/widgets/qdoublerangeslider.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# QDoubleRangeSlider
|
||||
|
||||
Float variant of [`QRangeSlider`](qrangeslider.md). (see that page for more details).
|
||||
|
||||
```python
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from superqt import QDoubleRangeSlider
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
slider = QDoubleRangeSlider(Qt.Orientation.Horizontal)
|
||||
slider.setRange(0, 1)
|
||||
slider.setValue((0.2, 0.8))
|
||||
slider.show()
|
||||
|
||||
app.exec_()
|
||||
```
|
||||
|
||||
{{ show_widget() }}
|
||||
|
||||
{{ show_members('superqt.QDoubleRangeSlider') }}
|
23
docs/widgets/qdoubleslider.md
Normal file
23
docs/widgets/qdoubleslider.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# QDoubleSlider
|
||||
|
||||
`QSlider` variant that accepts floating point values.
|
||||
|
||||
```python
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from superqt import QDoubleSlider
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
slider = QDoubleSlider(Qt.Orientation.Horizontal)
|
||||
slider.setRange(0, 1)
|
||||
slider.setValue(0.5)
|
||||
slider.show()
|
||||
|
||||
app.exec_()
|
||||
```
|
||||
|
||||
{{ show_widget() }}
|
||||
|
||||
{{ show_members('superqt.QDoubleSlider') }}
|
26
docs/widgets/qelidinglabel.md
Normal file
26
docs/widgets/qelidinglabel.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# QElidingLabel
|
||||
|
||||
`QLabel` variant that will elide text (i.e. add an ellipsis)
|
||||
if it is too long to fit in the available space.
|
||||
|
||||
```python
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from superqt import QElidingLabel
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
widget = QElidingLabel(
|
||||
"a skj skjfskfj sdlf sdfl sdlfk jsdf sdlkf jdsf dslfksdl sdlfk sdf sdl "
|
||||
"fjsdlf kjsdlfk laskdfsal as lsdfjdsl kfjdslf asfd dslkjfldskf sdlkfj"
|
||||
)
|
||||
widget.setWordWrap(True)
|
||||
widget.resize(300, 20)
|
||||
widget.show()
|
||||
|
||||
app.exec_()
|
||||
```
|
||||
|
||||
{{ show_widget(300) }}
|
||||
|
||||
{{ show_members('superqt.QElidingLabel') }}
|
72
docs/widgets/qenumcombobox.md
Normal file
72
docs/widgets/qenumcombobox.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# QEnumComboBox
|
||||
|
||||
`QEnumComboBox` is a variant of
|
||||
[`QComboBox`](https://doc.qt.io/qt-5/qcombobox.html) that populates the items in
|
||||
the combobox based on a python `Enum` class. In addition to all the methods
|
||||
provided by `QComboBox`, this subclass adds the methods
|
||||
`enumClass`/`setEnumClass` to get/set the current `Enum` class represented by
|
||||
the combobox, and `currentEnum`/`setCurrentEnum` to get/set the current `Enum`
|
||||
member in the combobox. There is also a new signal `currentEnumChanged(enum)`
|
||||
analogous to `currentIndexChanged` and `currentTextChanged`.
|
||||
|
||||
Method like `insertItem` and `addItem` are blocked and try of its usage will end
|
||||
with `RuntimeError`
|
||||
|
||||
```python
|
||||
from enum import Enum
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
from superqt import QEnumComboBox
|
||||
|
||||
|
||||
class SampleEnum(Enum):
|
||||
first = 1
|
||||
second = 2
|
||||
third = 3
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
combo = QEnumComboBox()
|
||||
combo.setEnumClass(SampleEnum)
|
||||
combo.show()
|
||||
|
||||
app.exec_()
|
||||
```
|
||||
|
||||
{{ show_widget() }}
|
||||
|
||||
Another option is to use optional `enum_class` argument of constructor and change
|
||||
|
||||
```python
|
||||
# option A:
|
||||
combo = QEnumComboBox()
|
||||
combo.setEnumClass(SampleEnum)
|
||||
# option B:
|
||||
combo = QEnumComboBox(enum_class=SampleEnum)
|
||||
```
|
||||
|
||||
## Allow `None`
|
||||
|
||||
`QEnumComboBox` also allows using `Optional` type annotation:
|
||||
|
||||
```python
|
||||
from enum import Enum
|
||||
|
||||
from superqt import QEnumComboBox
|
||||
|
||||
class SampleEnum(Enum):
|
||||
first = 1
|
||||
second = 2
|
||||
third = 3
|
||||
|
||||
# as usual:
|
||||
# you must create a QApplication before create a widget.
|
||||
|
||||
combo = QEnumComboBox()
|
||||
combo.setEnumClass(SampleEnum, allow_none=True)
|
||||
```
|
||||
|
||||
In this case there is added option `----` and the `currentEnum()` method will
|
||||
return `None` when it is selected.
|
||||
|
||||
{{ show_members('superqt.QEnumComboBox') }}
|
23
docs/widgets/qlabeleddoublerangeslider.md
Normal file
23
docs/widgets/qlabeleddoublerangeslider.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# QLabeledDoubleRangeSlider
|
||||
|
||||
Labeled Float variant of [`QRangeSlider`](qrangeslider.md). (see that page for more details).
|
||||
|
||||
```python
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from superqt import QLabeledDoubleRangeSlider
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
slider = QLabeledDoubleRangeSlider(Qt.Orientation.Horizontal)
|
||||
slider.setRange(0, 1)
|
||||
slider.setValue((0.2, 0.8))
|
||||
slider.show()
|
||||
|
||||
app.exec_()
|
||||
```
|
||||
|
||||
{{ show_widget() }}
|
||||
|
||||
{{ show_members('superqt.QLabeledDoubleRangeSlider') }}
|
24
docs/widgets/qlabeleddoubleslider.md
Normal file
24
docs/widgets/qlabeleddoubleslider.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# QLabeledDoubleSlider
|
||||
|
||||
[`QDoubleSlider`](./qdoubleslider.md) variant that shows an editable (SpinBox) label next to the slider.
|
||||
|
||||
|
||||
```python
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from superqt import QLabeledDoubleSlider
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
slider = QLabeledDoubleSlider(Qt.Orientation.Horizontal)
|
||||
slider.setRange(0, 2.5)
|
||||
slider.setValue(1.3)
|
||||
slider.show()
|
||||
|
||||
app.exec_()
|
||||
```
|
||||
|
||||
{{ show_widget() }}
|
||||
|
||||
{{ show_members('superqt.QLabeledDoubleSlider') }}
|
29
docs/widgets/qlabeledrangeslider.md
Normal file
29
docs/widgets/qlabeledrangeslider.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# QLabeledRangeSlider
|
||||
|
||||
Labeled variant of [`QRangeSlider`](qrangeslider.md). (see that page for more details).
|
||||
|
||||
```python
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from superqt import QLabeledRangeSlider
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
slider = QLabeledRangeSlider(Qt.Orientation.Horizontal)
|
||||
slider.setValue((20, 80))
|
||||
slider.show()
|
||||
|
||||
app.exec_()
|
||||
```
|
||||
|
||||
{{ show_widget() }}
|
||||
|
||||
{{ show_members('superqt.QLabeledRangeSlider') }}
|
||||
|
||||
----
|
||||
|
||||
If you find that you need to fine tune the position of the handle labels:
|
||||
|
||||
- `QLabeledRangeSlider.label_shift_x`: adjust horizontal label position
|
||||
- `QLabeledRangeSlider.label_shift_y`: adjust vertical label position
|
22
docs/widgets/qlabeledslider.md
Normal file
22
docs/widgets/qlabeledslider.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# QLabeledSlider
|
||||
|
||||
`QSlider` variant that shows an editable (SpinBox) label next to the slider.
|
||||
|
||||
```python
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from superqt import QLabeledSlider
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
slider = QLabeledSlider(Qt.Orientation.Horizontal)
|
||||
slider.setValue(42)
|
||||
slider.show()
|
||||
|
||||
app.exec_()
|
||||
```
|
||||
|
||||
{{ show_widget() }}
|
||||
|
||||
{{ show_members('superqt.QLabeledSlider') }}
|
23
docs/widgets/qlargeintspinbox.md
Normal file
23
docs/widgets/qlargeintspinbox.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# QLargeIntSpinBox
|
||||
|
||||
`QSpinBox` variant that allows to enter large integers, without overflow.
|
||||
|
||||
```python
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from superqt import QLargeIntSpinBox
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
slider = QLargeIntSpinBox()
|
||||
slider.setRange(0, 4.53e8)
|
||||
slider.setValue(4.53e8)
|
||||
slider.show()
|
||||
|
||||
app.exec_()
|
||||
```
|
||||
|
||||
{{ show_widget(150) }}
|
||||
|
||||
{{ show_members('superqt.QLargeIntSpinBox') }}
|
33
docs/widgets/qquantity.md
Normal file
33
docs/widgets/qquantity.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# QQuantity
|
||||
|
||||
A widget that allows the user to edit a quantity (a magnitude associated with a unit).
|
||||
|
||||
!!! note
|
||||
|
||||
This widget requires [`pint`](https://pint.readthedocs.io):
|
||||
|
||||
```
|
||||
pip install pint
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
pip install superqt[quantity]
|
||||
```
|
||||
|
||||
```python
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from superqt import QQuantity
|
||||
|
||||
app = QApplication([])
|
||||
w = QQuantity("1m")
|
||||
w.show()
|
||||
|
||||
app.exec()
|
||||
```
|
||||
|
||||
{{ show_widget(150) }}
|
||||
|
||||
{{ show_members('superqt.QQuantity') }}
|
229
docs/widgets/qrangeslider.md
Normal file
229
docs/widgets/qrangeslider.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# QRangeSlider
|
||||
|
||||
A multi-handle slider widget than can be used to
|
||||
select a range of values.
|
||||
|
||||
```python
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from superqt import QRangeSlider
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
slider = QRangeSlider(Qt.Orientation.Horizontal)
|
||||
slider.setValue((20, 80))
|
||||
slider.show()
|
||||
|
||||
app.exec_()
|
||||
```
|
||||
|
||||
{{ show_widget() }}
|
||||
|
||||
- `QRangeSlider` inherits from [`QSlider`](https://doc.qt.io/qt-5/qslider.html)
|
||||
and attempts to match the Qt API as closely as possible
|
||||
- It uses platform-specific styles (for handle, groove, & ticks) but also supports
|
||||
QSS style sheets.
|
||||
- Supports mouse wheel events
|
||||
- Supports more than 2 handles (e.g. `slider.setValue([0, 10, 60, 80])`)
|
||||
|
||||
As `QRangeSlider` inherits from
|
||||
[`QtWidgets.QSlider`](https://doc.qt.io/qt-5/qslider.html), you can use all of
|
||||
the same methods available in the [QSlider
|
||||
API](https://doc.qt.io/qt-5/qslider.html). The major difference is that `value()`
|
||||
and `sliderPosition()` are reimplemented as `tuples` of `int` (where the length of
|
||||
the tuple is equal to the number of handles in the slider.)
|
||||
|
||||
These options are in addition to the Qt QSlider API, and control the behavior of the bar between handles.
|
||||
|
||||
| getter | setter | type | default | description |
|
||||
| -------------------- | ------------------------------------------- | ------ | ------- | ------------------------------------------------------------------------------------------------ |
|
||||
| `barIsVisible` | `setBarIsVisible` <br>`hideBar` / `showBar` | `bool` | `True` | <small>Whether the bar between handles is visible.</small> |
|
||||
| `barMovesAllHandles` | `setBarMovesAllHandles` | `bool` | `True` | <small>Whether clicking on the bar moves all handles or just the nearest</small> |
|
||||
| `barIsRigid` | `setBarIsRigid` | `bool` | `True` | <small>Whether bar length is constant or "elastic" when dragging the bar beyond min/max.</small> |
|
||||
|
||||
### Screenshots
|
||||
|
||||
??? title "code that generates the images below"
|
||||
|
||||
```python
|
||||
import os
|
||||
|
||||
from qtpy import QtCore
|
||||
from qtpy import QtWidgets as QtW
|
||||
|
||||
# patch for Qt 5.15 on macos >= 12
|
||||
os.environ["USE_MAC_SLIDER_PATCH"] = "1"
|
||||
|
||||
from superqt import QRangeSlider # noqa
|
||||
|
||||
QSS = """
|
||||
QSlider {
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
QSlider::groove:horizontal {
|
||||
border: 0px;
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #888, stop:1 #ddd);
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
QSlider::handle {
|
||||
background: qradialgradient(cx:0, cy:0, radius: 1.2, fx:0.35,
|
||||
fy:0.3, stop:0 #eef, stop:1 #002);
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
QSlider::sub-page:horizontal {
|
||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #227, stop:1 #77a);
|
||||
border-top-left-radius: 10px;
|
||||
border-bottom-left-radius: 10px;
|
||||
}
|
||||
|
||||
QRangeSlider {
|
||||
qproperty-barColor: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #227, stop:1 #77a);
|
||||
}
|
||||
"""
|
||||
|
||||
Horizontal = QtCore.Qt.Orientation.Horizontal
|
||||
|
||||
|
||||
class DemoWidget(QtW.QWidget):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
reg_hslider = QtW.QSlider(Horizontal)
|
||||
reg_hslider.setValue(50)
|
||||
range_hslider = QRangeSlider(Horizontal)
|
||||
range_hslider.setValue((20, 80))
|
||||
multi_range_hslider = QRangeSlider(Horizontal)
|
||||
multi_range_hslider.setValue((11, 33, 66, 88))
|
||||
multi_range_hslider.setTickPosition(QtW.QSlider.TickPosition.TicksAbove)
|
||||
|
||||
styled_reg_hslider = QtW.QSlider(Horizontal)
|
||||
styled_reg_hslider.setValue(50)
|
||||
styled_reg_hslider.setStyleSheet(QSS)
|
||||
styled_range_hslider = QRangeSlider(Horizontal)
|
||||
styled_range_hslider.setValue((20, 80))
|
||||
styled_range_hslider.setStyleSheet(QSS)
|
||||
|
||||
reg_vslider = QtW.QSlider(QtCore.Qt.Orientation.Vertical)
|
||||
reg_vslider.setValue(50)
|
||||
range_vslider = QRangeSlider(QtCore.Qt.Orientation.Vertical)
|
||||
range_vslider.setValue((22, 77))
|
||||
|
||||
tick_vslider = QtW.QSlider(QtCore.Qt.Orientation.Vertical)
|
||||
tick_vslider.setValue(55)
|
||||
tick_vslider.setTickPosition(QtW.QSlider.TicksRight)
|
||||
range_tick_vslider = QRangeSlider(QtCore.Qt.Orientation.Vertical)
|
||||
range_tick_vslider.setValue((22, 77))
|
||||
range_tick_vslider.setTickPosition(QtW.QSlider.TicksLeft)
|
||||
|
||||
szp = QtW.QSizePolicy.Maximum
|
||||
left = QtW.QWidget()
|
||||
left.setLayout(QtW.QVBoxLayout())
|
||||
left.setContentsMargins(2, 2, 2, 2)
|
||||
label1 = QtW.QLabel("Regular QSlider Unstyled")
|
||||
label2 = QtW.QLabel("QRangeSliders Unstyled")
|
||||
label3 = QtW.QLabel("Styled Sliders (using same stylesheet)")
|
||||
label1.setSizePolicy(szp, szp)
|
||||
label2.setSizePolicy(szp, szp)
|
||||
label3.setSizePolicy(szp, szp)
|
||||
left.layout().addWidget(label1)
|
||||
left.layout().addWidget(reg_hslider)
|
||||
left.layout().addWidget(label2)
|
||||
left.layout().addWidget(range_hslider)
|
||||
left.layout().addWidget(multi_range_hslider)
|
||||
left.layout().addWidget(label3)
|
||||
left.layout().addWidget(styled_reg_hslider)
|
||||
left.layout().addWidget(styled_range_hslider)
|
||||
|
||||
right = QtW.QWidget()
|
||||
right.setLayout(QtW.QHBoxLayout())
|
||||
right.setContentsMargins(15, 5, 5, 0)
|
||||
right.layout().setSpacing(30)
|
||||
right.layout().addWidget(reg_vslider)
|
||||
right.layout().addWidget(range_vslider)
|
||||
right.layout().addWidget(tick_vslider)
|
||||
right.layout().addWidget(range_tick_vslider)
|
||||
|
||||
self.setLayout(QtW.QHBoxLayout())
|
||||
self.layout().addWidget(left)
|
||||
self.layout().addWidget(right)
|
||||
self.setGeometry(600, 300, 580, 300)
|
||||
self.activateWindow()
|
||||
self.show()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
dest = Path("screenshots")
|
||||
dest.mkdir(exist_ok=True)
|
||||
|
||||
app = QtW.QApplication([])
|
||||
demo = DemoWidget()
|
||||
|
||||
if "-snap" in sys.argv:
|
||||
import platform
|
||||
|
||||
QtW.QApplication.processEvents()
|
||||
demo.grab().save(str(dest / f"demo_{platform.system().lower()}.png"))
|
||||
else:
|
||||
app.exec_()
|
||||
```
|
||||
|
||||
#### macOS
|
||||
|
||||
##### Catalina
|
||||
|
||||
{ width=580; }
|
||||
|
||||
##### Big Sur
|
||||
|
||||
{ width=580; }
|
||||
|
||||
#### Windows
|
||||
|
||||

|
||||
|
||||
#### Linux
|
||||
|
||||

|
||||
|
||||
|
||||
{{ show_members('superqt.sliders._sliders._GenericRangeSlider') }}
|
||||
|
||||
## Type changes
|
||||
|
||||
Note the following changes in types compared to the `QSlider` API:
|
||||
|
||||
```python
|
||||
value() -> Tuple[int, ...]
|
||||
```
|
||||
|
||||
```python
|
||||
setValue(val: Sequence[int]) -> None
|
||||
```
|
||||
|
||||
```python
|
||||
# Signal
|
||||
valueChanged(Tuple[int, ...])
|
||||
```
|
||||
|
||||
```python
|
||||
sliderPosition() -> Tuple[int, ...]
|
||||
```
|
||||
|
||||
```python
|
||||
setSliderPosition(val: Sequence[int]) -> None
|
||||
```
|
||||
|
||||
```python
|
||||
sliderMoved(Tuple[int, ...])
|
||||
```
|
25
docs/widgets/qsearchablecombobox.md
Normal file
25
docs/widgets/qsearchablecombobox.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# QSearchableComboBox
|
||||
|
||||
`QSearchableComboBox` is a variant of
|
||||
[`QComboBox`](https://doc.qt.io/qt-5/qcombobox.html) that allow to filter list
|
||||
of options by enter part of text. It could be drop in replacement for
|
||||
`QComboBox`.
|
||||
|
||||
|
||||
```python
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from superqt import QSearchableComboBox
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
combo = QSearchableComboBox()
|
||||
combo.addItems(["foo", "bar", "baz", "foobar", "foobaz", "barbaz"])
|
||||
combo.show()
|
||||
|
||||
app.exec_()
|
||||
```
|
||||
|
||||
{{ show_widget() }}
|
||||
|
||||
{{ show_members('superqt.QSearchableComboBox') }}
|
28
docs/widgets/qsearchablelistwidget.md
Normal file
28
docs/widgets/qsearchablelistwidget.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# QSearchableListWidget
|
||||
|
||||
`QSearchableListWidget` is a variant of
|
||||
[`QListWidget`](https://doc.qt.io/qt-5/qlistwidget.html) that add text entry
|
||||
above list widget that allow to filter list of available options.
|
||||
|
||||
Due to implementation details, this widget it does not inherit directly from
|
||||
[`QListWidget`](https://doc.qt.io/qt-5/qlistwidget.html) but it does fully
|
||||
satisfy its api. The only limitation is that it cannot be used as argument of
|
||||
[`QListWidgetItem`](https://doc.qt.io/qt-5/qlistwidgetitem.html) constructor.
|
||||
|
||||
```python
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from superqt import QSearchableListWidget
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
slider = QSearchableListWidget()
|
||||
slider.addItems(["foo", "bar", "baz", "foobar", "foobaz", "barbaz"])
|
||||
slider.show()
|
||||
|
||||
app.exec_()
|
||||
```
|
||||
|
||||
{{ show_widget() }}
|
||||
|
||||
{{ show_members('superqt.QSearchableListWidget') }}
|
@@ -1,6 +1,7 @@
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from superqt import QRangeSlider
|
||||
from superqt.qtcompat.QtCore import Qt
|
||||
from superqt.qtcompat.QtWidgets import QApplication
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
|
32
examples/code_highlight.py
Normal file
32
examples/code_highlight.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from PyQt5.QtGui import QColor, QPalette
|
||||
from qtpy.QtWidgets import QApplication, QTextEdit
|
||||
|
||||
from superqt.utils import CodeSyntaxHighlight
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
text_area = QTextEdit()
|
||||
|
||||
highlight = CodeSyntaxHighlight(text_area.document(), "python", "monokai")
|
||||
|
||||
palette = text_area.palette()
|
||||
palette.setColor(QPalette.Base, QColor(highlight.background_color))
|
||||
text_area.setPalette(palette)
|
||||
text_area.setText(
|
||||
"""from argparse import ArgumentParser
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument("name", help="Your name")
|
||||
args = parser.parse_args()
|
||||
print(f"Hello {args.name}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
"""
|
||||
)
|
||||
|
||||
text_area.show()
|
||||
|
||||
app.exec_()
|
@@ -1,6 +1,12 @@
|
||||
from superqt import QRangeSlider
|
||||
from superqt.qtcompat import QtCore
|
||||
from superqt.qtcompat import QtWidgets as QtW
|
||||
import os
|
||||
|
||||
from qtpy import QtCore
|
||||
from qtpy import QtWidgets as QtW
|
||||
|
||||
# patch for Qt 5.15 on macos >= 12
|
||||
os.environ["USE_MAC_SLIDER_PATCH"] = "1"
|
||||
|
||||
from superqt import QRangeSlider # noqa
|
||||
|
||||
QSS = """
|
||||
QSlider {
|
||||
@@ -104,7 +110,6 @@ class DemoWidget(QtW.QWidget):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
@@ -1,12 +1,14 @@
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from superqt import QDoubleSlider
|
||||
from superqt.qtcompat.QtCore import Qt
|
||||
from superqt.qtcompat.QtWidgets import QApplication
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
slider = QDoubleSlider(Qt.Orientation.Horizontal)
|
||||
slider.setRange(0, 1)
|
||||
slider.setValue(0.5)
|
||||
slider.resize(500, 50)
|
||||
slider.show()
|
||||
|
||||
app.exec_()
|
@@ -1,5 +1,6 @@
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from superqt import QElidingLabel
|
||||
from superqt.qtcompat.QtWidgets import QApplication
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
|
||||
from superqt import QDoubleRangeSlider, QDoubleSlider, QRangeSlider
|
||||
from superqt.qtcompat.QtCore import Qt
|
||||
from superqt.qtcompat.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
|
@@ -6,9 +6,10 @@ except ImportError as e:
|
||||
"pip install git+https://github.com/tlambert03/fonticon-fontawesome5.git"
|
||||
)
|
||||
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtWidgets import QApplication, QPushButton
|
||||
|
||||
from superqt.fonticon import icon, pulse
|
||||
from superqt.qtcompat.QtCore import QSize
|
||||
from superqt.qtcompat.QtWidgets import QApplication, QPushButton
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
|
@@ -6,8 +6,9 @@ except ImportError as e:
|
||||
"pip install git+https://github.com/tlambert03/fonticon-fontawesome5.git"
|
||||
)
|
||||
|
||||
from qtpy.QtWidgets import QApplication, QPushButton
|
||||
|
||||
from superqt.fonticon import setTextIcon
|
||||
from superqt.qtcompat.QtWidgets import QApplication, QPushButton
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
|
@@ -6,9 +6,10 @@ except ImportError as e:
|
||||
"pip install git+https://github.com/tlambert03/fonticon-fontawesome5.git"
|
||||
)
|
||||
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtWidgets import QApplication, QPushButton
|
||||
|
||||
from superqt.fonticon import IconOpts, icon, pulse, spin
|
||||
from superqt.qtcompat.QtCore import QSize
|
||||
from superqt.qtcompat.QtWidgets import QApplication, QPushButton
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from superqt import QDoubleSlider
|
||||
from superqt.qtcompat.QtCore import Qt
|
||||
from superqt.qtcompat.QtWidgets import QApplication
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
from qtpy import QtCore, QtGui, QtWidgets
|
||||
from qtpy.QtCore import Qt
|
||||
|
||||
from superqt.fonticon._plugins import loaded
|
||||
from superqt.qtcompat import QtCore, QtGui, QtWidgets
|
||||
from superqt.qtcompat.QtCore import Qt
|
||||
|
||||
P = loaded(load_all=True)
|
||||
if not P:
|
||||
@@ -218,7 +219,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
self.previewGroupBox.setLayout(layout)
|
||||
|
||||
def createGlyphBox(self):
|
||||
self.glyphGroupBox = QtWidgets.QGroupBox("Glpyhs")
|
||||
self.glyphGroupBox = QtWidgets.QGroupBox("Glyphs")
|
||||
self.glyphGroupBox.setMinimumSize(480, 200)
|
||||
self.glyphTable = QtWidgets.QTableWidget()
|
||||
self.glyphTable.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
|
||||
@@ -368,7 +369,6 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
import sys
|
||||
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
|
@@ -1,11 +1,12 @@
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QApplication, QHBoxLayout, QVBoxLayout, QWidget
|
||||
|
||||
from superqt import (
|
||||
QLabeledDoubleRangeSlider,
|
||||
QLabeledDoubleSlider,
|
||||
QLabeledRangeSlider,
|
||||
QLabeledSlider,
|
||||
)
|
||||
from superqt.qtcompat.QtCore import Qt
|
||||
from superqt.qtcompat.QtWidgets import QApplication, QHBoxLayout, QVBoxLayout, QWidget
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from superqt import QRangeSlider
|
||||
from superqt.qtcompat.QtWidgets import QApplication
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
|
@@ -1,10 +1,13 @@
|
||||
"""Example for QCollapsible"""
|
||||
"""Example for QCollapsible."""
|
||||
from qtpy.QtWidgets import QApplication, QLabel, QPushButton
|
||||
|
||||
from superqt import QCollapsible
|
||||
from superqt.qtcompat.QtWidgets import QApplication, QLabel, QPushButton
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
collapsible = QCollapsible("Advanced analysis")
|
||||
collapsible.setCollapsedIcon("+")
|
||||
collapsible.setExpandedIcon("-")
|
||||
collapsible.addWidget(QLabel("This is the inside of the collapsible frame"))
|
||||
for i in range(10):
|
||||
collapsible.addWidget(QPushButton(f"Content button {i + 1}"))
|
9
examples/quantity.py
Normal file
9
examples/quantity.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from superqt import QQuantity
|
||||
|
||||
app = QApplication([])
|
||||
w = QQuantity("1m")
|
||||
w.show()
|
||||
|
||||
app.exec()
|
11
examples/searchable_combo_box.py
Normal file
11
examples/searchable_combo_box.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from superqt import QSearchableComboBox
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
slider = QSearchableComboBox()
|
||||
slider.addItems(["foo", "bar", "baz", "foobar", "foobaz", "barbaz"])
|
||||
slider.show()
|
||||
|
||||
app.exec_()
|
11
examples/searchable_list_widget.py
Normal file
11
examples/searchable_list_widget.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from superqt import QSearchableListWidget
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
slider = QSearchableListWidget()
|
||||
slider.addItems(["foo", "bar", "baz", "foobar", "foobaz", "barbaz"])
|
||||
slider.show()
|
||||
|
||||
app.exec_()
|
29
examples/searchable_tree_widget.py
Normal file
29
examples/searchable_tree_widget.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import logging
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from superqt import QSearchableTreeWidget
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format="%(asctime)s : %(levelname)s : %(filename)s : %(message)s",
|
||||
)
|
||||
|
||||
data = {
|
||||
"none": None,
|
||||
"str": "test",
|
||||
"int": 42,
|
||||
"list": [2, 3, 5],
|
||||
"dict": {
|
||||
"float": 0.5,
|
||||
"tuple": (22, 99),
|
||||
"bool": False,
|
||||
},
|
||||
}
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
tree = QSearchableTreeWidget.fromData(data)
|
||||
tree.show()
|
||||
|
||||
app.exec_()
|
29
examples/throttle_mouse_event.py
Normal file
29
examples/throttle_mouse_event.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from qtpy.QtCore import Signal
|
||||
from qtpy.QtWidgets import QApplication, QWidget
|
||||
|
||||
from superqt.utils import qthrottled
|
||||
|
||||
|
||||
class Demo(QWidget):
|
||||
positionChanged = Signal(int, int)
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.setMouseTracking(True)
|
||||
self.positionChanged.connect(self._show_location)
|
||||
|
||||
@qthrottled(timeout=400) # call this no more than once every 400ms
|
||||
def _show_location(self, x, y):
|
||||
print("Throttled event at", x, y)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
print("real move event at", event.x(), event.y())
|
||||
self.positionChanged.emit(event.x(), event.y())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication([])
|
||||
w = Demo()
|
||||
w.resize(600, 600)
|
||||
w.show()
|
||||
app.exec_()
|
280
examples/throttler_demo.py
Normal file
280
examples/throttler_demo.py
Normal file
@@ -0,0 +1,280 @@
|
||||
"""Adapted for python from the KDToolBox.
|
||||
|
||||
https://github.com/KDAB/KDToolBox/tree/master/qt/KDSignalThrottler
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (C) 2019-2022 Klarälvdalens Datakonsult AB, a KDAB Group company,
|
||||
info@kdab.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
"""
|
||||
|
||||
from typing import Deque
|
||||
|
||||
from qtpy.QtCore import QRect, QSize, Qt, QTimer, Signal
|
||||
from qtpy.QtGui import QPainter, QPen
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
QFormLayout,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QSpinBox,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from superqt.utils._throttler import (
|
||||
GenericSignalThrottler,
|
||||
QSignalDebouncer,
|
||||
QSignalThrottler,
|
||||
)
|
||||
|
||||
|
||||
class DrawSignalsWidget(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
|
||||
self.setAttribute(Qt.WA_OpaquePaintEvent)
|
||||
|
||||
self._scrollTimer = QTimer(self)
|
||||
self._scrollTimer.setInterval(10)
|
||||
self._scrollTimer.timeout.connect(self._scroll)
|
||||
self._scrollTimer.start()
|
||||
|
||||
self._signalActivations: Deque[int] = Deque()
|
||||
self._throttledSignalActivations: Deque[int] = Deque()
|
||||
|
||||
def sizeHint(self):
|
||||
return QSize(400, 200)
|
||||
|
||||
def addSignalActivation(self):
|
||||
self._signalActivations.appendleft(0)
|
||||
|
||||
def addThrottledSignalActivation(self):
|
||||
self._throttledSignalActivations.appendleft(0)
|
||||
|
||||
def _scroll(self):
|
||||
cutoff = self.width()
|
||||
self.scrollAndCut(self._signalActivations, cutoff)
|
||||
self.scrollAndCut(self._throttledSignalActivations, cutoff)
|
||||
|
||||
self.update()
|
||||
|
||||
def scrollAndCut(self, v: Deque[int], cutoff: int):
|
||||
L = len(v)
|
||||
for p in range(L):
|
||||
v[p] += 1
|
||||
if v[p] > cutoff:
|
||||
break
|
||||
|
||||
# TODO: fix this... delete old ones
|
||||
|
||||
def paintEvent(self, event):
|
||||
p = QPainter(self)
|
||||
p.fillRect(self.rect(), Qt.white)
|
||||
|
||||
h = self.height()
|
||||
h2 = h // 2
|
||||
w = self.width()
|
||||
|
||||
self._drawSignals(p, self._signalActivations, Qt.red, 0, h2)
|
||||
self._drawSignals(p, self._throttledSignalActivations, Qt.blue, h2, h)
|
||||
|
||||
p.drawText(
|
||||
QRect(0, 0, w, h2),
|
||||
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter,
|
||||
"Source signal",
|
||||
)
|
||||
p.drawText(
|
||||
QRect(0, h2, w, h2),
|
||||
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter,
|
||||
"Throttled signal",
|
||||
)
|
||||
|
||||
p.save()
|
||||
pen = QPen()
|
||||
pen.setWidthF(2.0)
|
||||
p.drawLine(0, h2, w, h2)
|
||||
p.restore()
|
||||
|
||||
def _drawSignals(self, p: QPainter, v: Deque[int], color, yStart, yEnd):
|
||||
p.save()
|
||||
pen = QPen()
|
||||
pen.setWidthF(2.0)
|
||||
pen.setColor(color)
|
||||
p.setPen(pen)
|
||||
|
||||
for i in v:
|
||||
p.drawLine(i, yStart, i, yEnd)
|
||||
p.restore()
|
||||
|
||||
|
||||
class DemoWidget(QWidget):
|
||||
signalToBeThrottled = Signal()
|
||||
_throttler: GenericSignalThrottler
|
||||
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self._createUi()
|
||||
self._throttler = None
|
||||
|
||||
self._throttlerKindComboBox.currentIndexChanged.connect(self._createThrottler)
|
||||
self._createThrottler()
|
||||
|
||||
self._throttlerTimeoutSpinBox.valueChanged.connect(self.setThrottlerTimeout)
|
||||
self.setThrottlerTimeout()
|
||||
|
||||
self._mainButton.clicked.connect(self.signalToBeThrottled)
|
||||
|
||||
self._autoTriggerTimer = QTimer(self)
|
||||
self._autoTriggerTimer.setTimerType(Qt.TimerType.PreciseTimer)
|
||||
self._autoTriggerCheckBox.clicked.connect(self._startOrStopAutoTriggerTimer)
|
||||
self._startOrStopAutoTriggerTimer()
|
||||
|
||||
self._autoTriggerIntervalSpinBox.valueChanged.connect(
|
||||
self._setAutoTriggerTimeout
|
||||
)
|
||||
self._setAutoTriggerTimeout()
|
||||
|
||||
self._autoTriggerTimer.timeout.connect(self.signalToBeThrottled)
|
||||
self.signalToBeThrottled.connect(self._drawSignalsWidget.addSignalActivation)
|
||||
|
||||
def _createThrottler(self) -> None:
|
||||
if self._throttler is not None:
|
||||
self._throttler.deleteLater()
|
||||
del self._throttler
|
||||
|
||||
if self._throttlerKindComboBox.currentIndex() < 2:
|
||||
cls = QSignalThrottler
|
||||
else:
|
||||
cls = QSignalDebouncer
|
||||
if self._throttlerKindComboBox.currentIndex() % 2:
|
||||
policy = QSignalThrottler.EmissionPolicy.Leading
|
||||
else:
|
||||
policy = QSignalThrottler.EmissionPolicy.Trailing
|
||||
|
||||
self._throttler: GenericSignalThrottler = cls(policy, self)
|
||||
|
||||
self._throttler.setTimerType(Qt.TimerType.PreciseTimer)
|
||||
self.signalToBeThrottled.connect(self._throttler.throttle)
|
||||
self._throttler.triggered.connect(
|
||||
self._drawSignalsWidget.addThrottledSignalActivation
|
||||
)
|
||||
|
||||
self.setThrottlerTimeout()
|
||||
|
||||
def setThrottlerTimeout(self):
|
||||
self._throttler.setTimeout(self._throttlerTimeoutSpinBox.value())
|
||||
|
||||
def _startOrStopAutoTriggerTimer(self):
|
||||
shouldStart = self._autoTriggerCheckBox.isChecked()
|
||||
if shouldStart:
|
||||
self._autoTriggerTimer.start()
|
||||
else:
|
||||
self._autoTriggerTimer.stop()
|
||||
|
||||
self._autoTriggerIntervalSpinBox.setEnabled(shouldStart)
|
||||
self._autoTriggerLabel.setEnabled(shouldStart)
|
||||
|
||||
def _setAutoTriggerTimeout(self):
|
||||
timeout = self._autoTriggerIntervalSpinBox.value()
|
||||
self._autoTriggerTimer.setInterval(timeout)
|
||||
|
||||
def _createUi(self):
|
||||
helpLabel = QLabel(self)
|
||||
helpLabel.setWordWrap(True)
|
||||
helpLabel.setText(
|
||||
"<h2>SignalThrottler example</h2>"
|
||||
"<p>This example demonstrates the differences between "
|
||||
"the different kinds of signal throttlers and debouncers."
|
||||
)
|
||||
|
||||
throttlerKindGroupBox = QGroupBox("Throttler configuration", self)
|
||||
|
||||
self._throttlerKindComboBox = QComboBox(throttlerKindGroupBox)
|
||||
self._throttlerKindComboBox.addItems(
|
||||
(
|
||||
"Throttler, trailing",
|
||||
"Throttler, leading",
|
||||
"Debouncer, trailing",
|
||||
"Debouncer, leading",
|
||||
)
|
||||
)
|
||||
|
||||
self._throttlerTimeoutSpinBox = QSpinBox(throttlerKindGroupBox)
|
||||
self._throttlerTimeoutSpinBox.setRange(1, 5000)
|
||||
self._throttlerTimeoutSpinBox.setValue(500)
|
||||
self._throttlerTimeoutSpinBox.setSuffix(" ms")
|
||||
|
||||
layout = QFormLayout(throttlerKindGroupBox)
|
||||
layout.addRow("Kind of throttler:", self._throttlerKindComboBox)
|
||||
layout.addRow("Timeout:", self._throttlerTimeoutSpinBox)
|
||||
throttlerKindGroupBox.setLayout(layout)
|
||||
|
||||
buttonGroupBox = QGroupBox("Throttler activation")
|
||||
self._mainButton = QPushButton(("Press me!"), buttonGroupBox)
|
||||
|
||||
self._autoTriggerCheckBox = QCheckBox("Trigger automatically")
|
||||
|
||||
autoTriggerLayout = QHBoxLayout()
|
||||
self._autoTriggerLabel = QLabel("Interval", buttonGroupBox)
|
||||
self._autoTriggerIntervalSpinBox = QSpinBox(buttonGroupBox)
|
||||
self._autoTriggerIntervalSpinBox.setRange(1, 5000)
|
||||
self._autoTriggerIntervalSpinBox.setValue(100)
|
||||
self._autoTriggerIntervalSpinBox.setSuffix(" ms")
|
||||
|
||||
autoTriggerLayout.setContentsMargins(0, 0, 0, 0)
|
||||
autoTriggerLayout.addWidget(self._autoTriggerLabel)
|
||||
autoTriggerLayout.addWidget(self._autoTriggerIntervalSpinBox)
|
||||
|
||||
layout = QVBoxLayout(buttonGroupBox)
|
||||
layout.addWidget(self._mainButton)
|
||||
layout.addWidget(self._autoTriggerCheckBox)
|
||||
layout.addLayout(autoTriggerLayout)
|
||||
buttonGroupBox.setLayout(layout)
|
||||
|
||||
resultGroupBox = QGroupBox("Result")
|
||||
self._drawSignalsWidget = DrawSignalsWidget(resultGroupBox)
|
||||
layout = QVBoxLayout(resultGroupBox)
|
||||
layout.addWidget(self._drawSignalsWidget)
|
||||
resultGroupBox.setLayout(layout)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(helpLabel)
|
||||
layout.addWidget(throttlerKindGroupBox)
|
||||
layout.addWidget(buttonGroupBox)
|
||||
layout.addWidget(resultGroupBox)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication([__name__])
|
||||
w = DemoWidget()
|
||||
w.resize(600, 600)
|
||||
w.show()
|
||||
app.exec_()
|
58
mkdocs.yml
Normal file
58
mkdocs.yml
Normal file
@@ -0,0 +1,58 @@
|
||||
site_name: superqt
|
||||
site_url: https://github.com/pyapp-kit/superqt
|
||||
site_description: >-
|
||||
missing widgets and components for PyQt/PySide
|
||||
# Repository
|
||||
repo_name: pyapp-kit/superqt
|
||||
repo_url: https://github.com/pyapp-kit/superqt
|
||||
|
||||
# Copyright
|
||||
copyright: Copyright © 2021 - 2022 Talley Lambert
|
||||
|
||||
extra_css:
|
||||
- stylesheets/extra.css
|
||||
|
||||
watch:
|
||||
- src
|
||||
|
||||
theme:
|
||||
name: material
|
||||
features:
|
||||
- navigation.instant
|
||||
- navigation.indexes
|
||||
- navigation.expand
|
||||
# - navigation.tracking
|
||||
# - navigation.tabs
|
||||
- search.highlight
|
||||
- search.suggest
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- pymdownx.details
|
||||
- pymdownx.superfences
|
||||
- tables
|
||||
- attr_list
|
||||
- md_in_html
|
||||
- pymdownx.emoji:
|
||||
emoji_index: !!python/name:materialx.emoji.twemoji
|
||||
emoji_generator: !!python/name:materialx.emoji.to_svg
|
||||
- toc:
|
||||
permalink: "#"
|
||||
|
||||
|
||||
plugins:
|
||||
- search
|
||||
- autorefs
|
||||
- mkdocstrings
|
||||
- macros:
|
||||
module_name: docs/_macros
|
||||
- mkdocstrings:
|
||||
handlers:
|
||||
python:
|
||||
import:
|
||||
- https://docs.python.org/3/objects.inv
|
||||
options:
|
||||
show_source: false
|
||||
docstring_style: numpy
|
||||
show_root_toc_entry: True
|
||||
show_root_heading: True
|
185
pyproject.toml
185
pyproject.toml
@@ -1,10 +1,183 @@
|
||||
# pyproject.toml
|
||||
# https://peps.python.org/pep-0517/
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["hatchling", "hatch-vcs"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.setuptools_scm]
|
||||
write_to = "src/superqt/_version.py"
|
||||
# https://peps.python.org/pep-0621/
|
||||
[project]
|
||||
name = "superqt"
|
||||
description = "Missing widgets and components for PyQt/PySide"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
license = { text = "BSD 3-Clause License" }
|
||||
authors = [{ email = "talley.lambert@gmail.com", name = "Talley Lambert" }]
|
||||
keywords = [
|
||||
"qt",
|
||||
"pyqt",
|
||||
"pyside",
|
||||
"widgets",
|
||||
"range slider",
|
||||
"components",
|
||||
"gui",
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: X11 Applications :: Qt",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: BSD License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Topic :: Desktop Environment",
|
||||
"Topic :: Software Development :: User Interfaces",
|
||||
"Topic :: Software Development :: Widget Sets",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
dependencies = [
|
||||
"packaging",
|
||||
"pygments>=2.4.0",
|
||||
"qtpy>=1.1.0",
|
||||
"typing-extensions",
|
||||
]
|
||||
|
||||
# extras
|
||||
# https://peps.python.org/pep-0621/#dependencies-optional-dependencies
|
||||
[project.optional-dependencies]
|
||||
test = ["pint", "pytest", "pytest-cov", "pytest-qt"]
|
||||
dev = [
|
||||
"black",
|
||||
"ipython",
|
||||
"ruff",
|
||||
"mypy",
|
||||
"pdbpp",
|
||||
"pre-commit",
|
||||
"pydocstyle",
|
||||
"rich",
|
||||
"types-Pygments",
|
||||
]
|
||||
docs = ["mkdocs-macros-plugin", "mkdocs-material", "mkdocstrings[python]"]
|
||||
quantity = ["pint"]
|
||||
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"]
|
||||
pyqt5 = ["pyqt5"]
|
||||
pyqt6 = ["pyqt6"]
|
||||
font-fa5 = ["fonticon-fontawesome5"]
|
||||
font-fa6 = ["fonticon-fontawesome6"]
|
||||
font-mi6 = ["fonticon-materialdesignicons6"]
|
||||
font-mi7 = ["fonticon-materialdesignicons7"]
|
||||
|
||||
[project.urls]
|
||||
Source = "https://github.com/pyapp-kit/superqt"
|
||||
Tracker = "https://github.com/pyapp-kit/superqt/issues"
|
||||
Changelog = "https://github.com/pyapp-kit/superqt/blob/main/CHANGELOG.md"
|
||||
|
||||
[tool.hatch.version]
|
||||
source = "vcs"
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
include = ["src", "tests", "CHANGELOG.md"]
|
||||
|
||||
# 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"]
|
||||
select = [
|
||||
"E", # style errors
|
||||
"F", # flakes
|
||||
"W", # flakes
|
||||
"D", # pydocstyle
|
||||
"I", # isort
|
||||
"UP", # pyupgrade
|
||||
"S", # bandit
|
||||
"C4", # flake8-comprehensions
|
||||
"B", # flake8-bugbear
|
||||
"A001", # flake8-builtins
|
||||
"RUF", # ruff-specific rules
|
||||
"TID", # 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
|
||||
]
|
||||
|
||||
|
||||
[tool.ruff.per-file-ignores]
|
||||
"tests/*.py" = ["D", "S101"]
|
||||
"examples/demo_widget.py" = ["E501"]
|
||||
"examples/*.py" = ["B", "D"]
|
||||
|
||||
# https://docs.pytest.org/en/6.2.x/customize.html
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "6.0"
|
||||
testpaths = ["tests"]
|
||||
filterwarnings = [
|
||||
"error",
|
||||
"ignore:QPixmapCache.find:DeprecationWarning:",
|
||||
"ignore:SelectableGroups dict interface:DeprecationWarning",
|
||||
"ignore:The distutils package is deprecated:DeprecationWarning",
|
||||
]
|
||||
|
||||
# https://mypy.readthedocs.io/en/stable/config_file.html
|
||||
[tool.mypy]
|
||||
files = "src/**/*.py"
|
||||
strict = true
|
||||
disallow_untyped_defs = false
|
||||
disallow_untyped_calls = false
|
||||
disallow_any_generics = false
|
||||
disallow_subclassing_any = false
|
||||
show_error_codes = true
|
||||
pretty = true
|
||||
exclude = ['tests/**/*']
|
||||
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = ["superqt.qtcompat.*"]
|
||||
ignore_missing_imports = true
|
||||
warn_unused_ignores = false
|
||||
allow_redefinition = true
|
||||
|
||||
# https://coverage.readthedocs.io/en/6.4/config.html
|
||||
[tool.coverage.report]
|
||||
exclude_lines = [
|
||||
"pragma: no cover",
|
||||
"if TYPE_CHECKING:",
|
||||
"@overload",
|
||||
"except ImportError",
|
||||
]
|
||||
|
||||
# https://github.com/mgedmin/check-manifest#configuration
|
||||
[tool.check-manifest]
|
||||
ignore = ["src/superqt/_version.py"]
|
||||
ignore = [
|
||||
".github_changelog_generator",
|
||||
".pre-commit-config.yaml",
|
||||
"tests/**/*",
|
||||
"src/superqt/_version.py",
|
||||
"mkdocs.yml",
|
||||
"docs/**/*",
|
||||
"examples/**/*",
|
||||
"CHANGELOG.md",
|
||||
"CONTRIBUTING.md",
|
||||
"codecov.yml",
|
||||
".ruff_cache/**/*",
|
||||
]
|
||||
|
117
setup.cfg
117
setup.cfg
@@ -1,117 +0,0 @@
|
||||
[metadata]
|
||||
name = superqt
|
||||
description = Missing widgets for PyQt/PySide
|
||||
long_description = file: README.md
|
||||
long_description_content_type = text/markdown
|
||||
url = https://github.com/napari/superqt
|
||||
author = Talley Lambert
|
||||
author_email = talley.lambert@gmail.com
|
||||
license = BSD-3-Clause
|
||||
license_file = LICENSE
|
||||
classifiers =
|
||||
Development Status :: 4 - Beta
|
||||
Environment :: X11 Applications :: Qt
|
||||
Intended Audience :: Developers
|
||||
License :: OSI Approved :: BSD License
|
||||
Operating System :: OS Independent
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
Programming Language :: Python :: Implementation :: CPython
|
||||
Topic :: Desktop Environment
|
||||
Topic :: Software Development
|
||||
Topic :: Software Development :: User Interfaces
|
||||
Topic :: Software Development :: Widget Sets
|
||||
keywords = qt, range slider, widget
|
||||
project_urls =
|
||||
Source = https://github.com/napari/superqt
|
||||
Tracker = https://github.com/napari/superqt/issues
|
||||
Changelog = https://github.com/napari/superqt/blob/master/CHANGELOG.md
|
||||
|
||||
[options]
|
||||
packages = find:
|
||||
install_requires =
|
||||
typing-extensions
|
||||
python_requires = >=3.7
|
||||
include_package_data = True
|
||||
package_dir =
|
||||
=src
|
||||
setup_requires =
|
||||
setuptools-scm
|
||||
zip_safe = False
|
||||
|
||||
[options.packages.find]
|
||||
where = src
|
||||
|
||||
[options.extras_require]
|
||||
dev =
|
||||
ipython
|
||||
isort
|
||||
jedi<0.18.0
|
||||
mypy
|
||||
pre-commit
|
||||
pyside2
|
||||
pytest
|
||||
pytest-cov
|
||||
pytest-qt
|
||||
tox
|
||||
tox-conda
|
||||
font_fa5 =
|
||||
fonticon-fontawesome5
|
||||
font_mi5 =
|
||||
fonticon-materialdesignicons5
|
||||
pyqt5 =
|
||||
pyqt5
|
||||
pyqt6 =
|
||||
pyqt6
|
||||
pyside2 =
|
||||
pyside2
|
||||
pyside6 =
|
||||
pyside6
|
||||
testing =
|
||||
pytest
|
||||
pytest-cov
|
||||
pytest-qt
|
||||
tox
|
||||
tox-conda
|
||||
|
||||
[options.package_data]
|
||||
superqt = py.typed
|
||||
|
||||
[flake8]
|
||||
exclude = _version.py,.eggs,examples
|
||||
docstring-convention = numpy
|
||||
ignore = E203,W503,E501,C901,F403,F405,D100
|
||||
per-file-ignores =
|
||||
src/superqt/qtcompat/QtCore.py:F401
|
||||
src/superqt/qtcompat/QtGui.py:F401
|
||||
src/superqt/qtcompat/QtWidgets.py:F401
|
||||
src/superqt/qtcompat/__init__.py:F401,F811
|
||||
|
||||
[pydocstyle]
|
||||
convention = numpy
|
||||
add_select = D402,D415,D417
|
||||
ignore = D100
|
||||
|
||||
[isort]
|
||||
profile = black
|
||||
|
||||
[tool:pytest]
|
||||
filterwarnings =
|
||||
error
|
||||
ignore:QPixmapCache.find:DeprecationWarning:
|
||||
ignore:SelectableGroups dict interface:DeprecationWarning
|
||||
ignore:The distutils package is deprecated:DeprecationWarning
|
||||
|
||||
[mypy]
|
||||
strict = True
|
||||
files = src/superqt
|
||||
|
||||
[mypy-superqt.qtcompat.*]
|
||||
ignore_missing_imports = True
|
||||
warn_unused_ignores = False
|
||||
allow_redefinition = True
|
@@ -1,13 +1,19 @@
|
||||
"""superqt is a collection of QtWidgets for python."""
|
||||
"""superqt is a collection of Qt components for python."""
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
try:
|
||||
from ._version import version as __version__
|
||||
except ImportError:
|
||||
__version__ = version("superqt")
|
||||
except PackageNotFoundError:
|
||||
__version__ = "unknown"
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .spinbox._quantity import QQuantity
|
||||
|
||||
from ._eliding_label import QElidingLabel
|
||||
from .collapsible import QCollapsible
|
||||
from .combobox import QEnumComboBox
|
||||
from .combobox import QEnumComboBox, QSearchableComboBox
|
||||
from .elidable import QElidingLabel, QElidingLineEdit
|
||||
from .selection import QSearchableListWidget, QSearchableTreeWidget
|
||||
from .sliders import (
|
||||
QDoubleRangeSlider,
|
||||
QDoubleSlider,
|
||||
@@ -24,15 +30,28 @@ __all__ = [
|
||||
"ensure_main_thread",
|
||||
"ensure_object_thread",
|
||||
"QDoubleRangeSlider",
|
||||
"QCollapsible",
|
||||
"QDoubleSlider",
|
||||
"QElidingLabel",
|
||||
"QElidingLineEdit",
|
||||
"QEnumComboBox",
|
||||
"QLabeledDoubleRangeSlider",
|
||||
"QLabeledDoubleSlider",
|
||||
"QLabeledRangeSlider",
|
||||
"QLabeledSlider",
|
||||
"QLargeIntSpinBox",
|
||||
"QMessageHandler",
|
||||
"QQuantity",
|
||||
"QRangeSlider",
|
||||
"QEnumComboBox",
|
||||
"QCollapsible",
|
||||
"QSearchableComboBox",
|
||||
"QSearchableListWidget",
|
||||
"QSearchableTreeWidget",
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(name: str) -> Any:
|
||||
if name == "QQuantity":
|
||||
from .spinbox._quantity import QQuantity
|
||||
|
||||
return QQuantity
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
@@ -1,110 +0,0 @@
|
||||
from typing import List
|
||||
|
||||
from superqt.qtcompat.QtCore import QPoint, QRect, QSize, Qt
|
||||
from superqt.qtcompat.QtGui import QFont, QFontMetrics, QResizeEvent, QTextLayout
|
||||
from superqt.qtcompat.QtWidgets import QLabel
|
||||
|
||||
|
||||
class QElidingLabel(QLabel):
|
||||
"""A QLabel variant that will elide text (add '…') to fit width.
|
||||
|
||||
QElidingLabel()
|
||||
QElidingLabel(parent: Optional[QWidget], f: Qt.WindowFlags = ...)
|
||||
QElidingLabel(text: str, parent: Optional[QWidget] = None, f: Qt.WindowFlags = ...)
|
||||
|
||||
For a multiline eliding label, use `setWordWrap(True)`. In this case, text
|
||||
will wrap to fit the width, and only the last line will be elided.
|
||||
When `wordWrap()` is True, `sizeHint()` will return the size required to fit
|
||||
the full text.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
self._elide_mode = Qt.TextElideMode.ElideRight
|
||||
super().__init__(*args, **kwargs)
|
||||
self.setText(args[0] if args and isinstance(args[0], str) else "")
|
||||
|
||||
# New Public methods
|
||||
|
||||
def elideMode(self) -> Qt.TextElideMode:
|
||||
"""The current Qt.TextElideMode."""
|
||||
return self._elide_mode
|
||||
|
||||
def setElideMode(self, mode: Qt.TextElideMode):
|
||||
"""Set the elide mode to a Qt.TextElideMode."""
|
||||
self._elide_mode = Qt.TextElideMode(mode)
|
||||
super().setText(self._elidedText())
|
||||
|
||||
@staticmethod
|
||||
def wrapText(text, width, font=None) -> List[str]:
|
||||
"""Returns `text`, split as it would be wrapped for `width`, given `font`.
|
||||
|
||||
Static method.
|
||||
"""
|
||||
tl = QTextLayout(text, font or QFont())
|
||||
tl.beginLayout()
|
||||
lines = []
|
||||
while True:
|
||||
ln = tl.createLine()
|
||||
if not ln.isValid():
|
||||
break
|
||||
ln.setLineWidth(width)
|
||||
start = ln.textStart()
|
||||
lines.append(text[start : start + ln.textLength()])
|
||||
tl.endLayout()
|
||||
return lines
|
||||
|
||||
# Reimplemented QT methods
|
||||
|
||||
def text(self) -> str:
|
||||
"""This property holds the label's text.
|
||||
|
||||
If no text has been set this will return an empty string.
|
||||
"""
|
||||
return self._text
|
||||
|
||||
def setText(self, txt: str):
|
||||
"""Set the label's text.
|
||||
|
||||
Setting the text clears any previous content.
|
||||
NOTE: we set the QLabel private text to the elided version
|
||||
"""
|
||||
self._text = txt
|
||||
super().setText(self._elidedText())
|
||||
|
||||
def resizeEvent(self, ev: QResizeEvent) -> None:
|
||||
ev.accept()
|
||||
super().setText(self._elidedText())
|
||||
|
||||
def setWordWrap(self, wrap: bool) -> None:
|
||||
super().setWordWrap(wrap)
|
||||
super().setText(self._elidedText())
|
||||
|
||||
def sizeHint(self) -> QSize:
|
||||
if not self.wordWrap():
|
||||
return super().sizeHint()
|
||||
fm = QFontMetrics(self.font())
|
||||
flags = int(self.alignment() | Qt.TextFlag.TextWordWrap)
|
||||
r = fm.boundingRect(QRect(QPoint(0, 0), self.size()), flags, self._text)
|
||||
return QSize(self.width(), r.height())
|
||||
|
||||
# private implementation methods
|
||||
|
||||
def _elidedText(self) -> str:
|
||||
"""Return `self._text` elided to `width`"""
|
||||
fm = QFontMetrics(self.font())
|
||||
# the 2 is a magic number that prevents the ellipses from going missing
|
||||
# in certain cases (?)
|
||||
width = self.width() - 2
|
||||
if not self.wordWrap():
|
||||
return fm.elidedText(self._text, self._elide_mode, width)
|
||||
|
||||
# get number of lines we can fit without eliding
|
||||
nlines = self.height() // fm.height() - 1
|
||||
# get the last line (elided)
|
||||
text = self._wrappedText()
|
||||
last_line = fm.elidedText("".join(text[nlines:]), self._elide_mode, width)
|
||||
# join them
|
||||
return "".join(text[:nlines] + [last_line])
|
||||
|
||||
def _wrappedText(self) -> List[str]:
|
||||
return QElidingLabel.wrapText(self._text, self.width(), self.font())
|
@@ -1,32 +1,47 @@
|
||||
"""A collapsible widget to hide and unhide child widgets"""
|
||||
from typing import Optional
|
||||
"""A collapsible widget to hide and unhide child widgets."""
|
||||
from typing import Optional, Union
|
||||
|
||||
from ..qtcompat.QtCore import (
|
||||
QAbstractAnimation,
|
||||
from qtpy.QtCore import (
|
||||
QEasingCurve,
|
||||
QEvent,
|
||||
QMargins,
|
||||
QObject,
|
||||
QPropertyAnimation,
|
||||
QRect,
|
||||
Qt,
|
||||
Signal,
|
||||
)
|
||||
from ..qtcompat.QtWidgets import QFrame, QPushButton, QVBoxLayout, QWidget
|
||||
from qtpy.QtGui import QIcon, QPainter, QPalette, QPixmap
|
||||
from qtpy.QtWidgets import QFrame, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
|
||||
class QCollapsible(QFrame):
|
||||
"""A collapsible widget to hide and unhide child widgets.
|
||||
|
||||
A signal is emitted when the widget is expanded (True) or collapsed (False).
|
||||
|
||||
Based on https://stackoverflow.com/a/68141638
|
||||
"""
|
||||
|
||||
_EXPANDED = "▼ "
|
||||
_COLLAPSED = "▲ "
|
||||
toggled = Signal(bool)
|
||||
|
||||
def __init__(self, title: str = "", parent: Optional[QWidget] = None):
|
||||
def __init__(
|
||||
self,
|
||||
title: str = "",
|
||||
parent: Optional[QWidget] = None,
|
||||
expandedIcon: Optional[Union[QIcon, str]] = "▼",
|
||||
collapsedIcon: Optional[Union[QIcon, str]] = "▲",
|
||||
):
|
||||
super().__init__(parent)
|
||||
self._locked = False
|
||||
self._is_animating = False
|
||||
self._text = title
|
||||
|
||||
self._toggle_btn = QPushButton(self._COLLAPSED + title)
|
||||
self._toggle_btn = QPushButton(title)
|
||||
self._toggle_btn.setCheckable(True)
|
||||
self._toggle_btn.setStyleSheet("text-align: left; background: transparent;")
|
||||
self.setCollapsedIcon(icon=collapsedIcon)
|
||||
self.setExpandedIcon(icon=expandedIcon)
|
||||
self._toggle_btn.setStyleSheet("text-align: left; border: none; outline: none;")
|
||||
self._toggle_btn.toggled.connect(self._toggle)
|
||||
|
||||
# frame layout
|
||||
@@ -38,6 +53,7 @@ class QCollapsible(QFrame):
|
||||
self._animation = QPropertyAnimation(self)
|
||||
self._animation.setPropertyName(b"maximumHeight")
|
||||
self._animation.setStartValue(0)
|
||||
self._animation.finished.connect(self._on_animation_done)
|
||||
self.setDuration(300)
|
||||
self.setEasingCurve(QEasingCurve.Type.InOutCubic)
|
||||
|
||||
@@ -48,16 +64,16 @@ class QCollapsible(QFrame):
|
||||
_content.layout().setContentsMargins(QMargins(5, 0, 0, 0))
|
||||
self.setContent(_content)
|
||||
|
||||
def setText(self, text: str):
|
||||
def setText(self, text: str) -> None:
|
||||
"""Set the text of the toggle button."""
|
||||
current = self._toggle_btn.text()[: len(self._EXPANDED)]
|
||||
current = self._toggle_btn.text()
|
||||
self._toggle_btn.setText(current + text)
|
||||
|
||||
def text(self) -> str:
|
||||
"""Return the text of the toggle button."""
|
||||
return self._toggle_btn.text()[len(self._EXPANDED) :]
|
||||
return self._toggle_btn.text()
|
||||
|
||||
def setContent(self, content: QWidget):
|
||||
def setContent(self, content: QWidget) -> None:
|
||||
"""Replace central widget (the widget that gets expanded/collapsed)."""
|
||||
self._content = content
|
||||
self.layout().addWidget(self._content)
|
||||
@@ -67,62 +83,130 @@ class QCollapsible(QFrame):
|
||||
"""Return the current content widget."""
|
||||
return self._content
|
||||
|
||||
def setDuration(self, msecs: int):
|
||||
def _convert_string_to_icon(self, symbol: str) -> QIcon:
|
||||
"""Create a QIcon from a string."""
|
||||
size = self._toggle_btn.font().pointSize()
|
||||
pixmap = QPixmap(size, size)
|
||||
pixmap.fill(Qt.GlobalColor.transparent)
|
||||
painter = QPainter(pixmap)
|
||||
color = self._toggle_btn.palette().color(QPalette.ColorRole.WindowText)
|
||||
painter.setPen(color)
|
||||
painter.drawText(QRect(0, 0, size, size), Qt.AlignmentFlag.AlignCenter, symbol)
|
||||
painter.end()
|
||||
return QIcon(pixmap)
|
||||
|
||||
def expandedIcon(self) -> QIcon:
|
||||
"""Returns the icon used when the widget is expanded."""
|
||||
return self._expanded_icon
|
||||
|
||||
def setExpandedIcon(self, icon: Optional[Union[QIcon, str]] = None) -> None:
|
||||
"""Set the icon on the toggle button when the widget is expanded."""
|
||||
if icon and isinstance(icon, QIcon):
|
||||
self._expanded_icon = icon
|
||||
elif icon and isinstance(icon, str):
|
||||
self._expanded_icon = self._convert_string_to_icon(icon)
|
||||
|
||||
if self.isExpanded():
|
||||
self._toggle_btn.setIcon(self._expanded_icon)
|
||||
|
||||
def collapsedIcon(self) -> QIcon:
|
||||
"""Returns the icon used when the widget is collapsed."""
|
||||
return self._collapsed_icon
|
||||
|
||||
def setCollapsedIcon(self, icon: Optional[Union[QIcon, str]] = None) -> None:
|
||||
"""Set the icon on the toggle button when the widget is collapsed."""
|
||||
if icon and isinstance(icon, QIcon):
|
||||
self._collapsed_icon = icon
|
||||
elif icon and isinstance(icon, str):
|
||||
self._collapsed_icon = self._convert_string_to_icon(icon)
|
||||
|
||||
if not self.isExpanded():
|
||||
self._toggle_btn.setIcon(self._collapsed_icon)
|
||||
|
||||
def setDuration(self, msecs: int) -> None:
|
||||
"""Set duration of the collapse/expand animation."""
|
||||
self._animation.setDuration(msecs)
|
||||
|
||||
def setEasingCurve(self, easing: QEasingCurve):
|
||||
"""Set the easing curve for the collapse/expand animation"""
|
||||
def setEasingCurve(self, easing: QEasingCurve) -> None:
|
||||
"""Set the easing curve for the collapse/expand animation."""
|
||||
self._animation.setEasingCurve(easing)
|
||||
|
||||
def addWidget(self, widget: QWidget):
|
||||
def addWidget(self, widget: QWidget) -> None:
|
||||
"""Add a widget to the central content widget's layout."""
|
||||
widget.installEventFilter(self)
|
||||
self._content.layout().addWidget(widget)
|
||||
|
||||
def removeWidget(self, widget: QWidget):
|
||||
def removeWidget(self, widget: QWidget) -> None:
|
||||
"""Remove widget from the central content widget's layout."""
|
||||
self._content.layout().removeWidget(widget)
|
||||
widget.removeEventFilter(self)
|
||||
|
||||
def expand(self, animate: bool = True):
|
||||
"""Expand (show) the collapsible section"""
|
||||
self._expand_collapse(QAbstractAnimation.Direction.Forward, animate)
|
||||
def expand(self, animate: bool = True) -> None:
|
||||
"""Expand (show) the collapsible section."""
|
||||
self._expand_collapse(QPropertyAnimation.Direction.Forward, animate)
|
||||
|
||||
def collapse(self, animate: bool = True):
|
||||
"""Collapse (hide) the collapsible section"""
|
||||
self._expand_collapse(QAbstractAnimation.Direction.Backward, animate)
|
||||
def collapse(self, animate: bool = True) -> None:
|
||||
"""Collapse (hide) the collapsible section."""
|
||||
self._expand_collapse(QPropertyAnimation.Direction.Backward, animate)
|
||||
|
||||
def isExpanded(self) -> bool:
|
||||
"""Return whether the collapsible section is visible"""
|
||||
"""Return whether the collapsible section is visible."""
|
||||
return self._toggle_btn.isChecked()
|
||||
|
||||
def setLocked(self, locked: bool = True):
|
||||
"""Set whether collapse/expand is disabled"""
|
||||
def setLocked(self, locked: bool = True) -> None:
|
||||
"""Set whether collapse/expand is disabled."""
|
||||
self._locked = locked
|
||||
self._toggle_btn.setCheckable(not locked)
|
||||
|
||||
def locked(self) -> bool:
|
||||
"""Return True if collapse/expand is disabled"""
|
||||
"""Return True if collapse/expand is disabled."""
|
||||
return self._locked
|
||||
|
||||
def _expand_collapse(
|
||||
self, direction: QAbstractAnimation.Direction, animate: bool = True
|
||||
):
|
||||
self,
|
||||
direction: QPropertyAnimation.Direction,
|
||||
animate: bool = True,
|
||||
emit: bool = True,
|
||||
) -> None:
|
||||
"""Set values for the widget based on whether it is expanding or collapsing.
|
||||
|
||||
An emit flag is included so that the toggle signal is only called once (it
|
||||
was being emitted a few times via eventFilter when the widget was expanding
|
||||
previously).
|
||||
"""
|
||||
if self._locked:
|
||||
return
|
||||
|
||||
forward = direction == QAbstractAnimation.Direction.Forward
|
||||
text = self._EXPANDED if forward else self._COLLAPSED
|
||||
|
||||
forward = direction == QPropertyAnimation.Direction.Forward
|
||||
icon = self._expanded_icon if forward else self._collapsed_icon
|
||||
self._toggle_btn.setIcon(icon)
|
||||
self._toggle_btn.setChecked(forward)
|
||||
self._toggle_btn.setText(text + self._toggle_btn.text()[len(self._EXPANDED) :])
|
||||
|
||||
_content_height = self._content.sizeHint().height() + 10
|
||||
if animate:
|
||||
self._animation.setDirection(direction)
|
||||
self._animation.setEndValue(_content_height)
|
||||
self._is_animating = True
|
||||
self._animation.start()
|
||||
else:
|
||||
self._content.setMaximumHeight(_content_height if forward else 0)
|
||||
if emit:
|
||||
self.toggled.emit(direction == QPropertyAnimation.Direction.Forward)
|
||||
|
||||
def _toggle(self):
|
||||
def _toggle(self) -> None:
|
||||
self.expand() if self.isExpanded() else self.collapse()
|
||||
|
||||
def eventFilter(self, a0: QObject, a1: QEvent) -> bool:
|
||||
"""If a child widget resizes, we need to update our expanded height."""
|
||||
if (
|
||||
a1.type() == QEvent.Type.Resize
|
||||
and self.isExpanded()
|
||||
and not self._is_animating
|
||||
):
|
||||
self._expand_collapse(
|
||||
QPropertyAnimation.Direction.Forward, animate=False, emit=False
|
||||
)
|
||||
return False
|
||||
|
||||
def _on_animation_done(self) -> None:
|
||||
self._is_animating = False
|
||||
|
@@ -1,3 +1,4 @@
|
||||
from ._enum_combobox import QEnumComboBox
|
||||
from ._searchable_combo_box import QSearchableComboBox
|
||||
|
||||
__all__ = ("QEnumComboBox",)
|
||||
__all__ = ("QEnumComboBox", "QSearchableComboBox")
|
||||
|
@@ -1,8 +1,8 @@
|
||||
from enum import Enum, EnumMeta
|
||||
from typing import Optional, TypeVar
|
||||
|
||||
from ..qtcompat.QtCore import Signal
|
||||
from ..qtcompat.QtWidgets import QComboBox
|
||||
from qtpy.QtCore import Signal
|
||||
from qtpy.QtWidgets import QComboBox
|
||||
|
||||
EnumType = TypeVar("EnumType", bound=Enum)
|
||||
|
||||
@@ -11,8 +11,11 @@ NONE_STRING = "----"
|
||||
|
||||
|
||||
def _get_name(enum_value: Enum):
|
||||
"""Create human readable name if user does not provide own implementation of __str__"""
|
||||
if enum_value.__str__.__module__ != "enum":
|
||||
"""Create human readable name if user does not implement `__str__`."""
|
||||
if (
|
||||
enum_value.__str__.__module__ != "enum"
|
||||
and not enum_value.__str__.__module__.startswith("shibokensupport")
|
||||
):
|
||||
# check if function was overloaded
|
||||
name = str(enum_value)
|
||||
else:
|
||||
@@ -21,8 +24,7 @@ def _get_name(enum_value: Enum):
|
||||
|
||||
|
||||
class QEnumComboBox(QComboBox):
|
||||
"""
|
||||
ComboBox presenting options from a python Enum.
|
||||
"""ComboBox presenting options from a python Enum.
|
||||
|
||||
If the Enum class does not implement `__str__` then a human readable name
|
||||
is created from the name of the enum member, replacing underscores with spaces.
|
||||
@@ -41,9 +43,7 @@ class QEnumComboBox(QComboBox):
|
||||
self.currentIndexChanged.connect(self._emit_signal)
|
||||
|
||||
def setEnumClass(self, enum: Optional[EnumMeta], allow_none=False):
|
||||
"""
|
||||
Set enum class from which members value should be selected
|
||||
"""
|
||||
"""Set enum class from which members value should be selected."""
|
||||
self.clear()
|
||||
self._enum_class = enum
|
||||
self._allow_none = allow_none and enum is not None
|
||||
@@ -52,11 +52,11 @@ class QEnumComboBox(QComboBox):
|
||||
super().addItems(list(map(_get_name, self._enum_class.__members__.values())))
|
||||
|
||||
def enumClass(self) -> Optional[EnumMeta]:
|
||||
"""return current Enum class"""
|
||||
"""Return current Enum class."""
|
||||
return self._enum_class
|
||||
|
||||
def isOptional(self) -> bool:
|
||||
"""return if current enum is with optional annotation"""
|
||||
"""Return if current enum is with optional annotation."""
|
||||
return self._allow_none
|
||||
|
||||
def clear(self):
|
||||
@@ -65,7 +65,7 @@ class QEnumComboBox(QComboBox):
|
||||
super().clear()
|
||||
|
||||
def currentEnum(self) -> Optional[EnumType]:
|
||||
"""current value as Enum member"""
|
||||
"""Current value as Enum member."""
|
||||
if self._enum_class is not None:
|
||||
if self._allow_none:
|
||||
if self.currentText() == NONE_STRING:
|
||||
@@ -88,7 +88,8 @@ class QEnumComboBox(QComboBox):
|
||||
return
|
||||
if not isinstance(value, self._enum_class):
|
||||
raise TypeError(
|
||||
f"setValue(self, Enum): argument 1 has unexpected type {type(value).__name__!r}"
|
||||
"setValue(self, Enum): argument 1 has unexpected type "
|
||||
f"{type(value).__name__!r}"
|
||||
)
|
||||
self.setCurrentText(_get_name(value))
|
||||
|
||||
|
48
src/superqt/combobox/_searchable_combo_box.py
Normal file
48
src/superqt/combobox/_searchable_combo_box.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from typing import Optional
|
||||
|
||||
from qtpy import QT_VERSION
|
||||
from qtpy.QtCore import Qt, Signal
|
||||
from qtpy.QtWidgets import QComboBox, QCompleter, QWidget
|
||||
|
||||
try:
|
||||
is_qt_bellow_5_14 = tuple(int(x) for x in QT_VERSION.split(".")[:2]) < (5, 14)
|
||||
except ValueError:
|
||||
is_qt_bellow_5_14 = False
|
||||
|
||||
|
||||
class QSearchableComboBox(QComboBox):
|
||||
"""ComboCox with completer for fast search in multiple options."""
|
||||
|
||||
if is_qt_bellow_5_14:
|
||||
textActivated = Signal(str) # pragma: no cover
|
||||
|
||||
def __init__(self, parent: Optional[QWidget] = None):
|
||||
super().__init__(parent)
|
||||
self.setEditable(True)
|
||||
self.completer_object = QCompleter()
|
||||
self.completer_object.setCaseSensitivity(Qt.CaseInsensitive)
|
||||
self.completer_object.setCompletionMode(QCompleter.PopupCompletion)
|
||||
self.completer_object.setFilterMode(Qt.MatchContains)
|
||||
self.setCompleter(self.completer_object)
|
||||
self.setInsertPolicy(QComboBox.NoInsert)
|
||||
if is_qt_bellow_5_14: # pragma: no cover
|
||||
self.currentIndexChanged.connect(self._text_activated)
|
||||
|
||||
def _text_activated(self): # pragma: no cover
|
||||
self.textActivated.emit(self.currentText())
|
||||
|
||||
def addItem(self, *args):
|
||||
super().addItem(*args)
|
||||
self.completer_object.setModel(self.model())
|
||||
|
||||
def addItems(self, *args):
|
||||
super().addItems(*args)
|
||||
self.completer_object.setModel(self.model())
|
||||
|
||||
def insertItem(self, *args) -> None:
|
||||
super().insertItem(*args)
|
||||
self.completer_object.setModel(self.model())
|
||||
|
||||
def insertItems(self, *args) -> None:
|
||||
super().insertItems(*args)
|
||||
self.completer_object.setModel(self.model())
|
4
src/superqt/elidable/__init__.py
Normal file
4
src/superqt/elidable/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from ._eliding_label import QElidingLabel
|
||||
from ._eliding_line_edit import QElidingLineEdit
|
||||
|
||||
__all__ = ["QElidingLabel", "QElidingLineEdit"]
|
78
src/superqt/elidable/_eliding.py
Normal file
78
src/superqt/elidable/_eliding.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from typing import List
|
||||
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtGui import QFont, QFontMetrics, QTextLayout
|
||||
|
||||
|
||||
class _GenericEliding:
|
||||
"""A mixin to provide capabilities to elide text (could add '…') to fit width."""
|
||||
|
||||
_elide_mode: Qt.TextElideMode = Qt.TextElideMode.ElideRight
|
||||
_text: str = ""
|
||||
# the 2 is a magic number that prevents the ellipses from going missing
|
||||
# in certain cases (?)
|
||||
_ellipses_width: int = 2
|
||||
|
||||
# Public methods
|
||||
|
||||
def elideMode(self) -> Qt.TextElideMode:
|
||||
"""The current Qt.TextElideMode."""
|
||||
return self._elide_mode
|
||||
|
||||
def setElideMode(self, mode: Qt.TextElideMode) -> None:
|
||||
"""Set the elide mode to a Qt.TextElideMode."""
|
||||
self._elide_mode = Qt.TextElideMode(mode)
|
||||
|
||||
def full_text(self) -> str:
|
||||
"""The current text without eliding."""
|
||||
return self._text
|
||||
|
||||
def setEllipsesWidth(self, width: int) -> None:
|
||||
"""A width value to take into account ellipses width when eliding text.
|
||||
|
||||
The value is deducted from the widget width when computing the elided version
|
||||
of the text.
|
||||
"""
|
||||
self._ellipses_width = width
|
||||
|
||||
@staticmethod
|
||||
def wrapText(text, width, font=None) -> List[str]:
|
||||
"""Returns `text`, split as it would be wrapped for `width`, given `font`.
|
||||
|
||||
Static method.
|
||||
"""
|
||||
tl = QTextLayout(text, font or QFont())
|
||||
tl.beginLayout()
|
||||
lines = []
|
||||
while True:
|
||||
ln = tl.createLine()
|
||||
if not ln.isValid():
|
||||
break
|
||||
ln.setLineWidth(width)
|
||||
start = ln.textStart()
|
||||
lines.append(text[start : start + ln.textLength()])
|
||||
tl.endLayout()
|
||||
return lines
|
||||
|
||||
# private implementation methods
|
||||
|
||||
def _elidedText(self) -> str:
|
||||
"""Return `self._text` elided to `width`."""
|
||||
fm = QFontMetrics(self.font())
|
||||
ellipses_width = 0
|
||||
if self._elide_mode != Qt.TextElideMode.ElideNone:
|
||||
ellipses_width = self._ellipses_width
|
||||
width = self.width() - ellipses_width
|
||||
if not getattr(self, "wordWrap", None) or not self.wordWrap():
|
||||
return fm.elidedText(self._text, self._elide_mode, width)
|
||||
|
||||
# get number of lines we can fit without eliding
|
||||
nlines = self.height() // fm.height() - 1
|
||||
# get the last line (elided)
|
||||
text = self._wrappedText()
|
||||
last_line = fm.elidedText("".join(text[nlines:]), self._elide_mode, width)
|
||||
# join them
|
||||
return "".join(text[:nlines] + [last_line])
|
||||
|
||||
def _wrappedText(self) -> List[str]:
|
||||
return _GenericEliding.wrapText(self._text, self.width(), self.font())
|
75
src/superqt/elidable/_eliding_label.py
Normal file
75
src/superqt/elidable/_eliding_label.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from qtpy.QtCore import QPoint, QRect, QSize, Qt
|
||||
from qtpy.QtGui import QFontMetrics, QResizeEvent
|
||||
from qtpy.QtWidgets import QLabel
|
||||
|
||||
from ._eliding import _GenericEliding
|
||||
|
||||
|
||||
class QElidingLabel(_GenericEliding, QLabel):
|
||||
"""
|
||||
A QLabel variant that will elide text (could add '…') to fit width.
|
||||
|
||||
QElidingLabel()
|
||||
QElidingLabel(parent: Optional[QWidget], f: Qt.WindowFlags = ...)
|
||||
QElidingLabel(text: str, parent: Optional[QWidget] = None, f: Qt.WindowFlags = ...)
|
||||
|
||||
For a multiline eliding label, use `setWordWrap(True)`. In this case, text
|
||||
will wrap to fit the width, and only the last line will be elided.
|
||||
When `wordWrap()` is True, `sizeHint()` will return the size required to fit
|
||||
the full text.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
if args and isinstance(args[0], str):
|
||||
self.setText(args[0])
|
||||
|
||||
# Reimplemented _GenericEliding methods
|
||||
|
||||
def setElideMode(self, mode: Qt.TextElideMode) -> None:
|
||||
"""Set the elide mode to a Qt.TextElideMode."""
|
||||
super().setElideMode(mode)
|
||||
super().setText(self._elidedText())
|
||||
|
||||
def setEllipsesWidth(self, width: int) -> None:
|
||||
"""A width value to take into account ellipses width when eliding text.
|
||||
|
||||
The value is deducted from the widget width when computing the elided version
|
||||
of the text.
|
||||
"""
|
||||
super().setEllipsesWidth(width)
|
||||
super().setText(self._elidedText())
|
||||
|
||||
# Reimplemented QT methods
|
||||
|
||||
def text(self) -> str:
|
||||
"""Return the label's text.
|
||||
|
||||
If no text has been set this will return an empty string.
|
||||
"""
|
||||
return self._text
|
||||
|
||||
def setText(self, txt: str) -> None:
|
||||
"""Set the label's text.
|
||||
|
||||
Setting the text clears any previous content.
|
||||
NOTE: we set the QLabel private text to the elided version
|
||||
"""
|
||||
self._text = txt
|
||||
super().setText(self._elidedText())
|
||||
|
||||
def resizeEvent(self, event: QResizeEvent) -> None:
|
||||
event.accept()
|
||||
super().setText(self._elidedText())
|
||||
|
||||
def setWordWrap(self, wrap: bool) -> None:
|
||||
super().setWordWrap(wrap)
|
||||
super().setText(self._elidedText())
|
||||
|
||||
def sizeHint(self) -> QSize:
|
||||
if not self.wordWrap():
|
||||
return super().sizeHint()
|
||||
fm = QFontMetrics(self.font())
|
||||
flags = int(self.alignment() | Qt.TextFlag.TextWordWrap)
|
||||
r = fm.boundingRect(QRect(QPoint(0, 0), self.size()), flags, self._text)
|
||||
return QSize(self.width(), r.height())
|
91
src/superqt/elidable/_eliding_line_edit.py
Normal file
91
src/superqt/elidable/_eliding_line_edit.py
Normal file
@@ -0,0 +1,91 @@
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtGui import QFocusEvent, QResizeEvent
|
||||
from qtpy.QtWidgets import QLineEdit
|
||||
|
||||
from ._eliding import _GenericEliding
|
||||
|
||||
|
||||
class QElidingLineEdit(_GenericEliding, QLineEdit):
|
||||
"""A QLineEdit variant that will elide text (could add '…') to fit width.
|
||||
|
||||
QElidingLineEdit()
|
||||
QElidingLineEdit(parent: Optional[QWidget])
|
||||
QElidingLineEdit(text: str, parent: Optional[QWidget] = None)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
if args and isinstance(args[0], str):
|
||||
self.setText(args[0])
|
||||
# The `textEdited` signal doesn't trigger the `textChanged` signal if
|
||||
# text is changed with `setText`, so we connect to `textEdited` to only
|
||||
# update _text when text is being edited by the user graphically.
|
||||
self.textEdited.connect(self._update_text)
|
||||
|
||||
# Reimplemented _GenericEliding methods
|
||||
|
||||
def setElideMode(self, mode: Qt.TextElideMode) -> None:
|
||||
"""Set the elide mode to a Qt.TextElideMode.
|
||||
|
||||
The text shown is updated to the elided version only if the widget is not
|
||||
focused.
|
||||
"""
|
||||
super().setElideMode(mode)
|
||||
if not self.hasFocus():
|
||||
super().setText(self._elidedText())
|
||||
|
||||
def setEllipsesWidth(self, width: int) -> None:
|
||||
"""A width value to take into account ellipses width when eliding text.
|
||||
|
||||
The value is deducted from the widget width when computing the elided version
|
||||
of the text. The text shown is updated to the elided version only if the widget
|
||||
is not focused.
|
||||
"""
|
||||
super().setEllipsesWidth(width)
|
||||
if not self.hasFocus():
|
||||
super().setText(self._elidedText())
|
||||
|
||||
# Reimplemented QT methods
|
||||
|
||||
def text(self) -> str:
|
||||
"""Return the label's text being shown.
|
||||
|
||||
If no text has been set this will return an empty string.
|
||||
"""
|
||||
return self._text
|
||||
|
||||
def setText(self, text) -> None:
|
||||
"""Set the line edit's text.
|
||||
|
||||
Setting the text clears any previous content.
|
||||
NOTE: we set the QLineEdit private text to the elided version
|
||||
"""
|
||||
self._text = text
|
||||
if not self.hasFocus():
|
||||
super().setText(self._elidedText())
|
||||
|
||||
def focusInEvent(self, event: QFocusEvent) -> None:
|
||||
"""Set the full text when the widget is focused."""
|
||||
super().setText(self._text)
|
||||
super().focusInEvent(event)
|
||||
|
||||
def focusOutEvent(self, event: QFocusEvent) -> None:
|
||||
"""Set an elided version of the text (if needed) when the focus is out."""
|
||||
super().setText(self._elidedText())
|
||||
super().focusOutEvent(event)
|
||||
|
||||
def resizeEvent(self, event: QResizeEvent) -> None:
|
||||
"""Update elided text being shown when the widget is resized."""
|
||||
if not self.hasFocus():
|
||||
super().setText(self._elidedText())
|
||||
super().resizeEvent(event)
|
||||
|
||||
# private implementation methods
|
||||
|
||||
def _update_text(self, text: str) -> None:
|
||||
"""Update only the actual text of the widget.
|
||||
|
||||
The actual text is the text the widget has without eliding.
|
||||
"""
|
||||
self._text = text
|
@@ -2,18 +2,19 @@ from __future__ import annotations
|
||||
|
||||
__all__ = [
|
||||
"addFont",
|
||||
"Animation",
|
||||
"ENTRY_POINT",
|
||||
"font",
|
||||
"icon",
|
||||
"IconFont",
|
||||
"IconFontMeta",
|
||||
"IconOpts",
|
||||
"Animation",
|
||||
"pulse",
|
||||
"setTextIcon",
|
||||
"spin",
|
||||
]
|
||||
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Tuple, Union
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ._animations import Animation, pulse, spin
|
||||
from ._iconfont import IconFont, IconFontMeta
|
||||
@@ -22,8 +23,8 @@ from ._qfont_icon import DEFAULT_SCALING_FACTOR, IconOptionDict, IconOpts
|
||||
from ._qfont_icon import QFontIconStore as _QFIS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from superqt.qtcompat.QtGui import QFont, QTransform
|
||||
from superqt.qtcompat.QtWidgets import QWidget
|
||||
from qtpy.QtGui import QFont, QTransform
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from ._qfont_icon import QFontIcon, ValidColor
|
||||
|
||||
@@ -38,21 +39,23 @@ ENTRY_POINT = _FIM.ENTRY_POINT
|
||||
def icon(
|
||||
glyph_key: str,
|
||||
scale_factor: float = DEFAULT_SCALING_FACTOR,
|
||||
color: ValidColor = None,
|
||||
color: ValidColor | None = None,
|
||||
opacity: float = 1,
|
||||
animation: Optional[Animation] = None,
|
||||
transform: Optional[QTransform] = None,
|
||||
states: Dict[str, Union[IconOptionDict, IconOpts]] = {},
|
||||
animation: Animation | None = None,
|
||||
transform: QTransform | None = None,
|
||||
states: dict[str, IconOptionDict | IconOpts] | None = None,
|
||||
) -> QFontIcon:
|
||||
"""Create a QIcon for `glyph_key`, with a number of optional settings
|
||||
"""Create a QIcon for `glyph_key`, with a number of optional settings.
|
||||
|
||||
The `glyph_key` (e.g. 'fa5s.smile') represents a Font-family & style, and a glpyh.
|
||||
The `glyph_key` (e.g. 'fa5s.smile') represents a Font-family & style, and a glyph.
|
||||
In most cases, the key should be provided by a plugin in the environment, like:
|
||||
|
||||
https://github.com/tlambert03/fonticon-fontawesome5 ('fa5s' & 'fa5r' prefixes)
|
||||
https://github.com/tlambert03/fonticon-materialdesignicons6 ('mdi6' prefix)
|
||||
- [fonticon-fontawesome5](https://pypi.org/project/fonticon-fontawesome5/) ('fa5s' &
|
||||
'fa5r' prefixes)
|
||||
- [fonticon-materialdesignicons6](https://pypi.org/project/fonticon-materialdesignicons6/)
|
||||
('mdi6' prefix)
|
||||
|
||||
...but fonts can also be added manually using :func:`addFont`.
|
||||
...but fonts can also be added manually using [`addFont`][superqt.fonticon.addFont].
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -86,7 +89,7 @@ def icon(
|
||||
`animation`, etc...)
|
||||
|
||||
Missing keys in the state dicts will be taken from the default options, provided
|
||||
by the paramters above.
|
||||
by the parameters above.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -96,19 +99,21 @@ def icon(
|
||||
|
||||
Examples
|
||||
--------
|
||||
# simple example (assumes the font-awesome5 plugin is installed)
|
||||
simple example (using the string `'fa5s.smile'` assumes the `fonticon-fontawesome5`
|
||||
plugin is installed)
|
||||
|
||||
>>> btn = QPushButton()
|
||||
>>> btn.setIcon(icon('fa5s.smile'))
|
||||
|
||||
# can also directly import from fonticon_fa5
|
||||
can also directly import from fonticon_fa5
|
||||
>>> from fonticon_fa5 import FA5S
|
||||
>>> btn.setIcon(icon(FA5S.smile))
|
||||
|
||||
# with animation
|
||||
with animation
|
||||
>>> btn2 = QPushButton()
|
||||
>>> btn2.setIcon(icon(FA5S.spinner, animation=pulse(btn2)))
|
||||
|
||||
# complicated example
|
||||
complicated example
|
||||
>>> btn = QPushButton()
|
||||
>>> btn.setIcon(
|
||||
... icon(
|
||||
@@ -140,11 +145,11 @@ def icon(
|
||||
opacity=opacity,
|
||||
animation=animation,
|
||||
transform=transform,
|
||||
states=states,
|
||||
states=states or {},
|
||||
)
|
||||
|
||||
|
||||
def setTextIcon(widget: QWidget, glyph_key: str, size: Optional[float] = None) -> None:
|
||||
def setTextIcon(widget: QWidget, glyph_key: str, size: float | None = None) -> None:
|
||||
"""Set text on a widget to a specific font & glyph.
|
||||
|
||||
This is an alternative to setting a QIcon with a pixmap. It may be easier to
|
||||
@@ -152,7 +157,7 @@ def setTextIcon(widget: QWidget, glyph_key: str, size: Optional[float] = None) -
|
||||
|
||||
Parameters
|
||||
----------
|
||||
wdg : QWidget
|
||||
widget : QWidget
|
||||
A widget supporting a `setText` method.
|
||||
glyph_key : str
|
||||
String encapsulating a font-family, style, and glyph. e.g. 'fa5s.smile'.
|
||||
@@ -162,8 +167,8 @@ def setTextIcon(widget: QWidget, glyph_key: str, size: Optional[float] = None) -
|
||||
return _QFIS.instance().setTextIcon(widget, glyph_key, size)
|
||||
|
||||
|
||||
def font(font_prefix: str, size: Optional[int] = None) -> QFont:
|
||||
"""Create QFont for `font_prefix`
|
||||
def font(font_prefix: str, size: int | None = None) -> QFont:
|
||||
"""Create QFont for `font_prefix`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -181,8 +186,8 @@ def font(font_prefix: str, size: Optional[int] = None) -> QFont:
|
||||
|
||||
|
||||
def addFont(
|
||||
filepath: str, prefix: str, charmap: Optional[Dict[str, str]] = None
|
||||
) -> Optional[Tuple[str, str]]:
|
||||
filepath: str, prefix: str, charmap: dict[str, str] | None = None
|
||||
) -> tuple[str, str] | None:
|
||||
"""Add OTF/TTF file at `filepath` to the registry under `prefix`.
|
||||
|
||||
If you'd like to later use a fontkey in the form of `prefix.some-name`, then
|
||||
@@ -190,10 +195,12 @@ def addFont(
|
||||
to their unicode numbers. If a charmap is not provided, glyphs must be directly
|
||||
accessed with their unicode as something like `key.\uffff`.
|
||||
|
||||
NOTE: in most cases, users will not need this.
|
||||
Instead, they should install a font plugin, like:
|
||||
https://github.com/tlambert03/fonticon-fontawesome5
|
||||
https://github.com/tlambert03/fonticon-materialdesignicons6
|
||||
!!! Note
|
||||
in most cases, users will not need this. Instead, they should install a
|
||||
font plugin, like:
|
||||
|
||||
- [fonticon-fontawesome5](https://pypi.org/project/fonticon-fontawesome5/)
|
||||
- [fonticon-materialdesignicons6](https://pypi.org/project/fonticon-materialdesignicons6/)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
@@ -1,11 +1,14 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
|
||||
from superqt.qtcompat.QtCore import QRectF, QTimer
|
||||
from superqt.qtcompat.QtGui import QPainter
|
||||
from superqt.qtcompat.QtWidgets import QWidget
|
||||
from qtpy.QtCore import QRectF, QTimer
|
||||
from qtpy.QtGui import QPainter
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
|
||||
class Animation(ABC):
|
||||
"""Base icon animation class."""
|
||||
|
||||
def __init__(self, parent_widget: QWidget, interval: int = 10, step: int = 1):
|
||||
self.parent_widget = parent_widget
|
||||
self.timer = QTimer()
|
||||
@@ -25,6 +28,8 @@ class Animation(ABC):
|
||||
|
||||
|
||||
class spin(Animation):
|
||||
"""Animation that smoothly spins an icon."""
|
||||
|
||||
def animate(self, painter: QPainter):
|
||||
if not self.timer.isActive():
|
||||
self.timer.start()
|
||||
@@ -36,5 +41,7 @@ class spin(Animation):
|
||||
|
||||
|
||||
class pulse(spin):
|
||||
def __init__(self, parent_widget: QWidget = None):
|
||||
"""Animation that spins an icon in slower, discrete steps."""
|
||||
|
||||
def __init__(self, parent_widget: Optional[QWidget] = None):
|
||||
super().__init__(parent_widget, interval=200, step=45)
|
||||
|
@@ -60,7 +60,6 @@ class IconFont(metaclass=IconFontMeta):
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
class FA5S(IconFont):
|
||||
__font_file__ = '...'
|
||||
some_char = 0xfa42
|
||||
@@ -73,10 +72,11 @@ class IconFont(metaclass=IconFontMeta):
|
||||
def namespace2font(namespace: Union[Mapping, Type], name: str) -> Type[IconFont]:
|
||||
"""Convenience to convert a namespace (class, module, dict) into an IconFont."""
|
||||
if isinstance(namespace, type):
|
||||
assert isinstance(
|
||||
getattr(namespace, FONTFILE_ATTR), str
|
||||
), "Not a valid font type"
|
||||
return namespace # type: ignore
|
||||
if not isinstance(getattr(namespace, FONTFILE_ATTR), str):
|
||||
raise TypeError(
|
||||
f"Invalid Font: must declare {FONTFILE_ATTR!r} attribute or classmethod"
|
||||
)
|
||||
return namespace
|
||||
elif hasattr(namespace, "__dict__"):
|
||||
ns = dict(namespace.__dict__)
|
||||
else:
|
||||
|
@@ -1,4 +1,5 @@
|
||||
from typing import Dict, List, Set, Tuple
|
||||
import contextlib
|
||||
from typing import ClassVar, Dict, List, Set, Tuple
|
||||
|
||||
from ._iconfont import IconFontMeta, namespace2font
|
||||
|
||||
@@ -9,15 +10,19 @@ except ImportError:
|
||||
|
||||
|
||||
class FontIconManager:
|
||||
|
||||
ENTRY_POINT = "superqt.fonticon"
|
||||
_PLUGINS: Dict[str, EntryPoint] = {}
|
||||
_LOADED: Dict[str, IconFontMeta] = {}
|
||||
_BLOCKED: Set[EntryPoint] = set()
|
||||
ENTRY_POINT: ClassVar[str] = "superqt.fonticon"
|
||||
_PLUGINS: ClassVar[Dict[str, EntryPoint]] = {}
|
||||
_LOADED: ClassVar[Dict[str, IconFontMeta]] = {}
|
||||
_BLOCKED: ClassVar[Set[EntryPoint]] = set()
|
||||
|
||||
def _discover_fonts(self) -> None:
|
||||
self._PLUGINS.clear()
|
||||
for ep in entry_points().get(self.ENTRY_POINT, {}):
|
||||
entries = entry_points()
|
||||
if hasattr(entries, "select"): # python>3.10
|
||||
_entries = entries.select(group=self.ENTRY_POINT) # type: ignore
|
||||
else:
|
||||
_entries = entries.get(self.ENTRY_POINT, [])
|
||||
for ep in _entries:
|
||||
if ep not in self._BLOCKED:
|
||||
self._PLUGINS[ep.name] = ep
|
||||
|
||||
@@ -93,10 +98,8 @@ def loaded(load_all=False) -> Dict[str, List[str]]:
|
||||
if load_all:
|
||||
discover()
|
||||
for x in available():
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
_manager._get_font_class(x)
|
||||
except Exception:
|
||||
continue
|
||||
return {
|
||||
key: sorted(filter(lambda x: not x.startswith("_"), cls.__dict__))
|
||||
for key, cls in _manager._LOADED.items()
|
||||
|
@@ -1,16 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from collections import abc
|
||||
from collections import abc, defaultdict
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import DefaultDict, Dict, Optional, Sequence, Tuple, Type, Union, cast
|
||||
from typing import ClassVar, DefaultDict, Sequence, Tuple, Union, cast
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from ..qtcompat import QT_VERSION
|
||||
from ..qtcompat.QtCore import QObject, QPoint, QRect, QSize, Qt
|
||||
from ..qtcompat.QtGui import (
|
||||
from qtpy import QT_VERSION
|
||||
from qtpy.QtCore import QObject, QPoint, QRect, QSize, Qt
|
||||
from qtpy.QtGui import (
|
||||
QColor,
|
||||
QFont,
|
||||
QFontDatabase,
|
||||
@@ -22,8 +20,11 @@ from ..qtcompat.QtGui import (
|
||||
QPixmapCache,
|
||||
QTransform,
|
||||
)
|
||||
from ..qtcompat.QtWidgets import QApplication, QStyleOption, QWidget
|
||||
from ..utils import QMessageHandler
|
||||
from qtpy.QtWidgets import QApplication, QStyleOption, QWidget
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from superqt.utils import QMessageHandler
|
||||
|
||||
from ._animations import Animation
|
||||
|
||||
|
||||
@@ -52,7 +53,7 @@ ValidColor = Union[
|
||||
|
||||
StateOrMode = Union[QIcon.State, QIcon.Mode]
|
||||
StateModeKey = Union[StateOrMode, str, Sequence[StateOrMode]]
|
||||
_SM_MAP: Dict[str, StateOrMode] = {
|
||||
_SM_MAP: dict[str, StateOrMode] = {
|
||||
"on": QIcon.State.On,
|
||||
"off": QIcon.State.Off,
|
||||
"normal": QIcon.Mode.Normal,
|
||||
@@ -62,8 +63,8 @@ _SM_MAP: Dict[str, StateOrMode] = {
|
||||
}
|
||||
|
||||
|
||||
def _norm_state_mode(key: StateModeKey) -> Tuple[QIcon.State, QIcon.Mode]:
|
||||
"""return state/mode tuple given a variety of valid inputs.
|
||||
def _norm_state_mode(key: StateModeKey) -> tuple[QIcon.State, QIcon.Mode]:
|
||||
"""Return state/mode tuple given a variety of valid inputs.
|
||||
|
||||
Input can be either a string, or a sequence of state or mode enums.
|
||||
Strings can be any combination of on, off, normal, active, selected, disabled,
|
||||
@@ -73,13 +74,13 @@ def _norm_state_mode(key: StateModeKey) -> Tuple[QIcon.State, QIcon.Mode]:
|
||||
if isinstance(key, str):
|
||||
try:
|
||||
_sm = [_SM_MAP[k.lower()] for k in key.split("_")]
|
||||
except KeyError:
|
||||
except KeyError as e:
|
||||
raise ValueError(
|
||||
f"{key!r} is not a valid state key, must be a combination of {{on, "
|
||||
"off, active, disabled, selected, normal} separated by underscore"
|
||||
)
|
||||
) from e
|
||||
else:
|
||||
_sm = key if isinstance(key, abc.Sequence) else [key] # type: ignore
|
||||
_sm = key if isinstance(key, abc.Sequence) else [key]
|
||||
|
||||
state = next((i for i in _sm if isinstance(i, QIcon.State)), QIcon.State.Off)
|
||||
mode = next((i for i in _sm if isinstance(i, QIcon.Mode)), QIcon.Mode.Normal)
|
||||
@@ -91,8 +92,8 @@ class IconOptionDict(TypedDict, total=False):
|
||||
scale_factor: float
|
||||
color: ValidColor
|
||||
opacity: float
|
||||
animation: Optional[Animation]
|
||||
transform: Optional[QTransform]
|
||||
animation: Animation | None
|
||||
transform: QTransform | None
|
||||
|
||||
|
||||
# public facing, for a nicer IDE experience than a dict
|
||||
@@ -102,12 +103,29 @@ class IconOptionDict(TypedDict, total=False):
|
||||
# IconOptions are.
|
||||
@dataclass
|
||||
class IconOpts:
|
||||
glyph_key: Union[str, Unset] = _Unset
|
||||
scale_factor: Union[float, Unset] = _Unset
|
||||
color: Union[ValidColor, Unset] = _Unset
|
||||
opacity: Union[float, Unset] = _Unset
|
||||
animation: Union[Animation, Unset, None] = _Unset
|
||||
transform: Union[QTransform, Unset, None] = _Unset
|
||||
"""Options for rendering an icon.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
glyph_key : str, optional
|
||||
The key of the glyph to use, e.g. `'fa5s.smile'`, by default `None`
|
||||
scale_factor : float, optional
|
||||
The scale factor to use, by default `None`
|
||||
color : ValidColor, optional
|
||||
The color to use, by default `None`. Colors may be specified as a string,
|
||||
`QColor`, `Qt.GlobalColor`, or a 3 or 4-tuple of integers.
|
||||
opacity : float, optional
|
||||
The opacity to use, by default `None`
|
||||
animation : Animation, optional
|
||||
The animation to use, by default `None`
|
||||
"""
|
||||
|
||||
glyph_key: str | Unset = _Unset
|
||||
scale_factor: float | Unset = _Unset
|
||||
color: ValidColor | Unset = _Unset
|
||||
opacity: float | Unset = _Unset
|
||||
animation: Animation | Unset | None = _Unset
|
||||
transform: QTransform | Unset | None = _Unset
|
||||
|
||||
def dict(self) -> IconOptionDict:
|
||||
# not using asdict due to pickle errors on animation
|
||||
@@ -123,8 +141,8 @@ class _IconOptions:
|
||||
scale_factor: float = DEFAULT_SCALING_FACTOR
|
||||
color: ValidColor = None
|
||||
opacity: float = DEFAULT_OPACITY
|
||||
animation: Optional[Animation] = None
|
||||
transform: Optional[QTransform] = None
|
||||
animation: Animation | None = None
|
||||
transform: QTransform | None = None
|
||||
|
||||
def _update(self, icon_opts: IconOpts) -> _IconOptions:
|
||||
return _IconOptions(**{**vars(self), **icon_opts.dict()})
|
||||
@@ -139,8 +157,8 @@ class _QFontIconEngine(QIconEngine):
|
||||
|
||||
def __init__(self, options: _IconOptions):
|
||||
super().__init__()
|
||||
self._opts: DefaultDict[
|
||||
QIcon.State, Dict[QIcon.Mode, Optional[_IconOptions]]
|
||||
self._opts: defaultdict[
|
||||
QIcon.State, dict[QIcon.Mode, _IconOptions | None]
|
||||
] = DefaultDict(dict)
|
||||
self._opts[QIcon.State.Off][QIcon.Mode.Normal] = options
|
||||
self.update_hash()
|
||||
@@ -213,7 +231,7 @@ class _QFontIconEngine(QIconEngine):
|
||||
|
||||
# font
|
||||
font = QFont()
|
||||
font.setFamily(family) # set sepeartely for Qt6
|
||||
font.setFamily(family) # set separately for Qt6
|
||||
font.setPixelSize(round(rect.height() * opts.scale_factor))
|
||||
if style:
|
||||
font.setStyleName(style)
|
||||
@@ -222,7 +240,7 @@ class _QFontIconEngine(QIconEngine):
|
||||
if isinstance(opts.color, tuple):
|
||||
color_args = opts.color
|
||||
else:
|
||||
color_args = (opts.color,) if opts.color else () # type: ignore
|
||||
color_args = (opts.color,) if opts.color else ()
|
||||
|
||||
# animation
|
||||
if opts.animation is not None:
|
||||
@@ -243,7 +261,9 @@ class _QFontIconEngine(QIconEngine):
|
||||
def pixmap(self, size: QSize, mode: QIcon.Mode, state: QIcon.State) -> QPixmap:
|
||||
# first look in cache
|
||||
pmckey = self._pmcKey(size, mode, state)
|
||||
pm = QPixmapCache.find(pmckey) if pmckey else None
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", "QPixmapCache.find")
|
||||
pm = QPixmapCache.find(pmckey) if pmckey else None
|
||||
if pm:
|
||||
return pm
|
||||
pixmap = QPixmap(size)
|
||||
@@ -302,12 +322,12 @@ class QFontIcon(QIcon):
|
||||
self,
|
||||
state: QIcon.State = QIcon.State.Off,
|
||||
mode: QIcon.Mode = QIcon.Mode.Normal,
|
||||
glyph_key: Union[str, Unset] = _Unset,
|
||||
scale_factor: Union[float, Unset] = _Unset,
|
||||
color: Union[ValidColor, Unset] = _Unset,
|
||||
opacity: Union[float, Unset] = _Unset,
|
||||
animation: Union[Animation, Unset, None] = _Unset,
|
||||
transform: Union[QTransform, Unset, None] = _Unset,
|
||||
glyph_key: str | Unset = _Unset,
|
||||
scale_factor: float | Unset = _Unset,
|
||||
color: ValidColor | Unset = _Unset,
|
||||
opacity: float | Unset = _Unset,
|
||||
animation: Animation | Unset | None = _Unset,
|
||||
transform: QTransform | Unset | None = _Unset,
|
||||
) -> None:
|
||||
"""Set icon options for a specific mode/state."""
|
||||
if glyph_key is not _Unset:
|
||||
@@ -325,22 +345,20 @@ class QFontIcon(QIcon):
|
||||
|
||||
|
||||
class QFontIconStore(QObject):
|
||||
|
||||
# map of key -> (font_family, font_style)
|
||||
_LOADED_KEYS: Dict[str, Tuple[str, Optional[str]]] = dict()
|
||||
_LOADED_KEYS: ClassVar[dict[str, tuple[str, str]]] = {}
|
||||
|
||||
# map of (font_family, font_style) -> character (char may include key)
|
||||
_CHARMAPS: Dict[Tuple[str, Optional[str]], Dict[str, str]] = dict()
|
||||
_CHARMAPS: ClassVar[dict[tuple[str, str | None], dict[str, str]]] = {}
|
||||
|
||||
# singleton instance, use `instance()` to retrieve
|
||||
__instance: Optional[QFontIconStore] = None
|
||||
__instance: ClassVar[QFontIconStore | None] = None
|
||||
|
||||
def __init__(self, parent: Optional[QObject] = None) -> None:
|
||||
def __init__(self, parent: QObject | None = None) -> None:
|
||||
super().__init__(parent=parent)
|
||||
# QT6 drops this
|
||||
dpi = getattr(Qt.ApplicationAttribute, "AA_UseHighDpiPixmaps", None)
|
||||
if dpi:
|
||||
QApplication.setAttribute(dpi)
|
||||
if tuple(cast(str, QT_VERSION).split(".")) < ("6", "0"):
|
||||
# QT6 drops this
|
||||
QApplication.setAttribute(Qt.ApplicationAttribute.AA_UseHighDpiPixmaps)
|
||||
|
||||
@classmethod
|
||||
def instance(cls) -> QFontIconStore:
|
||||
@@ -355,8 +373,8 @@ class QFontIconStore(QObject):
|
||||
QFontDatabase.removeAllApplicationFonts()
|
||||
|
||||
@classmethod
|
||||
def _key2family(cls, key: str) -> Tuple[str, Optional[str]]:
|
||||
"""Return (family, style) given a font `key`"""
|
||||
def _key2family(cls, key: str) -> tuple[str, str]:
|
||||
"""Return (family, style) given a font `key`."""
|
||||
key = key.split(".", maxsplit=1)[0]
|
||||
if key not in cls._LOADED_KEYS:
|
||||
from . import _plugins
|
||||
@@ -364,7 +382,7 @@ class QFontIconStore(QObject):
|
||||
try:
|
||||
font_cls = _plugins.get_font_class(key)
|
||||
result = cls.addFont(
|
||||
font_cls.__font_file__, key, charmap=font_cls.__dict__
|
||||
font_cls.__font_file__, key, charmap=dict(font_cls.__dict__)
|
||||
)
|
||||
if not result: # pragma: no cover
|
||||
raise Exception("Invalid font file")
|
||||
@@ -379,13 +397,15 @@ class QFontIconStore(QObject):
|
||||
|
||||
@classmethod
|
||||
def _ensure_char(cls, char: str, family: str, style: str) -> str:
|
||||
"""make sure that `char` is a glyph provided by `family` and `style`."""
|
||||
"""Make sure that `char` is a glyph provided by `family` and `style`."""
|
||||
if len(char) == 1 and ord(char) > 256:
|
||||
return char
|
||||
try:
|
||||
charmap = cls._CHARMAPS[(family, style)]
|
||||
except KeyError:
|
||||
raise KeyError(f"No charmap registered for font '{family} ({style})'")
|
||||
except KeyError as e:
|
||||
raise KeyError(
|
||||
f"No charmap registered for font '{family} ({style})'"
|
||||
) from e
|
||||
if char in charmap:
|
||||
# split in case the charmap includes the key
|
||||
return charmap[char].split(".", maxsplit=1)[-1]
|
||||
@@ -398,8 +418,8 @@ class QFontIconStore(QObject):
|
||||
raise ValueError(f"Font '{family} ({style})' has no glyph with the key {ident}")
|
||||
|
||||
@classmethod
|
||||
def key2glyph(cls, glyph_key: str) -> tuple[str, str, Optional[str]]:
|
||||
"""Return (char, family, style) given a `glyph_key`"""
|
||||
def key2glyph(cls, glyph_key: str) -> tuple[str, str, str | None]:
|
||||
"""Return (char, family, style) given a `glyph_key`."""
|
||||
if "." not in glyph_key:
|
||||
raise ValueError("Glyph key must contain a period")
|
||||
font_key, char = glyph_key.split(".", maxsplit=1)
|
||||
@@ -409,20 +429,20 @@ class QFontIconStore(QObject):
|
||||
|
||||
@classmethod
|
||||
def addFont(
|
||||
cls, filepath: str, prefix: str, charmap: Optional[Dict[str, str]] = None
|
||||
) -> Optional[Tuple[str, str]]:
|
||||
"""Add font at `filepath` to the registry under `key`.
|
||||
cls, filepath: str, prefix: str, charmap: dict[str, str] | None = None
|
||||
) -> tuple[str, str] | None:
|
||||
r"""Add font at `filepath` to the registry under `key`.
|
||||
|
||||
If you'd like to later use a fontkey in the form of `key.some-name`, then
|
||||
`charmap` must be provided and provide a mapping for all of the glyph names
|
||||
to their unicode numbers. If a charmap is not provided, glyphs must be directly
|
||||
accessed with their unicode as something like `key.\uffff`.
|
||||
accessed with their unicode as something like `key.\\uffff`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filepath : str
|
||||
Path to an OTF or TTF file containing the fonts
|
||||
key : str
|
||||
prefix : str
|
||||
A key that will represent this font file when used for lookup. For example,
|
||||
'fa5s' for 'Font-Awesome 5 Solid'.
|
||||
charmap : Dict[str, str], optional
|
||||
@@ -436,8 +456,8 @@ class QFontIconStore(QObject):
|
||||
something goes wrong.
|
||||
"""
|
||||
if prefix in cls._LOADED_KEYS:
|
||||
warnings.warn(f"Prefix {prefix} already loaded")
|
||||
return
|
||||
warnings.warn(f"Prefix {prefix} already loaded", stacklevel=2)
|
||||
return None
|
||||
|
||||
if not Path(filepath).exists():
|
||||
raise FileNotFoundError(f"Font file doesn't exist: {filepath}")
|
||||
@@ -446,28 +466,29 @@ class QFontIconStore(QObject):
|
||||
|
||||
fontId = QFontDatabase.addApplicationFont(str(Path(filepath).absolute()))
|
||||
if fontId < 0: # pragma: no cover
|
||||
warnings.warn(f"Cannot load font file: {filepath}")
|
||||
warnings.warn(f"Cannot load font file: {filepath}", stacklevel=2)
|
||||
return None
|
||||
|
||||
families = QFontDatabase.applicationFontFamilies(fontId)
|
||||
if not families: # pragma: no cover
|
||||
warnings.warn(f"Font file is empty!: {filepath}")
|
||||
warnings.warn(f"Font file is empty!: {filepath}", stacklevel=2)
|
||||
return None
|
||||
family: str = families[0]
|
||||
|
||||
# in Qt6, everything becomes a static member
|
||||
QFd: Union[QFontDatabase, Type[QFontDatabase]] = (
|
||||
QFontDatabase() # type: ignore
|
||||
if tuple(QT_VERSION.split(".")) < ("6", "0")
|
||||
QFd: QFontDatabase | type[QFontDatabase] = (
|
||||
QFontDatabase()
|
||||
if tuple(cast(str, QT_VERSION).split(".")) < ("6", "0")
|
||||
else QFontDatabase
|
||||
)
|
||||
|
||||
styles = QFd.styles(family) # type: ignore
|
||||
styles = QFd.styles(family)
|
||||
style: str = styles[-1] if styles else ""
|
||||
if not QFd.isSmoothlyScalable(family, style): # pragma: no cover
|
||||
warnings.warn(
|
||||
f"Registered font {family} ({style}) is not smoothly scalable. "
|
||||
"Icons may not look attractive."
|
||||
"Icons may not look attractive.",
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
cls._LOADED_KEYS[prefix] = (family, style)
|
||||
@@ -480,11 +501,11 @@ class QFontIconStore(QObject):
|
||||
glyph_key: str,
|
||||
*,
|
||||
scale_factor: float = DEFAULT_SCALING_FACTOR,
|
||||
color: ValidColor = None,
|
||||
color: ValidColor | None = None,
|
||||
opacity: float = 1,
|
||||
animation: Optional[Animation] = None,
|
||||
transform: Optional[QTransform] = None,
|
||||
states: Dict[str, Union[IconOptionDict, IconOpts]] = {},
|
||||
animation: Animation | None = None,
|
||||
transform: QTransform | None = None,
|
||||
states: dict[str, IconOptionDict | IconOpts] | None = None,
|
||||
) -> QFontIcon:
|
||||
self.key2glyph(glyph_key) # make sure it's a valid glyph_key
|
||||
default_opts = _IconOptions(
|
||||
@@ -496,18 +517,18 @@ class QFontIconStore(QObject):
|
||||
transform=transform,
|
||||
)
|
||||
icon = QFontIcon(default_opts)
|
||||
for kw, options in states.items():
|
||||
for kw, options in (states or {}).items():
|
||||
if isinstance(options, IconOpts):
|
||||
options = default_opts._update(options).dict()
|
||||
icon.addState(*_norm_state_mode(kw), **options)
|
||||
return icon
|
||||
|
||||
def setTextIcon(
|
||||
self, widget: QWidget, glyph_key: str, size: Optional[float] = None
|
||||
self, widget: QWidget, glyph_key: str, size: float | None = None
|
||||
) -> None:
|
||||
"""Sets text on a widget to a specific font & glyph.
|
||||
|
||||
This is an alternative to setting a QIcon with a pixmap. It may
|
||||
This is an alternative to setting a `QIcon` with a pixmap. It may
|
||||
be easier to combine with dynamic stylesheets.
|
||||
"""
|
||||
setText = getattr(widget, "setText", None)
|
||||
@@ -520,8 +541,8 @@ class QFontIconStore(QObject):
|
||||
widget.setFont(self.font(glyph_key, int(size)))
|
||||
setText(glyph)
|
||||
|
||||
def font(self, font_prefix: str, size: Optional[int] = None) -> QFont:
|
||||
"""Create QFont for `font_prefix`"""
|
||||
def font(self, font_prefix: str, size: int | None = None) -> QFont:
|
||||
"""Create QFont for `font_prefix`."""
|
||||
font_key, _ = font_prefix.split(".", maxsplit=1)
|
||||
family, style = self._key2family(font_key)
|
||||
font = QFont()
|
||||
@@ -534,7 +555,7 @@ class QFontIconStore(QObject):
|
||||
|
||||
|
||||
def _ensure_identifier(name: str) -> str:
|
||||
"""Normalize string to valid identifier"""
|
||||
"""Normalize string to valid identifier."""
|
||||
import keyword
|
||||
|
||||
if not name:
|
||||
@@ -551,5 +572,6 @@ def _ensure_identifier(name: str) -> str:
|
||||
# replace dashes and spaces with underscores
|
||||
name = name.replace("-", "_").replace(" ", "_")
|
||||
|
||||
assert str.isidentifier(name), f"Could not canonicalize name: {name}"
|
||||
if not str.isidentifier(name):
|
||||
raise ValueError(f"Could not canonicalize name: {name!r}. (not an identifier)")
|
||||
return name
|
||||
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.Qt3DAnimation import *
|
||||
from PyQt6.Qt3DAnimation import *
|
||||
from PySide2.Qt3DAnimation import *
|
||||
from PySide6.Qt3DAnimation import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.Qt3DCore import *
|
||||
from PyQt6.Qt3DCore import *
|
||||
from PySide2.Qt3DCore import *
|
||||
from PySide6.Qt3DCore import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.Qt3DExtras import *
|
||||
from PyQt6.Qt3DExtras import *
|
||||
from PySide2.Qt3DExtras import *
|
||||
from PySide6.Qt3DExtras import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.Qt3DInput import *
|
||||
from PyQt6.Qt3DInput import *
|
||||
from PySide2.Qt3DInput import *
|
||||
from PySide6.Qt3DInput import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.Qt3DLogic import *
|
||||
from PyQt6.Qt3DLogic import *
|
||||
from PySide2.Qt3DLogic import *
|
||||
from PySide6.Qt3DLogic import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.Qt3DRender import *
|
||||
from PyQt6.Qt3DRender import *
|
||||
from PySide2.Qt3DRender import *
|
||||
from PySide6.Qt3DRender import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtCharts import *
|
||||
from PyQt6.QtCharts import *
|
||||
from PySide2.QtCharts import *
|
||||
from PySide6.QtCharts import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtConcurrent import *
|
||||
from PyQt6.QtConcurrent import *
|
||||
from PySide2.QtConcurrent import *
|
||||
from PySide6.QtConcurrent import *
|
@@ -1,12 +0,0 @@
|
||||
# type: ignore
|
||||
from . import API_NAME, _get_qtmodule
|
||||
|
||||
_QtCore = _get_qtmodule(__name__)
|
||||
globals().update(_QtCore.__dict__)
|
||||
|
||||
if "PyQt" in API_NAME:
|
||||
Property = _QtCore.pyqtProperty
|
||||
Signal = _QtCore.pyqtSignal
|
||||
SignalInstance = getattr(_QtCore, "pyqtBoundSignal", None)
|
||||
Slot = _QtCore.pyqtSlot
|
||||
__version__ = _QtCore.QT_VERSION_STR
|
@@ -1,10 +0,0 @@
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt6.QtCore import *
|
||||
from PySide2.QtCore import *
|
||||
from PySide6.QtCore import *
|
||||
|
||||
Property = pyqtProperty
|
||||
Signal = pyqtSignal
|
||||
SignalInstance = pyqtBoundSignal
|
||||
Slot = pyqtSlot
|
||||
__version__: str
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtDataVisualization import *
|
||||
from PyQt6.QtDataVisualization import *
|
||||
from PySide2.QtDataVisualization import *
|
||||
from PySide6.QtDataVisualization import *
|
@@ -1,13 +0,0 @@
|
||||
# type: ignore
|
||||
from . import API_NAME, _get_qtmodule
|
||||
|
||||
_QtGui = _get_qtmodule(__name__)
|
||||
globals().update(_QtGui.__dict__)
|
||||
|
||||
if "6" in API_NAME:
|
||||
|
||||
def pos(self, *a):
|
||||
_pos = self.position(*a)
|
||||
return _pos.toPoint()
|
||||
|
||||
_QtGui.QMouseEvent.pos = pos
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt6.QtGui import *
|
||||
from PySide2.QtGui import *
|
||||
from PySide6.QtGui import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtHelp import *
|
||||
from PyQt6.QtHelp import *
|
||||
from PySide2.QtHelp import *
|
||||
from PySide6.QtHelp import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtLocation import *
|
||||
from PyQt6.QtLocation import *
|
||||
from PySide2.QtLocation import *
|
||||
from PySide6.QtLocation import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtMacExtras import *
|
||||
from PyQt6.QtMacExtras import *
|
||||
from PySide2.QtMacExtras import *
|
||||
from PySide6.QtMacExtras import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtMultimedia import *
|
||||
from PyQt6.QtMultimedia import *
|
||||
from PySide2.QtMultimedia import *
|
||||
from PySide6.QtMultimedia import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtMultimediaWidgets import *
|
||||
from PyQt6.QtMultimediaWidgets import *
|
||||
from PySide2.QtMultimediaWidgets import *
|
||||
from PySide6.QtMultimediaWidgets import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtNetwork import *
|
||||
from PyQt6.QtNetwork import *
|
||||
from PySide2.QtNetwork import *
|
||||
from PySide6.QtNetwork import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtOpenGL import *
|
||||
from PyQt6.QtOpenGL import *
|
||||
from PySide2.QtOpenGL import *
|
||||
from PySide6.QtOpenGL import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtOpenGLFunctions import *
|
||||
from PyQt6.QtOpenGLFunctions import *
|
||||
from PySide2.QtOpenGLFunctions import *
|
||||
from PySide6.QtOpenGLFunctions import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtPositioning import *
|
||||
from PyQt6.QtPositioning import *
|
||||
from PySide2.QtPositioning import *
|
||||
from PySide6.QtPositioning import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtPrintSupport import *
|
||||
from PyQt6.QtPrintSupport import *
|
||||
from PySide2.QtPrintSupport import *
|
||||
from PySide6.QtPrintSupport import *
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user