Compare commits

...

42 Commits

Author SHA1 Message Date
Talley Lambert
d1c056886f chore: changelog v0.3.6 2022-10-03 17:04:14 -04:00
Talley Lambert
a73e56bb83 fix: fix missing labels after setValue (#123) 2022-10-03 17:00:53 -04:00
Talley Lambert
6f71e46914 feat: add editing finished signal to LabeledSliders (#122)
* feat: add editing finished signal to LabeledSliders

* remove extra file
2022-10-03 17:00:29 -04:00
Talley Lambert
fbc67a745c fix: Offer patch for (unstyled) QSliders on macos 12 and Qt <6 (#117)
* wip

* opt-in patch

* finishes

* add patch to demo

* remove demo 2

* extend docs
2022-10-03 15:49:34 -04:00
Talley Lambert
77bd737e13 fix: Fix TypeError on slider rangeChanged signal (#121)
* fix: fix sliders type signals

* test: more tests

* fix for pyside2

* skip type check on py37
2022-10-03 15:10:40 -04:00
pre-commit-ci[bot]
ba626e8786 ci(pre-commit.ci): pre-commit autoupdate (#120)
updates:
- [github.com/pre-commit/mirrors-mypy: v0.971 → v0.981](https://github.com/pre-commit/mirrors-mypy/compare/v0.971...v0.981)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-10-03 15:09:47 -04:00
Grzegorz Bokota
04efa95511 dummy fix (#119) 2022-10-03 08:24:45 -04:00
pre-commit-ci[bot]
f401d6d59c [pre-commit.ci] pre-commit autoupdate (#116)
updates:
- https://github.com/myint/autoflakehttps://github.com/PyCQA/autoflake
- [github.com/PyCQA/autoflake: v1.5.3 → v1.6.1](https://github.com/PyCQA/autoflake/compare/v1.5.3...v1.6.1)
- [github.com/asottile/pyupgrade: v2.37.3 → v2.38.2](https://github.com/asottile/pyupgrade/compare/v2.37.3...v2.38.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-10-02 19:20:26 -04:00
pre-commit-ci[bot]
a3bd0d0edf [pre-commit.ci] pre-commit autoupdate (#114) 2022-09-05 16:06:28 -04:00
pre-commit-ci[bot]
e7e8dfc44c [pre-commit.ci] pre-commit autoupdate (#109)
updates:
- [github.com/PyCQA/flake8: 5.0.2 → 5.0.4](https://github.com/PyCQA/flake8/compare/5.0.2...5.0.4)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-08-17 12:55:20 -04:00
Talley Lambert
a556f16745 chore: changelog v0.3.5 (#110)
* chore: changelog v0.3.5

* try fix napari test

* again

* try another

* again

* fix again
2022-08-17 12:54:27 -04:00
sfhbarnett
2864058974 fix range slider drag crash on PyQt6 (#108)
* fix range slider drag crash on PyQt6

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-08-04 11:01:19 -04:00
pre-commit-ci[bot]
463332f4fc [pre-commit.ci] pre-commit autoupdate (#104)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/asottile/setup-cfg-fmt: v1.20.2 → v2.0.0](https://github.com/asottile/setup-cfg-fmt/compare/v1.20.2...v2.0.0)
- [github.com/PyCQA/flake8: 4.0.1 → 5.0.2](https://github.com/PyCQA/flake8/compare/4.0.1...5.0.2)
- [github.com/asottile/pyupgrade: v2.37.2 → v2.37.3](https://github.com/asottile/pyupgrade/compare/v2.37.2...v2.37.3)

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* version specs

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Talley Lambert <talley.lambert@gmail.com>
2022-08-03 09:36:12 -04:00
Markus Stabrin
f08e2d1720 Fix float value error in pyqt configuration (#106)
Co-authored-by: Markus Stabrin <markus.stabrin@gmail.com>
2022-08-03 09:35:56 -04:00
pre-commit-ci[bot]
39c10aa238 [pre-commit.ci] pre-commit autoupdate (#102)
updates:
- [github.com/asottile/setup-cfg-fmt: v1.20.1 → v1.20.2](https://github.com/asottile/setup-cfg-fmt/compare/v1.20.1...v1.20.2)
- [github.com/asottile/pyupgrade: v2.37.1 → v2.37.2](https://github.com/asottile/pyupgrade/compare/v2.37.1...v2.37.2)
- [github.com/pre-commit/mirrors-mypy: v0.961 → v0.971](https://github.com/pre-commit/mirrors-mypy/compare/v0.961...v0.971)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-07-31 11:46:56 -04:00
Talley Lambert
d5d40a35f3 changelog 0.3.4 2022-07-24 11:10:01 -04:00
Talley Lambert
5b92a19b82 fix: relax runtime typing extensions requirement (#101) 2022-07-24 11:08:00 -04:00
pre-commit-ci[bot]
a3b0f1b115 [pre-commit.ci] pre-commit autoupdate (#97)
updates:
- [github.com/asottile/pyupgrade: v2.34.0 → v2.37.1](https://github.com/asottile/pyupgrade/compare/v2.34.0...v2.37.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-07-13 11:57:21 -04:00
Talley Lambert
b1e6d55957 fix: catch qpixmap deprecation (#99) 2022-07-13 11:57:01 -04:00
Talley Lambert
55535b7600 chore: changelog v0.3.3 2022-07-10 10:15:33 -04:00
pre-commit-ci[bot]
31c834053c [pre-commit.ci] pre-commit autoupdate (#96)
updates:
- [github.com/psf/black: 22.3.0 → 22.6.0](https://github.com/psf/black/compare/22.3.0...22.6.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Talley Lambert <talley.lambert@gmail.com>
2022-07-07 17:08:52 -04:00
Talley Lambert
69219c846d Revert "update typing and namespace"
This reverts commit 2edb3c287e.
2022-07-07 16:49:26 -04:00
Talley Lambert
2edb3c287e update typing and namespace 2022-07-07 16:47:04 -04:00
Talley Lambert
218a7b4034 fix: fix deprecation warning on fonticon plugin discovery on python 3.10 (#95)
* fix: fix fonticon

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix entry points API

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-07-01 11:23:00 -04:00
pre-commit-ci[bot]
9ab24dbcf6 [pre-commit.ci] pre-commit autoupdate (#93)
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.2.0 → v4.3.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.2.0...v4.3.0)
- [github.com/asottile/pyupgrade: v2.32.1 → v2.34.0](https://github.com/asottile/pyupgrade/compare/v2.32.1...v2.34.0)
- [github.com/pre-commit/mirrors-mypy: v0.960 → v0.961](https://github.com/pre-commit/mirrors-mypy/compare/v0.960...v0.961)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-07-01 11:13:41 -04:00
pre-commit-ci[bot]
35acbbf5e6 [pre-commit.ci] pre-commit autoupdate (#90)
updates:
- [github.com/pre-commit/mirrors-mypy: v0.950 → v0.960](https://github.com/pre-commit/mirrors-mypy/compare/v0.950...v0.960)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-06-11 14:54:37 -04:00
Grzegorz Bokota
0ae3350c57 Add code syntax highlight utils (#88)
* add code syntax highlight code

* add example

* add documentation and fix example

* add tests

* add information about napari theme usage

* clean napari mention
2022-05-18 16:50:51 -04:00
pre-commit-ci[bot]
c7f8780900 [pre-commit.ci] pre-commit autoupdate (#87)
updates:
- [github.com/asottile/pyupgrade: v2.32.0 → v2.32.1](https://github.com/asottile/pyupgrade/compare/v2.32.0...v2.32.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-05-18 10:05:00 -04:00
Talley Lambert
cc25733ce8 Add changelog for v0.3.2 (#86)
* Add changelog for v0.3.2

* caps
2022-05-03 10:14:15 -04:00
pre-commit-ci[bot]
accb87021f [pre-commit.ci] pre-commit autoupdate (#85)
updates:
- [github.com/pre-commit/mirrors-mypy: v0.942 → v0.950](https://github.com/pre-commit/mirrors-mypy/compare/v0.942...v0.950)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-05-02 14:05:19 -04:00
Talley Lambert
ccad397838 fix crazy animation loop on collapsible (#84) 2022-05-02 14:01:17 -04:00
Talley Lambert
68248c920c reorder label update signal (#83) 2022-04-28 13:31:16 -04:00
Grzegorz Bokota
f8ac85aaf6 feat: Add QSearchableListWidget and QSearchableComboBox widgets (#80)
* implement widgets

* add basic documentation

* Add examples

* try version without packaging

Co-authored-by: Talley Lambert <talley.lambert@gmail.com>
2022-04-25 14:03:24 -04:00
Talley Lambert
bd6fba96ad fix deprecation warnings in tests (#82)
* stub

* update tests

* use util func

* add fallback for older versions

* don't test 3.6
2022-04-24 11:04:50 -04:00
Nekyo
7d31812858 Fix CSS for Collapsible (#79)
The button used for the Collapsible previously showed a black on
archlinux.
This fixes it to display properly.
Closes https://github.com/napari/superqt/issues/78
2022-04-17 10:58:05 -04:00
pre-commit-ci[bot]
f27377ab1b [pre-commit.ci] pre-commit autoupdate (#76)
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.1.0 → v4.2.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.1.0...v4.2.0)
- [github.com/asottile/setup-cfg-fmt: v1.20.0 → v1.20.1](https://github.com/asottile/setup-cfg-fmt/compare/v1.20.0...v1.20.1)
- [github.com/asottile/pyupgrade: v2.31.1 → v2.32.0](https://github.com/asottile/pyupgrade/compare/v2.31.1...v2.32.0)
- [github.com/psf/black: 22.1.0 → 22.3.0](https://github.com/psf/black/compare/22.1.0...22.3.0)
- [github.com/pre-commit/mirrors-mypy: v0.941 → v0.942](https://github.com/pre-commit/mirrors-mypy/compare/v0.941...v0.942)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-04-17 10:54:09 -04:00
pre-commit-ci[bot]
2052fb8310 [pre-commit.ci] pre-commit autoupdate (#75)
updates:
- [github.com/pre-commit/mirrors-mypy: v0.940 → v0.941](https://github.com/pre-commit/mirrors-mypy/compare/v0.940...v0.941)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-03-22 10:38:06 -04:00
pre-commit-ci[bot]
40d3e20bff [pre-commit.ci] pre-commit autoupdate (#73)
updates:
- [github.com/asottile/pyupgrade: v2.31.0 → v2.31.1](https://github.com/asottile/pyupgrade/compare/v2.31.0...v2.31.1)
- [github.com/pre-commit/mirrors-mypy: v0.931 → v0.940](https://github.com/pre-commit/mirrors-mypy/compare/v0.931...v0.940)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-03-14 16:43:55 -04:00
Talley Lambert
f4d9881b0c Fix height of expanded QCollapsible when child changes size (#72)
* update height when child changes

* return false
2022-03-11 14:16:03 -05:00
Talley Lambert
ba1ae92bcc changelog (#71) 2022-03-02 08:26:05 -05:00
Talley Lambert
8217a1cc71 check min requirements (#70) 2022-03-02 07:54:03 -05:00
Talley Lambert
96de1a261a add signals_blocked util (#69) 2022-02-20 11:25:10 -05:00
49 changed files with 1150 additions and 245 deletions

View File

@@ -70,9 +70,6 @@ jobs:
- 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
@@ -136,36 +133,59 @@ jobs:
name: screenshots ${{ runner.os }}
path: screenshots
test_napari:
name: napari tests
test_old_qtpy:
name: qtpy minreq
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
path: superqt
- uses: actions/checkout@v2
with:
repository: napari/napari
path: napari
fetch-depth: 2
- uses: tlambert03/setup-qt-libs@v1
- uses: actions/setup-python@v2
with:
python-version: '3.9'
python-version: '3.8'
- name: install
run: |
python -m pip install -U pip
python -m pip install -e ./napari[testing,pyqt5]
python -m pip install -e ./superqt
python -m pip install -e .[testing,pyqt5]
python -m pip install qtpy==1.1.0 typing-extensions==3.10.0.0
- name: Test napari magicgui
uses: GabrielBB/xvfb-action@v1
with:
run: python -m pytest --color=yes napari/napari/_qt
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: GabrielBB/xvfb-action@v1
with:
working-directory: napari-repo
run: python -m pytest --color=yes napari/_qt
check_manifest:
runs-on: ubuntu-latest

View File

@@ -1,21 +1,22 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.1.0
rev: v4.3.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v1.20.0
rev: v2.0.0
hooks:
- id: setup-cfg-fmt
args: ["--include-version-classifiers"]
- repo: https://github.com/PyCQA/flake8
rev: 4.0.1
rev: 5.0.4
hooks:
- id: flake8
additional_dependencies: [flake8-typing-imports==1.7.0]
exclude: examples
- repo: https://github.com/myint/autoflake
rev: v1.4
- repo: https://github.com/PyCQA/autoflake
rev: v1.6.1
hooks:
- id: autoflake
args: ["--in-place", "--remove-all-unused-imports"]
@@ -24,16 +25,16 @@ repos:
hooks:
- id: isort
- repo: https://github.com/asottile/pyupgrade
rev: v2.31.0
rev: v2.38.2
hooks:
- id: pyupgrade
args: [--py37-plus, --keep-runtime-typing]
- repo: https://github.com/psf/black
rev: 22.1.0
rev: 22.8.0
hooks:
- id: black
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.931
rev: v0.981
hooks:
- id: mypy
exclude: examples

View File

@@ -1,5 +1,92 @@
# Changelog
## [v0.3.6](https://github.com/napari/superqt/tree/v0.3.6) (2022-10-03)
[Full Changelog](https://github.com/napari/superqt/compare/v0.3.5...v0.3.6)
**Implemented enhancements:**
- feat: add editing finished signal to LabeledSliders [\#122](https://github.com/napari/superqt/pull/122) ([tlambert03](https://github.com/tlambert03))
**Fixed bugs:**
- fix: fix missing labels after setValue [\#123](https://github.com/napari/superqt/pull/123) ([tlambert03](https://github.com/tlambert03))
- fix: Fix TypeError on slider rangeChanged signal [\#121](https://github.com/napari/superqt/pull/121) ([tlambert03](https://github.com/tlambert03))
- Simple workaround for pyside 6 [\#119](https://github.com/napari/superqt/pull/119) ([Czaki](https://github.com/Czaki))
- fix: Offer patch for \(unstyled\) QSliders on macos 12 and Qt \<6 [\#117](https://github.com/napari/superqt/pull/117) ([tlambert03](https://github.com/tlambert03))
## [v0.3.5](https://github.com/napari/superqt/tree/v0.3.5) (2022-08-17)
[Full Changelog](https://github.com/napari/superqt/compare/v0.3.4...v0.3.5)
**Fixed bugs:**
- fix range slider drag crash on PyQt6 [\#108](https://github.com/napari/superqt/pull/108) ([sfhbarnett](https://github.com/sfhbarnett))
- Fix float value error in pyqt configuration [\#106](https://github.com/napari/superqt/pull/106) ([mstabrin](https://github.com/mstabrin))
**Merged pull requests:**
- chore: changelog v0.3.5 [\#110](https://github.com/napari/superqt/pull/110) ([tlambert03](https://github.com/tlambert03))
## [v0.3.4](https://github.com/napari/superqt/tree/v0.3.4) (2022-07-24)
[Full Changelog](https://github.com/napari/superqt/compare/v0.3.3...v0.3.4)
**Fixed bugs:**
- fix: relax runtime typing extensions requirement [\#101](https://github.com/napari/superqt/pull/101) ([tlambert03](https://github.com/tlambert03))
- fix: catch qpixmap deprecation [\#99](https://github.com/napari/superqt/pull/99) ([tlambert03](https://github.com/tlambert03))
## [v0.3.3](https://github.com/napari/superqt/tree/v0.3.3) (2022-07-10)
[Full Changelog](https://github.com/napari/superqt/compare/v0.3.2...v0.3.3)
**Implemented enhancements:**
- Add code syntax highlight utils [\#88](https://github.com/napari/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/napari/superqt/pull/95) ([tlambert03](https://github.com/tlambert03))
## [v0.3.2](https://github.com/napari/superqt/tree/v0.3.2) (2022-05-03)
[Full Changelog](https://github.com/napari/superqt/compare/v0.3.1...v0.3.2)
**Implemented enhancements:**
- Add QSearchableListWidget and QSearchableComboBox widgets [\#80](https://github.com/napari/superqt/pull/80) ([Czaki](https://github.com/Czaki))
**Fixed bugs:**
- Fix crazy animation loop on Qcollapsible [\#84](https://github.com/napari/superqt/pull/84) ([tlambert03](https://github.com/tlambert03))
- Reorder label update signal [\#83](https://github.com/napari/superqt/pull/83) ([tlambert03](https://github.com/tlambert03))
- Fix height of expanded QCollapsible when child changes size [\#72](https://github.com/napari/superqt/pull/72) ([tlambert03](https://github.com/tlambert03))
**Tests & CI:**
- Fix deprecation warnings in tests [\#82](https://github.com/napari/superqt/pull/82) ([tlambert03](https://github.com/tlambert03))
**Merged pull requests:**
- Add changelog for v0.3.2 [\#86](https://github.com/napari/superqt/pull/86) ([tlambert03](https://github.com/tlambert03))
## [v0.3.1](https://github.com/napari/superqt/tree/v0.3.1) (2022-03-02)
[Full Changelog](https://github.com/napari/superqt/compare/v0.3.0...v0.3.1)
**Implemented enhancements:**
- Add `signals_blocked` util [\#69](https://github.com/napari/superqt/pull/69) ([tlambert03](https://github.com/tlambert03))
**Fixed bugs:**
- put SignalInstance in TYPE\_CHECKING clause, check min requirements [\#70](https://github.com/napari/superqt/pull/70) ([tlambert03](https://github.com/tlambert03))
**Merged pull requests:**
- Add changelog for v0.3.1 [\#71](https://github.com/napari/superqt/pull/71) ([tlambert03](https://github.com/tlambert03))
## [v0.3.0](https://github.com/napari/superqt/tree/v0.3.0) (2022-02-16)
[Full Changelog](https://github.com/napari/superqt/compare/v0.2.5-1...v0.3.0)
@@ -25,6 +112,10 @@
- Use qtpy, deprecate superqt.qtcompat, drop support for Qt \<5.12 [\#39](https://github.com/napari/superqt/pull/39) ([tlambert03](https://github.com/tlambert03))
**Merged pull requests:**
- Add changelog for v0.3.0 [\#68](https://github.com/napari/superqt/pull/68) ([tlambert03](https://github.com/tlambert03))
## [v0.2.5-1](https://github.com/napari/superqt/tree/v0.2.5-1) (2021-11-23)
[Full Changelog](https://github.com/napari/superqt/compare/v0.2.5...v0.2.5-1)
@@ -101,13 +192,17 @@
## [v0.2.1](https://github.com/napari/superqt/tree/v0.2.1) (2021-07-10)
[Full Changelog](https://github.com/napari/superqt/compare/v0.2.0rc0...v0.2.1)
[Full Changelog](https://github.com/napari/superqt/compare/v0.2.0rc1...v0.2.1)
**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))
## [v0.2.0rc1](https://github.com/napari/superqt/tree/v0.2.0rc1) (2021-06-26)
[Full Changelog](https://github.com/napari/superqt/compare/v0.2.0rc0...v0.2.0rc1)
## [v0.2.0rc0](https://github.com/napari/superqt/tree/v0.2.0rc0) (2021-06-26)
[Full Changelog](https://github.com/napari/superqt/compare/v0.2.0...v0.2.0rc0)

View File

@@ -61,3 +61,8 @@ combo.setEnumClass(SampleEnum, allow_none=True)
```
In this case there is added option `----` and `currentEnum` will return `None` for it.
## 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`.

8
docs/listwidgets.md Normal file
View File

@@ -0,0 +1,8 @@
# ListWidget
## 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.
Because of implementation it does not inherit directly from `QListWidget` but satisfy it all api. The only limitation is that it cannot be used as argument of `QListWidgetItem` constructor.

View File

@@ -10,7 +10,7 @@
- Supports mouse wheel and keypress (soon) events
- Supports more than 2 handles (e.g. `slider.setValue([0, 10, 60, 80])`)
------
*Note: There is a Qt5 Bug that affects sliders in MacOS 12+, see fix at bottom of page.*
## Range Slider
@@ -221,12 +221,6 @@ 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
@@ -235,3 +229,29 @@ just like QSlider, but supports float values
```python
from superqt import QDoubleSlider
```
## Issues
### MacOS Monterey Slider issue
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://github.com/napari/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)
```

10
docs/utils.md Normal file
View File

@@ -0,0 +1,10 @@
# Utils
## Code highlighting
`superqt` provides 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/).

View 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_()

View File

@@ -1,7 +1,12 @@
import os
from qtpy import QtCore
from qtpy import QtWidgets as QtW
from superqt import QRangeSlider
# patch for Qt 5.15 on macos >= 12
os.environ["USE_MAC_SLIDER_PATCH"] = "1"
from superqt import QRangeSlider # noqa
QSS = """
QSlider {

View 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_()

View 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_()

View File

@@ -35,8 +35,10 @@ project_urls =
[options]
packages = find:
install_requires =
qtpy
typing-extensions>=3.10.0.0
packaging
pygments>=2.4.0
qtpy>=1.1.0
typing-extensions
python_requires = >=3.7
include_package_data = True
package_dir =

View File

@@ -7,7 +7,8 @@ except ImportError:
from ._eliding_label import QElidingLabel
from .collapsible import QCollapsible
from .combobox import QEnumComboBox
from .combobox import QEnumComboBox, QSearchableComboBox
from .selection import QSearchableListWidget
from .sliders import (
QDoubleRangeSlider,
QDoubleSlider,
@@ -26,13 +27,15 @@ __all__ = [
"QDoubleRangeSlider",
"QDoubleSlider",
"QElidingLabel",
"QEnumComboBox",
"QLabeledDoubleRangeSlider",
"QLabeledDoubleSlider",
"QLabeledRangeSlider",
"QLabeledSlider",
"QLargeIntSpinBox",
"QMessageHandler",
"QSearchableComboBox",
"QSearchableListWidget",
"QRangeSlider",
"QEnumComboBox",
"QCollapsible",
]

View File

@@ -1,13 +1,7 @@
"""A collapsible widget to hide and unhide child widgets"""
from typing import Optional
from qtpy.QtCore import (
QAbstractAnimation,
QEasingCurve,
QMargins,
QPropertyAnimation,
Qt,
)
from qtpy.QtCore import QEasingCurve, QEvent, QMargins, QObject, QPropertyAnimation, Qt
from qtpy.QtWidgets import QFrame, QPushButton, QVBoxLayout, QWidget
@@ -23,10 +17,11 @@ class QCollapsible(QFrame):
def __init__(self, title: str = "", parent: Optional[QWidget] = None):
super().__init__(parent)
self._locked = False
self._is_animating = False
self._toggle_btn = QPushButton(self._COLLAPSED + title)
self._toggle_btn.setCheckable(True)
self._toggle_btn.setStyleSheet("text-align: left; background: transparent;")
self._toggle_btn.setStyleSheet("text-align: left; border: none; outline: none;")
self._toggle_btn.toggled.connect(self._toggle)
# frame layout
@@ -38,6 +33,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)
@@ -77,19 +73,21 @@ class QCollapsible(QFrame):
def addWidget(self, widget: QWidget):
"""Add a widget to the central content widget's layout."""
widget.installEventFilter(self)
self._content.layout().addWidget(widget)
def removeWidget(self, widget: QWidget):
"""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)
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)
self._expand_collapse(QPropertyAnimation.Direction.Backward, animate)
def isExpanded(self) -> bool:
"""Return whether the collapsible section is visible"""
@@ -105,12 +103,12 @@ class QCollapsible(QFrame):
return self._locked
def _expand_collapse(
self, direction: QAbstractAnimation.Direction, animate: bool = True
self, direction: QPropertyAnimation.Direction, animate: bool = True
):
if self._locked:
return
forward = direction == QAbstractAnimation.Direction.Forward
forward = direction == QPropertyAnimation.Direction.Forward
text = self._EXPANDED if forward else self._COLLAPSED
self._toggle_btn.setChecked(forward)
@@ -120,9 +118,23 @@ class QCollapsible(QFrame):
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)
def _toggle(self):
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)
return False
def _on_animation_done(self):
self._is_animating = False

View File

@@ -1,3 +1,4 @@
from ._enum_combobox import QEnumComboBox
from ._searchable_combo_box import QSearchableComboBox
__all__ = ("QEnumComboBox",)
__all__ = ("QEnumComboBox", "QSearchableComboBox")

View File

@@ -12,7 +12,10 @@ 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":
if (
enum_value.__str__.__module__ != "enum"
and not enum_value.__str__.__module__.startswith("shibokensupport")
):
# check if function was overloaded
name = str(enum_value)
else:

View File

@@ -0,0 +1,48 @@
from qtpy import QT_VERSION
from qtpy.QtCore import Qt, Signal
from qtpy.QtWidgets import QComboBox, QCompleter
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=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())

View File

@@ -17,7 +17,12 @@ class FontIconManager:
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

View File

@@ -243,7 +243,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)

View File

@@ -0,0 +1,3 @@
from ._searchable_list_widget import QSearchableListWidget
__all__ = ("QSearchableListWidget",)

View File

@@ -0,0 +1,46 @@
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QLineEdit, QListWidget, QVBoxLayout, QWidget
class QSearchableListWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.list_widget = QListWidget()
self.filter_widget = QLineEdit()
self.filter_widget.textChanged.connect(self.update_visible)
layout = QVBoxLayout()
layout.addWidget(self.filter_widget)
layout.addWidget(self.list_widget)
self.setLayout(layout)
def __getattr__(self, item):
if hasattr(self.list_widget, item):
return getattr(self.list_widget, item)
return super().__getattr__(item)
def update_visible(self, text):
items_text = [
x.text() for x in self.list_widget.findItems(text, Qt.MatchContains)
]
for index in range(self.list_widget.count()):
item = self.item(index)
item.setHidden(item.text() not in items_text)
def addItems(self, *args):
self.list_widget.addItems(*args)
self.update_visible(self.filter_widget.text())
def addItem(self, *args):
self.list_widget.addItem(*args)
self.update_visible(self.filter_widget.text())
def insertItems(self, *args):
self.list_widget.insertItems(*args)
self.update_visible(self.filter_widget.text())
def insertItem(self, *args):
self.list_widget.insertItem(*args)
self.update_visible(self.filter_widget.text())

View File

@@ -4,6 +4,7 @@ from ._labeled import (
QLabeledRangeSlider,
QLabeledSlider,
)
from ._range_style import MONTEREY_SLIDER_STYLES_FIX
from ._sliders import QDoubleRangeSlider, QDoubleSlider, QRangeSlider
__all__ = [
@@ -14,4 +15,5 @@ __all__ = [
"QLabeledRangeSlider",
"QLabeledSlider",
"QRangeSlider",
"MONTEREY_SLIDER_STYLES_FIX",
]

View File

@@ -5,7 +5,11 @@ from qtpy.QtCore import Property, QEvent, QPoint, QPointF, QRect, QRectF, Qt, Si
from qtpy.QtWidgets import QSlider, QStyle, QStyleOptionSlider, QStylePainter
from ._generic_slider import CC_SLIDER, SC_GROOVE, SC_HANDLE, SC_NONE, _GenericSlider
from ._range_style import RangeSliderStyle, update_styles_from_stylesheet
from ._range_style import (
MONTEREY_SLIDER_STYLES_FIX,
RangeSliderStyle,
update_styles_from_stylesheet,
)
_T = TypeVar("_T")
@@ -32,6 +36,8 @@ class _GenericRangeSlider(_GenericSlider[Tuple], Generic[_T]):
_slidersMoved = Signal(tuple)
def __init__(self, *args, **kwargs):
self._style = RangeSliderStyle()
super().__init__(*args, **kwargs)
self.valueChanged = self._valuesChanged
self.sliderMoved = self._slidersMoved
@@ -55,9 +61,7 @@ class _GenericRangeSlider(_GenericSlider[Tuple], Generic[_T]):
# color
self._style = RangeSliderStyle()
self.setStyleSheet("")
update_styles_from_stylesheet(self)
# ############### New Public API #######################
@@ -97,6 +101,10 @@ class _GenericRangeSlider(_GenericSlider[Tuple], Generic[_T]):
def showBar(self) -> None:
self.setBarVisible(True)
def applyMacStylePatch(self) -> str:
super().applyMacStylePatch()
self._style._macpatch = True
# ############### QtOverrides #######################
def value(self) -> Tuple[_T, ...]:
@@ -131,12 +139,21 @@ class _GenericRangeSlider(_GenericSlider[Tuple], Generic[_T]):
self._doSliderMove()
def setStyleSheet(self, styleSheet: str) -> None:
return super().setStyleSheet(self._patch_style(styleSheet))
def _patch_style(self, style: str):
"""Override to patch style options before painting."""
# sub-page styles render on top of the lower sliders and don't work here.
if self._style._macpatch and not style:
style = MONTEREY_SLIDER_STYLES_FIX
override = f"""
\n{type(self).__name__}::sub-page:horizontal {{background: none}}
\n{type(self).__name__}::sub-page:vertical {{background: none}}
\n{type(self).__name__}::sub-page:horizontal
{{background: none; border: none}}
\n{type(self).__name__}::add-page:vertical
{{background: none; border: none}}
"""
return super().setStyleSheet(styleSheet + override)
return style + override
def event(self, ev: QEvent) -> bool:
if ev.type() == QEvent.Type.StyleChange:
@@ -146,11 +163,17 @@ class _GenericRangeSlider(_GenericSlider[Tuple], Generic[_T]):
def mouseMoveEvent(self, ev: QtGui.QMouseEvent) -> None:
if self._pressedControl == SC_BAR:
ev.accept()
delta = self._clickOffset - self._pixelPosToRangeValue(self._pick(ev.pos()))
delta = self._clickOffset - self._pixelPosToRangeValue(
self._pick(self._event_position(ev))
)
self._offsetAllPositions(-delta, self._sldPosAtPress)
else:
super().mouseMoveEvent(ev)
def _event_position(self, event):
# API changes between PyQt5 (.pos()) and PyQt6 (.position())
return event.pos() if hasattr(event, "pos") else event.position()
# ############### Implementation Details #######################
def _setPosition(self, val):

View File

@@ -19,10 +19,11 @@ So that's what `_GenericSlider` is below.
scalar (with one handle per item), and it forms the basis of
QRangeSlider.
"""
import os
import platform
from typing import Generic, TypeVar
from qtpy import QtGui
from qtpy import QT_VERSION, QtGui
from qtpy.QtCore import QEvent, QPoint, QPointF, QRect, Qt, Signal
from qtpy.QtWidgets import (
QApplication,
@@ -32,6 +33,8 @@ from qtpy.QtWidgets import (
QStylePainter,
)
from ._range_style import MONTEREY_SLIDER_STYLES_FIX
_T = TypeVar("_T")
SC_NONE = QStyle.SubControl.SC_None
@@ -42,11 +45,23 @@ SC_TICKMARKS = QStyle.SubControl.SC_SliderTickmarks
CC_SLIDER = QStyle.ComplexControl.CC_Slider
QOVERFLOW = 2**31 - 1
# whether to use the MONTEREY_SLIDER_STYLES_FIX QSS hack
# for fixing sliders on macos>=12 with QT < 6
# https://bugreports.qt.io/browse/QTBUG-98093
# https://github.com/napari/superqt/issues/74
USE_MAC_SLIDER_PATCH = (
QT_VERSION
and int(QT_VERSION.split(".")[0]) < 6
and platform.system() == "Darwin"
and int(platform.mac_ver()[0].split(".", maxsplit=1)[0]) >= 12
and os.getenv("USE_MAC_SLIDER_PATCH", "0") not in ("0", "False", "false")
)
class _GenericSlider(QSlider, Generic[_T]):
_fvalueChanged = Signal(float)
_fsliderMoved = Signal(float)
_frangeChanged = Signal(float, float)
_fvalueChanged = Signal(int)
_fsliderMoved = Signal(int)
_frangeChanged = Signal(int, int)
MAX_DISPLAY = 5000
@@ -79,6 +94,12 @@ class _GenericSlider(QSlider, Generic[_T]):
self.rangeChanged = self._frangeChanged
self.setAttribute(Qt.WidgetAttribute.WA_Hover)
self.setStyleSheet("")
if USE_MAC_SLIDER_PATCH:
self.applyMacStylePatch()
def applyMacStylePatch(self) -> str:
self.setStyleSheet(MONTEREY_SLIDER_STYLES_FIX)
# ############### QtOverrides #######################
@@ -134,8 +155,8 @@ class _GenericSlider(QSlider, Generic[_T]):
self.setRange(min(self._minimum, max), max)
def setRange(self, min: float, max_: float) -> None:
oldMin, self._minimum = self._minimum, float(min)
oldMax, self._maximum = self._maximum, float(max(min, max_))
oldMin, self._minimum = self._minimum, self._type_cast(min)
oldMax, self._maximum = self._maximum, self._type_cast(max(min, max_))
if oldMin != self._minimum or oldMax != self._maximum:
self.sliderChange(self.SliderChange.SliderRangeChange)
@@ -272,6 +293,27 @@ class _GenericSlider(QSlider, Generic[_T]):
opt.subControls |= SC_TICKMARKS
painter.drawComplexControl(CC_SLIDER, opt)
if (
opt.tickPosition != QSlider.TickPosition.NoTicks
and "MONTEREY_SLIDER_STYLES_FIX" in self.styleSheet()
):
# draw tick marks manually because they are badly behaved with style sheets
interval = opt.tickInterval or int(self._pageStep)
_range = self._maximum - self._minimum
nticks = (_range + interval) // interval
painter.setPen(QtGui.QColor("#C7C7C7"))
half_height = 3
for i in range(int(nticks)):
if self.orientation() == Qt.Orientation.Vertical:
y = int((self.height() - 8) * i / (nticks - 1)) + 1
x = self.rect().center().x()
painter.drawRect(x - half_height, y, 6, 1)
else:
x = int((self.width() - 3) * i / (nticks - 1)) + 1
y = self.rect().center().y()
painter.drawRect(x, y - half_height, 1, 6)
self._draw_handle(painter, opt)
# ############### Implementation Details #######################

View File

@@ -17,6 +17,7 @@ from qtpy.QtWidgets import (
QWidget,
)
from ..utils import signals_blocked
from ._sliders import QDoubleRangeSlider, QDoubleSlider, QRangeSlider
@@ -118,6 +119,8 @@ def _handle_overloaded_slider_sig(args, kwargs):
class QLabeledSlider(_SliderProxy, QAbstractSlider):
editingFinished = Signal()
EdgeLabelMode = EdgeLabelMode
_slider_class = QSlider
_slider: QSlider
@@ -128,7 +131,7 @@ class QLabeledSlider(_SliderProxy, QAbstractSlider):
super().__init__(parent)
self._slider = self._slider_class()
self._label = SliderLabel(self._slider, connect=self._slider.setValue)
self._label = SliderLabel(self._slider, connect=self._setValue)
self._edge_label_mode: EdgeLabelMode = EdgeLabelMode.LabelIsValue
self._rename_signals()
@@ -137,11 +140,19 @@ class QLabeledSlider(_SliderProxy, QAbstractSlider):
self._slider.sliderMoved.connect(self.sliderMoved.emit)
self._slider.sliderPressed.connect(self.sliderPressed.emit)
self._slider.sliderReleased.connect(self.sliderReleased.emit)
self._slider.valueChanged.connect(self.valueChanged.emit)
self._slider.valueChanged.connect(self._label.setValue)
self._slider.valueChanged.connect(self.valueChanged.emit)
self._label.editingFinished.connect(self.editingFinished)
self.setOrientation(orientation)
def _setValue(self, value: float):
"""
Convert the value from float to int before
setting the slider value
"""
self._slider.setValue(int(value))
def _rename_signals(self):
# for subclasses
pass
@@ -223,6 +234,8 @@ class QLabeledDoubleSlider(QLabeledSlider):
class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
_valueChanged = Signal(tuple)
editingFinished = Signal()
LabelPosition = LabelPosition
EdgeLabelMode = EdgeLabelMode
_slider_class = QRangeSlider
@@ -255,6 +268,8 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
alignment=Qt.AlignmentFlag.AlignRight,
connect=self._max_label_edited,
)
self._min_label.editingFinished.connect(self.editingFinished)
self._max_label.editingFinished.connect(self.editingFinished)
self.setEdgeLabelMode(EdgeLabelMode.LabelIsRange)
self._slider.valueChanged.connect(self._on_value_changed)
@@ -304,7 +319,10 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
self._reposition_labels()
def _reposition_labels(self):
if not self._handle_labels:
if (
not self._handle_labels
or self._handle_label_position == LabelPosition.NoLabel
):
return
horizontal = self.orientation() == Qt.Orientation.Horizontal
@@ -336,6 +354,7 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
label.move(pos)
last_edge = pos
label.clearFocus()
label.show()
self.update()
def _min_label_edited(self, val):
@@ -369,6 +388,7 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
for n, val in enumerate(self._slider.value()):
_cb = partial(self._slider.setSliderPosition, index=n)
s = SliderLabel(self._slider, parent=self, connect=_cb)
s.editingFinished.connect(self.editingFinished)
s.setValue(val)
self._handle_labels.append(s)
else:
@@ -484,9 +504,13 @@ class SliderLabel(QDoubleSpinBox):
self.setStyleSheet("background:transparent; border: 0;")
if connect is not None:
self.editingFinished.connect(lambda: connect(self.value()))
self.editingFinished.connect(self.clearFocus)
self.editingFinished.connect(self._silent_clear_focus)
self._update_size()
def _silent_clear_focus(self):
with signals_blocked(self):
self.clearFocus()
def setDecimals(self, prec: int) -> None:
super().setDecimals(prec)
self._update_size()

View File

@@ -36,6 +36,7 @@ class RangeSliderStyle:
v_offset: float | None = None
h_offset: float | None = None
has_stylesheet: bool = False
_macpatch: bool = False
def brush(self, opt: QStyleOptionSlider) -> QBrush:
cg = opt.palette.currentColorGroup()
@@ -86,15 +87,15 @@ class RangeSliderStyle:
val = QColor(val)
if opt.tickPosition != QSlider.TickPosition.NoTicks:
val.setAlphaF(self.tick_bar_alpha or SYSTEM_STYLE.tick_bar_alpha)
return val
def offset(self, opt: QStyleOptionSlider) -> int:
tp = opt.tickPosition
off = 0
if not self.has_stylesheet:
tp = opt.tickPosition
if opt.orientation == Qt.Orientation.Horizontal:
off += self.h_offset or SYSTEM_STYLE.h_offset or 0
if not self._macpatch:
off += self.h_offset or SYSTEM_STYLE.h_offset or 0
else:
off += self.v_offset or SYSTEM_STYLE.v_offset or 0
if tp == QSlider.TickPosition.TicksAbove:
@@ -259,7 +260,8 @@ def parse_color(color: str, default_attr) -> QColor | QGradient:
def update_styles_from_stylesheet(obj: _GenericRangeSlider):
qss = obj.styleSheet()
qss: str = obj.styleSheet()
parent = obj.parent()
while parent is not None:
@@ -268,6 +270,11 @@ def update_styles_from_stylesheet(obj: _GenericRangeSlider):
qss = QApplication.instance().styleSheet() + qss
if not qss:
return
if MONTEREY_SLIDER_STYLES_FIX in qss:
qss = qss.replace(MONTEREY_SLIDER_STYLES_FIX, "")
obj._style._macpatch = True
else:
obj._style._macpatch = False
# Find bar height/width
for orient, dim in (("horizontal", "height"), ("vertical", "width")):
@@ -279,3 +286,56 @@ def update_styles_from_stylesheet(obj: _GenericRangeSlider):
thickness = float(bgrd.groups()[-1])
setattr(obj._style, f"{orient}_thickness", thickness)
obj._style.has_stylesheet = True
# a fix for https://bugreports.qt.io/browse/QTBUG-98093
MONTEREY_SLIDER_STYLES_FIX = """
/* MONTEREY_SLIDER_STYLES_FIX */
QSlider::groove {
background: #DFDFDF;
border: 1px solid #DBDBDB;
border-radius: 2px;
}
QSlider::groove:horizontal {
height: 2px;
margin: 2px;
}
QSlider::groove:vertical {
width: 2px;
margin: 2px 0 6px 0;
}
QSlider::handle {
background: white;
border: 0.5px solid #DADADA;
width: 19.5px;
height: 19.5px;
border-radius: 10.5px;
}
QSlider::handle:horizontal {
margin: -10px -2px;
}
QSlider::handle:vertical {
margin: -2px -10px;
}
QSlider::handle:pressed {
background: #F0F0F0;
}
QSlider::sub-page:horizontal {
background: #0981FE;
border-radius: 2px;
margin: 2px;
height: 2px;
}
QSlider::add-page:vertical {
background: #0981FE;
border-radius: 2px;
margin: 2px 0 6px 0;
width: 2px;
}
""".strip()

View File

@@ -14,6 +14,10 @@ class _IntMixin:
class _FloatMixin:
_fvalueChanged = Signal(float)
_fsliderMoved = Signal(float)
_frangeChanged = Signal(float, float)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._singleStep = 0.01

View File

@@ -1,21 +1,25 @@
__all__ = (
"CodeSyntaxHighlight",
"create_worker",
"ensure_main_thread",
"ensure_object_thread",
"FunctionWorker",
"GeneratorWorker",
"new_worker_qthread",
"QMessageHandler",
"thread_worker",
"WorkerBase",
"qthrottled",
"qdebounced",
"QMessageHandler",
"QSignalDebouncer",
"QSignalThrottler",
"qthrottled",
"signals_blocked",
"thread_worker",
"WorkerBase",
)
from ._code_syntax_highlight import CodeSyntaxHighlight
from ._ensure_thread import ensure_main_thread, ensure_object_thread
from ._message_handler import QMessageHandler
from ._misc import signals_blocked
from ._qthreading import (
FunctionWorker,
GeneratorWorker,

View File

@@ -0,0 +1,93 @@
from itertools import takewhile
from pygments import highlight
from pygments.formatter import Formatter
from pygments.lexers import find_lexer_class, get_lexer_by_name
from pygments.util import ClassNotFound
from qtpy import QtGui
# inspired by https://github.com/Vector35/snippets/blob/master/QCodeEditor.py (MIT license) and
# https://pygments.org/docs/formatterdevelopment/#html-3-2-formatter
def get_text_char_format(style):
"""
Return a QTextCharFormat with the given attributes.
https://pygments.org/docs/formatterdevelopment/#html-3-2-formatter
"""
text_char_format = QtGui.QTextCharFormat()
text_char_format.setFontFamily("monospace")
if style.get("color"):
text_char_format.setForeground(QtGui.QColor(f"#{style['color']}"))
if style.get("bgcolor"):
text_char_format.setBackground(QtGui.QColor(style["bgcolor"]))
if style.get("bold"):
text_char_format.setFontWeight(QtGui.QFont.Bold)
if style.get("italic"):
text_char_format.setFontItalic(True)
if style.get("underline"):
text_char_format.setFontUnderline(True)
# TODO find if it is possible to support border style.
return text_char_format
class QFormatter(Formatter):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.data = []
self._style = {name: get_text_char_format(style) for name, style in self.style}
def format(self, tokensource, outfile):
"""
`outfile` is argument from parent class, but
in Qt we do not produce string output, but QTextCharFormat, so it needs to be
collected using `self.data`.
"""
self.data = []
for token, value in tokensource:
self.data.extend(
[
self._style[token],
]
* len(value)
)
class CodeSyntaxHighlight(QtGui.QSyntaxHighlighter):
def __init__(self, parent, lang, theme):
super().__init__(parent)
self.formatter = QFormatter(style=theme)
try:
self.lexer = get_lexer_by_name(lang)
except ClassNotFound:
self.lexer = find_lexer_class(lang)()
@property
def background_color(self):
return self.formatter.style.background_color
def highlightBlock(self, text):
cb = self.currentBlock()
p = cb.position()
text_ = self.document().toPlainText() + "\n"
highlight(text_, self.lexer, self.formatter)
enters = sum(1 for _ in takewhile(lambda x: x == "\n", text_))
# pygments lexer ignore leading empty lines, so we need to do correction
# here calculating the number of empty lines.
# dirty, dirty hack
# The core problem is that pygemnts by default use string streams,
# that will not handle QTextCharFormat, so wee need use `data` property to work around this.
for i in range(len(text)):
try:
self.setFormat(i, 1, self.formatter.data[p + i - enters])
except IndexError: # pragma: no cover
pass

View File

@@ -0,0 +1,15 @@
from contextlib import contextmanager
from typing import TYPE_CHECKING, Iterator
if TYPE_CHECKING:
from qtpy.QtCore import QObject
@contextmanager
def signals_blocked(obj: "QObject") -> Iterator[None]:
"""Context manager to temporarily block signals emitted by QObject: `obj`."""
previous = obj.blockSignals(True)
try:
yield
finally:
obj.blockSignals(previous)

View File

@@ -21,10 +21,8 @@ from typing import (
)
from qtpy.QtCore import QObject, QRunnable, QThread, QThreadPool, QTimer, Signal
from typing_extensions import Literal, ParamSpec
if TYPE_CHECKING:
_T = TypeVar("_T")
class SigInst(Generic[_T]):
@@ -40,11 +38,21 @@ if TYPE_CHECKING:
def emit(*args: _T) -> None:
...
from typing_extensions import Literal, ParamSpec
_P = ParamSpec("_P")
# maintain runtime compatibility with older typing_extensions
else:
try:
from typing_extensions import ParamSpec
_P = ParamSpec("_P")
except ImportError:
_P = TypeVar("_P")
_Y = TypeVar("_Y")
_S = TypeVar("_S")
_R = TypeVar("_R")
_P = ParamSpec("_P")
def as_generator_function(

View File

@@ -32,8 +32,23 @@ from enum import IntFlag, auto
from functools import wraps
from typing import TYPE_CHECKING, Callable, Generic, Optional, TypeVar, Union, overload
from qtpy.QtCore import QObject, Qt, QTimer, Signal, SignalInstance
from typing_extensions import Literal, ParamSpec
from qtpy.QtCore import QObject, Qt, QTimer, Signal
if TYPE_CHECKING:
from qtpy.QtCore import SignalInstance
from typing_extensions import Literal, ParamSpec
P = ParamSpec("P")
# maintain runtime compatibility with older typing_extensions
else:
try:
from typing_extensions import ParamSpec
P = ParamSpec("P")
except ImportError:
P = TypeVar("P")
R = TypeVar("R")
class Kind(IntFlag):
@@ -176,14 +191,12 @@ class QSignalDebouncer(GenericSignalThrottler):
# below here part is unique to superqt (not from KD)
P = ParamSpec("P")
R = TypeVar("R")
if TYPE_CHECKING:
from typing_extensions import Protocol
class ThrottledCallable(Generic[P, R], Protocol):
triggered: SignalInstance
triggered: "SignalInstance"
def cancel(self) -> None:
...
@@ -196,12 +209,12 @@ if TYPE_CHECKING:
if sys.version_info < (3, 9):
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Future:
def __call__(self, *args: "P.args", **kwargs: "P.kwargs") -> Future:
...
else:
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Future[R]:
def __call__(self, *args: "P.args", **kwargs: "P.kwargs") -> Future[R]:
...
@@ -217,7 +230,7 @@ def qthrottled(
@overload
def qthrottled(
func: Literal[None] = None,
func: "Literal[None]" = None,
timeout: int = 100,
leading: bool = True,
timer_type: Qt.TimerType = Qt.TimerType.PreciseTimer,
@@ -276,7 +289,7 @@ def qdebounced(
@overload
def qdebounced(
func: Literal[None] = None,
func: "Literal[None]" = None,
timeout: int = 100,
leading: bool = False,
timer_type: Qt.TimerType = Qt.TimerType.PreciseTimer,
@@ -344,7 +357,7 @@ def _make_decorator(
future: Optional[Future] = None
@wraps(func)
def inner(*args: P.args, **kwargs: P.kwargs) -> Future:
def inner(*args: "P.args", **kwargs: "P.kwargs") -> Future:
nonlocal last_f
nonlocal future
if last_f is not None:

View File

@@ -0,0 +1,19 @@
from qtpy.QtWidgets import QTextEdit
from superqt.utils import CodeSyntaxHighlight
def test_code_highlight(qtbot):
widget = QTextEdit()
qtbot.addWidget(widget)
code_highlight = CodeSyntaxHighlight(widget, "python", "default")
assert code_highlight.background_color == "#f8f8f8"
widget.setText("from argparse import ArgumentParser")
def test_code_highlight_by_name(qtbot):
widget = QTextEdit()
qtbot.addWidget(widget)
code_highlight = CodeSyntaxHighlight(widget, "Python Traceback", "monokai")
assert code_highlight.background_color == "#272822"
widget.setText("from argparse import ArgumentParser")

View File

@@ -0,0 +1,3 @@
Metadata-Version: 2.1
Name: fake-plugin
Version: 5.15.4

View File

@@ -0,0 +1,2 @@
[superqt.fonticon]
ico = fake_plugin:ICO

View File

@@ -0,0 +1 @@
fake_plugin

View File

@@ -0,0 +1,6 @@
from pathlib import Path
class ICO:
__font_file__ = str(Path(__file__).parent / "icontest.ttf")
smiley = "ico.\ue900"

View File

@@ -11,7 +11,7 @@ TEST_PREFIX = "ico"
TEST_CHARNAME = "smiley"
TEST_CHAR = "\ue900"
TEST_GLYPHKEY = f"{TEST_PREFIX}.{TEST_CHARNAME}"
FONT_FILE = Path(__file__).parent / "icontest.ttf"
FONT_FILE = Path(__file__).parent / "fixtures" / "fake_plugin" / "icontest.ttf"
@pytest.fixture

View File

@@ -7,41 +7,15 @@ from qtpy.QtGui import QIcon, QPixmap
from superqt.fonticon import _plugins, icon
from superqt.fonticon._qfont_icon import QFontIconStore
try:
from importlib.metadata import Distribution
except ImportError:
from importlib_metadata import Distribution # type: ignore
class ICO:
__font_file__ = str(Path(__file__).parent / "icontest.ttf")
smiley = "ico.\ue900"
FIXTURES = Path(__file__).parent / "fixtures"
@pytest.fixture
def plugin_store(qapp, monkeypatch):
class MockEntryPoint:
name = "ico"
group = _plugins.FontIconManager.ENTRY_POINT
value = "fake_plugin.ICO"
def load(self):
return ICO
class MockFinder:
def find_distributions(self, *a):
class D(Distribution):
name = "mock"
@property
def entry_points(self):
return [MockEntryPoint()]
return [D()]
_path = [str(FIXTURES)] + sys.path.copy()
store = QFontIconStore().instance()
with monkeypatch.context() as m:
m.setattr(sys, "meta_path", [MockFinder()])
m.setattr(sys, "path", _path)
yield store
store.clear()

View File

@@ -0,0 +1,35 @@
from superqt import QSearchableComboBox
class TestSearchableComboBox:
def test_constructor(self, qtbot):
widget = QSearchableComboBox()
qtbot.addWidget(widget)
def test_add_items(self, qtbot):
widget = QSearchableComboBox()
qtbot.addWidget(widget)
widget.addItems(["foo", "bar"])
assert widget.completer_object.model().rowCount() == 2
widget.addItem("foobar")
assert widget.completer_object.model().rowCount() == 3
widget.insertItem(1, "baz")
assert widget.completer_object.model().rowCount() == 4
widget.insertItems(2, ["bazbar", "foobaz"])
assert widget.completer_object.model().rowCount() == 6
assert widget.itemText(0) == "foo"
assert widget.itemText(1) == "baz"
assert widget.itemText(2) == "bazbar"
def test_completion(self, qtbot):
widget = QSearchableComboBox()
qtbot.addWidget(widget)
widget.addItems(["foo", "bar", "foobar", "baz", "bazbar", "foobaz"])
widget.completer_object.setCompletionPrefix("fo")
assert widget.completer_object.completionCount() == 3
assert widget.completer_object.currentCompletion() == "foo"
widget.completer_object.setCurrentRow(1)
assert widget.completer_object.currentCompletion() == "foobar"
widget.completer_object.setCurrentRow(2)
assert widget.completer_object.currentCompletion() == "foobaz"

View File

@@ -0,0 +1,34 @@
from superqt import QSearchableListWidget
class TestSearchableListWidget:
def test_create(self, qtbot):
widget = QSearchableListWidget()
qtbot.addWidget(widget)
widget.addItem("aaa")
assert widget.count() == 1
def test_add_items(self, qtbot):
widget = QSearchableListWidget()
qtbot.addWidget(widget)
widget.addItems(["foo", "bar"])
assert widget.count() == 2
widget.insertItems(1, ["baz", "foobaz"])
widget.insertItem(2, "foobar")
assert widget.count() == 5
assert widget.item(0).text() == "foo"
assert widget.item(1).text() == "baz"
assert widget.item(2).text() == "foobar"
def test_completion(self, qtbot):
widget = QSearchableListWidget()
qtbot.addWidget(widget)
widget.show()
widget.addItems(["foo", "bar", "foobar", "baz", "bazbar", "foobaz"])
widget.filter_widget.setText("fo")
assert widget.count() == 6
for i in range(widget.count()):
item = widget.item(i)
assert item.isHidden() == ("fo" not in item.text())
widget.hide()

View File

@@ -4,7 +4,7 @@ from platform import system
import pytest
from qtpy import QT_VERSION
from qtpy.QtCore import QEvent, QPoint, QPointF, Qt
from qtpy.QtGui import QMouseEvent, QWheelEvent
from qtpy.QtGui import QHoverEvent, QMouseEvent, QWheelEvent
QT_VERSION = tuple(int(x) for x in QT_VERSION.split("."))
@@ -68,7 +68,18 @@ def _wheel_event(arc):
)
def _linspace(start, stop, n):
def _hover_event(_type, position, old_position, widget=None):
with suppress(TypeError):
return QHoverEvent(
_type,
position,
widget.mapToGlobal(position),
old_position,
)
return QHoverEvent(_type, position, old_position)
def _linspace(start: int, stop: int, n: int):
h = (stop - start) / (n - 1)
for i in range(n):
yield start + h * i

View File

@@ -1,7 +1,9 @@
import math
import os
import pytest
from qtpy import API_NAME
from qtpy.QtWidgets import QStyleOptionSlider
from superqt import (
QDoubleRangeSlider,
@@ -10,6 +12,8 @@ from superqt import (
QLabeledDoubleSlider,
)
from ._testutil import _linspace
range_types = {QDoubleRangeSlider, QLabeledDoubleRangeSlider}
@@ -122,3 +126,15 @@ def test_signals(ds, qtbot):
with qtbot.waitSignal(ds.rangeChanged):
ds.setRange(1.2, 3.3)
@pytest.mark.parametrize("mag", list(range(4, 37, 4)) + list(range(-4, -37, -4)))
def test_slider_extremes(mag, qtbot):
sld = QDoubleSlider()
_mag = 10**mag
with qtbot.waitSignal(sld.rangeChanged):
sld.setRange(-_mag, _mag)
for i in _linspace(-_mag, _mag, 10):
sld.setValue(i)
assert math.isclose(sld.value(), i, rel_tol=1e-8)
sld.initStyleOption(QStyleOptionSlider())

View File

@@ -3,12 +3,11 @@ import platform
import pytest
from qtpy.QtCore import QEvent, QPoint, QPointF, Qt
from qtpy.QtGui import QHoverEvent
from qtpy.QtWidgets import QStyle, QStyleOptionSlider
from superqt.sliders._generic_slider import _GenericSlider, _sliderValueFromPosition
from ._testutil import _linspace, _mouse_event, _wheel_event, skip_on_linux_qt6
from ._testutil import _hover_event, _mouse_event, _wheel_event, skip_on_linux_qt6
@pytest.fixture(params=[Qt.Orientation.Horizontal, Qt.Orientation.Vertical])
@@ -118,6 +117,7 @@ def test_press_move_release(gslider: _GenericSlider, qtbot):
@skip_on_linux_qt6
def test_hover(gslider: _GenericSlider):
# stub
opt = QStyleOptionSlider()
gslider.initStyleOption(opt)
style = gslider.style()
@@ -128,11 +128,11 @@ def test_hover(gslider: _GenericSlider):
assert gslider._hoverControl == QStyle.SubControl.SC_None
gslider.event(QHoverEvent(QEvent.Type.HoverEnter, handle_pos, QPointF()))
gslider.event(_hover_event(QEvent.Type.HoverEnter, handle_pos, QPointF(), gslider))
assert gslider._hoverControl == QStyle.SubControl.SC_SliderHandle
gslider.event(
QHoverEvent(QEvent.Type.HoverLeave, QPointF(-1000, -1000), handle_pos)
_hover_event(QEvent.Type.HoverLeave, QPointF(-1000, -1000), handle_pos, gslider)
)
assert gslider._hoverControl == QStyle.SubControl.SC_None
@@ -163,17 +163,6 @@ def test_steps(gslider: _GenericSlider, qtbot):
assert gslider.pageStep() == 1.5e30
@pytest.mark.parametrize("mag", list(range(4, 37, 4)) + list(range(-4, -37, -4)))
def test_slider_extremes(gslider: _GenericSlider, mag, qtbot):
_mag = 10**mag
with qtbot.waitSignal(gslider.rangeChanged):
gslider.setRange(-_mag, _mag)
for i in _linspace(-_mag, _mag, 10):
gslider.setValue(i)
assert math.isclose(gslider.value(), i, rel_tol=1e-8)
gslider.initStyleOption(QStyleOptionSlider())
# args are (min: float, max: float, position: int, span: int, upsideDown: bool)
@pytest.mark.parametrize(
"args, result",

View File

@@ -1,4 +1,10 @@
from superqt import QLabeledRangeSlider
import sys
from typing import Any, Iterable
from unittest.mock import Mock
import pytest
from superqt import QLabeledDoubleSlider, QLabeledRangeSlider, QLabeledSlider
def test_labeled_slider_api(qtbot):
@@ -9,3 +15,64 @@ def test_labeled_slider_api(qtbot):
slider.setBarVisible()
slider.setBarMovesAllHandles()
slider.setBarIsRigid()
def test_slider_connect_works(qtbot):
slider = QLabeledSlider()
qtbot.addWidget(slider)
slider._label.editingFinished.emit()
def _assert_types(args: Iterable[Any], type_: type):
# sourcery skip: comprehension-to-generator
if sys.version_info >= (3, 8):
assert all([isinstance(v, type_) for v in args]), "invalid type"
@pytest.mark.parametrize("cls", [QLabeledDoubleSlider, QLabeledSlider])
def test_labeled_signals(cls, qtbot):
gslider = cls()
qtbot.addWidget(gslider)
type_ = float if cls == QLabeledDoubleSlider else int
mock = Mock()
gslider.valueChanged.connect(mock)
with qtbot.waitSignal(gslider.valueChanged):
gslider.setValue(10)
mock.assert_called_once_with(10)
_assert_types(mock.call_args.args, type_)
mock = Mock()
gslider.rangeChanged.connect(mock)
with qtbot.waitSignal(gslider.rangeChanged):
gslider.setMinimum(3)
mock.assert_called_once_with(3, 99)
_assert_types(mock.call_args.args, type_)
mock.reset_mock()
with qtbot.waitSignal(gslider.rangeChanged):
gslider.setMaximum(15)
mock.assert_called_once_with(3, 15)
_assert_types(mock.call_args.args, type_)
mock.reset_mock()
with qtbot.waitSignal(gslider.rangeChanged):
gslider.setRange(1, 2)
mock.assert_called_once_with(1, 2)
_assert_types(mock.call_args.args, type_)
@pytest.mark.parametrize(
"cls", [QLabeledDoubleSlider, QLabeledRangeSlider, QLabeledSlider]
)
def test_editing_finished_signal(cls):
slider = cls()
mock = Mock()
slider.editingFinished.connect(mock)
if hasattr(slider, "_label"):
slider._label.editingFinished.emit()
else:
slider._min_label.editingFinished.emit()
mock.assert_called_once()

View File

@@ -1,169 +1,257 @@
import math
import sys
from itertools import product
from typing import Any, Iterable
from unittest.mock import Mock
import pytest
from qtpy.QtCore import QEvent, QPoint, QPointF, Qt
from qtpy.QtGui import QHoverEvent
from qtpy.QtWidgets import QStyle, QStyleOptionSlider
from superqt import QDoubleRangeSlider, QRangeSlider
from superqt import QDoubleRangeSlider, QLabeledRangeSlider, QRangeSlider
from ._testutil import _linspace, _mouse_event, _wheel_event, skip_on_linux_qt6
from ._testutil import (
_hover_event,
_linspace,
_mouse_event,
_wheel_event,
skip_on_linux_qt6,
)
ALL_SLIDER_COMBOS = list(
product(
[QDoubleRangeSlider, QRangeSlider, QLabeledRangeSlider],
[Qt.Orientation.Horizontal, Qt.Orientation.Vertical],
)
)
FLOAT_SLIDERS = [c for c in ALL_SLIDER_COMBOS if c[0] == QDoubleRangeSlider]
@pytest.fixture(params=[Qt.Orientation.Horizontal, Qt.Orientation.Vertical])
def gslider(qtbot, request):
slider = QDoubleRangeSlider(request.param)
qtbot.addWidget(slider)
@pytest.mark.parametrize("cls, orientation", ALL_SLIDER_COMBOS)
def test_slider_init(qtbot, cls, orientation):
slider = cls(orientation)
assert slider.value() == (20, 80)
assert slider.minimum() == 0
assert slider.maximum() == 99
yield slider
slider.initStyleOption(QStyleOptionSlider())
slider.show()
qtbot.addWidget(slider)
def test_change_floatslider_range(gslider: QRangeSlider, qtbot):
with qtbot.waitSignals([gslider.rangeChanged, gslider.valueChanged]):
gslider.setMinimum(30)
@pytest.mark.parametrize("cls, orientation", ALL_SLIDER_COMBOS)
def test_change_floatslider_range(cls, orientation, qtbot):
sld = cls(orientation)
qtbot.addWidget(sld)
assert gslider.value()[0] == 30 == gslider.minimum()
assert gslider.maximum() == 99
with qtbot.waitSignals([sld.rangeChanged, sld.valueChanged]):
sld.setMinimum(30)
with qtbot.waitSignal(gslider.rangeChanged):
gslider.setMaximum(70)
assert gslider.value()[0] == 30 == gslider.minimum()
assert gslider.value()[1] == 70 == gslider.maximum()
assert sld.value()[0] == 30 == sld.minimum()
assert sld.maximum() == 99
with qtbot.waitSignals([gslider.rangeChanged, gslider.valueChanged]):
gslider.setRange(40, 60)
assert gslider.value()[0] == 40 == gslider.minimum()
assert gslider.maximum() == 60
with qtbot.waitSignal(sld.rangeChanged):
sld.setMaximum(70)
assert sld.value()[0] == 30 == sld.minimum()
assert sld.value()[1] == 70 == sld.maximum()
with qtbot.waitSignal(gslider.valueChanged):
gslider.setValue([40, 50])
assert gslider.value()[0] == 40 == gslider.minimum()
assert gslider.value()[1] == 50
with qtbot.waitSignals([sld.rangeChanged, sld.valueChanged]):
sld.setRange(40, 60)
assert sld.value()[0] == 40 == sld.minimum()
assert sld.maximum() == 60
with qtbot.waitSignals([gslider.rangeChanged, gslider.valueChanged]):
gslider.setMaximum(45)
assert gslider.value()[0] == 40 == gslider.minimum()
assert gslider.value()[1] == 45 == gslider.maximum()
with qtbot.waitSignal(sld.valueChanged):
sld.setValue([40, 50])
assert sld.value()[0] == 40 == sld.minimum()
assert sld.value()[1] == 50
with qtbot.waitSignals([sld.rangeChanged, sld.valueChanged]):
sld.setMaximum(45)
assert sld.value()[0] == 40 == sld.minimum()
assert sld.value()[1] == 45 == sld.maximum()
def test_float_values(gslider: QRangeSlider, qtbot):
with qtbot.waitSignal(gslider.rangeChanged):
gslider.setRange(0.1, 0.9)
assert gslider.minimum() == 0.1
assert gslider.maximum() == 0.9
@pytest.mark.parametrize("cls, orientation", FLOAT_SLIDERS)
def test_float_values(cls, orientation, qtbot):
sld = cls(orientation)
qtbot.addWidget(sld)
with qtbot.waitSignal(gslider.valueChanged):
gslider.setValue([0.4, 0.6])
assert gslider.value() == (0.4, 0.6)
with qtbot.waitSignal(sld.rangeChanged):
sld.setRange(0.1, 0.9)
assert sld.minimum() == 0.1
assert sld.maximum() == 0.9
with qtbot.waitSignal(gslider.valueChanged):
gslider.setValue([0, 1.9])
assert gslider.value()[0] == 0.1 == gslider.minimum()
assert gslider.value()[1] == 0.9 == gslider.maximum()
with qtbot.waitSignal(sld.valueChanged):
sld.setValue([0.4, 0.6])
assert sld.value() == (0.4, 0.6)
with qtbot.waitSignal(sld.valueChanged):
sld.setValue([0, 1.9])
assert sld.value()[0] == 0.1 == sld.minimum()
assert sld.value()[1] == 0.9 == sld.maximum()
def test_position(gslider: QRangeSlider, qtbot):
gslider.setSliderPosition([10, 80])
assert gslider.sliderPosition() == (10, 80)
@pytest.mark.parametrize("cls, orientation", ALL_SLIDER_COMBOS)
def test_position(cls, orientation, qtbot):
sld = cls(orientation)
qtbot.addWidget(sld)
sld.setSliderPosition([10, 80])
assert sld.sliderPosition() == (10, 80)
def test_steps(gslider: QRangeSlider, qtbot):
gslider.setSingleStep(0.1)
assert gslider.singleStep() == 0.1
@pytest.mark.parametrize("cls, orientation", ALL_SLIDER_COMBOS)
def test_steps(cls, orientation, qtbot):
sld = cls(orientation)
qtbot.addWidget(sld)
gslider.setSingleStep(1.5e20)
assert gslider.singleStep() == 1.5e20
sld.setSingleStep(0.1)
assert sld.singleStep() == 0.1
gslider.setPageStep(0.2)
assert gslider.pageStep() == 0.2
sld.setSingleStep(1.5e20)
assert sld.singleStep() == 1.5e20
gslider.setPageStep(1.5e30)
assert gslider.pageStep() == 1.5e30
sld.setPageStep(0.2)
assert sld.pageStep() == 0.2
sld.setPageStep(1.5e30)
assert sld.pageStep() == 1.5e30
@pytest.mark.parametrize("mag", list(range(4, 37, 4)) + list(range(-4, -37, -4)))
def test_slider_extremes(gslider: QRangeSlider, mag, qtbot):
@pytest.mark.parametrize("cls, orientation", FLOAT_SLIDERS)
def test_slider_extremes(cls, orientation, qtbot, mag):
sld = cls(orientation)
qtbot.addWidget(sld)
_mag = 10**mag
with qtbot.waitSignal(gslider.rangeChanged):
gslider.setRange(-_mag, _mag)
with qtbot.waitSignal(sld.rangeChanged):
sld.setRange(-_mag, _mag)
for i in _linspace(-_mag, _mag, 10):
gslider.setValue((i, _mag))
assert math.isclose(gslider.value()[0], i, rel_tol=1e-8)
gslider.initStyleOption(QStyleOptionSlider())
sld.setValue((i, _mag))
assert math.isclose(sld.value()[0], i, rel_tol=0.0001)
sld.initStyleOption(QStyleOptionSlider())
def test_ticks(gslider: QRangeSlider, qtbot):
gslider.setTickInterval(0.3)
assert gslider.tickInterval() == 0.3
gslider.setTickPosition(gslider.TickPosition.TicksAbove)
gslider.show()
@pytest.mark.parametrize("cls, orientation", ALL_SLIDER_COMBOS)
def test_ticks(cls, orientation, qtbot):
sld = cls(orientation)
qtbot.addWidget(sld)
sld.setTickInterval(0.3)
assert sld.tickInterval() == 0.3
sld.setTickPosition(sld.TickPosition.TicksAbove)
sld.show()
def test_show(gslider, qtbot):
gslider.show()
@pytest.mark.parametrize("cls, orientation", FLOAT_SLIDERS)
def test_press_move_release(cls, orientation, qtbot):
sld = cls(orientation)
qtbot.addWidget(sld)
def test_press_move_release(gslider: QRangeSlider, qtbot):
# this fail on vertical came with pyside6.2 ... need to debug
# still works in practice, but test fails to catch signals
if gslider.orientation() == Qt.Orientation.Vertical:
if sld.orientation() == Qt.Orientation.Vertical:
pytest.xfail()
assert gslider._pressedControl == QStyle.SubControl.SC_None
assert sld._pressedControl == QStyle.SubControl.SC_None
opt = QStyleOptionSlider()
gslider.initStyleOption(opt)
style = gslider.style()
sld.initStyleOption(opt)
style = sld.style()
hrect = style.subControlRect(
QStyle.ComplexControl.CC_Slider, opt, QStyle.SubControl.SC_SliderHandle
)
handle_pos = gslider.mapToGlobal(hrect.center())
handle_pos = sld.mapToGlobal(hrect.center())
with qtbot.waitSignal(gslider.sliderPressed):
qtbot.mousePress(gslider, Qt.MouseButton.LeftButton, pos=handle_pos)
with qtbot.waitSignal(sld.sliderPressed):
qtbot.mousePress(sld, Qt.MouseButton.LeftButton, pos=handle_pos)
assert gslider._pressedControl == QStyle.SubControl.SC_SliderHandle
assert sld._pressedControl == QStyle.SubControl.SC_SliderHandle
with qtbot.waitSignals([gslider.sliderMoved, gslider.valueChanged]):
with qtbot.waitSignals([sld.sliderMoved, sld.valueChanged]):
shift = (
QPoint(0, -8)
if gslider.orientation() == Qt.Orientation.Vertical
if sld.orientation() == Qt.Orientation.Vertical
else QPoint(8, 0)
)
gslider.mouseMoveEvent(_mouse_event(handle_pos + shift))
sld.mouseMoveEvent(_mouse_event(handle_pos + shift))
with qtbot.waitSignal(gslider.sliderReleased):
qtbot.mouseRelease(gslider, Qt.MouseButton.LeftButton, pos=handle_pos)
with qtbot.waitSignal(sld.sliderReleased):
qtbot.mouseRelease(sld, Qt.MouseButton.LeftButton, pos=handle_pos)
assert gslider._pressedControl == QStyle.SubControl.SC_None
assert sld._pressedControl == QStyle.SubControl.SC_None
gslider.show()
with qtbot.waitSignal(gslider.sliderPressed):
qtbot.mousePress(gslider, Qt.MouseButton.LeftButton, pos=handle_pos)
sld.show()
with qtbot.waitSignal(sld.sliderPressed):
qtbot.mousePress(sld, Qt.MouseButton.LeftButton, pos=handle_pos)
@skip_on_linux_qt6
def test_hover(gslider: QRangeSlider):
@pytest.mark.parametrize("cls, orientation", FLOAT_SLIDERS)
def test_hover(cls, orientation, qtbot):
sld = cls(orientation)
qtbot.addWidget(sld)
hrect = gslider._handleRect(0)
handle_pos = QPointF(gslider.mapToGlobal(hrect.center()))
hrect = sld._handleRect(0)
handle_pos = QPointF(sld.mapToGlobal(hrect.center()))
assert gslider._hoverControl == QStyle.SubControl.SC_None
assert sld._hoverControl == QStyle.SubControl.SC_None
gslider.event(QHoverEvent(QEvent.Type.HoverEnter, handle_pos, QPointF()))
assert gslider._hoverControl == QStyle.SubControl.SC_SliderHandle
sld.event(_hover_event(QEvent.Type.HoverEnter, handle_pos, QPointF(), sld))
assert sld._hoverControl == QStyle.SubControl.SC_SliderHandle
gslider.event(
QHoverEvent(QEvent.Type.HoverLeave, QPointF(-1000, -1000), handle_pos)
sld.event(
_hover_event(QEvent.Type.HoverLeave, QPointF(-1000, -1000), handle_pos, sld)
)
assert gslider._hoverControl == QStyle.SubControl.SC_None
assert sld._hoverControl == QStyle.SubControl.SC_None
def test_wheel(gslider: QRangeSlider, qtbot):
with qtbot.waitSignal(gslider.valueChanged):
gslider.wheelEvent(_wheel_event(120))
@pytest.mark.parametrize("cls, orientation", FLOAT_SLIDERS)
def test_wheel(cls, orientation, qtbot):
sld = cls(orientation)
qtbot.addWidget(sld)
gslider.wheelEvent(_wheel_event(0))
with qtbot.waitSignal(sld.valueChanged):
sld.wheelEvent(_wheel_event(120))
sld.wheelEvent(_wheel_event(0))
def _assert_types(args: Iterable[Any], type_: type):
# sourcery skip: comprehension-to-generator
if sys.version_info >= (3, 8):
assert all([isinstance(v, type_) for v in args]), "invalid type"
@pytest.mark.parametrize("cls, orientation", ALL_SLIDER_COMBOS)
def test_rangeslider_signals(cls, orientation, qtbot):
sld = cls(orientation)
qtbot.addWidget(sld)
type_ = float if cls == QDoubleRangeSlider else int
mock = Mock()
sld.valueChanged.connect(mock)
with qtbot.waitSignal(sld.valueChanged):
sld.setValue((20, 40))
mock.assert_called_once_with((20, 40))
_assert_types(mock.call_args.args, tuple)
_assert_types(mock.call_args.args[0], type_)
mock = Mock()
sld.rangeChanged.connect(mock)
with qtbot.waitSignal(sld.rangeChanged):
sld.setMinimum(3)
mock.assert_called_once_with(3, 99)
_assert_types(mock.call_args.args, type_)
mock.reset_mock()
with qtbot.waitSignal(sld.rangeChanged):
sld.setMaximum(15)
mock.assert_called_once_with(3, 15)
_assert_types(mock.call_args.args, type_)
mock.reset_mock()
with qtbot.waitSignal(sld.rangeChanged):
sld.setRange(1, 2)
mock.assert_called_once_with(1, 2)
_assert_types(mock.call_args.args, type_)

View File

@@ -4,7 +4,6 @@ from contextlib import suppress
import pytest
from qtpy.QtCore import QEvent, QPoint, QPointF, Qt
from qtpy.QtGui import QHoverEvent
from qtpy.QtWidgets import QSlider, QStyle, QStyleOptionSlider
from superqt import QDoubleSlider, QLabeledDoubleSlider, QLabeledSlider
@@ -12,6 +11,7 @@ from superqt.sliders._generic_slider import _GenericSlider
from ._testutil import (
QT_VERSION,
_hover_event,
_linspace,
_mouse_event,
_wheel_event,
@@ -167,12 +167,12 @@ def test_hover(sld: _GenericSlider):
with suppress(AttributeError): # for QSlider
assert _real_sld._hoverControl == QStyle.SubControl.SC_None
_real_sld.event(QHoverEvent(QEvent.Type.HoverEnter, handle_pos, QPointF()))
_real_sld.event(_hover_event(QEvent.Type.HoverEnter, handle_pos, QPointF(), sld))
with suppress(AttributeError): # for QSlider
assert _real_sld._hoverControl == QStyle.SubControl.SC_SliderHandle
_real_sld.event(
QHoverEvent(QEvent.Type.HoverLeave, QPointF(-1000, -1000), handle_pos)
_hover_event(QEvent.Type.HoverLeave, QPointF(-1000, -1000), handle_pos, sld)
)
with suppress(AttributeError): # for QSlider
assert _real_sld._hoverControl == QStyle.SubControl.SC_None

29
tests/test_utils.py Normal file
View File

@@ -0,0 +1,29 @@
from unittest.mock import Mock
from qtpy.QtCore import QObject, Signal
from superqt.utils import signals_blocked
def test_signal_blocker(qtbot):
"""make sure context manager signal blocker works"""
class Emitter(QObject):
sig = Signal()
obj = Emitter()
receiver = Mock()
obj.sig.connect(receiver)
# make sure signal works
with qtbot.waitSignal(obj.sig):
obj.sig.emit()
receiver.assert_called_once()
receiver.reset_mock()
with signals_blocked(obj):
obj.sig.emit()
qtbot.wait(10)
receiver.assert_not_called()