diff --git a/.gitignore b/.gitignore
index bc8255a..4ffe446 100644
--- a/.gitignore
+++ b/.gitignore
@@ -82,3 +82,4 @@ src/superqt/_version.py
screenshots
.mypy_cache
+docs/_auto_images/
diff --git a/README.md b/README.md
index 5d00199..6867d38 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,5 @@
#  superqt!
-
[](https://github.com/napari/superqt/raw/master/LICENSE)
[](https://pypi.org/project/superqt)
[
+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)
-- [Labeled Sliders](docs/sliders.md#labeled-sliders) (sliders with linked
- spinboxes)
-
-- 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
diff --git a/docs/_macros.py b/docs/_macros.py
new file mode 100644
index 0000000..9d2694c
--- /dev/null
+++ b/docs/_macros.py
@@ -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"{{ 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`{first_q}`\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
diff --git a/docs/combobox.md b/docs/combobox.md
deleted file mode 100644
index b7b88bf..0000000
--- a/docs/combobox.md
+++ /dev/null
@@ -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`.
diff --git a/docs/faq.md b/docs/faq.md
new file mode 100644
index 0000000..558e37b
--- /dev/null
+++ b/docs/faq.md
@@ -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)
+ ```
diff --git a/docs/fonticon.md b/docs/fonticon.md
deleted file mode 100644
index e69de29..0000000
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..3d9b860
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,29 @@
+# superqt
+
+##  "missing" widgets and components for PyQt/PySide
+
+This repository aims to provide high-quality community-contributed Qt widgets
+and components for [PyQt](https://riverbankcomputing.com/software/pyqt/) &
+[PySide](https://www.qt.io/qt-for-python) that are not provided in the native
+QtWidgets module.
+
+Components are tested on:
+
+- macOS, Windows, & Linux
+- Python 3.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.
diff --git a/docs/listwidgets.md b/docs/listwidgets.md
deleted file mode 100644
index d869634..0000000
--- a/docs/listwidgets.md
+++ /dev/null
@@ -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.
diff --git a/docs/sliders.md b/docs/sliders.md
deleted file mode 100644
index 17cc0fe..0000000
--- a/docs/sliders.md
+++ /dev/null
@@ -1,257 +0,0 @@
-# Sliders
-
-
-
-
-- `QRangeSlider` inherits from [`QSlider`](https://doc.qt.io/qt-5/qslider.html)
- and attempts to match the Qt API as closely as possible
-- Uses platform-specific styles (for handle, groove, & ticks) but also supports
- QSS style sheets.
-- Supports mouse wheel and keypress (soon) events
-- Supports more than 2 handles (e.g. `slider.setValue([0, 10, 60, 80])`)
-
-*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`
`hideBar` / `showBar` | `bool` | `True` | Whether the bar between handles is visible. |
-| `barMovesAllHandles` | `setBarMovesAllHandles` | `bool` | `True` | Whether clicking on the bar moves all handles or just the nearest |
-| `barIsRigid` | `setBarIsRigid` | `bool` | `True` | Whether bar length is constant or "elastic" when dragging the bar beyond min/max. |
-------
-
-### 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)
-
-
-See style sheet used for this example
-
-```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;
-}
-```
-
-
-
-#### macOS
-
-##### Catalina
-
-
-
-##### Big Sur
-
-
-
-#### Windows
-
-
-
-#### Linux
-
-
-
-## Labeled Sliders
-
-This package also includes two "labeled" slider variants. One for `QRangeSlider`, and one for the native `QSlider`:
-
-### `QLabeledRangeSlider`
-
-
-
-```python
-from superqt import QLabeledRangeSlider
-```
-
-This has the same API as `QRangeSlider` with the following additional options:
-
-#### `handleLabelPosition`/`setHandleLabelPosition`
-
-Where/whether labels are shown adjacent to slider handles.
-
-**type:** `QLabeledRangeSlider.LabelPosition`
-
-**default:** `LabelPosition.LabelsAbove`
-
-*options:*
-
-- `LabelPosition.NoLabel` (no labels shown adjacent to handles)
-- `LabelPosition.LabelsAbove`
-- `LabelPosition.LabelsBelow`
-- `LabelPosition.LabelsRight` (alias for `LabelPosition.LabelsAbove`)
-- `LabelPosition.LabelsLeft` (alias for `LabelPosition.LabelsBelow`)
-
-#### `edgeLabelMode`/`setEdgeLabelMode`
-
-**type:** `QLabeledRangeSlider.EdgeLabelMode`
-
-**default:** `EdgeLabelMode.LabelIsRange`
-
-*options:*
-
-- `EdgeLabelMode.NoLabel`: no labels shown at slider extremes
-- `EdgeLabelMode.LabelIsRange`: edge labels shown the min/max values
-- `EdgeLabelMode.LabelIsValue`: edge labels shown the slider range
-
-#### fine tuning position of labels:
-
-If you find that you need to fine tune the position of the handle labels:
-
-- `QLabeledRangeSlider.label_shift_x`: adjust horizontal label position
-- `QLabeledRangeSlider.label_shift_y`: adjust vertical label position
-
-### `QLabeledSlider`
-
-
-
-```python
-from superqt import QLabeledSlider
-```
-
-(no additional options at this point)
-
-
-## 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)
-```
diff --git a/docs/utilities/code_syntax_highlight.md b/docs/utilities/code_syntax_highlight.md
new file mode 100644
index 0000000..225a3eb
--- /dev/null
+++ b/docs/utilities/code_syntax_highlight.md
@@ -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') }}
diff --git a/docs/utilities/fonticon.md b/docs/utilities/fonticon.md
new file mode 100644
index 0000000..aeb0ec1
--- /dev/null
+++ b/docs/utilities/fonticon.md
@@ -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
+
+-
+-
+-
+
+`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:
+
+-
+-
+-
+
+## 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
diff --git a/docs/utilities/index.md b/docs/utilities/index.md
new file mode 100644
index 0000000..dbe6c35
--- /dev/null
+++ b/docs/utilities/index.md
@@ -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. |
diff --git a/docs/utilities/qmessagehandler.md b/docs/utilities/qmessagehandler.md
new file mode 100644
index 0000000..eeb928e
--- /dev/null
+++ b/docs/utilities/qmessagehandler.md
@@ -0,0 +1,8 @@
+# QMessageHandler
+
+::: superqt.utils.QMessageHandler
+ options:
+ heading_level: 3
+ show_signature_annotations: True
+ docstring_style: numpy
+ show_bases: False
diff --git a/docs/decorators.md b/docs/utilities/thread_decorators.md
similarity index 79%
rename from docs/decorators.md
rename to docs/utilities/thread_decorators.md
index 083d969..7ad4afd 100644
--- a/docs/decorators.md
+++ b/docs/utilities/thread_decorators.md
@@ -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
diff --git a/docs/utilities/threading.md b/docs/utilities/threading.md
new file mode 100644
index 0000000..efa100d
--- /dev/null
+++ b/docs/utilities/threading.md
@@ -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
diff --git a/docs/utilities/throttling.md b/docs/utilities/throttling.md
new file mode 100644
index 0000000..8b2283a
--- /dev/null
+++ b/docs/utilities/throttling.md
@@ -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:
+
+-
+-
+
+::: 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
diff --git a/docs/utils.md b/docs/utils.md
deleted file mode 100644
index c43efdc..0000000
--- a/docs/utils.md
+++ /dev/null
@@ -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/).
diff --git a/docs/widgets/index.md b/docs/widgets/index.md
new file mode 100644
index 0000000..2e5534f
--- /dev/null
+++ b/docs/widgets/index.md
@@ -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. |
diff --git a/docs/widgets/qcollapsible.md b/docs/widgets/qcollapsible.md
new file mode 100644
index 0000000..9e52fc2
--- /dev/null
+++ b/docs/widgets/qcollapsible.md
@@ -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') }}
diff --git a/docs/widgets/qdoublerangeslider.md b/docs/widgets/qdoublerangeslider.md
new file mode 100644
index 0000000..128ee67
--- /dev/null
+++ b/docs/widgets/qdoublerangeslider.md
@@ -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') }}
diff --git a/docs/widgets/qdoubleslider.md b/docs/widgets/qdoubleslider.md
new file mode 100644
index 0000000..137a338
--- /dev/null
+++ b/docs/widgets/qdoubleslider.md
@@ -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') }}
diff --git a/docs/widgets/qelidinglabel.md b/docs/widgets/qelidinglabel.md
new file mode 100644
index 0000000..0bc42e4
--- /dev/null
+++ b/docs/widgets/qelidinglabel.md
@@ -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') }}
diff --git a/docs/widgets/qenumcombobox.md b/docs/widgets/qenumcombobox.md
new file mode 100644
index 0000000..466d952
--- /dev/null
+++ b/docs/widgets/qenumcombobox.md
@@ -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') }}
diff --git a/docs/widgets/qlabeleddoublerangeslider.md b/docs/widgets/qlabeleddoublerangeslider.md
new file mode 100644
index 0000000..0829c52
--- /dev/null
+++ b/docs/widgets/qlabeleddoublerangeslider.md
@@ -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') }}
diff --git a/docs/widgets/qlabeleddoubleslider.md b/docs/widgets/qlabeleddoubleslider.md
new file mode 100644
index 0000000..29a17ce
--- /dev/null
+++ b/docs/widgets/qlabeleddoubleslider.md
@@ -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') }}
diff --git a/docs/widgets/qlabeledrangeslider.md b/docs/widgets/qlabeledrangeslider.md
new file mode 100644
index 0000000..10012b2
--- /dev/null
+++ b/docs/widgets/qlabeledrangeslider.md
@@ -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
diff --git a/docs/widgets/qlabeledslider.md b/docs/widgets/qlabeledslider.md
new file mode 100644
index 0000000..66cb2de
--- /dev/null
+++ b/docs/widgets/qlabeledslider.md
@@ -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') }}
diff --git a/docs/widgets/qlargeintspinbox.md b/docs/widgets/qlargeintspinbox.md
new file mode 100644
index 0000000..69f0ee4
--- /dev/null
+++ b/docs/widgets/qlargeintspinbox.md
@@ -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') }}
diff --git a/docs/widgets/qrangeslider.md b/docs/widgets/qrangeslider.md
new file mode 100644
index 0000000..237f87b
--- /dev/null
+++ b/docs/widgets/qrangeslider.md
@@ -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`
`hideBar` / `showBar` | `bool` | `True` | Whether the bar between handles is visible. |
+| `barMovesAllHandles` | `setBarMovesAllHandles` | `bool` | `True` | Whether clicking on the bar moves all handles or just the nearest |
+| `barIsRigid` | `setBarIsRigid` | `bool` | `True` | Whether bar length is constant or "elastic" when dragging the bar beyond min/max. |
+
+### Screenshots
+
+??? title "code that generates the images below"
+
+ ```python
+ import os
+
+ from qtpy import QtCore
+ from qtpy import QtWidgets as QtW
+
+ # patch for Qt 5.15 on macos >= 12
+ os.environ["USE_MAC_SLIDER_PATCH"] = "1"
+
+ from superqt import QRangeSlider # noqa
+
+ QSS = """
+ QSlider {
+ min-height: 20px;
+ }
+
+ QSlider::groove:horizontal {
+ border: 0px;
+ background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #888, stop:1 #ddd);
+ height: 20px;
+ border-radius: 10px;
+ }
+
+ QSlider::handle {
+ background: qradialgradient(cx:0, cy:0, radius: 1.2, fx:0.35,
+ fy:0.3, stop:0 #eef, stop:1 #002);
+ height: 20px;
+ width: 20px;
+ border-radius: 10px;
+ }
+
+ QSlider::sub-page:horizontal {
+ background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #227, stop:1 #77a);
+ border-top-left-radius: 10px;
+ border-bottom-left-radius: 10px;
+ }
+
+ QRangeSlider {
+ qproperty-barColor: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #227, stop:1 #77a);
+ }
+ """
+
+ Horizontal = QtCore.Qt.Orientation.Horizontal
+
+
+ class DemoWidget(QtW.QWidget):
+ def __init__(self) -> None:
+ super().__init__()
+
+ reg_hslider = QtW.QSlider(Horizontal)
+ reg_hslider.setValue(50)
+ range_hslider = QRangeSlider(Horizontal)
+ range_hslider.setValue((20, 80))
+ multi_range_hslider = QRangeSlider(Horizontal)
+ multi_range_hslider.setValue((11, 33, 66, 88))
+ multi_range_hslider.setTickPosition(QtW.QSlider.TickPosition.TicksAbove)
+
+ styled_reg_hslider = QtW.QSlider(Horizontal)
+ styled_reg_hslider.setValue(50)
+ styled_reg_hslider.setStyleSheet(QSS)
+ styled_range_hslider = QRangeSlider(Horizontal)
+ styled_range_hslider.setValue((20, 80))
+ styled_range_hslider.setStyleSheet(QSS)
+
+ reg_vslider = QtW.QSlider(QtCore.Qt.Orientation.Vertical)
+ reg_vslider.setValue(50)
+ range_vslider = QRangeSlider(QtCore.Qt.Orientation.Vertical)
+ range_vslider.setValue((22, 77))
+
+ tick_vslider = QtW.QSlider(QtCore.Qt.Orientation.Vertical)
+ tick_vslider.setValue(55)
+ tick_vslider.setTickPosition(QtW.QSlider.TicksRight)
+ range_tick_vslider = QRangeSlider(QtCore.Qt.Orientation.Vertical)
+ range_tick_vslider.setValue((22, 77))
+ range_tick_vslider.setTickPosition(QtW.QSlider.TicksLeft)
+
+ szp = QtW.QSizePolicy.Maximum
+ left = QtW.QWidget()
+ left.setLayout(QtW.QVBoxLayout())
+ left.setContentsMargins(2, 2, 2, 2)
+ label1 = QtW.QLabel("Regular QSlider Unstyled")
+ label2 = QtW.QLabel("QRangeSliders Unstyled")
+ label3 = QtW.QLabel("Styled Sliders (using same stylesheet)")
+ label1.setSizePolicy(szp, szp)
+ label2.setSizePolicy(szp, szp)
+ label3.setSizePolicy(szp, szp)
+ left.layout().addWidget(label1)
+ left.layout().addWidget(reg_hslider)
+ left.layout().addWidget(label2)
+ left.layout().addWidget(range_hslider)
+ left.layout().addWidget(multi_range_hslider)
+ left.layout().addWidget(label3)
+ left.layout().addWidget(styled_reg_hslider)
+ left.layout().addWidget(styled_range_hslider)
+
+ right = QtW.QWidget()
+ right.setLayout(QtW.QHBoxLayout())
+ right.setContentsMargins(15, 5, 5, 0)
+ right.layout().setSpacing(30)
+ right.layout().addWidget(reg_vslider)
+ right.layout().addWidget(range_vslider)
+ right.layout().addWidget(tick_vslider)
+ right.layout().addWidget(range_tick_vslider)
+
+ self.setLayout(QtW.QHBoxLayout())
+ self.layout().addWidget(left)
+ self.layout().addWidget(right)
+ self.setGeometry(600, 300, 580, 300)
+ self.activateWindow()
+ self.show()
+
+
+ if __name__ == "__main__":
+
+ import sys
+ from pathlib import Path
+
+ dest = Path("screenshots")
+ dest.mkdir(exist_ok=True)
+
+ app = QtW.QApplication([])
+ demo = DemoWidget()
+
+ if "-snap" in sys.argv:
+ import platform
+
+ QtW.QApplication.processEvents()
+ demo.grab().save(str(dest / f"demo_{platform.system().lower()}.png"))
+ else:
+ app.exec_()
+ ```
+
+#### macOS
+
+##### Catalina
+
+{ width=580; }
+
+##### Big Sur
+
+{ width=580; }
+
+#### Windows
+
+
+
+#### Linux
+
+
+
+
+{{ show_members('superqt.sliders._sliders._GenericRangeSlider') }}
+
+## Type changes
+
+Note the following changes in types compared to the `QSlider` API:
+
+```python
+value() -> Tuple[int, ...]
+```
+
+```python
+setValue(val: Sequence[int]) -> None
+```
+
+```python
+# Signal
+valueChanged(Tuple[int, ...])
+```
+
+```python
+sliderPosition() -> Tuple[int, ...]
+```
+
+```python
+setSliderPosition(val: Sequence[int]) -> None
+```
+
+```python
+sliderMoved(Tuple[int, ...])
+```
diff --git a/docs/widgets/qsearchablecombobox.md b/docs/widgets/qsearchablecombobox.md
new file mode 100644
index 0000000..f5dea86
--- /dev/null
+++ b/docs/widgets/qsearchablecombobox.md
@@ -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') }}
diff --git a/docs/widgets/qsearchablelistwidget.md b/docs/widgets/qsearchablelistwidget.md
new file mode 100644
index 0000000..ba90579
--- /dev/null
+++ b/docs/widgets/qsearchablelistwidget.md
@@ -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') }}
diff --git a/examples/basic_float.py b/examples/double_slider.py
similarity index 91%
rename from examples/basic_float.py
rename to examples/double_slider.py
index 82cc6a5..eb0351f 100644
--- a/examples/basic_float.py
+++ b/examples/double_slider.py
@@ -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_()
diff --git a/examples/collapsible.py b/examples/qcollapsible.py
similarity index 100%
rename from examples/collapsible.py
rename to examples/qcollapsible.py
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..f753ede
--- /dev/null
+++ b/mkdocs.yml
@@ -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 © 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
diff --git a/pyproject.toml b/pyproject.toml
index 38ea762..4654cea 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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"]
diff --git a/setup.cfg b/setup.cfg
index b5fa5db..0f20449 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -63,6 +63,10 @@ dev =
pytest-qt
tox
tox-conda
+docs =
+ mkdocs-macros-plugin
+ mkdocs-material
+ mkdocstrings[python]
font_fa5 =
fonticon-fontawesome5
font_mi5 =
diff --git a/src/superqt/collapsible/_collapsible.py b/src/superqt/collapsible/_collapsible.py
index 37b47b9..f5325dc 100644
--- a/src/superqt/collapsible/_collapsible.py
+++ b/src/superqt/collapsible/_collapsible.py
@@ -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 = "▼ "
diff --git a/src/superqt/fonticon/__init__.py b/src/superqt/fonticon/__init__.py
index 7b4538c..6062ab0 100644
--- a/src/superqt/fonticon/__init__.py
+++ b/src/superqt/fonticon/__init__.py
@@ -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
----------
diff --git a/src/superqt/fonticon/_animations.py b/src/superqt/fonticon/_animations.py
index c86bab3..e01c4ab 100644
--- a/src/superqt/fonticon/_animations.py
+++ b/src/superqt/fonticon/_animations.py
@@ -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)
diff --git a/src/superqt/fonticon/_qfont_icon.py b/src/superqt/fonticon/_qfont_icon.py
index 60f2109..4491660 100644
--- a/src/superqt/fonticon/_qfont_icon.py
+++ b/src/superqt/fonticon/_qfont_icon.py
@@ -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)
diff --git a/src/superqt/sliders/_generic_range_slider.py b/src/superqt/sliders/_generic_range_slider.py
index 02a4136..6087747 100644
--- a/src/superqt/sliders/_generic_range_slider.py
+++ b/src/superqt/sliders/_generic_range_slider.py
@@ -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:
diff --git a/src/superqt/sliders/_generic_slider.py b/src/superqt/sliders/_generic_slider.py
index c748b47..6d5bdcb 100644
--- a/src/superqt/sliders/_generic_slider.py
+++ b/src/superqt/sliders/_generic_slider.py
@@ -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 #######################
diff --git a/src/superqt/sliders/_labeled.py b/src/superqt/sliders/_labeled.py
index b587e98..1bf50ed 100644
--- a/src/superqt/sliders/_labeled.py
+++ b/src/superqt/sliders/_labeled.py
@@ -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()
diff --git a/src/superqt/utils/_qthreading.py b/src/superqt/utils/_qthreading.py
index 2494b46..eb8eab8 100644
--- a/src/superqt/utils/_qthreading.py
+++ b/src/superqt/utils/_qthreading.py
@@ -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
- `_
+ 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
- `_
+ 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):
diff --git a/src/superqt/utils/_throttler.py b/src/superqt/utils/_throttler.py
index fbb5176..97563a2 100644
--- a/src/superqt/utils/_throttler.py
+++ b/src/superqt/utils/_throttler.py
@@ -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: