sciwidgets 0.1.2__py3-none-any.whl

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.
@@ -0,0 +1,36 @@
1
+ """Labeled sliders: single-value, range, and multi-variable."""
2
+
3
+ from .base import SliderFeature, SliderWidgetBase
4
+ from .multi import MultiVariableSlider
5
+ from .range import (
6
+ DualHandleSlider,
7
+ FloatRangeSlider,
8
+ IntRangeSlider,
9
+ LabeledRangeSliderBase,
10
+ NumericRangeSliderBase,
11
+ )
12
+ from .single import (
13
+ ArraySlider,
14
+ FloatSlider,
15
+ IntSlider,
16
+ LabeledSliderBase,
17
+ NumericSliderBase,
18
+ SpecialValue,
19
+ )
20
+
21
+ __all__ = [
22
+ "ArraySlider",
23
+ "DualHandleSlider",
24
+ "FloatRangeSlider",
25
+ "FloatSlider",
26
+ "IntRangeSlider",
27
+ "IntSlider",
28
+ "LabeledRangeSliderBase",
29
+ "LabeledSliderBase",
30
+ "MultiVariableSlider",
31
+ "NumericRangeSliderBase",
32
+ "NumericSliderBase",
33
+ "SliderFeature",
34
+ "SliderWidgetBase",
35
+ "SpecialValue",
36
+ ]
@@ -0,0 +1,215 @@
1
+ """Demo for the sliders: ``python -m sciwidgets.params.sliders``."""
2
+
3
+ import sys
4
+
5
+ from PySide6.QtCore import Qt
6
+ from PySide6.QtWidgets import (
7
+ QApplication,
8
+ QCheckBox,
9
+ QComboBox,
10
+ QGroupBox,
11
+ QHBoxLayout,
12
+ QLabel,
13
+ QPlainTextEdit,
14
+ QVBoxLayout,
15
+ QWidget,
16
+ )
17
+
18
+ from sciwidgets.params.sliders.base import SliderFeature
19
+ from sciwidgets.params.sliders.single import ArraySlider, FloatSlider, IntSlider
20
+ from sciwidgets.params.sliders.range import FloatRangeSlider, IntRangeSlider
21
+ from sciwidgets.params.sliders.multi import MultiVariableSlider
22
+ from sciwidgets.style import apply_sci_style
23
+
24
+
25
+ def _demo_main() -> int:
26
+ """Run a demo of the single-value sliders.
27
+
28
+ Returns:
29
+ int: Qt application exit code.
30
+ """
31
+ app = QApplication(sys.argv)
32
+ apply_sci_style(app)
33
+
34
+ win = QWidget()
35
+ win.setWindowTitle("Sliders demo — right-click a slider for its menu")
36
+ root = QVBoxLayout(win)
37
+
38
+ # --- Signal log -----------------------------------------------------
39
+ log = QPlainTextEdit()
40
+ log.setReadOnly(True)
41
+ log.setMaximumHeight(140)
42
+
43
+ def watch(name: str, slider) -> None:
44
+ """Log the user-action signals of a slider."""
45
+ slider.valueChanged.connect(lambda v: log.appendPlainText(f"{name}: value → {v}"))
46
+ slider.defaultChanged.connect(lambda d: log.appendPlainText(f"{name}: default → {d}"))
47
+ slider.configChanged.connect(lambda c: log.appendPlainText(f"{name}: config → {c}"))
48
+
49
+ # --- Horizontal sliders -----------------------------------------------
50
+ t2 = FloatSlider( # starts at +inf: "no dephasing"
51
+ label="T_2", unit=r"\mathrm{ms}", min_val=0.1, max_val=10.0,
52
+ init_val="+inf", val_fmt="{:.2f}", min_limit=0.0, min_limit_inclusive=False,
53
+ )
54
+ h_items = [
55
+ ("N_z", IntSlider( # integer, all menu features
56
+ label="N_z", min_val=10, max_val=1000, init_val=200, min_limit=1,
57
+ )),
58
+ ("T_1", FloatSlider( # float with a strict lower limit
59
+ label="T_1", unit=r"\mathrm{ms}", min_val=0.1, max_val=10.0, init_val=1.0,
60
+ val_fmt="{:.2f}", min_limit=0.0, min_limit_inclusive=False,
61
+ features=SliderFeature.DEFAULTS | SliderFeature.SET_VALUE | SliderFeature.CONFIG,
62
+ )),
63
+ ("T_2", t2),
64
+ ("gamma", FloatSlider( # custom scientific format
65
+ label=r"\gamma", unit=r"\mathrm{s^{-1}}", min_val=1e-3, max_val=1e-1,
66
+ init_val=1e-2, val_fmt="{:.2S}", min_limit=0.0, min_limit_inclusive=False,
67
+ )),
68
+ ("Delta_omega", FloatSlider( # signed values, no ±inf
69
+ label=r"\Delta\omega", unit=r"\mathrm{MHz}", min_val=-5.0, max_val=5.0,
70
+ init_val=0.0, val_fmt="{:+.2f}",
71
+ features=SliderFeature.DEFAULTS | SliderFeature.SET_VALUE | SliderFeature.CONFIG,
72
+ )),
73
+ ("z", ArraySlider( # picks from a grid of positions
74
+ label="z", unit=r"\mathrm{m}", arr_length=51, min_val=0.0, max_val=1.0,
75
+ init_index=25, val_fmt="{:.2f}",
76
+ features=SliderFeature.DEFAULTS | SliderFeature.CONFIG,
77
+ )),
78
+ ("seed", IntSlider( # no menu at all
79
+ label=r"\mathrm{seed}", min_val=0, max_val=99, init_val=42,
80
+ features=SliderFeature.NONE,
81
+ )),
82
+ ("nu", FloatRangeSlider( # a frequency band
83
+ label=r"\nu", unit=r"\mathrm{GHz}", min_val=6.60, max_val=6.75,
84
+ init_vals=(6.65, 6.70), val_fmt="{:.3f}",
85
+ )),
86
+ ("channels", IntRangeSlider( # integer range
87
+ label=r"\mathrm{ch}", min_val=0, max_val=1023, init_vals=(100, 900),
88
+ )),
89
+ ("I", FloatRangeSlider( # starts in margins mode, scientific format
90
+ label="I", unit=r"\mathrm{Jy}", min_val=0.0, max_val=1e3,
91
+ init_vals=(10.0, 500.0), val_fmt="{:.2S}", min_limit=0.0,
92
+ use_margins=True, margins=(5, 10),
93
+ )),
94
+ ]
95
+
96
+ h_box = QGroupBox("Horizontal")
97
+ h_col = QVBoxLayout(h_box)
98
+ for name, s in h_items:
99
+ h_col.addWidget(s)
100
+ watch(name, s)
101
+ h_col.addStretch(1)
102
+
103
+ def realign(_w: int = 0) -> None:
104
+ """Align the name and value columns of the horizontal sliders."""
105
+ sliders = [s for _, s in h_items]
106
+ name_w = max(s.get_name_width() for s in sliders)
107
+ value_w = max(s.get_value_width() for s in sliders)
108
+ for s in sliders:
109
+ s.set_name_width(name_w)
110
+ s.set_value_width(value_w)
111
+
112
+ for _, s in h_items:
113
+ s.nameWidthChanged.connect(realign)
114
+ s.valueWidthChanged.connect(realign)
115
+ realign()
116
+
117
+ # --- Vertical sliders ---------------------------------------------------
118
+ v_items = [
119
+ (f"E_{axis}", FloatSlider(
120
+ label=f"E_{axis}", min_val=-1.0, max_val=1.0, init_val=0.0,
121
+ val_fmt="{:.2f}", orientation=Qt.Vertical,
122
+ ))
123
+ for axis in "xyz"
124
+ ]
125
+ v_items.append(("t", FloatRangeSlider(
126
+ label="t", min_val=0.0, max_val=1.0, init_vals=(0.2, 0.8),
127
+ val_fmt="{:.2f}", orientation=Qt.Vertical,
128
+ )))
129
+ v_box = QGroupBox("Vertical")
130
+ v_row = QHBoxLayout(v_box)
131
+ for name, s in v_items:
132
+ v_row.addWidget(s)
133
+ watch(name, s)
134
+
135
+ top = QHBoxLayout()
136
+ top.addWidget(h_box, 3)
137
+ top.addWidget(v_box, 1)
138
+ root.addLayout(top, 1)
139
+
140
+ # --- Multi-variable slider ------------------------------------------------
141
+ tau = MultiVariableSlider( # one decay time per spectral component
142
+ label=r"\tau_k", html_label="τ<sub>k</sub>", unit=r"\mathrm{d}",
143
+ min_val=0.0, max_val=100.0, init_vals=[10, 25, 40, "+inf", 80],
144
+ val_fmt="{:.1f}", min_limit=0.0,
145
+ )
146
+ idx_combo = QComboBox()
147
+ idx_combo.addItems([str(i) for i in range(tau.get_arr_length())])
148
+ idx_combo.currentIndexChanged.connect(tau.set_index)
149
+ same_cb = QCheckBox("Same value for all")
150
+ same_cb.toggled.connect(tau.set_use_same_values)
151
+
152
+ arr_label = QLabel()
153
+
154
+ def show_arr(_=None) -> None:
155
+ """Show the whole array as the slider stores it."""
156
+ texts = (tau.get_value_text(i) for i in range(tau.get_arr_length()))
157
+ arr_label.setText("Array: [" + ", ".join(texts) + "]")
158
+
159
+ tau.arrayChanged.connect(show_arr)
160
+ same_cb.toggled.connect(show_arr)
161
+ show_arr()
162
+ watch("tau", tau)
163
+ tau.arrayChanged.connect(lambda a: log.appendPlainText(f"tau: array → {a}"))
164
+
165
+ m_box = QGroupBox("Multi-variable")
166
+ m_col = QVBoxLayout(m_box)
167
+ m_row = QHBoxLayout()
168
+ m_row.addWidget(QLabel("Index:"))
169
+ m_row.addWidget(idx_combo)
170
+ m_row.addWidget(same_cb)
171
+ m_row.addStretch(1)
172
+ m_col.addLayout(m_row)
173
+ m_col.addWidget(tau)
174
+ m_col.addWidget(arr_label)
175
+ root.addWidget(m_box)
176
+
177
+ # --- Programmatic controls ----------------------------------------------
178
+ all_sliders = [s for _, s in h_items + v_items] + [tau]
179
+
180
+ enabled = QCheckBox("Sliders enabled")
181
+ enabled.setChecked(True)
182
+ ranges = QCheckBox("Show ranges")
183
+ ranges.setChecked(True)
184
+ allow_inf = QCheckBox("Allow T₂ = +inf")
185
+ allow_inf.setChecked(True)
186
+
187
+ def set_enabled(on: bool) -> None:
188
+ for s in all_sliders:
189
+ s.setEnabled(on)
190
+
191
+ def set_ranges(on: bool) -> None:
192
+ for s in all_sliders:
193
+ s.set_show_range(on)
194
+
195
+ enabled.toggled.connect(set_enabled)
196
+ ranges.toggled.connect(set_ranges)
197
+ allow_inf.toggled.connect(lambda on: t2.set_special_value_allowed("+inf", on))
198
+
199
+ controls = QHBoxLayout()
200
+ for cb in (enabled, ranges, allow_inf):
201
+ controls.addWidget(cb)
202
+ controls.addStretch(1)
203
+ root.addLayout(controls)
204
+
205
+ log_box = QGroupBox("Signals (user actions only)")
206
+ QVBoxLayout(log_box).addWidget(log)
207
+ root.addWidget(log_box)
208
+
209
+ win.resize(1000, 620)
210
+ win.show()
211
+ return app.exec()
212
+
213
+
214
+ if __name__ == "__main__":
215
+ raise SystemExit(_demo_main())
@@ -0,0 +1,36 @@
1
+ """Default sizes and slider border styling used by the parameter widgets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+
7
+ SLIDER_LABEL_SIZE = 20
8
+ """Default font size, in points, for slider name labels."""
9
+
10
+ UNIT_LABEL_SIZE = 18
11
+ """Default font size, in points, for unit labels."""
12
+
13
+ SLIDER_BORDER_STYLE_SHEET = """
14
+ QWidget#labeled_slider[borderState="normal"] {
15
+ border: 1px solid gray;
16
+ border-radius: 3px;
17
+ background: transparent;
18
+ }
19
+ QWidget#labeled_slider[borderState="error"] {
20
+ border: 1px solid red;
21
+ border-radius: 3px;
22
+ background: transparent;
23
+ }
24
+ QWidget#labeled_slider[borderState="off"] {
25
+ border: 0px;
26
+ }
27
+ """
28
+ """Style sheet fragment used for custom slider border states."""
29
+
30
+
31
+ class SliderBorderState(str, Enum):
32
+ """State values used when drawing custom slider borders."""
33
+
34
+ OFF = "off"
35
+ NORMAL = "normal"
36
+ ERROR = "error"
@@ -0,0 +1,174 @@
1
+ """Out-of-range questions shown by the slider widgets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ from PySide6.QtWidgets import QWidget
8
+
9
+ from ...dialogs import AskResult, ask_question
10
+
11
+
12
+ def slider_ask_clamp_value(
13
+ proposed_range: str,
14
+ old_cur: str,
15
+ new_cur: str,
16
+ old_dft: str,
17
+ new_dft: str,
18
+ parent: Optional[QWidget] = None,
19
+ ) -> AskResult:
20
+ """Ask whether a slider value should be clamped to a proposed range.
21
+
22
+ Args:
23
+ proposed_range: Proposed allowed range.
24
+ old_cur: Previous current value.
25
+ new_cur: Clamped current value.
26
+ old_dft: Previous default value.
27
+ new_dft: Clamped default value.
28
+ parent: Optional parent widget.
29
+
30
+ Returns:
31
+ User's selected answer.
32
+ """
33
+
34
+ return ask_question(
35
+ "Out of range",
36
+ "<b>Current</b>/<b>default</b> value is outside the proposed range.",
37
+ f"<b>Proposed range:</b> <code>{proposed_range}</code><br>"
38
+ f"<b>Current:</b> <code>{old_cur}</code> → <code>{new_cur}</code><br>"
39
+ f"<b>Default:</b> <code>{old_dft}</code> → <code>{new_dft}</code><br><br>"
40
+ "Press <b>OK</b> to clamp values to the proposed range, or <b>Cancel</b> to abort.",
41
+ yes_btn_label="OK",
42
+ cancel_btn_label="Cancel",
43
+ parent=parent,
44
+ )
45
+
46
+
47
+ def range_slider_ask_clamp_value(
48
+ proposed_range: str,
49
+ old_cur: str,
50
+ new_cur: str,
51
+ old_dft: str,
52
+ new_dft: str,
53
+ parent: Optional[QWidget] = None,
54
+ ) -> AskResult:
55
+ """Ask whether range-slider values should be clamped to proposed limits.
56
+
57
+ Args:
58
+ proposed_range: Proposed allowed range limits.
59
+ old_cur: Previous current range.
60
+ new_cur: Clamped current range.
61
+ old_dft: Previous default range.
62
+ new_dft: Clamped default range.
63
+ parent: Optional parent widget.
64
+
65
+ Returns:
66
+ User's selected answer.
67
+ """
68
+
69
+ return ask_question(
70
+ "Out of range",
71
+ "<b>Current</b>/<b>default</b> values are outside the proposed range limits.",
72
+ f"<b>Proposed range limits:</b> <code>{proposed_range}</code><br>"
73
+ f"<b>Current:</b> <code>{old_cur}</code> → <code>{new_cur}</code><br>"
74
+ f"<b>Default:</b> <code>{old_dft}</code> → <code>{new_dft}</code><br><br>"
75
+ "Press <b>OK</b> to clamp values to the proposed range, or <b>Cancel</b> to abort.",
76
+ yes_btn_label="OK",
77
+ cancel_btn_label="Cancel",
78
+ parent=parent,
79
+ )
80
+
81
+
82
+ def multi_slider_ask_clamp_value(
83
+ proposed_range: str,
84
+ cur_min: str,
85
+ cur_max: str,
86
+ dft_min: str,
87
+ dft_max: str,
88
+ parent: Optional[QWidget] = None,
89
+ ) -> AskResult:
90
+ """Ask whether array-slider values should be clamped to a proposed range.
91
+
92
+ Args:
93
+ proposed_range: Proposed allowed range.
94
+ cur_min: Minimum current value.
95
+ cur_max: Maximum current value.
96
+ dft_min: Minimum default value.
97
+ dft_max: Maximum default value.
98
+ parent: Optional parent widget.
99
+
100
+ Returns:
101
+ User's selected answer.
102
+ """
103
+
104
+ return ask_question(
105
+ "Out of range",
106
+ "<b>Current</b>/<b>default</b> values are outside the proposed range.",
107
+ f"<b>Proposed range:</b> <code>{proposed_range}</code><br>"
108
+ f"<b>Current values range:</b> <code>[{cur_min}, {cur_max}]</code><br>"
109
+ f"<b>Default values range:</b> <code>[{dft_min}, {dft_max}]</code><br><br>"
110
+ "Press <b>OK</b> to clamp values to the proposed range, or <b>Cancel</b> to abort.",
111
+ yes_btn_label="OK",
112
+ cancel_btn_label="Cancel",
113
+ parent=parent,
114
+ )
115
+
116
+
117
+ def slider_ask_extend_range(
118
+ proposed_val: str,
119
+ old_range: str,
120
+ new_range: str,
121
+ parent: Optional[QWidget] = None,
122
+ ) -> AskResult:
123
+ """Ask whether a slider range should be extended to include a new value.
124
+
125
+ Args:
126
+ proposed_val: Proposed new value.
127
+ old_range: Existing range.
128
+ new_range: Extended range.
129
+ parent: Optional parent widget.
130
+
131
+ Returns:
132
+ User's selected answer.
133
+ """
134
+
135
+ return ask_question(
136
+ "Out of range",
137
+ "The proposed <b>value</b> is outside the current range.",
138
+ f"<b>Proposed value:</b> <code>{proposed_val}</code><br>"
139
+ f"<b>Range:</b> <code>{old_range}</code> → <code>{new_range}</code><br><br>"
140
+ "Press <b>OK</b> to extend the range to include this value, or <b>Cancel</b> to abort.",
141
+ yes_btn_label="OK",
142
+ cancel_btn_label="Cancel",
143
+ parent=parent,
144
+ )
145
+
146
+
147
+ def range_slider_ask_extend_range(
148
+ proposed_vals: str,
149
+ old_range: str,
150
+ new_range: str,
151
+ parent: Optional[QWidget] = None,
152
+ ) -> AskResult:
153
+ """Ask whether a range-slider range should be extended for new values.
154
+
155
+ Args:
156
+ proposed_vals: Proposed new range values.
157
+ old_range: Existing range.
158
+ new_range: Extended range.
159
+ parent: Optional parent widget.
160
+
161
+ Returns:
162
+ User's selected answer.
163
+ """
164
+
165
+ return ask_question(
166
+ "Out of range",
167
+ "The proposed <b>values</b> are outside the current range limits.",
168
+ f"<b>Proposed values:</b> <code>{proposed_vals}</code><br>"
169
+ f"<b>Range:</b> <code>{old_range}</code> → <code>{new_range}</code><br><br>"
170
+ "Press <b>OK</b> to extend the range to include these values, or <b>Cancel</b> to abort.",
171
+ yes_btn_label="OK",
172
+ cancel_btn_label="Cancel",
173
+ parent=parent,
174
+ )
@@ -0,0 +1,115 @@
1
+ """Transparent overlay that translates clicks into context-menu actions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from PySide6.QtCore import QPoint, Qt
6
+ from PySide6.QtWidgets import QWidget
7
+
8
+
9
+ class RightClickOverlay(QWidget):
10
+ """Transparent overlay that forwards clicks as context-menu requests.
11
+
12
+ The overlay is positioned over a target widget when needed. Mouse input on the
13
+ overlay is consumed so the underlying widget does not receive it directly. A
14
+ right-click release is translated into a context-menu request on the owner
15
+ widget.
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ target_widget: QWidget,
21
+ owner_widget: QWidget,
22
+ parent: QWidget | None = None,
23
+ ) -> None:
24
+ """Initialize the overlay.
25
+
26
+ Args:
27
+ target_widget: Widget whose geometry is mirrored by the overlay.
28
+ owner_widget: Widget that handles the forwarded context-menu request.
29
+ parent: Parent widget that contains the overlay.
30
+ """
31
+ super().__init__(parent)
32
+ self._target_widget = target_widget
33
+ self._owner_widget = owner_widget
34
+
35
+ self.setAttribute(Qt.WA_NoSystemBackground, True)
36
+ self.raise_()
37
+
38
+ def sync_to_target(self) -> None:
39
+ """Sync the overlay geometry and visibility with the target widget.
40
+
41
+ The overlay is hidden when no target widget is available or when the
42
+ target widget is enabled. Otherwise, the overlay is resized and moved to
43
+ cover the target widget within the parent coordinate system.
44
+ """
45
+ if self._target_widget is None or self._target_widget.isEnabled():
46
+ self.hide()
47
+ return
48
+
49
+ parent = self.parentWidget()
50
+ if parent is None:
51
+ return
52
+
53
+ pos = self._target_widget.mapTo(parent, QPoint(0, 0))
54
+ self.setGeometry(
55
+ pos.x(),
56
+ pos.y(),
57
+ self._target_widget.width(),
58
+ self._target_widget.height(),
59
+ )
60
+ self.raise_()
61
+ self.show()
62
+
63
+ def mouseReleaseEvent(self, event) -> None:
64
+ """Handle mouse-release events on the overlay.
65
+
66
+ Args:
67
+ event: Qt mouse event received by the overlay.
68
+
69
+ A right-button release is forwarded to the owner widget as a context-menu
70
+ request using the global cursor position.
71
+ """
72
+ if event.button() == Qt.RightButton:
73
+ self._owner_widget._show_context_menu(event.globalPosition().toPoint())
74
+ event.accept()
75
+
76
+ def paintEvent(self, event) -> None:
77
+ """Handle paint events for the overlay.
78
+
79
+ Args:
80
+ event: Qt paint event.
81
+
82
+ The overlay is intentionally transparent and does not draw any content.
83
+ """
84
+ pass
85
+
86
+ def mousePressEvent(self, event) -> None:
87
+ """Handle mouse-press events on the overlay.
88
+
89
+ Args:
90
+ event: Qt mouse event.
91
+
92
+ The event is accepted so the underlying widget does not process it.
93
+ """
94
+ event.accept()
95
+
96
+ def mouseDoubleClickEvent(self, event) -> None:
97
+ """Handle mouse double-click events on the overlay.
98
+
99
+ Args:
100
+ event: Qt mouse event.
101
+
102
+ The event is accepted so the underlying widget does not process it.
103
+ """
104
+ event.accept()
105
+
106
+ def contextMenuEvent(self, event) -> None:
107
+ """Handle context-menu events on the overlay.
108
+
109
+ Args:
110
+ event: Qt context-menu event.
111
+
112
+ The event is accepted because context-menu handling is managed through the
113
+ mouse-release forwarding logic.
114
+ """
115
+ event.accept()