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.
- sciwidgets/__init__.py +8 -0
- sciwidgets/dialogs/__init__.py +31 -0
- sciwidgets/dialogs/__main__.py +128 -0
- sciwidgets/dialogs/message_dialogs.py +553 -0
- sciwidgets/formatting.py +161 -0
- sciwidgets/labels/__init__.py +3 -0
- sciwidgets/labels/__main__.py +124 -0
- sciwidgets/labels/math_label.py +505 -0
- sciwidgets/params/__init__.py +18 -0
- sciwidgets/params/__main__.py +81 -0
- sciwidgets/params/base.py +220 -0
- sciwidgets/params/combo_box.py +106 -0
- sciwidgets/params/line_edit.py +132 -0
- sciwidgets/params/numeric_line_edit.py +680 -0
- sciwidgets/params/sliders/__init__.py +36 -0
- sciwidgets/params/sliders/__main__.py +215 -0
- sciwidgets/params/sliders/_defaults.py +36 -0
- sciwidgets/params/sliders/_dialogs.py +174 -0
- sciwidgets/params/sliders/_right_click_overlay.py +115 -0
- sciwidgets/params/sliders/base.py +592 -0
- sciwidgets/params/sliders/multi.py +660 -0
- sciwidgets/params/sliders/range.py +1841 -0
- sciwidgets/params/sliders/single.py +2042 -0
- sciwidgets/params/toggle.py +116 -0
- sciwidgets/style.py +175 -0
- sciwidgets-0.1.2.dist-info/METADATA +173 -0
- sciwidgets-0.1.2.dist-info/RECORD +30 -0
- sciwidgets-0.1.2.dist-info/WHEEL +5 -0
- sciwidgets-0.1.2.dist-info/licenses/LICENSE +21 -0
- sciwidgets-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -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()
|