Compare commits

...

27 Commits

Author SHA1 Message Date
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
39 changed files with 627 additions and 92 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,6 +133,28 @@ 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

View File

@@ -1,11 +1,11 @@
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: 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.31.0
rev: v2.37.1
hooks:
- id: pyupgrade
args: [--py37-plus, --keep-runtime-typing]
- repo: https://github.com/psf/black
rev: 22.1.0
rev: 22.6.0
hooks:
- id: black
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.931
rev: v0.961
hooks:
- id: mypy
exclude: examples

View File

@@ -1,5 +1,64 @@
# 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)
@@ -25,6 +84,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,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.0rc0...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.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)
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*

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.

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

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

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

@@ -137,8 +137,8 @@ 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.setOrientation(orientation)

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,6 +68,17 @@ def _wheel_event(arc):
)
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, stop, n):
h = (stop - start) / (n - 1)
for i in range(n):

View File

@@ -3,12 +3,17 @@ 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,
_linspace,
_mouse_event,
_wheel_event,
skip_on_linux_qt6,
)
@pytest.fixture(params=[Qt.Orientation.Horizontal, Qt.Orientation.Vertical])
@@ -118,6 +123,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 +134,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

View File

@@ -2,12 +2,17 @@ import math
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 ._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,
)
@pytest.fixture(params=[Qt.Orientation.Horizontal, Qt.Orientation.Vertical])
@@ -153,11 +158,11 @@ def test_hover(gslider: QRangeSlider):
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

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