sciwidgets 0.1.2__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sciwidgets-0.1.2/src/sciwidgets.egg-info → sciwidgets-0.2.0}/PKG-INFO +12 -10
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/README.md +9 -8
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/pyproject.toml +3 -1
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/labels/__main__.py +1 -1
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/params/__main__.py +1 -1
- sciwidgets-0.2.0/src/sciwidgets/window/__init__.py +6 -0
- sciwidgets-0.2.0/src/sciwidgets/window/__main__.py +106 -0
- sciwidgets-0.2.0/src/sciwidgets/window/menu_bar.py +247 -0
- sciwidgets-0.2.0/src/sciwidgets/window/status_bar.py +263 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0/src/sciwidgets.egg-info}/PKG-INFO +12 -10
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets.egg-info/SOURCES.txt +5 -1
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets.egg-info/requires.txt +1 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/LICENSE +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/setup.cfg +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/__init__.py +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/dialogs/__init__.py +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/dialogs/__main__.py +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/dialogs/message_dialogs.py +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/formatting.py +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/labels/__init__.py +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/labels/math_label.py +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/params/__init__.py +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/params/base.py +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/params/combo_box.py +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/params/line_edit.py +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/params/numeric_line_edit.py +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/params/sliders/__init__.py +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/params/sliders/__main__.py +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/params/sliders/_defaults.py +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/params/sliders/_dialogs.py +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/params/sliders/_right_click_overlay.py +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/params/sliders/base.py +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/params/sliders/multi.py +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/params/sliders/range.py +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/params/sliders/single.py +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/params/toggle.py +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets/style.py +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets.egg-info/dependency_links.txt +0 -0
- {sciwidgets-0.1.2 → sciwidgets-0.2.0}/src/sciwidgets.egg-info/top_level.txt +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sciwidgets
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: PySide6 widgets for scientific desktop apps: math labels, sliders, parameter inputs and dialogs
|
|
5
5
|
Author-email: Vahid Anari <vahid.anari8@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/vahid-anari/sciwidgets
|
|
8
8
|
Project-URL: Repository, https://github.com/vahid-anari/sciwidgets
|
|
9
9
|
Project-URL: Issues, https://github.com/vahid-anari/sciwidgets/issues
|
|
10
|
-
Keywords: pyside6,qt,gui,widgets,scientific,slider,range-slider,latex,matplotlib,dialogs
|
|
10
|
+
Keywords: pyside6,qt,gui,widgets,scientific,slider,range-slider,latex,matplotlib,dialogs,status-bar,menu
|
|
11
11
|
Classifier: Development Status :: 3 - Alpha
|
|
12
12
|
Classifier: Intended Audience :: Science/Research
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
@@ -24,6 +24,7 @@ License-File: LICENSE
|
|
|
24
24
|
Requires-Dist: PySide6<7,>=6.5
|
|
25
25
|
Requires-Dist: matplotlib>=3.7
|
|
26
26
|
Requires-Dist: numpy>=1.24
|
|
27
|
+
Requires-Dist: psutil>=5.9
|
|
27
28
|
Provides-Extra: dev
|
|
28
29
|
Requires-Dist: build; extra == "dev"
|
|
29
30
|
Requires-Dist: twine; extra == "dev"
|
|
@@ -35,7 +36,7 @@ PySide6 widgets for scientific desktop apps: math labels rendered with
|
|
|
35
36
|
LaTeX/mathtext, dialogs, and parameter inputs such as sliders, range sliders
|
|
36
37
|
and validated number fields.
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
<img src="https://raw.githubusercontent.com/vahid-anari/sciwidgets/main/docs/images/sliders_demo.png" width="900" alt="Sliders demo">
|
|
39
40
|
|
|
40
41
|
Right-click menu, Set Value and Edit Configuration:
|
|
41
42
|
|
|
@@ -64,13 +65,14 @@ and moved into this package so other apps can share them.
|
|
|
64
65
|
|
|
65
66
|
## Components
|
|
66
67
|
|
|
67
|
-
| Import | Contents |
|
|
68
|
-
|
|
69
|
-
| `sciwidgets.
|
|
70
|
-
| `sciwidgets.params` | `
|
|
71
|
-
| `sciwidgets.
|
|
72
|
-
| `sciwidgets.
|
|
73
|
-
| `sciwidgets.
|
|
68
|
+
| Import | Contents | Docs |
|
|
69
|
+
|--------|----------|------|
|
|
70
|
+
| `sciwidgets.params` (sliders) | `FloatSlider`, `IntSlider`, `ArraySlider`, `FloatRangeSlider`, `IntRangeSlider`, `MultiVariableSlider`, `SliderFeature` | [Sliders](https://github.com/vahid-anari/sciwidgets/blob/main/docs/sliders.md) |
|
|
71
|
+
| `sciwidgets.params` (inputs) | `ParameterLineEdit`, `ParameterComboBox`, `ParameterToggle`, `NumericLineEdit` | [Parameter widgets](https://github.com/vahid-anari/sciwidgets/blob/main/docs/params.md) |
|
|
72
|
+
| `sciwidgets.labels` | `MathLabel`, `set_latex_enabled` | [Labels](https://github.com/vahid-anari/sciwidgets/blob/main/docs/labels.md) |
|
|
73
|
+
| `sciwidgets.dialogs` | `show_information`, `show_warning`, `show_critical`, `show_rich_information`, `show_about_dialog`, `ask_question` | [Dialogs](https://github.com/vahid-anari/sciwidgets/blob/main/docs/dialogs.md) |
|
|
74
|
+
| `sciwidgets.window` | `MenuBarController`, `StatusBarController` | [Window helpers](https://github.com/vahid-anari/sciwidgets/blob/main/docs/window.md) |
|
|
75
|
+
| `sciwidgets.style`, `sciwidgets.formatting` | `apply_sci_style`, `SciStyle`, `value_to_text`, `pretty_sci_text` | [Style and formatting](https://github.com/vahid-anari/sciwidgets/blob/main/docs/style.md) |
|
|
74
76
|
|
|
75
77
|
## Install
|
|
76
78
|
|
|
@@ -4,7 +4,7 @@ PySide6 widgets for scientific desktop apps: math labels rendered with
|
|
|
4
4
|
LaTeX/mathtext, dialogs, and parameter inputs such as sliders, range sliders
|
|
5
5
|
and validated number fields.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
<img src="https://raw.githubusercontent.com/vahid-anari/sciwidgets/main/docs/images/sliders_demo.png" width="900" alt="Sliders demo">
|
|
8
8
|
|
|
9
9
|
Right-click menu, Set Value and Edit Configuration:
|
|
10
10
|
|
|
@@ -33,13 +33,14 @@ and moved into this package so other apps can share them.
|
|
|
33
33
|
|
|
34
34
|
## Components
|
|
35
35
|
|
|
36
|
-
| Import | Contents |
|
|
37
|
-
|
|
38
|
-
| `sciwidgets.
|
|
39
|
-
| `sciwidgets.params` | `
|
|
40
|
-
| `sciwidgets.
|
|
41
|
-
| `sciwidgets.
|
|
42
|
-
| `sciwidgets.
|
|
36
|
+
| Import | Contents | Docs |
|
|
37
|
+
|--------|----------|------|
|
|
38
|
+
| `sciwidgets.params` (sliders) | `FloatSlider`, `IntSlider`, `ArraySlider`, `FloatRangeSlider`, `IntRangeSlider`, `MultiVariableSlider`, `SliderFeature` | [Sliders](https://github.com/vahid-anari/sciwidgets/blob/main/docs/sliders.md) |
|
|
39
|
+
| `sciwidgets.params` (inputs) | `ParameterLineEdit`, `ParameterComboBox`, `ParameterToggle`, `NumericLineEdit` | [Parameter widgets](https://github.com/vahid-anari/sciwidgets/blob/main/docs/params.md) |
|
|
40
|
+
| `sciwidgets.labels` | `MathLabel`, `set_latex_enabled` | [Labels](https://github.com/vahid-anari/sciwidgets/blob/main/docs/labels.md) |
|
|
41
|
+
| `sciwidgets.dialogs` | `show_information`, `show_warning`, `show_critical`, `show_rich_information`, `show_about_dialog`, `ask_question` | [Dialogs](https://github.com/vahid-anari/sciwidgets/blob/main/docs/dialogs.md) |
|
|
42
|
+
| `sciwidgets.window` | `MenuBarController`, `StatusBarController` | [Window helpers](https://github.com/vahid-anari/sciwidgets/blob/main/docs/window.md) |
|
|
43
|
+
| `sciwidgets.style`, `sciwidgets.formatting` | `apply_sci_style`, `SciStyle`, `value_to_text`, `pretty_sci_text` | [Style and formatting](https://github.com/vahid-anari/sciwidgets/blob/main/docs/style.md) |
|
|
43
44
|
|
|
44
45
|
## Install
|
|
45
46
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sciwidgets"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "PySide6 widgets for scientific desktop apps: math labels, sliders, parameter inputs and dialogs"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -14,6 +14,7 @@ authors = [{ name = "Vahid Anari", email = "vahid.anari8@gmail.com" }]
|
|
|
14
14
|
keywords = [
|
|
15
15
|
"pyside6", "qt", "gui", "widgets", "scientific",
|
|
16
16
|
"slider", "range-slider", "latex", "matplotlib", "dialogs",
|
|
17
|
+
"status-bar", "menu",
|
|
17
18
|
]
|
|
18
19
|
classifiers = [
|
|
19
20
|
"Development Status :: 3 - Alpha",
|
|
@@ -31,6 +32,7 @@ dependencies = [
|
|
|
31
32
|
"PySide6>=6.5,<7",
|
|
32
33
|
"matplotlib>=3.7",
|
|
33
34
|
"numpy>=1.24",
|
|
35
|
+
"psutil>=5.9",
|
|
34
36
|
]
|
|
35
37
|
|
|
36
38
|
[project.optional-dependencies]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Demo for the window helpers: ``python -m sciwidgets.window``."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from PySide6.QtCore import QTimer
|
|
7
|
+
from PySide6.QtGui import QAction, QKeySequence
|
|
8
|
+
from PySide6.QtWidgets import QApplication, QMainWindow, QPlainTextEdit
|
|
9
|
+
|
|
10
|
+
from sciwidgets.window import MenuBarController, StatusBarController
|
|
11
|
+
|
|
12
|
+
MENU_SPEC = {
|
|
13
|
+
"File": [
|
|
14
|
+
{"id": "open", "text": "Open…", "shortcut": QKeySequence.Open},
|
|
15
|
+
{"id": "recent", "submenu": "Open Recent", "items": [
|
|
16
|
+
{"id": "recent_1", "text": "maser_run_1.json"},
|
|
17
|
+
{"id": "recent_2", "text": "maser_run_2.json"},
|
|
18
|
+
{"id": "sep"},
|
|
19
|
+
{"id": "clear_recent", "text": "Clear Menu"},
|
|
20
|
+
]},
|
|
21
|
+
{"id": "sep"},
|
|
22
|
+
{"id": "save", "text": "Save", "shortcut": QKeySequence.Save},
|
|
23
|
+
{"id": "sep"},
|
|
24
|
+
{"id": "quit", "text": "Quit", "shortcut": QKeySequence.Quit, "role": QAction.QuitRole},
|
|
25
|
+
],
|
|
26
|
+
"Edit": [
|
|
27
|
+
{"id": "modify", "text": "Change Something", "shortcut": "Ctrl+E"},
|
|
28
|
+
],
|
|
29
|
+
"Run": [
|
|
30
|
+
{"id": "solve", "text": "Solve (2 s, window stays responsive)", "shortcut": "Ctrl+R"},
|
|
31
|
+
{"id": "solve_blocking", "text": "Solve (2 s, blocking)", "shortcut": "Ctrl+Shift+R"},
|
|
32
|
+
{"id": "sep"},
|
|
33
|
+
{"id": "fail", "text": "Simulate Error"},
|
|
34
|
+
],
|
|
35
|
+
"View": [
|
|
36
|
+
{"id": "toolbar", "text": "Show Toolbar", "checkable": True, "checked": True},
|
|
37
|
+
],
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _demo_main() -> int:
|
|
42
|
+
"""Run a main window with a menu bar, a toolbar and a status bar.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
int: Qt application exit code.
|
|
46
|
+
"""
|
|
47
|
+
app = QApplication(sys.argv)
|
|
48
|
+
win = QMainWindow()
|
|
49
|
+
win.setWindowTitle("Window helpers demo")
|
|
50
|
+
|
|
51
|
+
log = QPlainTextEdit()
|
|
52
|
+
log.setReadOnly(True)
|
|
53
|
+
win.setCentralWidget(log)
|
|
54
|
+
|
|
55
|
+
menu = MenuBarController(win, MENU_SPEC, native_menubar=False)
|
|
56
|
+
status = StatusBarController(win, path_label="File")
|
|
57
|
+
|
|
58
|
+
# The same QActions can also live on a toolbar
|
|
59
|
+
toolbar = win.addToolBar("Main")
|
|
60
|
+
for action_id in ("open", "save", "solve"):
|
|
61
|
+
toolbar.addAction(menu.action(action_id))
|
|
62
|
+
|
|
63
|
+
def finish_solve() -> None:
|
|
64
|
+
status.set_state("Ready")
|
|
65
|
+
menu.set_enabled("solve", True)
|
|
66
|
+
menu.set_enabled("solve_blocking", True)
|
|
67
|
+
log.appendPlainText("solve finished")
|
|
68
|
+
|
|
69
|
+
def on_action(menu_path: str, action_id: str, checked: bool) -> None:
|
|
70
|
+
log.appendPlainText(f"{menu_path} → {action_id} (checked={checked})")
|
|
71
|
+
if action_id in ("open", "recent_1", "recent_2"):
|
|
72
|
+
name = "maser_run_2.json" if action_id == "recent_2" else "maser_run_1.json"
|
|
73
|
+
status.set_path(f"~/runs/{name}")
|
|
74
|
+
elif action_id == "save":
|
|
75
|
+
status.set_path(status.get_path() or "~/runs/untitled.json")
|
|
76
|
+
elif action_id == "modify":
|
|
77
|
+
status.set_modified(True)
|
|
78
|
+
elif action_id == "solve":
|
|
79
|
+
menu.set_enabled("solve", False)
|
|
80
|
+
menu.set_enabled("solve_blocking", False)
|
|
81
|
+
status.set_state("Solving…", busy=True)
|
|
82
|
+
QTimer.singleShot(2000, finish_solve)
|
|
83
|
+
elif action_id == "solve_blocking":
|
|
84
|
+
status.set_state("Solving…", busy=True)
|
|
85
|
+
|
|
86
|
+
def blocking_work() -> None:
|
|
87
|
+
time.sleep(2.0) # stands in for a long computation
|
|
88
|
+
status.set_state("Ready")
|
|
89
|
+
|
|
90
|
+
QTimer.singleShot(50, blocking_work) # return to the event loop first, so the state is drawn
|
|
91
|
+
elif action_id == "fail":
|
|
92
|
+
status.set_state("Error")
|
|
93
|
+
elif action_id == "toolbar":
|
|
94
|
+
toolbar.setVisible(checked)
|
|
95
|
+
elif action_id == "quit":
|
|
96
|
+
win.close()
|
|
97
|
+
|
|
98
|
+
menu.actionTriggered.connect(on_action)
|
|
99
|
+
|
|
100
|
+
win.resize(640, 360)
|
|
101
|
+
win.show()
|
|
102
|
+
return app.exec()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
raise SystemExit(_demo_main())
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Declarative menu-bar controller for a main window.
|
|
2
|
+
|
|
3
|
+
The menu bar is described by a plain Python mapping and built in one call.
|
|
4
|
+
Nested submenus, checkable actions, shortcuts and menu roles are supported,
|
|
5
|
+
and every action is reported through a single signal.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Dict, List, Optional, Union
|
|
11
|
+
|
|
12
|
+
from PySide6.QtCore import QObject, Signal
|
|
13
|
+
from PySide6.QtGui import QAction, QKeySequence
|
|
14
|
+
from PySide6.QtWidgets import QMainWindow, QMenu
|
|
15
|
+
|
|
16
|
+
__all__ = ["MenuBarController", "MenuSpec"]
|
|
17
|
+
|
|
18
|
+
MenuSpec = Dict[str, List[Dict[str, Any]]]
|
|
19
|
+
"""Menu specification: menu title -> list of item dictionaries."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MenuBarController(QObject):
|
|
23
|
+
"""Build and manage a menu bar from a declarative specification.
|
|
24
|
+
|
|
25
|
+
The specification maps each top-level menu title to a list of items. An
|
|
26
|
+
item is one of:
|
|
27
|
+
|
|
28
|
+
* an action: ``{"id": "save", "text": "Save", "shortcut": "Ctrl+S"}``
|
|
29
|
+
with optional ``enabled``, ``checkable``, ``checked`` and ``role``;
|
|
30
|
+
* a separator: ``{"id": "sep"}``;
|
|
31
|
+
* a submenu: ``{"submenu": "Open Recent", "items": [...]}``.
|
|
32
|
+
|
|
33
|
+
Action ids must be unique across the whole menu bar; they are used to
|
|
34
|
+
look actions up later.
|
|
35
|
+
|
|
36
|
+
Signals:
|
|
37
|
+
actionTriggered (str, str, bool): Emitted when the user triggers an
|
|
38
|
+
action, with the menu path (e.g. ``"File/Open Recent"``), the
|
|
39
|
+
action id and the checked state.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
actionTriggered = Signal(str, str, bool)
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
window: QMainWindow,
|
|
47
|
+
menu_spec: Optional[MenuSpec] = None,
|
|
48
|
+
*,
|
|
49
|
+
native_menubar: bool = True,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Initialize the controller and build the menu bar.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
window: Main window that owns the menu bar.
|
|
55
|
+
menu_spec: Menu specification. ``None`` builds an empty menu bar.
|
|
56
|
+
native_menubar: Whether to use the platform's native menu bar
|
|
57
|
+
(the global menu bar on macOS) when available.
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
ValueError: If an action has no ``id`` or an id is used twice.
|
|
61
|
+
"""
|
|
62
|
+
super().__init__(window)
|
|
63
|
+
self._w = window
|
|
64
|
+
self._menu_spec: MenuSpec = {}
|
|
65
|
+
self._actions: Dict[str, QAction] = {}
|
|
66
|
+
self.set_menu_spec(menu_spec or {}, native_menubar=native_menubar)
|
|
67
|
+
|
|
68
|
+
# ----- validation -----
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _check_ids(menu_spec: MenuSpec) -> None:
|
|
71
|
+
"""Check that every action has an id and that no id is repeated.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
menu_spec: Menu specification to check.
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
ValueError: If an action has no ``id`` or an id is used twice.
|
|
78
|
+
"""
|
|
79
|
+
seen: Dict[str, str] = {}
|
|
80
|
+
|
|
81
|
+
def walk(items: List[Dict[str, Any]], path: str) -> None:
|
|
82
|
+
for item in items:
|
|
83
|
+
if "submenu" in item:
|
|
84
|
+
walk(item.get("items", []), f"{path}/{item['submenu']}")
|
|
85
|
+
continue
|
|
86
|
+
action_id = str(item.get("id", "")).strip()
|
|
87
|
+
if not action_id:
|
|
88
|
+
raise ValueError(f"Menu item in '{path}' must define 'id': {item}")
|
|
89
|
+
if action_id == "sep":
|
|
90
|
+
continue
|
|
91
|
+
if action_id in seen:
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"Duplicate menu action id {action_id!r} in '{path}' "
|
|
94
|
+
f"(already used in '{seen[action_id]}')"
|
|
95
|
+
)
|
|
96
|
+
seen[action_id] = path
|
|
97
|
+
|
|
98
|
+
for menu_name, items in menu_spec.items():
|
|
99
|
+
walk(items, menu_name)
|
|
100
|
+
|
|
101
|
+
# ----- construction -----
|
|
102
|
+
@staticmethod
|
|
103
|
+
def _to_keyseq(shortcut: Union[None, str, QKeySequence, QKeySequence.StandardKey]) -> Optional[QKeySequence]:
|
|
104
|
+
"""Convert a shortcut value to a ``QKeySequence``.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
shortcut: ``None``, a string such as ``"Ctrl+S"``, a
|
|
108
|
+
``QKeySequence`` or a standard key such as
|
|
109
|
+
``QKeySequence.Save``.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
The key sequence, or ``None`` if no shortcut was given.
|
|
113
|
+
"""
|
|
114
|
+
if shortcut is None:
|
|
115
|
+
return None
|
|
116
|
+
if isinstance(shortcut, QKeySequence):
|
|
117
|
+
return shortcut
|
|
118
|
+
return QKeySequence(shortcut)
|
|
119
|
+
|
|
120
|
+
def _add_action(self, menu: QMenu, menu_name: str, item: Dict[str, Any]) -> None:
|
|
121
|
+
"""Create an action from an item specification and add it to a menu.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
menu: Menu that receives the action.
|
|
125
|
+
menu_name: Menu path reported by ``actionTriggered``.
|
|
126
|
+
item: Item specification with at least an ``id``.
|
|
127
|
+
"""
|
|
128
|
+
action_id = str(item["id"]).strip()
|
|
129
|
+
act = QAction(str(item.get("text", action_id)), self._w)
|
|
130
|
+
act.setEnabled(bool(item.get("enabled", True)))
|
|
131
|
+
checkable = bool(item.get("checkable", False))
|
|
132
|
+
act.setCheckable(checkable)
|
|
133
|
+
if checkable:
|
|
134
|
+
act.setChecked(bool(item.get("checked", False)))
|
|
135
|
+
|
|
136
|
+
ks = self._to_keyseq(item.get("shortcut"))
|
|
137
|
+
if ks is not None:
|
|
138
|
+
act.setShortcut(ks)
|
|
139
|
+
role = item.get("role")
|
|
140
|
+
if role is not None:
|
|
141
|
+
act.setMenuRole(role)
|
|
142
|
+
|
|
143
|
+
act.triggered.connect(lambda c, m=menu_name, a=action_id: self.actionTriggered.emit(m, a, c))
|
|
144
|
+
menu.addAction(act)
|
|
145
|
+
self._actions[action_id] = act
|
|
146
|
+
|
|
147
|
+
def _populate_menu(self, menu: QMenu, menu_name: str, items: List[Dict[str, Any]]) -> None:
|
|
148
|
+
"""Fill a menu from a list of item specifications.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
menu: Menu to fill.
|
|
152
|
+
menu_name: Menu path of this branch.
|
|
153
|
+
items: Item specifications.
|
|
154
|
+
"""
|
|
155
|
+
for item in items:
|
|
156
|
+
if "submenu" in item:
|
|
157
|
+
sub = menu.addMenu(str(item["submenu"]))
|
|
158
|
+
self._populate_menu(sub, f"{menu_name}/{item['submenu']}", item.get("items", []))
|
|
159
|
+
elif str(item["id"]).strip() == "sep":
|
|
160
|
+
menu.addSeparator()
|
|
161
|
+
else:
|
|
162
|
+
self._add_action(menu, menu_name, item)
|
|
163
|
+
|
|
164
|
+
def _build_menus(self, native_menubar: bool) -> None:
|
|
165
|
+
"""Build the menu bar from the stored specification.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
native_menubar: Whether to use the platform's native menu bar.
|
|
169
|
+
"""
|
|
170
|
+
menubar = self._w.menuBar()
|
|
171
|
+
menubar.setNativeMenuBar(native_menubar)
|
|
172
|
+
menubar.clear()
|
|
173
|
+
for menu_name, items in self._menu_spec.items():
|
|
174
|
+
menu = menubar.addMenu(menu_name)
|
|
175
|
+
self._populate_menu(menu, menu_name, items)
|
|
176
|
+
|
|
177
|
+
# ----- public API -----
|
|
178
|
+
def set_menu_spec(self, menu_spec: MenuSpec, native_menubar: bool = True) -> None:
|
|
179
|
+
"""Replace the specification and rebuild the menu bar.
|
|
180
|
+
|
|
181
|
+
The new specification is checked before the current menu bar is
|
|
182
|
+
touched.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
menu_spec: New menu specification.
|
|
186
|
+
native_menubar: Whether to use the platform's native menu bar.
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
ValueError: If an action has no ``id`` or an id is used twice.
|
|
190
|
+
"""
|
|
191
|
+
self._check_ids(menu_spec)
|
|
192
|
+
self._menu_spec = menu_spec
|
|
193
|
+
self._actions.clear()
|
|
194
|
+
self._build_menus(native_menubar)
|
|
195
|
+
|
|
196
|
+
def action(self, action_id: str) -> QAction:
|
|
197
|
+
"""Return the ``QAction`` for an id, e.g. to add it to a toolbar.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
action_id: Action identifier.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
The action.
|
|
204
|
+
|
|
205
|
+
Raises:
|
|
206
|
+
KeyError: If no action has this id.
|
|
207
|
+
"""
|
|
208
|
+
try:
|
|
209
|
+
return self._actions[action_id]
|
|
210
|
+
except KeyError:
|
|
211
|
+
raise KeyError(f"unknown menu action id {action_id!r}") from None
|
|
212
|
+
|
|
213
|
+
def set_enabled(self, action_id: str, enabled: bool) -> None:
|
|
214
|
+
"""Enable or disable an action. Unknown ids are ignored.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
action_id: Action identifier.
|
|
218
|
+
enabled: New enabled state.
|
|
219
|
+
"""
|
|
220
|
+
act = self._actions.get(action_id)
|
|
221
|
+
if act is not None:
|
|
222
|
+
act.setEnabled(enabled)
|
|
223
|
+
|
|
224
|
+
def set_checked(self, action_id: str, checked: bool) -> None:
|
|
225
|
+
"""Set the checked state of a checkable action (no signal).
|
|
226
|
+
|
|
227
|
+
Unknown ids are ignored.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
action_id: Action identifier.
|
|
231
|
+
checked: New checked state.
|
|
232
|
+
"""
|
|
233
|
+
act = self._actions.get(action_id)
|
|
234
|
+
if act is not None:
|
|
235
|
+
act.setChecked(checked)
|
|
236
|
+
|
|
237
|
+
def get_checked(self, action_id: str) -> bool:
|
|
238
|
+
"""Return the checked state of an action.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
action_id: Action identifier.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
The checked state, or ``False`` if the id is unknown.
|
|
245
|
+
"""
|
|
246
|
+
act = self._actions.get(action_id)
|
|
247
|
+
return act.isChecked() if act is not None else False
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Status-bar controller showing a state, a document path, elapsed time and memory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from time import perf_counter_ns
|
|
8
|
+
from typing import Iterable, Optional, Union
|
|
9
|
+
|
|
10
|
+
import psutil
|
|
11
|
+
from PySide6.QtCore import QObject, Qt, QTimer
|
|
12
|
+
from PySide6.QtWidgets import QFrame, QHBoxLayout, QLabel, QLayout, QMainWindow, QWidget
|
|
13
|
+
|
|
14
|
+
__all__ = ["StatusBarController"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _compact_number(value: float) -> str:
|
|
18
|
+
"""Format a number with fewer decimals as it grows (``1.23``, ``12.3``, ``123``)."""
|
|
19
|
+
if value < 10:
|
|
20
|
+
return f"{value:.2f}"
|
|
21
|
+
if value < 100:
|
|
22
|
+
return f"{value:.1f}"
|
|
23
|
+
return f"{value:.0f}"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _format_duration(ms: float) -> str:
|
|
27
|
+
"""Format a duration in milliseconds with an automatic unit (ms, s, m, h)."""
|
|
28
|
+
if ms < 1000:
|
|
29
|
+
return f"{_compact_number(ms)} ms"
|
|
30
|
+
s = ms / 1000.0
|
|
31
|
+
if s < 60:
|
|
32
|
+
return f"{_compact_number(s)} s"
|
|
33
|
+
m = s / 60.0
|
|
34
|
+
if m < 60:
|
|
35
|
+
return f"{_compact_number(m)} m"
|
|
36
|
+
return f"{_compact_number(m / 60.0)} h"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _format_bytes(n: float) -> str:
|
|
40
|
+
"""Format a byte count with an automatic unit (B, KB, MB, GB)."""
|
|
41
|
+
for unit in ("B", "KB", "MB"):
|
|
42
|
+
if n < 1000:
|
|
43
|
+
return f"{_compact_number(n)} {unit}"
|
|
44
|
+
n /= 1024.0
|
|
45
|
+
return f"{_compact_number(n)} GB"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class StatusBarController(QObject):
|
|
49
|
+
"""Manage a main window's status bar.
|
|
50
|
+
|
|
51
|
+
The left side shows the current state (e.g. ``"Ready"``) and, optionally,
|
|
52
|
+
the current document path with a ``*`` when there are unsaved changes.
|
|
53
|
+
The right side shows the process memory and the elapsed time of the last
|
|
54
|
+
busy state.
|
|
55
|
+
|
|
56
|
+
States are free text chosen by the app. A state set with ``busy=True``
|
|
57
|
+
starts the elapsed-time clock; the next state stops it and leaves the
|
|
58
|
+
final time visible::
|
|
59
|
+
|
|
60
|
+
status.set_state("Solving…", busy=True)
|
|
61
|
+
solve()
|
|
62
|
+
status.set_state("Ready")
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
window: QMainWindow,
|
|
68
|
+
*,
|
|
69
|
+
ready_text: str = "Ready",
|
|
70
|
+
states: Iterable[Union[str, Enum]] = (),
|
|
71
|
+
show_path: bool = True,
|
|
72
|
+
path_label: str = "Path",
|
|
73
|
+
show_memory: bool = True,
|
|
74
|
+
show_time: bool = True,
|
|
75
|
+
timer_interval: int = 700,
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Initialize the controller and build the status-bar widgets.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
window: Main window whose status bar is managed.
|
|
81
|
+
ready_text: Initial state text.
|
|
82
|
+
states: All state texts the app will show. The state label is
|
|
83
|
+
sized for the widest one, so the path next to it does not
|
|
84
|
+
move when the state changes. A wider state shown later still
|
|
85
|
+
fits; the label then grows and keeps that width.
|
|
86
|
+
show_path: Whether to show the document path section.
|
|
87
|
+
path_label: Caption of the path section, e.g. ``"File"``.
|
|
88
|
+
show_memory: Whether to show the process memory.
|
|
89
|
+
show_time: Whether to show the elapsed time of busy states.
|
|
90
|
+
timer_interval: Refresh interval, in milliseconds, of the time and
|
|
91
|
+
memory displays while a busy state is active.
|
|
92
|
+
"""
|
|
93
|
+
super().__init__(window)
|
|
94
|
+
self._sb = window.statusBar()
|
|
95
|
+
self._process = psutil.Process(os.getpid())
|
|
96
|
+
self._start_ns = 0
|
|
97
|
+
self._busy = False
|
|
98
|
+
|
|
99
|
+
self._path = ""
|
|
100
|
+
self._modified = False
|
|
101
|
+
self._path_label = path_label
|
|
102
|
+
|
|
103
|
+
self._timer = QTimer(self)
|
|
104
|
+
self._timer.setInterval(timer_interval)
|
|
105
|
+
self._timer.timeout.connect(self._refresh)
|
|
106
|
+
|
|
107
|
+
self._state_lbl = QLabel(ready_text)
|
|
108
|
+
self._path_lbl = QLabel() if show_path else None
|
|
109
|
+
self._memory_lbl = self._fixed_label("0.00 MB", 7) if show_memory else None
|
|
110
|
+
self._time_lbl = self._fixed_label("0.00 s", 7) if show_time else None
|
|
111
|
+
|
|
112
|
+
self._make_layout()
|
|
113
|
+
self._fit_state_width([ready_text, *states])
|
|
114
|
+
self._update_path_label()
|
|
115
|
+
self._update_memory()
|
|
116
|
+
|
|
117
|
+
# ----- construction -----
|
|
118
|
+
@staticmethod
|
|
119
|
+
def _fixed_label(text: str, chars: int) -> QLabel:
|
|
120
|
+
"""Create a right-aligned label wide enough for ``chars`` digits."""
|
|
121
|
+
lbl = QLabel(text, alignment=Qt.AlignRight | Qt.AlignVCenter)
|
|
122
|
+
lbl.setFixedWidth(lbl.fontMetrics().horizontalAdvance("0" * chars) + 8)
|
|
123
|
+
return lbl
|
|
124
|
+
|
|
125
|
+
def _vline(self) -> QFrame:
|
|
126
|
+
"""Create a sunken vertical separator."""
|
|
127
|
+
line = QFrame(self._sb)
|
|
128
|
+
line.setFrameShape(QFrame.VLine)
|
|
129
|
+
line.setFrameShadow(QFrame.Sunken)
|
|
130
|
+
return line
|
|
131
|
+
|
|
132
|
+
def _add_separator(self, layout: QLayout) -> None:
|
|
133
|
+
"""Add a padded vertical separator to a layout."""
|
|
134
|
+
layout.addSpacing(10)
|
|
135
|
+
layout.addWidget(self._vline())
|
|
136
|
+
layout.addSpacing(10)
|
|
137
|
+
|
|
138
|
+
def _make_layout(self) -> None:
|
|
139
|
+
"""Build the left (state, path) and right (memory, time) sections."""
|
|
140
|
+
left = QWidget()
|
|
141
|
+
ll = QHBoxLayout(left)
|
|
142
|
+
ll.setContentsMargins(0, 0, 0, 0)
|
|
143
|
+
ll.setSpacing(0)
|
|
144
|
+
ll.addSpacing(10)
|
|
145
|
+
ll.addWidget(self._state_lbl)
|
|
146
|
+
if self._path_lbl is not None:
|
|
147
|
+
self._add_separator(ll)
|
|
148
|
+
ll.addWidget(self._path_lbl)
|
|
149
|
+
ll.addStretch(1)
|
|
150
|
+
self._sb.addWidget(left, 1)
|
|
151
|
+
|
|
152
|
+
if self._memory_lbl is None and self._time_lbl is None:
|
|
153
|
+
return
|
|
154
|
+
right = QWidget()
|
|
155
|
+
rl = QHBoxLayout(right)
|
|
156
|
+
rl.setContentsMargins(0, 0, 0, 0)
|
|
157
|
+
rl.setSpacing(0)
|
|
158
|
+
for caption, lbl in (("Memory:", self._memory_lbl), ("Time:", self._time_lbl)):
|
|
159
|
+
if lbl is not None:
|
|
160
|
+
self._add_separator(rl)
|
|
161
|
+
rl.addWidget(QLabel(caption))
|
|
162
|
+
rl.addWidget(lbl)
|
|
163
|
+
rl.addSpacing(8)
|
|
164
|
+
self._sb.addPermanentWidget(right)
|
|
165
|
+
|
|
166
|
+
# ----- updates -----
|
|
167
|
+
def _update_time(self) -> None:
|
|
168
|
+
"""Show the time elapsed since the current busy state started."""
|
|
169
|
+
if self._time_lbl is not None:
|
|
170
|
+
ms = (perf_counter_ns() - self._start_ns) / 1.0e6
|
|
171
|
+
self._time_lbl.setText(_format_duration(ms))
|
|
172
|
+
|
|
173
|
+
def _update_memory(self) -> None:
|
|
174
|
+
"""Show the current memory use (resident set size) of the process."""
|
|
175
|
+
if self._memory_lbl is not None:
|
|
176
|
+
self._memory_lbl.setText(_format_bytes(self._process.memory_info().rss))
|
|
177
|
+
|
|
178
|
+
def _refresh(self) -> None:
|
|
179
|
+
"""Timer slot: refresh time and memory."""
|
|
180
|
+
self._update_time()
|
|
181
|
+
self._update_memory()
|
|
182
|
+
|
|
183
|
+
def _update_path_label(self) -> None:
|
|
184
|
+
"""Show the path, ``Untitled`` when empty, and ``*`` when modified."""
|
|
185
|
+
if self._path_lbl is None:
|
|
186
|
+
return
|
|
187
|
+
mark = " *" if self._modified else ""
|
|
188
|
+
text = f"{self._path_label}{mark}: {self._path or 'Untitled'}"
|
|
189
|
+
self._path_lbl.setText(text)
|
|
190
|
+
self._path_lbl.setToolTip(text)
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def _state_text(state: Union[str, Enum]) -> str:
|
|
194
|
+
"""Return the display text of a state (an enum's value, or the string)."""
|
|
195
|
+
return state.value if isinstance(state, Enum) else str(state)
|
|
196
|
+
|
|
197
|
+
def _fit_state_width(self, states: Iterable[Union[str, Enum]]) -> None:
|
|
198
|
+
"""Widen the state label to fit the given states; it never shrinks."""
|
|
199
|
+
fm = self._state_lbl.fontMetrics()
|
|
200
|
+
width = max((fm.horizontalAdvance(self._state_text(s)) for s in states), default=0) + 4
|
|
201
|
+
if width > self._state_lbl.minimumWidth():
|
|
202
|
+
self._state_lbl.setMinimumWidth(width)
|
|
203
|
+
|
|
204
|
+
# ----- public API -----
|
|
205
|
+
def set_state(self, state: Union[str, Enum], busy: bool = False) -> None:
|
|
206
|
+
"""Show a new state.
|
|
207
|
+
|
|
208
|
+
A busy state starts the elapsed-time clock; any following state stops
|
|
209
|
+
it and keeps the final time on display. The new state appears when
|
|
210
|
+
control returns to the event loop, so run long tasks in a worker
|
|
211
|
+
thread, or start them with ``QTimer.singleShot`` after this call.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
state: Text to show, or an enum whose value is the text.
|
|
215
|
+
busy: Whether a long-running task starts now.
|
|
216
|
+
"""
|
|
217
|
+
self._timer.stop()
|
|
218
|
+
if self._busy:
|
|
219
|
+
self._update_time()
|
|
220
|
+
self._busy = bool(busy)
|
|
221
|
+
if self._busy:
|
|
222
|
+
self._start_ns = perf_counter_ns()
|
|
223
|
+
self._timer.start()
|
|
224
|
+
|
|
225
|
+
self._update_memory()
|
|
226
|
+
text = self._state_text(state)
|
|
227
|
+
self._fit_state_width([text])
|
|
228
|
+
self._state_lbl.setText(text)
|
|
229
|
+
|
|
230
|
+
def is_busy(self) -> bool:
|
|
231
|
+
"""Return whether a busy state is active."""
|
|
232
|
+
return self._busy
|
|
233
|
+
|
|
234
|
+
def set_path(self, path: str) -> None:
|
|
235
|
+
"""Show a document path.
|
|
236
|
+
|
|
237
|
+
A non-empty path clears the modified mark (the document was just
|
|
238
|
+
opened or saved); an empty path shows ``Untitled`` and keeps the mark.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
path: Path to show.
|
|
242
|
+
"""
|
|
243
|
+
self._path = path
|
|
244
|
+
if path:
|
|
245
|
+
self._modified = False
|
|
246
|
+
self._update_path_label()
|
|
247
|
+
|
|
248
|
+
def get_path(self) -> str:
|
|
249
|
+
"""Return the shown document path."""
|
|
250
|
+
return self._path
|
|
251
|
+
|
|
252
|
+
def set_modified(self, modified: bool = True) -> None:
|
|
253
|
+
"""Mark or unmark the document as having unsaved changes.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
modified: Whether there are unsaved changes.
|
|
257
|
+
"""
|
|
258
|
+
self._modified = bool(modified)
|
|
259
|
+
self._update_path_label()
|
|
260
|
+
|
|
261
|
+
def is_modified(self) -> bool:
|
|
262
|
+
"""Return whether the document is marked as modified."""
|
|
263
|
+
return self._modified
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sciwidgets
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: PySide6 widgets for scientific desktop apps: math labels, sliders, parameter inputs and dialogs
|
|
5
5
|
Author-email: Vahid Anari <vahid.anari8@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/vahid-anari/sciwidgets
|
|
8
8
|
Project-URL: Repository, https://github.com/vahid-anari/sciwidgets
|
|
9
9
|
Project-URL: Issues, https://github.com/vahid-anari/sciwidgets/issues
|
|
10
|
-
Keywords: pyside6,qt,gui,widgets,scientific,slider,range-slider,latex,matplotlib,dialogs
|
|
10
|
+
Keywords: pyside6,qt,gui,widgets,scientific,slider,range-slider,latex,matplotlib,dialogs,status-bar,menu
|
|
11
11
|
Classifier: Development Status :: 3 - Alpha
|
|
12
12
|
Classifier: Intended Audience :: Science/Research
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
@@ -24,6 +24,7 @@ License-File: LICENSE
|
|
|
24
24
|
Requires-Dist: PySide6<7,>=6.5
|
|
25
25
|
Requires-Dist: matplotlib>=3.7
|
|
26
26
|
Requires-Dist: numpy>=1.24
|
|
27
|
+
Requires-Dist: psutil>=5.9
|
|
27
28
|
Provides-Extra: dev
|
|
28
29
|
Requires-Dist: build; extra == "dev"
|
|
29
30
|
Requires-Dist: twine; extra == "dev"
|
|
@@ -35,7 +36,7 @@ PySide6 widgets for scientific desktop apps: math labels rendered with
|
|
|
35
36
|
LaTeX/mathtext, dialogs, and parameter inputs such as sliders, range sliders
|
|
36
37
|
and validated number fields.
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
<img src="https://raw.githubusercontent.com/vahid-anari/sciwidgets/main/docs/images/sliders_demo.png" width="900" alt="Sliders demo">
|
|
39
40
|
|
|
40
41
|
Right-click menu, Set Value and Edit Configuration:
|
|
41
42
|
|
|
@@ -64,13 +65,14 @@ and moved into this package so other apps can share them.
|
|
|
64
65
|
|
|
65
66
|
## Components
|
|
66
67
|
|
|
67
|
-
| Import | Contents |
|
|
68
|
-
|
|
69
|
-
| `sciwidgets.
|
|
70
|
-
| `sciwidgets.params` | `
|
|
71
|
-
| `sciwidgets.
|
|
72
|
-
| `sciwidgets.
|
|
73
|
-
| `sciwidgets.
|
|
68
|
+
| Import | Contents | Docs |
|
|
69
|
+
|--------|----------|------|
|
|
70
|
+
| `sciwidgets.params` (sliders) | `FloatSlider`, `IntSlider`, `ArraySlider`, `FloatRangeSlider`, `IntRangeSlider`, `MultiVariableSlider`, `SliderFeature` | [Sliders](https://github.com/vahid-anari/sciwidgets/blob/main/docs/sliders.md) |
|
|
71
|
+
| `sciwidgets.params` (inputs) | `ParameterLineEdit`, `ParameterComboBox`, `ParameterToggle`, `NumericLineEdit` | [Parameter widgets](https://github.com/vahid-anari/sciwidgets/blob/main/docs/params.md) |
|
|
72
|
+
| `sciwidgets.labels` | `MathLabel`, `set_latex_enabled` | [Labels](https://github.com/vahid-anari/sciwidgets/blob/main/docs/labels.md) |
|
|
73
|
+
| `sciwidgets.dialogs` | `show_information`, `show_warning`, `show_critical`, `show_rich_information`, `show_about_dialog`, `ask_question` | [Dialogs](https://github.com/vahid-anari/sciwidgets/blob/main/docs/dialogs.md) |
|
|
74
|
+
| `sciwidgets.window` | `MenuBarController`, `StatusBarController` | [Window helpers](https://github.com/vahid-anari/sciwidgets/blob/main/docs/window.md) |
|
|
75
|
+
| `sciwidgets.style`, `sciwidgets.formatting` | `apply_sci_style`, `SciStyle`, `value_to_text`, `pretty_sci_text` | [Style and formatting](https://github.com/vahid-anari/sciwidgets/blob/main/docs/style.md) |
|
|
74
76
|
|
|
75
77
|
## Install
|
|
76
78
|
|
|
@@ -30,4 +30,8 @@ src/sciwidgets/params/sliders/_right_click_overlay.py
|
|
|
30
30
|
src/sciwidgets/params/sliders/base.py
|
|
31
31
|
src/sciwidgets/params/sliders/multi.py
|
|
32
32
|
src/sciwidgets/params/sliders/range.py
|
|
33
|
-
src/sciwidgets/params/sliders/single.py
|
|
33
|
+
src/sciwidgets/params/sliders/single.py
|
|
34
|
+
src/sciwidgets/window/__init__.py
|
|
35
|
+
src/sciwidgets/window/__main__.py
|
|
36
|
+
src/sciwidgets/window/menu_bar.py
|
|
37
|
+
src/sciwidgets/window/status_bar.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|