* wip

* wip

* more wip

* progress

* more docs

* more changes

* add link

* more examples and improvements

* fix check-manifest

* sort members

* remove autogen images

* remove _images

* add font docs

* add link to utils
This commit is contained in:
Talley Lambert
2022-10-05 08:59:27 -04:00
committed by GitHub
parent d1c056886f
commit 97bb814451
45 changed files with 1388 additions and 535 deletions

1
.gitignore vendored
View File

@@ -82,3 +82,4 @@ src/superqt/_version.py
screenshots
.mypy_cache
docs/_auto_images/

View File

@@ -1,6 +1,5 @@
# ![tiny](https://user-images.githubusercontent.com/1609449/120636353-8c3f3800-c43b-11eb-8732-a14dec578897.png) superqt!
[![License](https://img.shields.io/pypi/l/superqt.svg?color=green)](https://github.com/napari/superqt/raw/master/LICENSE)
[![PyPI](https://img.shields.io/pypi/v/superqt.svg?color=green)](https://pypi.org/project/superqt)
[![Python
@@ -20,25 +19,33 @@ Components are tested on:
- PyQt5 (5.11 and above) & PyQt6
- PySide2 (5.11 and above) & PySide6
## Documentation
Documentation is available at https://napari.org/superqt
## Widgets
Widgets include:
superqt provides a variety of widgets that are not included in the native QtWidgets module, including multihandle (range) sliders, comboboxes, and more.
- [Float Slider](docs/sliders.md#float-slider)
See the [widgets documentation](https://napari.org/superqt/widgets) for a full list of widgets.
- [Range Slider](docs/sliders.md#range-slider) (multi-handle slider)
<img src="https://raw.githubusercontent.com/napari/superqt/main/docs/images/demo_darwin10.png" alt="range sliders" width=680>
- [Labeled Sliders](docs/sliders.md#labeled-sliders) (sliders with linked
spinboxes)
<img src="https://raw.githubusercontent.com/napari/superqt/main/docs/images/labeled_qslider.png" alt="range sliders" width=680>
<img src="https://raw.githubusercontent.com/napari/superqt/main/docs/images/labeled_range.png" alt="range sliders" width=680>
- Unbound Integer SpinBox (backed by python `int`)
## Utilities
superqt includes a number of utitlities for working with Qt, including:
- tools and decorators for working with threads in qt.
- `superqt.fonticon` for generating icons from font files (such as [Material Design Icons](https://materialdesignicons.com/) and [Font Awesome](https://fontawesome.com/))
See the [utilities documentation](https://napari.org/superqt/utilities/) for a full list of widgets.
## Contributing

144
docs/_macros.py Normal file
View File

@@ -0,0 +1,144 @@
import sys
from enum import EnumMeta
from importlib import import_module
from pathlib import Path
from textwrap import dedent
from typing import TYPE_CHECKING
from jinja2 import pass_context
from qtpy.QtCore import QObject, Signal
if TYPE_CHECKING:
from mkdocs_macros.plugin import MacrosPlugin
EXAMPLES = Path(__file__).parent.parent / "examples"
IMAGES = Path(__file__).parent / "_auto_images"
IMAGES.mkdir(exist_ok=True, parents=True)
def define_env(env: "MacrosPlugin"):
@env.macro
@pass_context
def show_widget(context, width: int = 500) -> list[Path]:
# extract all fenced code blocks starting with "python"
page = context["page"]
dest = IMAGES / f"{page.title}.png"
if "build" in sys.argv:
dest.unlink(missing_ok=True)
codeblocks = [
b[6:].strip()
for b in page.markdown.split("```")
if b.startswith("python")
]
src = codeblocks[0].strip()
src = src.replace(
"QApplication([])", "QApplication.instance() or QApplication([])"
)
src = src.replace("app.exec_()", "")
exec(src)
_grab(dest, width)
return f"![{page.title}](../{dest.parent.name}/{dest.name}){{ loading=lazy; width={width} }}\n\n"
@env.macro
def show_members(cls: str):
# import class
module, name = cls.rsplit(".", 1)
_cls = getattr(import_module(module), name)
first_q = next(
(
b.__name__
for b in _cls.__mro__
if issubclass(b, QObject) and ".Qt" in b.__module__
),
None,
)
inherited_members = set()
for base in _cls.__mro__:
if issubclass(base, QObject) and ".Qt" in base.__module__:
inherited_members.update(
{k for k in dir(base) if not k.startswith("_")}
)
new_signals = {
k
for k, v in vars(_cls).items()
if not k.startswith("_") and isinstance(v, Signal)
}
self_members = {
k
for k in dir(_cls)
if not k.startswith("_") and k not in inherited_members | new_signals
}
enums = []
for m in list(self_members):
if isinstance(getattr(_cls, m), EnumMeta):
self_members.remove(m)
enums.append(m)
out = ""
if first_q:
url = f"https://doc.qt.io/qt-6/{first_q.lower()}.html"
out += f"## Qt Class\n\n<a href='{url}'>`{first_q}`</a>\n\n"
out += ""
if new_signals:
out += "## Signals\n\n"
for sig in new_signals:
out += f"### `{sig}`\n\n"
if enums:
out += "## Enums\n\n"
for e in enums:
out += f"### `{_cls.__name__}.{e}`\n\n"
for m in getattr(_cls, e):
out += f"- `{m.name}`\n\n"
if self_members:
out += dedent(
f"""
## Methods
::: {cls}
options:
heading_level: 3
show_source: False
show_inherited_members: false
show_signature_annotations: True
members: {sorted(self_members)}
docstring_style: numpy
show_bases: False
show_root_toc_entry: False
show_root_heading: False
"""
)
return out
def _grab(dest: str | Path, width) -> list[Path]:
"""Grab the top widgets of the application."""
from qtpy.QtCore import QTimer
from qtpy.QtWidgets import QApplication
w = QApplication.topLevelWidgets()[-1]
w.setFixedWidth(width)
w.activateWindow()
w.setMinimumHeight(40)
w.grab().save(str(dest))
# hack to make sure the object is truly closed and deleted
while True:
QTimer.singleShot(10, w.deleteLater)
QApplication.processEvents()
try:
w.parent()
except RuntimeError:
return

View File

@@ -1,68 +0,0 @@
# ComboBox
## Enum Combo Box
`QEnumComboBox` is a variant of [`QComboBox`](https://doc.qt.io/qt-5/qcombobox.html)
that populates the items in the combobox based on a python `Enum` class. In addition to all
the methods provided by `QComboBox`, this subclass adds the methods
`enumClass`/`setEnumClass` to get/set the current `Enum` class represented by the combobox,
and `currentEnum`/`setCurrentEnum` to get/set the current `Enum` member in the combobox.
There is also a new signal `currentEnumChanged(enum)` analogous to `currentIndexChanged` and `currentTextChanged`.
Method like `insertItem` and `addItem` are blocked and try of its usage will end with `RuntimeError`
```python
from enum import Enum
from superqt import QEnumComboBox
class SampleEnum(Enum):
first = 1
second = 2
third = 3
# as usual:
# you must create a QApplication before create a widget.
combo = QEnumComboBox()
combo.setEnumClass(SampleEnum)
```
other option is to use optional `enum_class` argument of constructor and change
```python
combo = QEnumComboBox()
combo.setEnumClass(SampleEnum)
```
to
```python
combo = QEnumComboBox(enum_class=SampleEnum)
```
### Allow `None`
`QEnumComboBox` allow using Optional type annotation:
```python
from enum import Enum
from superqt import QEnumComboBox
class SampleEnum(Enum):
first = 1
second = 2
third = 3
# as usual:
# you must create a QApplication before create a widget.
combo = QEnumComboBox()
combo.setEnumClass(SampleEnum, allow_none=True)
```
In this case there is added option `----` and `currentEnum` will return `None` for it.
## 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`.

26
docs/faq.md Normal file
View File

@@ -0,0 +1,26 @@
# FAQ
## Sliders not dragging properly on MacOS 12+
??? details
On MacOS Monterey, with Qt5, there is a bug that causes all sliders
(including native Qt sliders) to not respond properly to drag events. See:
- [https://bugreports.qt.io/browse/QTBUG-98093](https://bugreports.qt.io/browse/QTBUG-98093)
- [https://github.com/napari/superqt/issues/74](https://github.com/napari/superqt/issues/74)
Superqt includes a workaround for this issue, but it is not perfect, and it requires using a custom stylesheet (which may interfere with your own styles). Note that you
may not see this issue if you're already using custom stylesheets.
To opt in to the workaround, do any of the following:
- set the environment variable `USE_MAC_SLIDER_PATCH=1` before importing superqt
(note: this is safe to use even if you're targeting more than just MacOS 12, it will only be applied when needed)
- call the `applyMacStylePatch()` method on any of the superqt slider subclasses (note, this will override your slider styles)
- apply the stylesheet manually:
```python
from superqt.sliders import MONTEREY_SLIDER_STYLES_FIX
slider.setStyleSheet(MONTEREY_SLIDER_STYLES_FIX)
```

View File

29
docs/index.md Normal file
View File

@@ -0,0 +1,29 @@
# superqt
## ![tiny](https://user-images.githubusercontent.com/1609449/120636353-8c3f3800-c43b-11eb-8732-a14dec578897.png) "missing" widgets and components for PyQt/PySide
This repository aims to provide high-quality community-contributed Qt widgets
and components for [PyQt](https://riverbankcomputing.com/software/pyqt/) &
[PySide](https://www.qt.io/qt-for-python) that are not provided in the native
QtWidgets module.
Components are tested on:
- macOS, Windows, & Linux
- Python 3.7 and above
- PyQt5 (5.11 and above) & PyQt6
- PySide2 (5.11 and above) & PySide6
## Installation
```bash
pip install superqt
```
```bash
conda install -c conda-forge superqt
```
## Usage
See the [Widgets](./widgets/) and [Utilities](./utilities/) pages for features offered by superqt.

View File

@@ -1,8 +0,0 @@
# ListWidget
## QSearchableListWidget
`QSearchableListWidget` is a variant of [`QListWidget`](https://doc.qt.io/qt-5/qlistwidget.html) that add text entry above list widget that allow to filter list
of available options.
Because of implementation it does not inherit directly from `QListWidget` but satisfy it all api. The only limitation is that it cannot be used as argument of `QListWidgetItem` constructor.

View File

@@ -1,257 +0,0 @@
# Sliders
![slider](images/slider.png)
- `QRangeSlider` inherits from [`QSlider`](https://doc.qt.io/qt-5/qslider.html)
and attempts to match the Qt API as closely as possible
- Uses platform-specific styles (for handle, groove, & ticks) but also supports
QSS style sheets.
- Supports mouse wheel and keypress (soon) events
- Supports more than 2 handles (e.g. `slider.setValue([0, 10, 60, 80])`)
*Note: There is a Qt5 Bug that affects sliders in MacOS 12+, see fix at bottom of page.*
## Range Slider
```python
from superqt import QRangeSlider
# as usual:
# you must create a QApplication before create a widget.
range_slider = QRangeSlider()
```
As `QRangeSlider` inherits from `QtWidgets.QSlider`, you can use all of the
same methods available in the [QSlider API](https://doc.qt.io/qt-5/qslider.html). The major difference is that `value` and `sliderPosition` are reimplemented as `tuples` of `int` (where the length of the tuple is equal to the number of handles in the slider.)
### `value: Tuple[int, ...]`
This property holds the current value of all handles in the slider.
The slider forces all values to be within the legal range:
`minimum <= value <= maximum`.
Changing the value also changes the sliderPosition.
##### Access Functions:
```python
range_slider.value() -> Tuple[int, ...]
```
```python
range_slider.setValue(val: Sequence[int]) -> None
```
##### Notifier Signal:
```python
valueChanged(Tuple[int, ...])
```
### `sliderPosition: Tuple[int, ...]`
This property holds the current slider positions. It is a `tuple` with length equal to the number of handles.
If [tracking](https://doc.qt.io/qt-5/qabstractslider.html#tracking-prop) is enabled (the default), this is identical to [`value`](#value--tupleint-).
##### Access Functions:
```python
range_slider.sliderPosition() -> Tuple[int, ...]
```
```python
range_slider.setSliderPosition(val: Sequence[int]) -> None
```
##### Notifier Signal:
```python
sliderMoved(Tuple[int, ...])
```
### Additional properties
These options are in addition to the Qt QSlider API, and control the behavior of the bar between handles.
| getter | setter | type | default | description |
| -------------------- | ------------------------------------------- | ------ | ------- | ------------------------------------------------------------------------------------------------ |
| `barIsVisible` | `setBarIsVisible` <br>`hideBar` / `showBar` | `bool` | `True` | <small>Whether the bar between handles is visible.</small> |
| `barMovesAllHandles` | `setBarMovesAllHandles` | `bool` | `True` | <small>Whether clicking on the bar moves all handles or just the nearest</small> |
| `barIsRigid` | `setBarIsRigid` | `bool` | `True` | <small>Whether bar length is constant or "elastic" when dragging the bar beyond min/max.</small> |
------
### Examples
These screenshots show `QRangeSlider` (multiple handles) next to the native `QSlider`
(single handle). With no styles applied, `QRangeSlider` will match the native OS
style of `QSlider` with or without tick marks. When styles have been applied
using [Qt Style Sheets](https://doc.qt.io/qt-5/stylesheet-reference.html), then
`QRangeSlider` will inherit any styles applied to `QSlider` (since it inherits
from QSlider). If you'd like to style `QRangeSlider` differently than `QSlider`,
then you can also target it directly in your style sheet. The one "special"
property for QRangeSlider is `qproperty-barColor`, which sets the color of the
bar between the handles.
> The code for these example widgets is [here](../examples/demo_widget.py)
<details>
<summary><em>See style sheet used for this example</em></summary>
```css
/*
Because QRangeSlider inherits from QSlider, it will also inherit styles
*/
QSlider {
min-height: 20px;
}
QSlider::groove:horizontal {
border: 0px;
background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
stop:0 #777, stop:1 #aaa);
height: 20px;
border-radius: 10px;
}
QSlider::handle {
background: qradialgradient(cx:0, cy:0, radius: 1.2, fx:0.5,
fy:0.5, stop:0 #eef, stop:1 #000);
height: 20px;
width: 20px;
border-radius: 10px;
}
/*
"QSlider::sub-page" is the one exception ...
(it styles the area to the left of the QSlider handle)
*/
QSlider::sub-page:horizontal {
background: #447;
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
}
/*
for QRangeSlider: use "qproperty-barColor". "sub-page" will not work.
*/
QRangeSlider {
qproperty-barColor: #447;
}
```
</details>
#### macOS
##### Catalina
![mac10](images/demo_darwin10.png)
##### Big Sur
![mac11](images/demo_darwin11.png)
#### Windows
![window](images/demo_windows.png)
#### Linux
![linux](images/demo_linux.png)
## Labeled Sliders
This package also includes two "labeled" slider variants. One for `QRangeSlider`, and one for the native `QSlider`:
### `QLabeledRangeSlider`
![labeled_range](images/labeled_range.png)
```python
from superqt import QLabeledRangeSlider
```
This has the same API as `QRangeSlider` with the following additional options:
#### `handleLabelPosition`/`setHandleLabelPosition`
Where/whether labels are shown adjacent to slider handles.
**type:** `QLabeledRangeSlider.LabelPosition`
**default:** `LabelPosition.LabelsAbove`
*options:*
- `LabelPosition.NoLabel` (no labels shown adjacent to handles)
- `LabelPosition.LabelsAbove`
- `LabelPosition.LabelsBelow`
- `LabelPosition.LabelsRight` (alias for `LabelPosition.LabelsAbove`)
- `LabelPosition.LabelsLeft` (alias for `LabelPosition.LabelsBelow`)
#### `edgeLabelMode`/`setEdgeLabelMode`
**type:** `QLabeledRangeSlider.EdgeLabelMode`
**default:** `EdgeLabelMode.LabelIsRange`
*options:*
- `EdgeLabelMode.NoLabel`: no labels shown at slider extremes
- `EdgeLabelMode.LabelIsRange`: edge labels shown the min/max values
- `EdgeLabelMode.LabelIsValue`: edge labels shown the slider range
#### fine tuning position of labels:
If you find that you need to fine tune the position of the handle labels:
- `QLabeledRangeSlider.label_shift_x`: adjust horizontal label position
- `QLabeledRangeSlider.label_shift_y`: adjust vertical label position
### `QLabeledSlider`
![labeled_range](images/labeled_qslider.png)
```python
from superqt import QLabeledSlider
```
(no additional options at this point)
## Float Slider
just like QSlider, but supports float values
```python
from superqt import QDoubleSlider
```
## Issues
### MacOS Monterey Slider issue
On MacOS Monterey, with Qt5, there is a bug that causes all sliders
(including native Qt sliders) to not respond properly to drag events. See:
- https://bugreports.qt.io/browse/QTBUG-98093
- https://github.com/napari/superqt/issues/74
Superqt includes a workaround for this issue, but it is not perfect, and it requires using a custom stylesheet (which may interfere with your own styles). Note that you
may not see this issue if you're already using custom stylesheets.
To opt in to the workaround, do any of the following:
- set the environment variable `USE_MAC_SLIDER_PATCH=1` before importing superqt
(note: this is safe to use even if you're targeting more than just MacOS 12, it will only be applied when needed)
- call the `applyMacStylePatch()` method on any of the superqt slider subclasses (note, this will override your slider styles)
- apply the stylesheet manually:
```python
from superqt.sliders import MONTEREY_SLIDER_STYLES_FIX
slider.setStyleSheet(MONTEREY_SLIDER_STYLES_FIX)
```

View File

@@ -0,0 +1,52 @@
# CodeSyntaxHighlight
A code highlighter subclass of `QSyntaxHighlighter`
that can be used to highlight code in a QTextEdit.
Code lexer and available styles are from [`pygments`](https://pygments.org/) python library
List of available languages are available [here](https://pygments.org/languages/).
List of available styles are available [here](https://pygments.org/styles/).
## Example
```python
from qtpy.QtGui import QColor, QPalette
from qtpy.QtWidgets import QApplication, QTextEdit
from superqt.utils import CodeSyntaxHighlight
app = QApplication([])
text_area = QTextEdit()
highlight = CodeSyntaxHighlight(text_area.document(), "python", "monokai")
palette = text_area.palette()
palette.setColor(QPalette.Base, QColor(highlight.background_color))
text_area.setPalette(palette)
text_area.setText(
"""from argparse import ArgumentParser
def main():
parser = ArgumentParser()
parser.add_argument("name", help="Your name")
args = parser.parse_args()
print(f"Hello {args.name}")
if __name__ == "__main__":
main()
"""
)
text_area.show()
text_area.resize(400, 200)
app.exec_()
```
{{ show_widget() }}
{{ show_members('superqt.utils.CodeSyntaxHighlight') }}

101
docs/utilities/fonticon.md Normal file
View File

@@ -0,0 +1,101 @@
# Font icons
The `superqt.fonticon` module provides a set of utilities for working with font
icons such as [Font Awesome](https://fontawesome.com/) or [Material Design
Icons](https://materialdesignicons.com/).
## Basic Example
```python
from fonticon_fa5 import FA5S
from qtpy.QtCore import QSize
from qtpy.QtWidgets import QApplication, QPushButton
from superqt.fonticon import icon, pulse
app = QApplication([])
btn2 = QPushButton()
btn2.setIcon(icon(FA5S.smile, color="blue"))
btn2.setIconSize(QSize(225, 225))
btn2.show()
app.exec()
```
{{ show_widget(225) }}
## Font Icon plugins
Ready-made fonticon packs are available as plugins:
### [Font Awesome 5](https://fontawesome.com/v5/search)
```bash
pip install fonticon-fontawesome5
```
### [Font Awesome 6](https://fontawesome.com/v6/search)
```bash
pip install fonticon-fontawesome6
```
### [Material Design Icons](https://materialdesignicons.com/)
```bash
pip install fonticon-materialdesignicons6
```
### See also
- <https://github.com/tlambert03/fonticon-bootstrapicons>
- <https://github.com/tlambert03/fonticon-linearicons>
- <https://github.com/tlambert03/fonticon-feather>
`superqt.fonticon` is a pluggable system, and font icon packs may use the `"superqt.fonticon"`
entry point to register themselves with superqt. See [`fonticon-cookiecutter`](https://github.com/tlambert03/fonticon-cookiecutter) for a template, or look through the following repos for examples:
- <https://github.com/tlambert03/fonticon-fontawesome6>
- <https://github.com/tlambert03/fonticon-fontawesome5>
- <https://github.com/tlambert03/fonticon-materialdesignicons6>
## API
::: superqt.fonticon.icon
options:
heading_level: 3
::: superqt.fonticon.setTextIcon
options:
heading_level: 3
::: superqt.fonticon.font
options:
heading_level: 3
::: superqt.fonticon.IconOpts
options:
heading_level: 3
::: superqt.fonticon.addFont
options:
heading_level: 3
## Animations
the `animation` parameter to `icon()` accepts a subclass of
`Animation` that will be
::: superqt.fonticon.Animation
options:
heading_level: 3
::: superqt.fonticon.pulse
options:
heading_level: 3
::: superqt.fonticon.spin
options:
heading_level: 3

31
docs/utilities/index.md Normal file
View File

@@ -0,0 +1,31 @@
# Utilities
## Font Icons
| Object | Description |
| ----------- | --------------------- |
| [`addFont`](./fonticon.md#superqt.fonticon.addFont) | Add an `OTF/TTF` file at to the font registry. |
| [`font`](./fonticon.md#superqt.fonticon.font) | Create `QFont` for a given font-icon font family key |
| [`icon`](./fonticon.md#superqt.fonticon.icon) | Create a `QIcon` for font-con glyph key |
| [`setTextIcon`](./fonticon.md#superqt.fonticon.setTextIcon) | Set text on a `QWidget` to a specific font & glyph. |
| [`IconFont`](./fonticon.md#superqt.fonticon.IconFont) | Helper class that provides a standard way to create an `IconFont`. |
| [`IconOpts`](./fonticon.md#superqt.fonticon.IconOpts) | Options for rendering an icon |
| [`Animation`](./fonticon.md#superqt.fonticon.Animation) | Base class for adding animations to a font-icon. |
## Threading tools
| Object | Description |
| ----------- | --------------------- |
| [`ensure_main_thread`](./thread_decorators.md#ensure_main_thread) | Decorator that ensures a function is called in the main `QApplication` thread. |
| [`ensure_object_thread`](./thread_decorators.md#ensure_object_thread) | Decorator that ensures a `QObject` method is called in the object's thread. |
| [`FunctionWorker`](./threading.md#superqt.utils.FunctionWorker) | `QRunnable` with signals that wraps a simple long-running function. |
| [`GeneratorWorker`](./threading.md#superqt.utils.GeneratorWorker) | `QRunnable` with signals that wraps a long-running generator. |
| [`create_worker`](./threading.md#superqt.utils.create_worker) | Create a worker to run a target function in another thread. |
| [`thread_worker`](./threading.md#superqt.utils.thread_worker) | Decorator for `create_worker`, turn a function into a worker. |
## Miscellaneous
| Object | Description |
| ----------- | --------------------- |
| [`QMessageHandler`](./qmessagehandler.md) | A context manager to intercept messages from Qt. |
| [`CodeSyntaxHighlight`](./code_syntax_highlight.md) | A `QSyntaxHighlighter` for code syntax highlighting. |

View File

@@ -0,0 +1,8 @@
# QMessageHandler
::: superqt.utils.QMessageHandler
options:
heading_level: 3
show_signature_annotations: True
docstring_style: numpy
show_bases: False

View File

@@ -1,18 +1,24 @@
# Decorators
## Move to thread decorators
# Threading decorators
`superqt` provides two decorators that help to ensure that given function is
running in the desired thread:
* `ensure_main_thread` - ensures that the decorated function/method runs in the main thread
* `ensure_object_thread` - ensures that a decorated bound method of a `QObject` runs in the
thread in which the instance lives ([qt
documentation](https://doc.qt.io/qt-5/threads-qobject.html#accessing-qobject-subclasses-from-other-threads)).
## `ensure_main_thread`
`ensure_main_thread` ensures that the decorated function/method runs in the main thread
## `ensure_object_thread`
`ensure_object_thread` ensures that a decorated bound method of a `QObject` runs
in the thread in which the instance lives ([see qt documentation for
details](https://doc.qt.io/qt-5/threads-qobject.html#accessing-qobject-subclasses-from-other-threads)).
## Usage
By default, functions are executed asynchronously (they return immediately with
an instance of
[`concurrent.futures.Future`](https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Future)).
To block and wait for the result, see [Synchronous mode](#synchronous-mode)
```python
@@ -57,12 +63,14 @@ As can be seen in this example these decorators can also be used for setters.
These decorators should not be used as replacement of Qt Signals but rather to
interact with Qt objects from non Qt code.
### Synchronous mode
## Synchronous mode
If you'd like for the program to block and wait for the result of your function
call, use the `await_return=True` parameter, and optionally specify a timeout.
> *Note: Using synchronous mode may significantly impact performance.*
!!! important
Using synchronous mode may significantly impact performance.
```python
from superqt import ensure_main_thread

View File

@@ -0,0 +1,36 @@
# Thread workers
The objects in this module provide utilities for running tasks in a separate
thread. In general (with the exception of `new_worker_qthread`), everything
here wraps Qt's [QRunnable API](https://doc.qt.io/qt-6/qrunnable.html).
The highest level object is the
[`@thread_worker`][superqt.utils.thread_worker] decorator. It was originally
written for `napari`, and was later extracted into `superqt`. You may also be
interested in reading the [napari
documentation](https://napari.org/stable/guides/threading.html#threading-in-napari-with-thread-worker) on this feature,
which provides a more in-depth/introductory usage guide.
For additional control, you can create your own
[`FunctionWorker`][superqt.utils.FunctionWorker] or
[`GeneratorWorker`][superqt.utils.GeneratorWorker] objects.
::: superqt.utils.WorkerBase
::: superqt.utils.FunctionWorker
::: superqt.utils.GeneratorWorker
## Convenience functions
::: superqt.utils.thread_worker
options:
heading_level: 3
::: superqt.utils.create_worker
options:
heading_level: 3
::: superqt.utils.new_worker_qthread
options:
heading_level: 3

View File

@@ -0,0 +1,46 @@
# Throttling & Debouncing
These utilities allow you to throttle or debounce a function. This is useful
when you have a function that is called multiple times in a short period of
time, and you want to make sure it is only "actually" called once (or at least
no more than a certain frequency).
For background on throttling and debouncing, see:
- <https://blog.openreplay.com/forever-functional-debouncing-and-throttling-for-performance>
- <https://css-tricks.com/debouncing-throttling-explained-examples/>
::: superqt.utils.qdebounced
options:
show_source: false
docstring_style: numpy
show_root_toc_entry: True
show_root_heading: True
::: superqt.utils.qthrottled
options:
show_source: false
docstring_style: numpy
show_root_toc_entry: True
show_root_heading: True
::: superqt.utils.QSignalDebouncer
options:
show_source: false
docstring_style: numpy
show_root_toc_entry: True
show_root_heading: True
::: superqt.utils.QSignalThrottler
options:
show_source: false
docstring_style: numpy
show_root_toc_entry: True
show_root_heading: True
::: superqt.utils._throttler.GenericSignalThrottler
options:
show_source: false
docstring_style: numpy
show_root_toc_entry: True
show_root_heading: True

View File

@@ -1,10 +0,0 @@
# 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/).

31
docs/widgets/index.md Normal file
View File

@@ -0,0 +1,31 @@
# Widgets
The following are QWidget subclasses:
## Sliders and Numerical Inputs
| Widget | Description |
| ----------- | --------------------- |
| [`QDoubleRangeSlider`](./qdoublerangeslider.md) | Multi-handle slider for float values |
| [`QDoubleSlider`](./qdoubleslider.md) | Slider for float values |
| [`QLabeledDoubleRangeSlider`](./qlabeleddoublerangeslider.md) | `QDoubleRangeSlider` variant with editable labels for each handle |
| [`QLabeledDoubleSlider`](./qlabeleddoubleslider.md) | `QSlider` for float values with editable `QSpinBox` with the current value |
| [`QLabeledRangeSlider`](./qlabeledrangeslider.md) | `QRangeSlider` variant, with editable labels for each handle |
| [`QLabeledSlider`](./qlabeledslider.md) | `QSlider` with editable `QSpinBox` that shows the current value |
| [`QLargeIntSpinBox`](./qlargeintspinbox.md) | `QSpinbox` that accepts arbitrarily large integers |
| [`QRangeSlider`](./qrangeslider.md) | Multi-handle slider |
## Labels and categorical inputs
| Widget | Description |
| ----------- | --------------------- |
| [`QElidingLabel`](./qelidinglabel.md) | A `QLabel` variant that will elide text (add `…`) to fit width. |
| [`QEnumComboBox`](./qenumcombobox.md) | `QComboBox` that populates the combobox from a python `Enum` |
| [`QSearchableComboBox`](./qsearchablecombobox.md) | `QComboBox` variant that filters available options based on text input |
| [`QSearchableListWidget`](./qsearchablelistwidget.md) | `QListWidget` variant with search field that filters available options |
## Frames and containers
| Widget | Description |
| ----------- | --------------------- |
| [`QCollapsible`](./qcollapsible.md) | A collapsible widget to hide and unhide child widgets. |

View File

@@ -0,0 +1,24 @@
# QCollapsible
Collapsible `QFrame` that can be expanded or collapsed by clicking on the header.
```python
from qtpy.QtWidgets import QApplication, QLabel, QPushButton
from superqt import QCollapsible
app = QApplication([])
collapsible = QCollapsible("Advanced analysis")
collapsible.addWidget(QLabel("This is the inside of the collapsible frame"))
for i in range(10):
collapsible.addWidget(QPushButton(f"Content button {i + 1}"))
collapsible.expand(animate=False)
collapsible.show()
app.exec_()
```
{{ show_widget(350) }}
{{ show_members('superqt.QCollapsible') }}

View File

@@ -0,0 +1,23 @@
# QDoubleRangeSlider
Float variant of [`QRangeSlider`](qrangeslider.md). (see that page for more details).
```python
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QApplication
from superqt import QDoubleRangeSlider
app = QApplication([])
slider = QDoubleRangeSlider(Qt.Orientation.Horizontal)
slider.setRange(0, 1)
slider.setValue((0.2, 0.8))
slider.show()
app.exec_()
```
{{ show_widget() }}
{{ show_members('superqt.QDoubleRangeSlider') }}

View File

@@ -0,0 +1,23 @@
# QDoubleSlider
`QSlider` variant that accepts floating point values.
```python
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QApplication
from superqt import QDoubleSlider
app = QApplication([])
slider = QDoubleSlider(Qt.Orientation.Horizontal)
slider.setRange(0, 1)
slider.setValue(0.5)
slider.show()
app.exec_()
```
{{ show_widget() }}
{{ show_members('superqt.QDoubleSlider') }}

View File

@@ -0,0 +1,26 @@
# QElidingLabel
`QLabel` variant that will elide text (i.e. add an ellipsis)
if it is too long to fit in the available space.
```python
from qtpy.QtWidgets import QApplication
from superqt import QElidingLabel
app = QApplication([])
widget = QElidingLabel(
"a skj skjfskfj sdlf sdfl sdlfk jsdf sdlkf jdsf dslfksdl sdlfk sdf sdl "
"fjsdlf kjsdlfk laskdfsal as lsdfjdsl kfjdslf asfd dslkjfldskf sdlkfj"
)
widget.setWordWrap(True)
widget.resize(300, 20)
widget.show()
app.exec_()
```
{{ show_widget(300) }}
{{ show_members('superqt.QElidingLabel') }}

View File

@@ -0,0 +1,72 @@
# QEnumComboBox
`QEnumComboBox` is a variant of
[`QComboBox`](https://doc.qt.io/qt-5/qcombobox.html) that populates the items in
the combobox based on a python `Enum` class. In addition to all the methods
provided by `QComboBox`, this subclass adds the methods
`enumClass`/`setEnumClass` to get/set the current `Enum` class represented by
the combobox, and `currentEnum`/`setCurrentEnum` to get/set the current `Enum`
member in the combobox. There is also a new signal `currentEnumChanged(enum)`
analogous to `currentIndexChanged` and `currentTextChanged`.
Method like `insertItem` and `addItem` are blocked and try of its usage will end
with `RuntimeError`
```python
from enum import Enum
from qtpy.QtWidgets import QApplication
from superqt import QEnumComboBox
class SampleEnum(Enum):
first = 1
second = 2
third = 3
app = QApplication([])
combo = QEnumComboBox()
combo.setEnumClass(SampleEnum)
combo.show()
app.exec_()
```
{{ show_widget() }}
Another option is to use optional `enum_class` argument of constructor and change
```python
# option A:
combo = QEnumComboBox()
combo.setEnumClass(SampleEnum)
# option B:
combo = QEnumComboBox(enum_class=SampleEnum)
```
## Allow `None`
`QEnumComboBox` also allows using `Optional` type annotation:
```python
from enum import Enum
from superqt import QEnumComboBox
class SampleEnum(Enum):
first = 1
second = 2
third = 3
# as usual:
# you must create a QApplication before create a widget.
combo = QEnumComboBox()
combo.setEnumClass(SampleEnum, allow_none=True)
```
In this case there is added option `----` and the `currentEnum()` method will
return `None` when it is selected.
{{ show_members('superqt.QEnumComboBox') }}

View File

@@ -0,0 +1,23 @@
# QLabeledDoubleRangeSlider
Labeled Float variant of [`QRangeSlider`](qrangeslider.md). (see that page for more details).
```python
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QApplication
from superqt import QLabeledDoubleRangeSlider
app = QApplication([])
slider = QLabeledDoubleRangeSlider(Qt.Orientation.Horizontal)
slider.setRange(0, 1)
slider.setValue((0.2, 0.8))
slider.show()
app.exec_()
```
{{ show_widget() }}
{{ show_members('superqt.QLabeledDoubleRangeSlider') }}

View File

@@ -0,0 +1,24 @@
# QLabeledDoubleSlider
[`QDoubleSlider`](./qdoubleslider.md) variant that shows an editable (SpinBox) label next to the slider.
```python
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QApplication
from superqt import QLabeledDoubleSlider
app = QApplication([])
slider = QLabeledDoubleSlider(Qt.Orientation.Horizontal)
slider.setRange(0, 2.5)
slider.setValue(1.3)
slider.show()
app.exec_()
```
{{ show_widget() }}
{{ show_members('superqt.QLabeledDoubleSlider') }}

View File

@@ -0,0 +1,29 @@
# QLabeledRangeSlider
Labeled variant of [`QRangeSlider`](qrangeslider.md). (see that page for more details).
```python
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QApplication
from superqt import QLabeledRangeSlider
app = QApplication([])
slider = QLabeledRangeSlider(Qt.Orientation.Horizontal)
slider.setValue((20, 80))
slider.show()
app.exec_()
```
{{ show_widget() }}
{{ show_members('superqt.QLabeledRangeSlider') }}
----
If you find that you need to fine tune the position of the handle labels:
- `QLabeledRangeSlider.label_shift_x`: adjust horizontal label position
- `QLabeledRangeSlider.label_shift_y`: adjust vertical label position

View File

@@ -0,0 +1,22 @@
# QLabeledSlider
`QSlider` variant that shows an editable (SpinBox) label next to the slider.
```python
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QApplication
from superqt import QLabeledSlider
app = QApplication([])
slider = QLabeledSlider(Qt.Orientation.Horizontal)
slider.setValue(42)
slider.show()
app.exec_()
```
{{ show_widget() }}
{{ show_members('superqt.QLabeledSlider') }}

View File

@@ -0,0 +1,23 @@
# QLargeIntSpinBox
`QSpinBox` variant that allows to enter large integers, without overflow.
```python
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QApplication
from superqt import QLargeIntSpinBox
app = QApplication([])
slider = QLargeIntSpinBox()
slider.setRange(0, 4.53e8)
slider.setValue(4.53e8)
slider.show()
app.exec_()
```
{{ show_widget(150) }}
{{ show_members('superqt.QLargeIntSpinBox') }}

View File

@@ -0,0 +1,229 @@
# QRangeSlider
A multi-handle slider widget than can be used to
select a range of values.
```python
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QApplication
from superqt import QRangeSlider
app = QApplication([])
slider = QRangeSlider(Qt.Orientation.Horizontal)
slider.setValue((20, 80))
slider.show()
app.exec_()
```
{{ show_widget() }}
- `QRangeSlider` inherits from [`QSlider`](https://doc.qt.io/qt-5/qslider.html)
and attempts to match the Qt API as closely as possible
- It uses platform-specific styles (for handle, groove, & ticks) but also supports
QSS style sheets.
- Supports mouse wheel events
- Supports more than 2 handles (e.g. `slider.setValue([0, 10, 60, 80])`)
As `QRangeSlider` inherits from
[`QtWidgets.QSlider`](https://doc.qt.io/qt-5/qslider.html), you can use all of
the same methods available in the [QSlider
API](https://doc.qt.io/qt-5/qslider.html). The major difference is that `value()`
and `sliderPosition()` are reimplemented as `tuples` of `int` (where the length of
the tuple is equal to the number of handles in the slider.)
These options are in addition to the Qt QSlider API, and control the behavior of the bar between handles.
| getter | setter | type | default | description |
| -------------------- | ------------------------------------------- | ------ | ------- | ------------------------------------------------------------------------------------------------ |
| `barIsVisible` | `setBarIsVisible` <br>`hideBar` / `showBar` | `bool` | `True` | <small>Whether the bar between handles is visible.</small> |
| `barMovesAllHandles` | `setBarMovesAllHandles` | `bool` | `True` | <small>Whether clicking on the bar moves all handles or just the nearest</small> |
| `barIsRigid` | `setBarIsRigid` | `bool` | `True` | <small>Whether bar length is constant or "elastic" when dragging the bar beyond min/max.</small> |
### Screenshots
??? title "code that generates the images below"
```python
import os
from qtpy import QtCore
from qtpy import QtWidgets as QtW
# patch for Qt 5.15 on macos >= 12
os.environ["USE_MAC_SLIDER_PATCH"] = "1"
from superqt import QRangeSlider # noqa
QSS = """
QSlider {
min-height: 20px;
}
QSlider::groove:horizontal {
border: 0px;
background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #888, stop:1 #ddd);
height: 20px;
border-radius: 10px;
}
QSlider::handle {
background: qradialgradient(cx:0, cy:0, radius: 1.2, fx:0.35,
fy:0.3, stop:0 #eef, stop:1 #002);
height: 20px;
width: 20px;
border-radius: 10px;
}
QSlider::sub-page:horizontal {
background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #227, stop:1 #77a);
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
}
QRangeSlider {
qproperty-barColor: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #227, stop:1 #77a);
}
"""
Horizontal = QtCore.Qt.Orientation.Horizontal
class DemoWidget(QtW.QWidget):
def __init__(self) -> None:
super().__init__()
reg_hslider = QtW.QSlider(Horizontal)
reg_hslider.setValue(50)
range_hslider = QRangeSlider(Horizontal)
range_hslider.setValue((20, 80))
multi_range_hslider = QRangeSlider(Horizontal)
multi_range_hslider.setValue((11, 33, 66, 88))
multi_range_hslider.setTickPosition(QtW.QSlider.TickPosition.TicksAbove)
styled_reg_hslider = QtW.QSlider(Horizontal)
styled_reg_hslider.setValue(50)
styled_reg_hslider.setStyleSheet(QSS)
styled_range_hslider = QRangeSlider(Horizontal)
styled_range_hslider.setValue((20, 80))
styled_range_hslider.setStyleSheet(QSS)
reg_vslider = QtW.QSlider(QtCore.Qt.Orientation.Vertical)
reg_vslider.setValue(50)
range_vslider = QRangeSlider(QtCore.Qt.Orientation.Vertical)
range_vslider.setValue((22, 77))
tick_vslider = QtW.QSlider(QtCore.Qt.Orientation.Vertical)
tick_vslider.setValue(55)
tick_vslider.setTickPosition(QtW.QSlider.TicksRight)
range_tick_vslider = QRangeSlider(QtCore.Qt.Orientation.Vertical)
range_tick_vslider.setValue((22, 77))
range_tick_vslider.setTickPosition(QtW.QSlider.TicksLeft)
szp = QtW.QSizePolicy.Maximum
left = QtW.QWidget()
left.setLayout(QtW.QVBoxLayout())
left.setContentsMargins(2, 2, 2, 2)
label1 = QtW.QLabel("Regular QSlider Unstyled")
label2 = QtW.QLabel("QRangeSliders Unstyled")
label3 = QtW.QLabel("Styled Sliders (using same stylesheet)")
label1.setSizePolicy(szp, szp)
label2.setSizePolicy(szp, szp)
label3.setSizePolicy(szp, szp)
left.layout().addWidget(label1)
left.layout().addWidget(reg_hslider)
left.layout().addWidget(label2)
left.layout().addWidget(range_hslider)
left.layout().addWidget(multi_range_hslider)
left.layout().addWidget(label3)
left.layout().addWidget(styled_reg_hslider)
left.layout().addWidget(styled_range_hslider)
right = QtW.QWidget()
right.setLayout(QtW.QHBoxLayout())
right.setContentsMargins(15, 5, 5, 0)
right.layout().setSpacing(30)
right.layout().addWidget(reg_vslider)
right.layout().addWidget(range_vslider)
right.layout().addWidget(tick_vslider)
right.layout().addWidget(range_tick_vslider)
self.setLayout(QtW.QHBoxLayout())
self.layout().addWidget(left)
self.layout().addWidget(right)
self.setGeometry(600, 300, 580, 300)
self.activateWindow()
self.show()
if __name__ == "__main__":
import sys
from pathlib import Path
dest = Path("screenshots")
dest.mkdir(exist_ok=True)
app = QtW.QApplication([])
demo = DemoWidget()
if "-snap" in sys.argv:
import platform
QtW.QApplication.processEvents()
demo.grab().save(str(dest / f"demo_{platform.system().lower()}.png"))
else:
app.exec_()
```
#### macOS
##### Catalina
![mac10](../images/demo_darwin10.png){ width=580; }
##### Big Sur
![mac11](../images/demo_darwin11.png){ width=580; }
#### Windows
![window](../images/demo_windows.png)
#### Linux
![linux](../images/demo_linux.png)
{{ show_members('superqt.sliders._sliders._GenericRangeSlider') }}
## Type changes
Note the following changes in types compared to the `QSlider` API:
```python
value() -> Tuple[int, ...]
```
```python
setValue(val: Sequence[int]) -> None
```
```python
# Signal
valueChanged(Tuple[int, ...])
```
```python
sliderPosition() -> Tuple[int, ...]
```
```python
setSliderPosition(val: Sequence[int]) -> None
```
```python
sliderMoved(Tuple[int, ...])
```

View File

@@ -0,0 +1,25 @@
# QSearchableComboBox
`QSearchableComboBox` is a variant of
[`QComboBox`](https://doc.qt.io/qt-5/qcombobox.html) that allow to filter list
of options by enter part of text. It could be drop in replacement for
`QComboBox`.
```python
from qtpy.QtWidgets import QApplication
from superqt import QSearchableComboBox
app = QApplication([])
combo = QSearchableComboBox()
combo.addItems(["foo", "bar", "baz", "foobar", "foobaz", "barbaz"])
combo.show()
app.exec_()
```
{{ show_widget() }}
{{ show_members('superqt.QSearchableComboBox') }}

View File

@@ -0,0 +1,28 @@
# QSearchableListWidget
`QSearchableListWidget` is a variant of
[`QListWidget`](https://doc.qt.io/qt-5/qlistwidget.html) that add text entry
above list widget that allow to filter list of available options.
Due to implementation details, this widget it does not inherit directly from
[`QListWidget`](https://doc.qt.io/qt-5/qlistwidget.html) but it does fully
satisfy its api. The only limitation is that it cannot be used as argument of
[`QListWidgetItem`](https://doc.qt.io/qt-5/qlistwidgetitem.html) constructor.
```python
from qtpy.QtWidgets import QApplication
from superqt import QSearchableListWidget
app = QApplication([])
slider = QSearchableListWidget()
slider.addItems(["foo", "bar", "baz", "foobar", "foobaz", "barbaz"])
slider.show()
app.exec_()
```
{{ show_widget() }}
{{ show_members('superqt.QSearchableListWidget') }}

View File

@@ -8,6 +8,7 @@ app = QApplication([])
slider = QDoubleSlider(Qt.Orientation.Horizontal)
slider.setRange(0, 1)
slider.setValue(0.5)
slider.resize(500, 50)
slider.show()
app.exec_()

55
mkdocs.yml Normal file
View File

@@ -0,0 +1,55 @@
site_name: superqt
site_url: https://github.com/napari/superqt
site_description: >-
missing widgets and components for PyQt/PySide
# Repository
repo_name: napari/superqt
repo_url: https://github.com/napari/superqt
# Copyright
copyright: Copyright &copy; 2021 - 2022 Talley Lambert
extra_css:
- stylesheets/extra.css
watch:
- src
theme:
name: material
features:
- navigation.instant
- navigation.indexes
- navigation.expand
# - navigation.tracking
# - navigation.tabs
- search.highlight
- search.suggest
markdown_extensions:
- admonition
- pymdownx.details
- pymdownx.superfences
- tables
- attr_list
- md_in_html
- pymdownx.emoji:
emoji_index: !!python/name:materialx.emoji.twemoji
emoji_generator: !!python/name:materialx.emoji.to_svg
plugins:
- search
- autorefs
- mkdocstrings
- macros:
module_name: docs/_macros
- mkdocstrings:
handlers:
python:
import:
- https://docs.python.org/3/objects.inv
options:
show_source: false
docstring_style: numpy
show_root_toc_entry: True
show_root_heading: True

View File

@@ -7,4 +7,4 @@ build-backend = "setuptools.build_meta"
write_to = "src/superqt/_version.py"
[tool.check-manifest]
ignore = ["src/superqt/_version.py"]
ignore = ["src/superqt/_version.py", "mkdocs.yml"]

View File

@@ -63,6 +63,10 @@ dev =
pytest-qt
tox
tox-conda
docs =
mkdocs-macros-plugin
mkdocs-material
mkdocstrings[python]
font_fa5 =
fonticon-fontawesome5
font_mi5 =

View File

@@ -8,7 +8,7 @@ from qtpy.QtWidgets import QFrame, QPushButton, QVBoxLayout, QWidget
class QCollapsible(QFrame):
"""A collapsible widget to hide and unhide child widgets.
Based on https://stackoverflow.com/a/68141638
Based on [https://stackoverflow.com/a/68141638](https://stackoverflow.com/a/68141638)
"""
_EXPANDED = ""

View File

@@ -2,14 +2,15 @@ from __future__ import annotations
__all__ = [
"addFont",
"Animation",
"ENTRY_POINT",
"font",
"icon",
"IconFont",
"IconFontMeta",
"IconOpts",
"Animation",
"pulse",
"setTextIcon",
"spin",
]
@@ -49,10 +50,11 @@ def icon(
The `glyph_key` (e.g. 'fa5s.smile') represents a Font-family & style, and a glpyh.
In most cases, the key should be provided by a plugin in the environment, like:
https://github.com/tlambert03/fonticon-fontawesome5 ('fa5s' & 'fa5r' prefixes)
https://github.com/tlambert03/fonticon-materialdesignicons6 ('mdi6' prefix)
...but fonts can also be added manually using :func:`addFont`.
- [fonticon-fontawesome5](https://pypi.org/project/fonticon-fontawesome5/) ('fa5s' & 'fa5r' prefixes)
- [fonticon-materialdesignicons6](https://pypi.org/project/fonticon-materialdesignicons6/) ('mdi6' prefix)
...but fonts can also be added manually using [`addFont`][superqt.fonticon.addFont].
Parameters
----------
@@ -96,19 +98,22 @@ def icon(
Examples
--------
# simple example (assumes the font-awesome5 plugin is installed)
simple example (using the string `'fa5s.smile'` assumes the `fonticon-fontawesome5`
plugin is installed)
>>> btn = QPushButton()
>>> btn.setIcon(icon('fa5s.smile'))
# can also directly import from fonticon_fa5
can also directly import from fonticon_fa5
>>> from fonticon_fa5 import FA5S
>>> btn.setIcon(icon(FA5S.smile))
# with animation
with animation
>>> btn2 = QPushButton()
>>> btn2.setIcon(icon(FA5S.spinner, animation=pulse(btn2)))
# complicated example
complicated example
>>> btn = QPushButton()
>>> btn.setIcon(
... icon(
@@ -152,7 +157,7 @@ def setTextIcon(widget: QWidget, glyph_key: str, size: Optional[float] = None) -
Parameters
----------
wdg : QWidget
widget : QWidget
A widget supporting a `setText` method.
glyph_key : str
String encapsulating a font-family, style, and glyph. e.g. 'fa5s.smile'.
@@ -190,10 +195,12 @@ def addFont(
to their unicode numbers. If a charmap is not provided, glyphs must be directly
accessed with their unicode as something like `key.\uffff`.
NOTE: in most cases, users will not need this.
Instead, they should install a font plugin, like:
https://github.com/tlambert03/fonticon-fontawesome5
https://github.com/tlambert03/fonticon-materialdesignicons6
!!! Note
in most cases, users will not need this. Instead, they should install a
font plugin, like:
- [fonticon-fontawesome5](https://pypi.org/project/fonticon-fontawesome5/)
- [fonticon-materialdesignicons6](https://pypi.org/project/fonticon-materialdesignicons6/)
Parameters
----------

View File

@@ -6,6 +6,8 @@ from qtpy.QtWidgets import QWidget
class Animation(ABC):
"""Base icon animation class."""
def __init__(self, parent_widget: QWidget, interval: int = 10, step: int = 1):
self.parent_widget = parent_widget
self.timer = QTimer()
@@ -25,6 +27,8 @@ class Animation(ABC):
class spin(Animation):
"""Animation that smoothly spins an icon."""
def animate(self, painter: QPainter):
if not self.timer.isActive():
self.timer.start()
@@ -36,5 +40,7 @@ class spin(Animation):
class pulse(spin):
"""Animation that spins an icon in slower, discrete steps."""
def __init__(self, parent_widget: QWidget = None):
super().__init__(parent_widget, interval=200, step=45)

View File

@@ -102,6 +102,23 @@ class IconOptionDict(TypedDict, total=False):
# IconOptions are.
@dataclass
class IconOpts:
"""Options for rendering an icon.
Parameters
----------
glyph_key : str, optional
The key of the glyph to use, e.g. `'fa5s.smile'`, by default `None`
scale_factor : float, optional
The scale factor to use, by default `None`
color : ValidColor, optional
The color to use, by default `None`. Colors may be specified as a string,
`QColor`, `Qt.GlobalColor`, or a 3 or 4-tuple of integers.
opacity : float, optional
The opacity to use, by default `None`
animation : Animation, optional
The animation to use, by default `None`
"""
glyph_key: Union[str, Unset] = _Unset
scale_factor: Union[float, Unset] = _Unset
color: Union[ValidColor, Unset] = _Unset
@@ -418,7 +435,7 @@ class QFontIconStore(QObject):
If you'd like to later use a fontkey in the form of `key.some-name`, then
`charmap` must be provided and provide a mapping for all of the glyph names
to their unicode numbers. If a charmap is not provided, glyphs must be directly
accessed with their unicode as something like `key.\uffff`.
accessed with their unicode as something like `key.\\uffff`.
Parameters
----------
@@ -509,7 +526,7 @@ class QFontIconStore(QObject):
) -> None:
"""Sets text on a widget to a specific font & glyph.
This is an alternative to setting a QIcon with a pixmap. It may
This is an alternative to setting a `QIcon` with a pixmap. It may
be easier to combine with dynamic stylesheets.
"""
setText = getattr(widget, "setText", None)

View File

@@ -68,14 +68,14 @@ class _GenericRangeSlider(_GenericSlider[Tuple], Generic[_T]):
def barIsRigid(self) -> bool:
"""Whether bar length is constant when dragging the bar.
If False, the bar can shorten when dragged beyond min/max. Default is True.
If `False`, the bar can shorten when dragged beyond min/max. Default is `True`.
"""
return self._bar_is_rigid
def setBarIsRigid(self, val: bool = True) -> None:
"""Whether bar length is constant when dragging the bar.
If False, the bar can shorten when dragged beyond min/max. Default is True.
If `False`, the bar can shorten when dragged beyond min/max. Default is `True`.
"""
self._bar_is_rigid = bool(val)
@@ -96,12 +96,18 @@ class _GenericRangeSlider(_GenericSlider[Tuple], Generic[_T]):
self._should_draw_bar = bool(val)
def hideBar(self) -> None:
"""Hide the bar between the first and last handle."""
self.setBarVisible(False)
def showBar(self) -> None:
"""Show the bar between the first and last handle."""
self.setBarVisible(True)
def applyMacStylePatch(self) -> str:
"""Apply a QSS patch to fix sliders on macos>=12 with QT < 6.
see [FAQ](../faq.md#sliders-not-dragging-properly-on-macos-12) for more details.
"""
super().applyMacStylePatch()
self._style._macpatch = True
@@ -205,6 +211,7 @@ class _GenericRangeSlider(_GenericSlider[Tuple], Generic[_T]):
self._style.brush_active = color
barColor = Property(QtGui.QBrush, _getBarColor, _setBarColor)
"""The color of the bar between the first and last handle."""
def _offsetAllPositions(self, offset: float, ref=None) -> None:
if ref is None:

View File

@@ -99,6 +99,10 @@ class _GenericSlider(QSlider, Generic[_T]):
self.applyMacStylePatch()
def applyMacStylePatch(self) -> str:
"""Apply a QSS patch to fix sliders on macos>=12 with QT < 6.
see [FAQ](../faq.md#sliders-not-dragging-properly-on-macos-12) for more details.
"""
self.setStyleSheet(MONTEREY_SLIDER_STYLES_FIX)
# ############### QtOverrides #######################

View File

@@ -185,9 +185,11 @@ class QLabeledSlider(_SliderProxy, QAbstractSlider):
self.setLayout(layout)
def edgeLabelMode(self) -> EdgeLabelMode:
"""Return current `EdgeLabelMode`."""
return self._edge_label_mode
def setEdgeLabelMode(self, opt: EdgeLabelMode) -> None:
"""Set the `EdgeLabelMode`."""
if opt is EdgeLabelMode.LabelIsRange:
raise ValueError(
"mode must be one of 'EdgeLabelMode.NoLabel' or "
@@ -283,9 +285,11 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
self.valueChanged = self._valueChanged
def handleLabelPosition(self) -> LabelPosition:
"""Return where/whether labels are shown adjacent to slider handles."""
return self._handle_label_position
def setHandleLabelPosition(self, opt: LabelPosition) -> LabelPosition:
"""Set where/whether labels are shown adjacent to slider handles."""
self._handle_label_position = opt
for lbl in self._handle_labels:
if not opt:
@@ -295,9 +299,11 @@ class QLabeledRangeSlider(_SliderProxy, QAbstractSlider):
self.setOrientation(self.orientation())
def edgeLabelMode(self) -> EdgeLabelMode:
"""Return current `EdgeLabelMode`."""
return self._edge_label_mode
def setEdgeLabelMode(self, opt: EdgeLabelMode):
"""Set `EdgeLabelMode`, controls what is shown at the min/max labels."""
self._edge_label_mode = opt
if not self._edge_label_mode:
self._min_label.hide()

View File

@@ -92,7 +92,7 @@ class WorkerBase(QRunnable, Generic[_R]):
signal emitter object. To allow identify which worker thread emitted signal.
"""
#: A set of Workers. Add to set using :meth:`WorkerBase.start`
#: A set of Workers. Add to set using `WorkerBase.start`
_worker_set: Set[WorkerBase] = set()
returned: SigInst[_R]
errored: SigInst[Exception]
@@ -113,11 +113,11 @@ class WorkerBase(QRunnable, Generic[_R]):
def __getattr__(self, name: str) -> SigInst:
"""Pass through attr requests to signals to simplify connection API.
The goal is to enable ``worker.yielded.connect`` instead of
``worker.signals.yielded.connect``. Because multiple inheritance of Qt
The goal is to enable `worker.yielded.connect` instead of
`worker.signals.yielded.connect`. Because multiple inheritance of Qt
classes is not well supported in PyQt, we have to use composition here
(signals are provided by QObjects, and QRunnable is not a QObject). So
this passthrough allows us to connect to signals on the ``_signals``
this passthrough allows us to connect to signals on the `_signals`
object.
"""
# the Signal object is actually a class attribute
@@ -134,11 +134,10 @@ class WorkerBase(QRunnable, Generic[_R]):
def quit(self) -> None:
"""Send a request to abort the worker.
.. note::
!!! note
It is entirely up to subclasses to honor this method by checking
``self.abort_requested`` periodically in their ``worker.work``
method, and exiting if ``True``.
`self.abort_requested` periodically in their `worker.work`
method, and exiting if `True`.
"""
self._abort_requested = True
@@ -160,20 +159,20 @@ class WorkerBase(QRunnable, Generic[_R]):
The order of method calls when starting a worker is:
.. code-block:: none
```
calls QThreadPool.globalInstance().start(worker)
| triggered by the QThreadPool.start() method
| | called by worker.run
| | |
V V V
worker.start -> worker.run -> worker.work
```
**This** is the function that actually gets called when calling
:func:`QThreadPool.start(worker)`. It simply wraps the :meth:`work`
`QThreadPool.start(worker)`. It simply wraps the `work()`
method, and emits a few signals. Subclasses should NOT override this
method (except with good reason), and instead should implement
:meth:`work`.
`work()`.
"""
self.started.emit()
self._running = True
@@ -208,26 +207,26 @@ class WorkerBase(QRunnable, Generic[_R]):
The end-user should never need to call this function.
But subclasses must implement this method (See
:meth:`GeneratorFunction.work` for an example implementation).
Minimally, it should check ``self.abort_requested`` periodically and
[`GeneratorFunction.work`][superqt.utils._qthreading.GeneratorWorker.work] for an example implementation).
Minimally, it should check `self.abort_requested` periodically and
exit if True.
Examples
--------
.. code-block:: python
```python
class MyWorker(WorkerBase):
class MyWorker(WorkerBase):
def work(self):
i = 0
while True:
if self.abort_requested:
self.aborted.emit()
break
i += 1
if i > max_iters:
break
time.sleep(0.5)
def work(self):
i = 0
while True:
if self.abort_requested:
self.aborted.emit()
break
i += 1
if i > max_iters:
break
time.sleep(0.5)
```
"""
raise NotImplementedError(
f'"{self.__class__.__name__}" failed to define work() method'
@@ -238,14 +237,14 @@ class WorkerBase(QRunnable, Generic[_R]):
The order of method calls when starting a worker is:
.. code-block:: none
```
calls QThreadPool.globalInstance().start(worker)
| triggered by the QThreadPool.start() method
| | called by worker.run
| | |
V V V
worker.start -> worker.run -> worker.work
```
"""
if self in self._worker_set:
raise RuntimeError("This worker is already started!")
@@ -271,33 +270,33 @@ class WorkerBase(QRunnable, Generic[_R]):
def await_workers(cls, msecs: int = None) -> None:
"""Ask all workers to quit, and wait up to `msec` for quit.
Attempts to clean up all running workers by calling ``worker.quit()``
method. Any workers in the ``WorkerBase._worker_set`` set will have this
Attempts to clean up all running workers by calling `worker.quit()`
method. Any workers in the `WorkerBase._worker_set` set will have this
method.
By default, this function will block indefinitely, until worker threads
finish. If a timeout is provided, a ``RuntimeError`` will be raised if
finish. If a timeout is provided, a `RuntimeError` will be raised if
the workers do not gracefully exit in the time requests, but the threads
will NOT be killed. It is (currently) left to the user to use their OS
to force-quit rogue threads.
.. important::
!!! important
If the user does not put any yields in their function, and the function
is super long, it will just hang... For instance, there's no graceful
way to kill this thread in python:
.. code-block:: python
@thread_worker
def ZZZzzz():
time.sleep(10000000)
```python
@thread_worker
def ZZZzzz():
time.sleep(10000000)
```
This is why it's always advisable to use a generator that periodically
yields for long-running computations in another thread.
See `this stack-overflow post
<https://stackoverflow.com/questions/323972/is-there-any-way-to-kill-a-thread>`_
See [this stack-overflow
post](https://stackoverflow.com/questions/323972/is-there-any-way-to-kill-a-thread)
for a good discussion on the difficulty of killing a rogue python thread:
Parameters
@@ -326,12 +325,11 @@ class WorkerBase(QRunnable, Generic[_R]):
class FunctionWorker(WorkerBase[_R]):
"""QRunnable with signals that wraps a simple long-running function.
.. note::
``FunctionWorker`` does not provide a way to stop a very long-running
function (e.g. ``time.sleep(10000)``). So whenever possible, it is
better to implement your long running function as a generator that
yields periodically, and use the :class:`GeneratorWorker` instead.
!!! note
`FunctionWorker` does not provide a way to stop a very long-running
function (e.g. `time.sleep(10000)`). So whenever possible, it is better to
implement your long running function as a generator that yields periodically,
and use the [`GeneratorWorker`][superqt.utils.GeneratorWorker] instead.
Parameters
----------
@@ -345,7 +343,7 @@ class FunctionWorker(WorkerBase[_R]):
Raises
------
TypeError
If ``func`` is a generator function and not a regular function.
If `func` is a generator function and not a regular function.
"""
def __init__(self, func: Callable[_P, _R], *args, **kwargs):
@@ -454,7 +452,7 @@ class GeneratorWorker(WorkerBase, Generic[_Y, _S, _R]):
return exc.value
except RuntimeError as exc:
# The worker has probably been deleted. warning will be
# emitted in ``WorkerBase.run``
# emitted in `WorkerBase.run`
return exc
return None
@@ -534,38 +532,39 @@ def create_worker(
) -> Union[FunctionWorker, GeneratorWorker]:
"""Convenience function to start a function in another thread.
By default, uses :class:`Worker`, but a custom ``WorkerBase`` subclass may
be provided. If so, it must be a subclass of :class:`Worker`, which
defines a standard set of signals and a run method.
By default, uses `FunctionWorker` for functions and `GeneratorWorker` for
generators, but a custom `WorkerBase` subclass may be provided. If so, it must be a
subclass of `WorkerBase`, which defines a standard set of signals and a run method.
Parameters
----------
func : Callable
The function to call in another thread.
_start_thread : bool, optional
_start_thread : bool
Whether to immediaetly start the thread. If False, the returned worker
must be manually started with ``worker.start()``. by default it will be
``False`` if the ``_connect`` argument is ``None``, otherwise ``True``.
must be manually started with `worker.start()`. by default it will be
`False` if the `_connect` argument is `None`, otherwise `True`.
_connect : Dict[str, Union[Callable, Sequence]], optional
A mapping of ``"signal_name"`` -> ``callable`` or list of ``callable``:
A mapping of `"signal_name"` -> `callable` or list of `callable`:
callback functions to connect to the various signals offered by the
worker class. by default None
_worker_class : type of GeneratorWorker or FunctionWorker, optional
The :class`WorkerBase` to instantiate, by default
:class:`FunctionWorker` will be used if ``func`` is a regular function,
and :class:`GeneratorWorker` will be used if it is a generator.
_ignore_errors : bool, optional
If ``False`` (the default), errors raised in the other thread will be
worker class. by default `None`
_worker_class : type of `GeneratorWorker` or `FunctionWorker`, optional
The [`WorkerBase`][superqt.utils.WorkerBase] to instantiate, by default
[`FunctionWorker`][superqt.utils.FunctionWorker] will be used if `func` is a
regular function, and [`GeneratorWorker`][superqt.utils.GeneratorWorker] will be
used if it is a generator.
_ignore_errors : bool
If `False` (the default), errors raised in the other thread will be
reraised in the main thread (makes debugging significantly easier).
*args
will be passed to ``func``
will be passed to `func`
**kwargs
will be passed to ``func``
will be passed to `func`
Returns
-------
worker : WorkerBase
An instantiated worker. If ``_start_thread`` was ``False``, the worker
An instantiated worker. If `_start_thread` was `False`, the worker
will have a `.start()` method that can be used to start the thread.
Raises
@@ -573,18 +572,17 @@ def create_worker(
TypeError
If a worker_class is provided that is not a subclass of WorkerBase.
TypeError
If _connect is provided and is not a dict of ``{str: callable}``
If _connect is provided and is not a dict of `{str: callable}`
Examples
--------
.. code-block:: python
def long_function(duration):
import time
time.sleep(duration)
worker = create_worker(long_function, 10)
```python
def long_function(duration):
import time
time.sleep(duration)
worker = create_worker(long_function, 10)
```
"""
worker: Union[FunctionWorker, GeneratorWorker]
@@ -616,7 +614,7 @@ def create_worker(
getattr(worker, key).connect(v)
# if the user has not provided a default connection for the "errored"
# signal... and they have not explicitly set ``ignore_errors=True``
# signal... and they have not explicitly set `ignore_errors=True`
# Then rereaise any errors from the thread.
if not _ignore_errors and not (_connect or {}).get("errored", False):
@@ -672,55 +670,55 @@ def thread_worker(
):
"""Decorator that runs a function in a separate thread when called.
When called, the decorated function returns a :class:`WorkerBase`. See
:func:`create_worker` for additional keyword arguments that can be used
When called, the decorated function returns a [`WorkerBase`][superqt.utils.WorkerBase]. See
[`create_worker`][superqt.utils.create_worker] for additional keyword arguments that can be used
when calling the function.
The returned worker will have these signals:
- *started*: emitted when the work is started
- *finished*: emitted when the work is finished
- *returned*: emitted with return value
- *errored*: emitted with error object on Exception
- **started**: emitted when the work is started
- **finished**: emitted when the work is finished
- **returned**: emitted with return value
- **errored**: emitted with error object on Exception
It will also have a ``worker.start()`` method that can be used to start
It will also have a `worker.start()` method that can be used to start
execution of the function in another thread. (useful if you need to connect
callbacks to signals prior to execution)
If the decorated function is a generator, the returned worker will also
provide these signals:
- *yielded*: emitted with yielded values
- *paused*: emitted when a running job has successfully paused
- *resumed*: emitted when a paused job has successfully resumed
- *aborted*: emitted when a running job is successfully aborted
- **yielded**: emitted with yielded values
- **paused**: emitted when a running job has successfully paused
- **resumed**: emitted when a paused job has successfully resumed
- **aborted**: emitted when a running job is successfully aborted
And these methods:
- *quit*: ask the thread to quit
- *toggle_paused*: toggle the running state of the thread.
- *send*: send a value into the generator. (This requires that your
decorator function uses the ``value = yield`` syntax)
- **quit**: ask the thread to quit
- **toggle_paused**: toggle the running state of the thread.
- **send**: send a value into the generator. (This requires that your
decorator function uses the `value = yield` syntax)
Parameters
----------
function : callable
Function to call in another thread. For communication between threads
may be a generator function.
start_thread : bool, optional
start_thread : bool
Whether to immediaetly start the thread. If False, the returned worker
must be manually started with ``worker.start()``. by default it will be
``False`` if the ``_connect`` argument is ``None``, otherwise ``True``.
connect : Dict[str, Union[Callable, Sequence]], optional
A mapping of ``"signal_name"`` -> ``callable`` or list of ``callable``:
must be manually started with `worker.start()`. by default it will be
`False` if the `_connect` argument is `None`, otherwise `True`.
connect : Dict[str, Union[Callable, Sequence]]
A mapping of `"signal_name"` -> `callable` or list of `callable`:
callback functions to connect to the various signals offered by the
worker class. by default None
worker_class : Type[WorkerBase], optional
The :class`WorkerBase` to instantiate, by default
:class:`FunctionWorker` will be used if ``func`` is a regular function,
and :class:`GeneratorWorker` will be used if it is a generator.
ignore_errors : bool, optional
If ``False`` (the default), errors raised in the other thread will be
worker_class : Type[WorkerBase]
The [`WorkerBase`][superqt.utils.WorkerBase] to instantiate, by default
[`FunctionWorker`][superqt.utils.FunctionWorker] will be used if `func` is a regular function,
and [`GeneratorWorker`][superqt.utils.GeneratorWorker] will be used if it is a generator.
ignore_errors : bool
If `False` (the default), errors raised in the other thread will be
reraised in the main thread (makes debugging significantly easier).
Returns
@@ -731,25 +729,26 @@ def thread_worker(
Examples
--------
.. code-block:: python
```python
@thread_worker
def long_function(start, end):
# do work, periodically yielding
i = start
while i <= end:
time.sleep(0.1)
yield i
@thread_worker
def long_function(start, end):
# do work, periodically yielding
i = start
while i <= end:
time.sleep(0.1)
yield i
# do teardown
return 'anything'
# do teardown
return 'anything'
# call the function to start running in another thread.
worker = long_function()
# call the function to start running in another thread.
worker = long_function()
# connect signals here if desired... or they may be added using the
# `connect` argument in the `@thread_worker` decorator... in which
# case the worker will start immediately when long_function() is called
worker.start()
# connect signals here if desired... or they may be added using the
# `connect` argument in the `@thread_worker` decorator... in which
# case the worker will start immediately when long_function() is called
worker.start()
```
"""
def _inner(func):
@@ -804,36 +803,35 @@ def new_worker_qthread(
_connect: Dict[str, Callable] = None,
**kwargs,
):
"""This is a convenience function to start a worker in a Qthread.
"""This is a convenience function to start a worker in a `QThread`.
In most cases, the @thread_worker decorator is sufficient and preferable.
But this allows the user to completely customize the Worker object.
However, they must then maintain control over the thread and clean up
In most cases, the [thread_worker][superqt.utils.thread_worker] decorator is
sufficient and preferable. But this allows the user to completely customize the
Worker object. However, they must then maintain control over the thread and clean up
appropriately.
It follows the pattern described here:
https://www.qt.io/blog/2010/06/17/youre-doing-it-wrong
and
https://doc.qt.io/qt-5/qthread.html#details
It follows the pattern described
[here](https://www.qt.io/blog/2010/06/17/youre-doing-it-wrong) and in the [qt thread
docs](https://doc.qt.io/qt-5/qthread.html#details)
see also:
https://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/
A QThread object is not a thread! It should be thought of as a class to
*manage* a thread, not as the actual code or object that runs in that
A QThread object is not a thread! It should be thought of as a class to *manage* a
thread, not as the actual code or object that runs in that
thread. The QThread object is created on the main thread and lives there.
Worker objects which derive from QObject are the things that actually do
the work. They can be moved to a QThread as is done here.
.. note:: Mostly ignorable detail
??? "Mostly ignorable detail"
While the signals/slots syntax of the worker looks very similar to
standard "single-threaded" signals & slots, note that inter-thread
signals and slots (automatically) use an event-based QueuedConnection,
while intra-thread signals use a DirectConnection. See `Signals and
Slots Across Threads
<https://doc.qt.io/qt-5/threads-qobject.html#signals-and-slots-across-threads>`_
signals and slots (automatically) use an event-based QueuedConnection, while
intra-thread signals use a DirectConnection. See [Signals and Slots Across
Threads](https://doc.qt.io/qt-5/threads-qobject.html#signals-and-slots-across-threads>)
Parameters
----------
@@ -843,7 +841,7 @@ def new_worker_qthread(
_start_thread : bool
If True, thread will be started immediately, otherwise, thread must
be manually started with thread.start().
_connect : dict, optional
_connect : dict
Optional dictionary of {signal: function} to connect to the new worker.
for instance: _connect = {'incremented': myfunc} will result in:
worker.incremented.connect(myfunc)
@@ -863,33 +861,33 @@ def new_worker_qthread(
--------
Create some QObject that has a long-running work method:
.. code-block:: python
```python
class Worker(QObject):
class Worker(QObject):
finished = Signal()
increment = Signal(int)
finished = Signal()
increment = Signal(int)
def __init__(self, argument):
super().__init__()
self.argument = argument
def __init__(self, argument):
super().__init__()
self.argument = argument
@Slot()
def work(self):
# some long running task...
import time
for i in range(10):
time.sleep(1)
self.increment.emit(i)
self.finished.emit()
worker, thread = new_worker_qthread(
Worker,
'argument',
_start_thread=True,
_connect={'increment': print},
)
@Slot()
def work(self):
# some long running task...
import time
for i in range(10):
time.sleep(1)
self.increment.emit(i)
self.finished.emit()
worker, thread = new_worker_qthread(
Worker,
'argument',
_start_thread=True,
_connect={'increment': print},
)
```
"""
if _connect and not isinstance(_connect, dict):

View File

@@ -103,7 +103,7 @@ class GenericSignalThrottler(QObject):
self.timeoutChanged.emit(timeout)
def timerType(self) -> Qt.TimerType:
"""Return current Qt.TimerType."""
"""Return current `Qt.TimerType`."""
return self._timer.timerType()
def setTimerType(self, timerType: Qt.TimerType) -> None:
@@ -136,11 +136,11 @@ class GenericSignalThrottler(QObject):
assert self._timer.isActive()
def cancel(self) -> None:
""" "Cancel and pending emissions."""
"""Cancel any pending emissions."""
self._hasPendingEmission = False
def flush(self) -> None:
""" "Force emission of any pending emissions."""
"""Force emission of any pending emissions."""
self._maybeEmitTriggered()
def _emitTriggered(self) -> None: