mirror of
https://github.com/pyapp-kit/superqt.git
synced 2025-07-21 12:11:07 +02:00
Compare commits
42 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
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 |
72
.github/workflows/test_and_deploy.yml
vendored
72
.github/workflows/test_and_deploy.yml
vendored
@@ -70,17 +70,8 @@ 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
|
||||
platform: ubuntu-latest
|
||||
backend: pyside511
|
||||
- python-version: 3.7
|
||||
platform: ubuntu-latest
|
||||
backend: pyqt511
|
||||
- python-version: 3.7
|
||||
platform: ubuntu-latest
|
||||
backend: pyqt512
|
||||
@@ -91,8 +82,6 @@ jobs:
|
||||
platform: ubuntu-latest
|
||||
backend: pyqt514
|
||||
|
||||
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -110,13 +99,13 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools tox tox-gh-actions
|
||||
python -m pip install setuptools tox tox-gh-actions
|
||||
|
||||
- name: Test with tox
|
||||
uses: GabrielBB/xvfb-action@v1
|
||||
timeout-minutes: 3
|
||||
with:
|
||||
run: tox
|
||||
run: python -m tox
|
||||
env:
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
BACKEND: ${{ matrix.backend }}
|
||||
@@ -144,6 +133,59 @@ jobs:
|
||||
name: screenshots ${{ runner.os }}
|
||||
path: screenshots
|
||||
|
||||
test_old_qtpy:
|
||||
name: qtpy minreq
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: tlambert03/setup-qt-libs@v1
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.8'
|
||||
|
||||
- name: install
|
||||
run: |
|
||||
python -m pip install -U pip
|
||||
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
|
||||
|
||||
|
||||
test_napari:
|
||||
name: napari tests
|
||||
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'
|
||||
|
||||
- name: install
|
||||
run: |
|
||||
python -m pip install -U pip
|
||||
python -m pip install -e ./napari[testing,pyqt5]
|
||||
python -m pip install -e ./superqt
|
||||
|
||||
- name: Test napari magicgui
|
||||
uses: GabrielBB/xvfb-action@v1
|
||||
with:
|
||||
run: python -m pytest --color=yes napari/napari/_qt
|
||||
|
||||
check_manifest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -183,3 +225,7 @@ jobs:
|
||||
python -m build
|
||||
twine check dist/*
|
||||
twine upload dist/*
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
generate_release_notes: true
|
||||
|
@@ -5,4 +5,5 @@ user=napari
|
||||
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"]}}
|
||||
|
@@ -1,11 +1,11 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.0.1
|
||||
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: v1.20.1
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
@@ -24,16 +24,16 @@ repos:
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.29.1
|
||||
rev: v2.37.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py37-plus, --keep-runtime-typing]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 21.11b1
|
||||
rev: 22.6.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v0.910-1
|
||||
rev: v0.961
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: examples
|
||||
|
111
CHANGELOG.md
111
CHANGELOG.md
@@ -1,5 +1,101 @@
|
||||
# Changelog
|
||||
|
||||
## [0.3.4](https://github.com/napari/superqt/tree/0.3.4) (2022-07-24)
|
||||
|
||||
[Full Changelog](https://github.com/napari/superqt/compare/v0.3.3...0.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)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Qthrottler and debouncer [\#62](https://github.com/napari/superqt/pull/62) ([tlambert03](https://github.com/tlambert03))
|
||||
- add edgeLabelMode option to QLabeledSlider [\#59](https://github.com/napari/superqt/pull/59) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Fix nested threadworker not starting [\#63](https://github.com/napari/superqt/pull/63) ([tlambert03](https://github.com/tlambert03))
|
||||
- Add missing signals on proxy sliders [\#54](https://github.com/napari/superqt/pull/54) ([tlambert03](https://github.com/tlambert03))
|
||||
- Ugly but functional workaround for pyside6.2.1 breakages [\#51](https://github.com/napari/superqt/pull/51) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Tests & CI:**
|
||||
|
||||
- add napari test to CI [\#67](https://github.com/napari/superqt/pull/67) ([tlambert03](https://github.com/tlambert03))
|
||||
- add gh-release action [\#65](https://github.com/napari/superqt/pull/65) ([tlambert03](https://github.com/tlambert03))
|
||||
- fix xvfb tests [\#61](https://github.com/napari/superqt/pull/61) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Refactors:**
|
||||
|
||||
- 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)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- typing-extensions version pinning [\#46](https://github.com/napari/superqt/pull/46) ([AhmetCanSolak](https://github.com/AhmetCanSolak))
|
||||
|
||||
## [v0.2.5](https://github.com/napari/superqt/tree/v0.2.5) (2021-11-22)
|
||||
|
||||
[Full Changelog](https://github.com/napari/superqt/compare/v0.2.4...v0.2.5)
|
||||
@@ -17,10 +113,17 @@
|
||||
- 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:**
|
||||
**Tests & CI:**
|
||||
|
||||
- reskip test\_object\_thread\_return on ci [\#43](https://github.com/napari/superqt/pull/43) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Refactors:**
|
||||
|
||||
- refactoring qtcompat [\#34](https://github.com/napari/superqt/pull/34) ([tlambert03](https://github.com/tlambert03))
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Fix-manifest, move font tests [\#44](https://github.com/napari/superqt/pull/44) ([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))
|
||||
|
||||
@@ -61,17 +164,13 @@
|
||||
|
||||
## [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.0rc1...v0.2.1)
|
||||
[Full Changelog](https://github.com/napari/superqt/compare/v0.2.0...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.0...v0.2.0rc1)
|
||||
|
||||
|
||||
|
||||
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
|
||||
|
@@ -31,8 +31,6 @@ All widgets must be well-tested, and should work on:
|
||||
- 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
|
||||
|
||||
|
@@ -6,7 +6,7 @@
|
||||
[](https://python.org)
|
||||
[](https://github.com/napari/superqt/actions/workflows/test_and_deploy.yml)
|
||||
[](https://codecov.io/gh/napari/superqt)
|
||||
[](https://codecov.io/gh/napari/superqt)
|
||||
|
||||
### "missing" widgets and components for PyQt/PySide
|
||||
|
||||
|
@@ -1,6 +1,5 @@
|
||||
ignore:
|
||||
- superqt/_version.py
|
||||
- superqt/qtcompat/*
|
||||
- '*_tests*'
|
||||
coverage:
|
||||
status:
|
||||
|
@@ -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`.
|
||||
|
@@ -16,7 +16,7 @@ an instance of
|
||||
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
|
||||
|
8
docs/listwidgets.md
Normal file
8
docs/listwidgets.md
Normal 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.
|
10
docs/utils.md
Normal file
10
docs/utils.md
Normal 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/).
|
@@ -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([])
|
||||
|
||||
|
@@ -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([])
|
||||
|
||||
|
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,7 @@
|
||||
"""Example for QCollapsible"""
|
||||
from qtpy.QtWidgets import QApplication, QLabel, QPushButton
|
||||
|
||||
from superqt import QCollapsible
|
||||
from superqt.qtcompat.QtWidgets import QApplication, QLabel, QPushButton
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
from qtpy import QtCore
|
||||
from qtpy import QtWidgets as QtW
|
||||
|
||||
from superqt import QRangeSlider
|
||||
from superqt.qtcompat import QtCore
|
||||
from superqt.qtcompat import QtWidgets as QtW
|
||||
|
||||
QSS = """
|
||||
QSlider {
|
||||
|
@@ -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:
|
||||
|
@@ -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([])
|
||||
|
||||
|
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/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_()
|
282
examples/throttler_demo.py
Normal file
282
examples/throttler_demo.py
Normal file
@@ -0,0 +1,282 @@
|
||||
"""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):
|
||||
x = 0
|
||||
L = len(v)
|
||||
for p in range(L):
|
||||
v[p] += 1
|
||||
if v[p] > cutoff:
|
||||
x = p
|
||||
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_()
|
10
setup.cfg
10
setup.cfg
@@ -35,7 +35,10 @@ project_urls =
|
||||
[options]
|
||||
packages = find:
|
||||
install_requires =
|
||||
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 =
|
||||
@@ -86,11 +89,6 @@ superqt = py.typed
|
||||
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
|
||||
|
@@ -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",
|
||||
]
|
||||
|
@@ -1,8 +1,8 @@
|
||||
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
|
||||
from qtpy.QtCore import QPoint, QRect, QSize, Qt
|
||||
from qtpy.QtGui import QFont, QFontMetrics, QResizeEvent, QTextLayout
|
||||
from qtpy.QtWidgets import QLabel
|
||||
|
||||
|
||||
class QElidingLabel(QLabel):
|
||||
|
@@ -1,14 +1,8 @@
|
||||
"""A collapsible widget to hide and unhide child widgets"""
|
||||
from typing import Optional
|
||||
|
||||
from ..qtcompat.QtCore import (
|
||||
QAbstractAnimation,
|
||||
QEasingCurve,
|
||||
QMargins,
|
||||
QPropertyAnimation,
|
||||
Qt,
|
||||
)
|
||||
from ..qtcompat.QtWidgets import QFrame, QPushButton, QVBoxLayout, QWidget
|
||||
from qtpy.QtCore import QEasingCurve, QEvent, QMargins, QObject, QPropertyAnimation, Qt
|
||||
from qtpy.QtWidgets import QFrame, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
|
||||
class QCollapsible(QFrame):
|
||||
@@ -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
|
||||
|
@@ -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)
|
||||
|
||||
|
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 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())
|
@@ -22,8 +22,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
|
||||
|
||||
|
@@ -1,8 +1,8 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
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):
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -6,11 +6,9 @@ from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import DefaultDict, Dict, Optional, Sequence, Tuple, Type, 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,7 +20,9 @@ from ..qtcompat.QtGui import (
|
||||
QPixmapCache,
|
||||
QTransform,
|
||||
)
|
||||
from ..qtcompat.QtWidgets import QApplication, QStyleOption, QWidget
|
||||
from qtpy.QtWidgets import QApplication, QStyleOption, QWidget
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from ..utils import QMessageHandler
|
||||
from ._animations import Animation
|
||||
|
||||
@@ -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)
|
||||
|
@@ -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 *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtQml import *
|
||||
from PyQt6.QtQml import *
|
||||
from PySide2.QtQml import *
|
||||
from PySide6.QtQml import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtQuick import *
|
||||
from PyQt6.QtQuick import *
|
||||
from PySide2.QtQuick import *
|
||||
from PySide6.QtQuick import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtQuickControls2 import *
|
||||
from PyQt6.QtQuickControls2 import *
|
||||
from PySide2.QtQuickControls2 import *
|
||||
from PySide6.QtQuickControls2 import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtQuickWidgets import *
|
||||
from PyQt6.QtQuickWidgets import *
|
||||
from PySide2.QtQuickWidgets import *
|
||||
from PySide6.QtQuickWidgets import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtRemoteObjects import *
|
||||
from PyQt6.QtRemoteObjects import *
|
||||
from PySide2.QtRemoteObjects import *
|
||||
from PySide6.QtRemoteObjects import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtScript import *
|
||||
from PyQt6.QtScript import *
|
||||
from PySide2.QtScript import *
|
||||
from PySide6.QtScript import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtScriptTools import *
|
||||
from PyQt6.QtScriptTools import *
|
||||
from PySide2.QtScriptTools import *
|
||||
from PySide6.QtScriptTools import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtScxml import *
|
||||
from PyQt6.QtScxml import *
|
||||
from PySide2.QtScxml import *
|
||||
from PySide6.QtScxml import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtSensors import *
|
||||
from PyQt6.QtSensors import *
|
||||
from PySide2.QtSensors import *
|
||||
from PySide6.QtSensors import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtSerialPort import *
|
||||
from PyQt6.QtSerialPort import *
|
||||
from PySide2.QtSerialPort import *
|
||||
from PySide6.QtSerialPort import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtSql import *
|
||||
from PyQt6.QtSql import *
|
||||
from PySide2.QtSql import *
|
||||
from PySide6.QtSql import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtSvg import *
|
||||
from PyQt6.QtSvg import *
|
||||
from PySide2.QtSvg import *
|
||||
from PySide6.QtSvg import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtTest import *
|
||||
from PyQt6.QtTest import *
|
||||
from PySide2.QtTest import *
|
||||
from PySide6.QtTest import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtTextToSpeech import *
|
||||
from PyQt6.QtTextToSpeech import *
|
||||
from PySide2.QtTextToSpeech import *
|
||||
from PySide6.QtTextToSpeech import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtUiTools import *
|
||||
from PyQt6.QtUiTools import *
|
||||
from PySide2.QtUiTools import *
|
||||
from PySide6.QtUiTools import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtWebChannel import *
|
||||
from PyQt6.QtWebChannel import *
|
||||
from PySide2.QtWebChannel import *
|
||||
from PySide6.QtWebChannel import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtWebEngine import *
|
||||
from PyQt6.QtWebEngine import *
|
||||
from PySide2.QtWebEngine import *
|
||||
from PySide6.QtWebEngine import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtWebEngineCore import *
|
||||
from PyQt6.QtWebEngineCore import *
|
||||
from PySide2.QtWebEngineCore import *
|
||||
from PySide6.QtWebEngineCore import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtWebEngineWidgets import *
|
||||
from PyQt6.QtWebEngineWidgets import *
|
||||
from PySide2.QtWebEngineWidgets import *
|
||||
from PySide6.QtWebEngineWidgets import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtWebSockets import *
|
||||
from PyQt6.QtWebSockets import *
|
||||
from PySide2.QtWebSockets import *
|
||||
from PySide6.QtWebSockets import *
|
@@ -1,16 +0,0 @@
|
||||
# type: ignore
|
||||
from . import API_NAME, _get_qtmodule
|
||||
|
||||
_QtWidgets = _get_qtmodule(__name__)
|
||||
globals().update(_QtWidgets.__dict__)
|
||||
|
||||
|
||||
QApplication = _QtWidgets.QApplication
|
||||
if not hasattr(QApplication, "exec"):
|
||||
QApplication.exec = _QtWidgets.QApplication.exec_
|
||||
|
||||
# backwargs compat with qt5
|
||||
if "6" in API_NAME:
|
||||
_QtGui = _get_qtmodule("QtGui")
|
||||
QAction = _QtGui.QAction
|
||||
QShortcut = _QtGui.QShortcut
|
@@ -1,12 +0,0 @@
|
||||
from PyQt5.QtWidgets import *
|
||||
from PyQt6.QtWidgets import *
|
||||
from PySide2.QtWidgets import *
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
QApplication.exec_ = QApplication.exec
|
||||
|
||||
from PyQt6 import QtGui
|
||||
from PySide6 import QtGui
|
||||
|
||||
QAction = QtGui.QAction
|
||||
QShortcut = QtGui.QShortcut
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtXml import *
|
||||
from PyQt6.QtXml import *
|
||||
from PySide2.QtXml import *
|
||||
from PySide6.QtXml import *
|
@@ -1,4 +0,0 @@
|
||||
from PyQt5.QtXmlPatterns import *
|
||||
from PyQt6.QtXmlPatterns import *
|
||||
from PySide2.QtXmlPatterns import *
|
||||
from PySide6.QtXmlPatterns import *
|
@@ -1,114 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from importlib import abc, import_module, util
|
||||
from typing import TYPE_CHECKING, Optional, Sequence, Union
|
||||
from importlib import abc, util
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from importlib.machinery import ModuleSpec
|
||||
from types import ModuleType
|
||||
from qtpy import * # noqa
|
||||
|
||||
warnings.warn(
|
||||
"The superqt.qtcompat module is deprecated as of v0.3.0. "
|
||||
"Please import from `qtpy` instead."
|
||||
)
|
||||
|
||||
|
||||
class QtMissingError(ImportError):
|
||||
"""Error raise if no bindings could be selected."""
|
||||
|
||||
|
||||
VALID_APIS = {
|
||||
"pyqt5": "PyQt5",
|
||||
"pyqt6": "PyQt6",
|
||||
"pyside2": "PySide2",
|
||||
"pyside6": "PySide6",
|
||||
}
|
||||
|
||||
# Detecting if a binding was specified by the user
|
||||
_requested_api = os.getenv("QT_API", "").lower()
|
||||
_forced_api = os.getenv("FORCE_QT_API")
|
||||
|
||||
# warn if an invalid API has been requested
|
||||
if _requested_api and _requested_api not in VALID_APIS:
|
||||
warnings.warn(
|
||||
f"invalid QT_API specified: {_requested_api}. "
|
||||
f"Valid values include {set(VALID_APIS)}"
|
||||
)
|
||||
_forced_api = None
|
||||
_requested_api = ""
|
||||
|
||||
# TODO: FORCE_QT_API requires also using QT_API ... does that make sense?
|
||||
|
||||
# now we'll try to import QtCore
|
||||
_QtCore: Optional[ModuleType] = None
|
||||
|
||||
# If `FORCE_QT_API` is not set, we first look for previously imported bindings
|
||||
if not _forced_api:
|
||||
for api_name, module_name in VALID_APIS.items():
|
||||
if module_name in sys.modules:
|
||||
_QtCore = import_module(f"{module_name}.QtCore")
|
||||
break
|
||||
|
||||
if _QtCore is None:
|
||||
# try the requested API first, and if _forced_api is True,
|
||||
# raise an ImportError if it doesn't work.
|
||||
# Otherwise go through the list of Valid APIs until something imports
|
||||
requested = VALID_APIS.get(_requested_api)
|
||||
for module_name in sorted(VALID_APIS.values(), key=lambda x: x != requested):
|
||||
try:
|
||||
_QtCore = import_module(f"{module_name}.QtCore")
|
||||
break
|
||||
except ImportError:
|
||||
if _forced_api:
|
||||
ImportError(
|
||||
"FORCE_QT_API set and unable to import requested QT_API: {e}"
|
||||
)
|
||||
|
||||
# didn't find one... not going to work
|
||||
if _QtCore is None:
|
||||
raise QtMissingError(f"No QtCore could be found. Tried: {VALID_APIS.values()}")
|
||||
|
||||
# load variables based on what we found.
|
||||
if not _QtCore.__package__:
|
||||
raise RuntimeError("QtCore does not declare __package__?")
|
||||
|
||||
API_NAME = _QtCore.__package__
|
||||
PYSIDE2 = API_NAME == "PySide2"
|
||||
PYSIDE6 = API_NAME == "PySide6"
|
||||
PYQT5 = API_NAME == "PyQt5"
|
||||
PYQT6 = API_NAME == "PyQt6"
|
||||
QT_VERSION = getattr(_QtCore, "QT_VERSION_STR", "") or getattr(_QtCore, "__version__")
|
||||
|
||||
# lastly, emit a warning if we ended up with an API other than the one requested
|
||||
if _requested_api and API_NAME != VALID_APIS[_requested_api]:
|
||||
warnings.warn(
|
||||
f"Selected binding {_requested_api!r} could not be found, using {API_NAME!r}"
|
||||
)
|
||||
|
||||
|
||||
# Setup the meta path finder that lets us import anything using `superqt.qtcompat.Mod`
|
||||
# forward any requests for superqt.qtcompat.* to qtpy.*
|
||||
class SuperQtImporter(abc.MetaPathFinder):
|
||||
def find_spec(
|
||||
self,
|
||||
fullname: str,
|
||||
path: Optional[Sequence[Union[bytes, str]]],
|
||||
target: Optional[ModuleType] = None,
|
||||
) -> Optional[ModuleSpec]:
|
||||
"""Find a spec for the specified module.
|
||||
|
||||
If fullname is superqt.X or superqt.qtcompat.Xx ...
|
||||
it will look for API_NAME.X instead...
|
||||
|
||||
See https://docs.python.org/3/reference/import.html#the-meta-path
|
||||
"""
|
||||
def find_spec(self, fullname: str, path, target=None): # type: ignore
|
||||
if fullname.startswith(__name__):
|
||||
spec = fullname.replace(__name__, API_NAME)
|
||||
return util.find_spec(spec)
|
||||
return None
|
||||
|
||||
|
||||
def _get_qtmodule(mod_name: str) -> ModuleType:
|
||||
"""Convenience to get a submodule from the current QT_API"""
|
||||
_mod_name = mod_name.rsplit(".", maxsplit=1)[-1]
|
||||
return import_module(f"{API_NAME}.{_mod_name}")
|
||||
return util.find_spec(fullname.replace(__name__, "qtpy"))
|
||||
|
||||
|
||||
sys.meta_path.append(SuperQtImporter())
|
||||
|
3
src/superqt/selection/__init__.py
Normal file
3
src/superqt/selection/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from ._searchable_list_widget import QSearchableListWidget
|
||||
|
||||
__all__ = ("QSearchableListWidget",)
|
46
src/superqt/selection/_searchable_list_widget.py
Normal file
46
src/superqt/selection/_searchable_list_widget.py
Normal 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())
|
@@ -1,4 +1,4 @@
|
||||
from superqt.qtcompat.QtWidgets import QSlider
|
||||
from qtpy.QtWidgets import QSlider
|
||||
|
||||
from ._generic_range_slider import _GenericRangeSlider
|
||||
from ._generic_slider import _GenericSlider
|
||||
|
@@ -1,17 +1,9 @@
|
||||
from typing import Generic, List, Sequence, Tuple, TypeVar, Union
|
||||
|
||||
from ..qtcompat import QtGui
|
||||
from ..qtcompat.QtCore import (
|
||||
Property,
|
||||
QEvent,
|
||||
QPoint,
|
||||
QPointF,
|
||||
QRect,
|
||||
QRectF,
|
||||
Qt,
|
||||
Signal,
|
||||
)
|
||||
from ..qtcompat.QtWidgets import QSlider, QStyle, QStyleOptionSlider, QStylePainter
|
||||
from qtpy import QtGui
|
||||
from qtpy.QtCore import Property, QEvent, QPoint, QPointF, QRect, QRectF, Qt, Signal
|
||||
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
|
||||
|
||||
@@ -32,16 +24,17 @@ class _GenericRangeSlider(_GenericSlider[Tuple], Generic[_T]):
|
||||
"""
|
||||
|
||||
# Emitted when the slider value has changed, with the new slider values
|
||||
valueChanged = Signal(tuple)
|
||||
_valuesChanged = Signal(tuple)
|
||||
|
||||
# Emitted when sliderDown is true and the slider moves
|
||||
# This usually happens when the user is dragging the slider
|
||||
# The value is the positions of *all* handles.
|
||||
sliderMoved = Signal(tuple)
|
||||
_slidersMoved = Signal(tuple)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.valueChanged = self._valuesChanged
|
||||
self.sliderMoved = self._slidersMoved
|
||||
# list of values
|
||||
self._value: List[_T] = [20, 80]
|
||||
|
||||
|
@@ -22,9 +22,9 @@ QRangeSlider.
|
||||
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
from ..qtcompat import QtGui
|
||||
from ..qtcompat.QtCore import QEvent, QPoint, QPointF, QRect, Qt, Signal
|
||||
from ..qtcompat.QtWidgets import (
|
||||
from qtpy import QtGui
|
||||
from qtpy.QtCore import QEvent, QPoint, QPointF, QRect, Qt, Signal
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QSlider,
|
||||
QStyle,
|
||||
@@ -40,13 +40,13 @@ SC_GROOVE = QStyle.SubControl.SC_SliderGroove
|
||||
SC_TICKMARKS = QStyle.SubControl.SC_SliderTickmarks
|
||||
|
||||
CC_SLIDER = QStyle.ComplexControl.CC_Slider
|
||||
QOVERFLOW = 2 ** 31 - 1
|
||||
QOVERFLOW = 2**31 - 1
|
||||
|
||||
|
||||
class _GenericSlider(QSlider, Generic[_T]):
|
||||
valueChanged = Signal(float)
|
||||
sliderMoved = Signal(float)
|
||||
rangeChanged = Signal(float, float)
|
||||
_fvalueChanged = Signal(float)
|
||||
_fsliderMoved = Signal(float)
|
||||
_frangeChanged = Signal(float, float)
|
||||
|
||||
MAX_DISPLAY = 5000
|
||||
|
||||
@@ -74,6 +74,10 @@ class _GenericSlider(QSlider, Generic[_T]):
|
||||
self._control_fraction = 0.04
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
self.valueChanged = self._fvalueChanged
|
||||
self.sliderMoved = self._fsliderMoved
|
||||
self.rangeChanged = self._frangeChanged
|
||||
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_Hover)
|
||||
|
||||
# ############### QtOverrides #######################
|
||||
|
@@ -2,9 +2,9 @@ from enum import IntEnum
|
||||
from functools import partial
|
||||
from typing import Any
|
||||
|
||||
from ..qtcompat.QtCore import QPoint, QSize, Qt, Signal
|
||||
from ..qtcompat.QtGui import QFontMetrics, QValidator
|
||||
from ..qtcompat.QtWidgets import (
|
||||
from qtpy.QtCore import QPoint, QSize, Qt, Signal
|
||||
from qtpy.QtGui import QFontMetrics, QValidator
|
||||
from qtpy.QtWidgets import (
|
||||
QAbstractSlider,
|
||||
QApplication,
|
||||
QDoubleSpinBox,
|
||||
@@ -16,6 +16,7 @@ from ..qtcompat.QtWidgets import (
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from ._sliders import QDoubleRangeSlider, QDoubleSlider, QRangeSlider
|
||||
|
||||
|
||||
@@ -117,6 +118,7 @@ def _handle_overloaded_slider_sig(args, kwargs):
|
||||
|
||||
|
||||
class QLabeledSlider(_SliderProxy, QAbstractSlider):
|
||||
EdgeLabelMode = EdgeLabelMode
|
||||
_slider_class = QSlider
|
||||
_slider: QSlider
|
||||
|
||||
@@ -127,16 +129,27 @@ class QLabeledSlider(_SliderProxy, QAbstractSlider):
|
||||
|
||||
self._slider = self._slider_class()
|
||||
self._label = SliderLabel(self._slider, connect=self._slider.setValue)
|
||||
self._edge_label_mode: EdgeLabelMode = EdgeLabelMode.LabelIsValue
|
||||
|
||||
self._rename_signals()
|
||||
self._slider.actionTriggered.connect(self.actionTriggered.emit)
|
||||
self._slider.rangeChanged.connect(self.rangeChanged.emit)
|
||||
self._slider.valueChanged.connect(self.valueChanged.emit)
|
||||
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._label.setValue)
|
||||
self._slider.valueChanged.connect(self.valueChanged.emit)
|
||||
|
||||
self.setOrientation(orientation)
|
||||
|
||||
def _rename_signals(self):
|
||||
# for subclasses
|
||||
pass
|
||||
|
||||
def setOrientation(self, orientation):
|
||||
"""Set orientation, value will be 'horizontal' or 'vertical'."""
|
||||
self._slider.setOrientation(orientation)
|
||||
marg = (0, 0, 0, 0)
|
||||
if orientation == Qt.Orientation.Vertical:
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(self._slider, alignment=Qt.AlignmentFlag.AlignHCenter)
|
||||
@@ -144,6 +157,9 @@ class QLabeledSlider(_SliderProxy, QAbstractSlider):
|
||||
self._label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.setSpacing(1)
|
||||
else:
|
||||
if self._edge_label_mode == EdgeLabelMode.NoLabel:
|
||||
marg = (0, 0, 5, 0)
|
||||
|
||||
layout = QHBoxLayout()
|
||||
layout.addWidget(self._slider)
|
||||
layout.addWidget(self._label)
|
||||
@@ -154,20 +170,50 @@ class QLabeledSlider(_SliderProxy, QAbstractSlider):
|
||||
if old_layout is not None:
|
||||
QWidget().setLayout(old_layout)
|
||||
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setContentsMargins(*marg)
|
||||
self.setLayout(layout)
|
||||
|
||||
def edgeLabelMode(self) -> EdgeLabelMode:
|
||||
return self._edge_label_mode
|
||||
|
||||
def setEdgeLabelMode(self, opt: EdgeLabelMode) -> None:
|
||||
if opt is EdgeLabelMode.LabelIsRange:
|
||||
raise ValueError(
|
||||
"mode must be one of 'EdgeLabelMode.NoLabel' or "
|
||||
"'EdgeLabelMode.LabelIsValue'."
|
||||
)
|
||||
|
||||
self._edge_label_mode = opt
|
||||
if not self._edge_label_mode:
|
||||
self._label.hide()
|
||||
w = 5 if self.orientation() == Qt.Orientation.Horizontal else 0
|
||||
self.layout().setContentsMargins(0, 0, w, 0)
|
||||
else:
|
||||
if self.isVisible():
|
||||
self._label.show()
|
||||
self._label.setMode(opt)
|
||||
self._label.setValue(self._slider.value())
|
||||
self.layout().setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
QApplication.processEvents()
|
||||
|
||||
|
||||
class QLabeledDoubleSlider(QLabeledSlider):
|
||||
_slider_class = QDoubleSlider
|
||||
_slider: QDoubleSlider
|
||||
valueChanged = Signal(float)
|
||||
rangeChanged = Signal(float, float)
|
||||
_fvalueChanged = Signal(float)
|
||||
_fsliderMoved = Signal(float)
|
||||
_frangeChanged = Signal(float, float)
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.setDecimals(2)
|
||||
|
||||
def _rename_signals(self):
|
||||
self.valueChanged = self._fvalueChanged
|
||||
self.sliderMoved = self._fsliderMoved
|
||||
self.rangeChanged = self._frangeChanged
|
||||
|
||||
def decimals(self) -> int:
|
||||
return self._label.decimals()
|
||||
|
||||
@@ -176,7 +222,7 @@ class QLabeledDoubleSlider(QLabeledSlider):
|
||||
|
||||
|
||||
class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
|
||||
valueChanged = Signal(tuple)
|
||||
_valueChanged = Signal(tuple)
|
||||
LabelPosition = LabelPosition
|
||||
EdgeLabelMode = EdgeLabelMode
|
||||
_slider_class = QRangeSlider
|
||||
@@ -185,6 +231,8 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
parent, orientation = _handle_overloaded_slider_sig(args, kwargs)
|
||||
super().__init__(parent)
|
||||
self._rename_signals()
|
||||
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_ShowWithoutActivating)
|
||||
self._handle_labels = []
|
||||
self._handle_label_position: LabelPosition = LabelPosition.LabelsAbove
|
||||
@@ -216,6 +264,9 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
|
||||
self._on_range_changed(self._slider.minimum(), self._slider.maximum())
|
||||
self.setOrientation(orientation)
|
||||
|
||||
def _rename_signals(self):
|
||||
self.valueChanged = self._valueChanged
|
||||
|
||||
def handleLabelPosition(self) -> LabelPosition:
|
||||
return self._handle_label_position
|
||||
|
||||
@@ -392,12 +443,16 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
|
||||
class QLabeledDoubleRangeSlider(QLabeledRangeSlider):
|
||||
_slider_class = QDoubleRangeSlider
|
||||
_slider: QDoubleRangeSlider
|
||||
rangeChanged = Signal(float, float)
|
||||
_frangeChanged = Signal(float, float)
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.setDecimals(2)
|
||||
|
||||
def _rename_signals(self):
|
||||
super()._rename_signals()
|
||||
self.rangeChanged = self._frangeChanged
|
||||
|
||||
def decimals(self) -> int:
|
||||
return self._min_label.decimals()
|
||||
|
||||
@@ -462,7 +517,7 @@ class SliderLabel(QDoubleSpinBox):
|
||||
)
|
||||
self.setFixedSize(size)
|
||||
|
||||
def setValue(self, val):
|
||||
def setValue(self, val: Any) -> None:
|
||||
super().setValue(val)
|
||||
if self._mode == EdgeLabelMode.LabelIsRange:
|
||||
self._update_size()
|
||||
|
@@ -5,9 +5,9 @@ import re
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ..qtcompat import QT_VERSION
|
||||
from ..qtcompat.QtCore import Qt
|
||||
from ..qtcompat.QtGui import (
|
||||
from qtpy import QT_VERSION
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtGui import (
|
||||
QBrush,
|
||||
QColor,
|
||||
QGradient,
|
||||
@@ -15,7 +15,7 @@ from ..qtcompat.QtGui import (
|
||||
QPalette,
|
||||
QRadialGradient,
|
||||
)
|
||||
from ..qtcompat.QtWidgets import QApplication, QSlider, QStyleOptionSlider
|
||||
from qtpy.QtWidgets import QApplication, QSlider, QStyleOptionSlider
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._generic_range_slider import _GenericRangeSlider
|
||||
|
@@ -1,4 +1,5 @@
|
||||
from ..qtcompat.QtCore import Signal
|
||||
from qtpy.QtCore import Signal
|
||||
|
||||
from ._generic_range_slider import _GenericRangeSlider
|
||||
from ._generic_slider import _GenericSlider
|
||||
|
||||
|
@@ -1,8 +1,8 @@
|
||||
from enum import Enum
|
||||
|
||||
from ..qtcompat.QtCore import QSize, Qt, Signal
|
||||
from ..qtcompat.QtGui import QFontMetrics, QValidator
|
||||
from ..qtcompat.QtWidgets import QAbstractSpinBox, QStyle, QStyleOptionSpinBox
|
||||
from qtpy.QtCore import QSize, Qt, Signal
|
||||
from qtpy.QtGui import QFontMetrics, QValidator
|
||||
from qtpy.QtWidgets import QAbstractSpinBox, QStyle, QStyleOptionSpinBox
|
||||
|
||||
|
||||
class _EmitPolicy(Enum):
|
||||
@@ -40,7 +40,7 @@ class QLargeIntSpinBox(QAbstractSpinBox):
|
||||
super().__init__(parent)
|
||||
self._value: int = 0
|
||||
self._minimum: int = 0
|
||||
self._maximum: int = 2 ** 64 - 1
|
||||
self._maximum: int = 2**64 - 1
|
||||
self._single_step: int = 1
|
||||
self._pending_emit = False
|
||||
validator = _AnyIntValidator(self)
|
||||
|
@@ -1,17 +1,25 @@
|
||||
__all__ = (
|
||||
"CodeSyntaxHighlight",
|
||||
"create_worker",
|
||||
"ensure_main_thread",
|
||||
"ensure_object_thread",
|
||||
"FunctionWorker",
|
||||
"GeneratorWorker",
|
||||
"new_worker_qthread",
|
||||
"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,
|
||||
@@ -20,3 +28,4 @@ from ._qthreading import (
|
||||
new_worker_qthread,
|
||||
thread_worker,
|
||||
)
|
||||
from ._throttler import QSignalDebouncer, QSignalThrottler, qdebounced, qthrottled
|
||||
|
93
src/superqt/utils/_code_syntax_highlight.py
Normal file
93
src/superqt/utils/_code_syntax_highlight.py
Normal 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
|
@@ -3,7 +3,7 @@ from concurrent.futures import Future
|
||||
from functools import wraps
|
||||
from typing import Callable, List, Optional
|
||||
|
||||
from superqt.qtcompat.QtCore import (
|
||||
from qtpy.QtCore import (
|
||||
QCoreApplication,
|
||||
QMetaObject,
|
||||
QObject,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user