bec-widgets 1.12.0__py3-none-any.whl → 1.14.0__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.
- .gitlab-ci.yml +1 -0
- CHANGELOG.md +5727 -0
- PKG-INFO +4 -3
- bec_widgets/cli/auto_updates.py +45 -61
- bec_widgets/cli/client.py +19 -2
- bec_widgets/cli/client_utils.py +142 -198
- bec_widgets/cli/generate_cli.py +2 -2
- bec_widgets/cli/rpc/__init__.py +0 -0
- bec_widgets/cli/rpc/rpc_base.py +177 -0
- bec_widgets/cli/server.py +66 -29
- bec_widgets/qt_utils/error_popups.py +4 -2
- bec_widgets/tests/utils.py +8 -0
- bec_widgets/utils/bec_connector.py +1 -1
- bec_widgets/utils/widget_io.py +85 -5
- bec_widgets/widgets/containers/dock/dock.py +1 -1
- bec_widgets/widgets/containers/dock/dock_area.py +40 -2
- bec_widgets/widgets/containers/layout_manager/layout_manager.py +1 -1
- bec_widgets/widgets/containers/main_window/main_window.py +33 -1
- bec_widgets/widgets/games/__init__.py +3 -0
- bec_widgets/widgets/games/minesweeper.py +413 -0
- bec_widgets/widgets/games/minesweeper.pyproject +1 -0
- bec_widgets/widgets/games/minesweeper_plugin.py +54 -0
- bec_widgets/widgets/games/register_minesweeper.py +15 -0
- {bec_widgets-1.12.0.dist-info → bec_widgets-1.14.0.dist-info}/METADATA +4 -3
- {bec_widgets-1.12.0.dist-info → bec_widgets-1.14.0.dist-info}/RECORD +31 -24
- {bec_widgets-1.12.0.dist-info → bec_widgets-1.14.0.dist-info}/WHEEL +1 -1
- pyproject.toml +2 -2
- /bec_widgets/cli/{rpc_register.py → rpc/rpc_register.py} +0 -0
- /bec_widgets/cli/{rpc_wigdet_handler.py → rpc/rpc_widget_handler.py} +0 -0
- {bec_widgets-1.12.0.dist-info → bec_widgets-1.14.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-1.12.0.dist-info → bec_widgets-1.14.0.dist-info}/licenses/LICENSE +0 -0
bec_widgets/cli/server.py
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
import functools
|
3
4
|
import json
|
4
5
|
import signal
|
5
6
|
import sys
|
6
|
-
|
7
|
+
import types
|
8
|
+
from contextlib import contextmanager, redirect_stderr, redirect_stdout
|
7
9
|
from typing import Union
|
8
10
|
|
9
11
|
from bec_lib.endpoints import MessageEndpoints
|
@@ -12,7 +14,8 @@ from bec_lib.service_config import ServiceConfig
|
|
12
14
|
from bec_lib.utils.import_utils import lazy_import
|
13
15
|
from qtpy.QtCore import Qt, QTimer
|
14
16
|
|
15
|
-
from bec_widgets.cli.rpc_register import RPCRegister
|
17
|
+
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
18
|
+
from bec_widgets.qt_utils.error_popups import ErrorPopupUtility
|
16
19
|
from bec_widgets.utils import BECDispatcher
|
17
20
|
from bec_widgets.utils.bec_connector import BECConnector
|
18
21
|
from bec_widgets.widgets.containers.dock import BECDockArea
|
@@ -23,6 +26,27 @@ messages = lazy_import("bec_lib.messages")
|
|
23
26
|
logger = bec_logger.logger
|
24
27
|
|
25
28
|
|
29
|
+
@contextmanager
|
30
|
+
def rpc_exception_hook(err_func):
|
31
|
+
"""This context replaces the popup message box for error display with a specific hook"""
|
32
|
+
# get error popup utility singleton
|
33
|
+
popup = ErrorPopupUtility()
|
34
|
+
# save current setting
|
35
|
+
old_exception_hook = popup.custom_exception_hook
|
36
|
+
|
37
|
+
# install err_func, if it is a callable
|
38
|
+
def custom_exception_hook(self, exc_type, value, tb, **kwargs):
|
39
|
+
err_func({"error": popup.get_error_message(exc_type, value, tb)})
|
40
|
+
|
41
|
+
popup.custom_exception_hook = types.MethodType(custom_exception_hook, popup)
|
42
|
+
|
43
|
+
try:
|
44
|
+
yield popup
|
45
|
+
finally:
|
46
|
+
# restore state of error popup utility singleton
|
47
|
+
popup.custom_exception_hook = old_exception_hook
|
48
|
+
|
49
|
+
|
26
50
|
class BECWidgetsCLIServer:
|
27
51
|
|
28
52
|
def __init__(
|
@@ -57,18 +81,19 @@ class BECWidgetsCLIServer:
|
|
57
81
|
def on_rpc_update(self, msg: dict, metadata: dict):
|
58
82
|
request_id = metadata.get("request_id")
|
59
83
|
logger.debug(f"Received RPC instruction: {msg}, metadata: {metadata}")
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
84
|
+
with rpc_exception_hook(functools.partial(self.send_response, request_id, False)):
|
85
|
+
try:
|
86
|
+
obj = self.get_object_from_config(msg["parameter"])
|
87
|
+
method = msg["action"]
|
88
|
+
args = msg["parameter"].get("args", [])
|
89
|
+
kwargs = msg["parameter"].get("kwargs", {})
|
90
|
+
res = self.run_rpc(obj, method, args, kwargs)
|
91
|
+
except Exception as e:
|
92
|
+
logger.error(f"Error while executing RPC instruction: {e}")
|
93
|
+
self.send_response(request_id, False, {"error": str(e)})
|
94
|
+
else:
|
95
|
+
logger.debug(f"RPC instruction executed successfully: {res}")
|
96
|
+
self.send_response(request_id, True, {"result": res})
|
72
97
|
|
73
98
|
def send_response(self, request_id: str, accepted: bool, msg: dict):
|
74
99
|
self.client.connector.set_and_publish(
|
@@ -181,14 +206,8 @@ def main():
|
|
181
206
|
|
182
207
|
import bec_widgets
|
183
208
|
|
184
|
-
bec_logger.level = bec_logger.LOGLEVEL.DEBUG
|
185
|
-
if __name__ != "__main__":
|
186
|
-
# if not running as main, set the log level to critical
|
187
|
-
# pylint: disable=protected-access
|
188
|
-
bec_logger._stderr_log_level = bec_logger.LOGLEVEL.CRITICAL
|
189
|
-
|
190
209
|
parser = argparse.ArgumentParser(description="BEC Widgets CLI Server")
|
191
|
-
parser.add_argument("--id", type=str, help="The id of the server")
|
210
|
+
parser.add_argument("--id", type=str, default="test", help="The id of the server")
|
192
211
|
parser.add_argument(
|
193
212
|
"--gui_class",
|
194
213
|
type=str,
|
@@ -199,10 +218,20 @@ def main():
|
|
199
218
|
|
200
219
|
args = parser.parse_args()
|
201
220
|
|
202
|
-
if args.
|
203
|
-
|
204
|
-
|
221
|
+
if args.hide:
|
222
|
+
# if we start hidden, it means we are under control of the client
|
223
|
+
# -> set the log level to critical to not see all the messages
|
224
|
+
# pylint: disable=protected-access
|
225
|
+
# bec_logger._stderr_log_level = bec_logger.LOGLEVEL.CRITICAL
|
226
|
+
bec_logger.level = bec_logger.LOGLEVEL.CRITICAL
|
227
|
+
else:
|
228
|
+
# verbose log
|
229
|
+
bec_logger.level = bec_logger.LOGLEVEL.DEBUG
|
230
|
+
|
231
|
+
if args.gui_class == "BECDockArea":
|
205
232
|
gui_class = BECDockArea
|
233
|
+
elif args.gui_class == "BECFigure":
|
234
|
+
gui_class = BECFigure
|
206
235
|
else:
|
207
236
|
print(
|
208
237
|
"Please specify a valid gui_class to run. Use -h for help."
|
@@ -213,8 +242,10 @@ def main():
|
|
213
242
|
with redirect_stdout(SimpleFileLikeFromLogOutputFunc(logger.info)):
|
214
243
|
with redirect_stderr(SimpleFileLikeFromLogOutputFunc(logger.error)):
|
215
244
|
app = QApplication(sys.argv)
|
216
|
-
|
217
|
-
|
245
|
+
# set close on last window, only if not under control of client ;
|
246
|
+
# indeed, Qt considers a hidden window a closed window, so if all windows
|
247
|
+
# are hidden by default it exits
|
248
|
+
app.setQuitOnLastWindowClosed(not args.hide)
|
218
249
|
module_path = os.path.dirname(bec_widgets.__file__)
|
219
250
|
icon = QIcon()
|
220
251
|
icon.addFile(
|
@@ -222,6 +253,8 @@ def main():
|
|
222
253
|
size=QSize(48, 48),
|
223
254
|
)
|
224
255
|
app.setWindowIcon(icon)
|
256
|
+
# store gui id within QApplication object, to make it available to all widgets
|
257
|
+
app.gui_id = args.id
|
225
258
|
|
226
259
|
server = _start_server(args.id, gui_class, args.config)
|
227
260
|
|
@@ -233,7 +266,6 @@ def main():
|
|
233
266
|
|
234
267
|
gui = server.gui
|
235
268
|
win.setCentralWidget(gui)
|
236
|
-
win.resize(800, 600)
|
237
269
|
if not args.hide:
|
238
270
|
win.show()
|
239
271
|
|
@@ -242,6 +274,12 @@ def main():
|
|
242
274
|
def sigint_handler(*args):
|
243
275
|
# display message, for people to let it terminate gracefully
|
244
276
|
print("Caught SIGINT, exiting")
|
277
|
+
# first hide all top level windows
|
278
|
+
# this is to discriminate the cases between "user clicks on [X]"
|
279
|
+
# (which should be filtered, to not close -see BECDockArea-)
|
280
|
+
# or "app is asked to close"
|
281
|
+
for window in app.topLevelWidgets():
|
282
|
+
window.hide() # so, we know we can exit because it is hidden
|
245
283
|
app.quit()
|
246
284
|
|
247
285
|
signal.signal(signal.SIGINT, sigint_handler)
|
@@ -250,6 +288,5 @@ def main():
|
|
250
288
|
sys.exit(app.exec())
|
251
289
|
|
252
290
|
|
253
|
-
if __name__ == "__main__":
|
254
|
-
sys.argv = ["bec_widgets.cli.server", "--id", "e2860", "--gui_class", "BECDockArea"]
|
291
|
+
if __name__ == "__main__":
|
255
292
|
main()
|
@@ -169,12 +169,14 @@ class _ErrorPopupUtility(QObject):
|
|
169
169
|
error_message = " ".join(captured_message)
|
170
170
|
return error_message
|
171
171
|
|
172
|
+
def get_error_message(self, exctype, value, tb):
|
173
|
+
return "".join(traceback.format_exception(exctype, value, tb))
|
174
|
+
|
172
175
|
def custom_exception_hook(self, exctype, value, tb, popup_error=False):
|
173
176
|
if popup_error or self.enable_error_popup:
|
174
|
-
error_message = traceback.format_exception(exctype, value, tb)
|
175
177
|
self.error_occurred.emit(
|
176
178
|
"Method error" if popup_error else "Application Error",
|
177
|
-
|
179
|
+
self.get_error_message(exctype, value, tb),
|
178
180
|
self.parent(),
|
179
181
|
)
|
180
182
|
else:
|
bec_widgets/tests/utils.py
CHANGED
@@ -224,3 +224,11 @@ DEVICES = [
|
|
224
224
|
Positioner("test", limits=[-10, 10], read_value=2.0),
|
225
225
|
Device("test_device"),
|
226
226
|
]
|
227
|
+
|
228
|
+
|
229
|
+
def check_remote_data_size(widget, plot_name, num_elements):
|
230
|
+
"""
|
231
|
+
Check if the remote data has the correct number of elements.
|
232
|
+
Used in the qtbot.waitUntil function.
|
233
|
+
"""
|
234
|
+
return len(widget.get_all_data()[plot_name]["x"]) == num_elements
|
@@ -12,7 +12,7 @@ from pydantic import BaseModel, Field, field_validator
|
|
12
12
|
from qtpy.QtCore import QObject, QRunnable, QThreadPool, Signal
|
13
13
|
from qtpy.QtWidgets import QApplication
|
14
14
|
|
15
|
-
from bec_widgets.cli.rpc_register import RPCRegister
|
15
|
+
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
16
16
|
from bec_widgets.qt_utils.error_popups import ErrorPopupUtility
|
17
17
|
from bec_widgets.qt_utils.error_popups import SafeSlot as pyqtSlot
|
18
18
|
from bec_widgets.utils.yaml_dialog import load_yaml, load_yaml_gui, save_yaml, save_yaml_gui
|
bec_widgets/utils/widget_io.py
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
# pylint: disable=no-name-in-module
|
2
2
|
from abc import ABC, abstractmethod
|
3
|
-
from typing import Literal
|
4
3
|
|
5
4
|
from qtpy.QtWidgets import (
|
6
5
|
QApplication,
|
@@ -28,6 +27,15 @@ class WidgetHandler(ABC):
|
|
28
27
|
def set_value(self, widget: QWidget, value):
|
29
28
|
"""Set a value on the widget instance."""
|
30
29
|
|
30
|
+
def connect_change_signal(self, widget: QWidget, slot):
|
31
|
+
"""
|
32
|
+
Connect a change signal from this widget to the given slot.
|
33
|
+
If the widget type doesn't have a known "value changed" signal, do nothing.
|
34
|
+
|
35
|
+
slot: a function accepting two arguments (widget, value)
|
36
|
+
"""
|
37
|
+
pass
|
38
|
+
|
31
39
|
|
32
40
|
class LineEditHandler(WidgetHandler):
|
33
41
|
"""Handler for QLineEdit widgets."""
|
@@ -38,6 +46,9 @@ class LineEditHandler(WidgetHandler):
|
|
38
46
|
def set_value(self, widget: QLineEdit, value: str) -> None:
|
39
47
|
widget.setText(value)
|
40
48
|
|
49
|
+
def connect_change_signal(self, widget: QLineEdit, slot):
|
50
|
+
widget.textChanged.connect(lambda text, w=widget: slot(w, text))
|
51
|
+
|
41
52
|
|
42
53
|
class ComboBoxHandler(WidgetHandler):
|
43
54
|
"""Handler for QComboBox widgets."""
|
@@ -53,6 +64,11 @@ class ComboBoxHandler(WidgetHandler):
|
|
53
64
|
if isinstance(value, int):
|
54
65
|
widget.setCurrentIndex(value)
|
55
66
|
|
67
|
+
def connect_change_signal(self, widget: QComboBox, slot):
|
68
|
+
# currentIndexChanged(int) or currentIndexChanged(str) both possible.
|
69
|
+
# We use currentIndexChanged(int) for a consistent behavior.
|
70
|
+
widget.currentIndexChanged.connect(lambda idx, w=widget: slot(w, self.get_value(w)))
|
71
|
+
|
56
72
|
|
57
73
|
class TableWidgetHandler(WidgetHandler):
|
58
74
|
"""Handler for QTableWidget widgets."""
|
@@ -72,6 +88,16 @@ class TableWidgetHandler(WidgetHandler):
|
|
72
88
|
item = QTableWidgetItem(str(cell_value))
|
73
89
|
widget.setItem(row, col, item)
|
74
90
|
|
91
|
+
def connect_change_signal(self, widget: QTableWidget, slot):
|
92
|
+
# If desired, we could connect cellChanged(row, col) and then fetch all data.
|
93
|
+
# This might be noisy if table is large.
|
94
|
+
# For demonstration, connect cellChanged to update entire table value.
|
95
|
+
def on_cell_changed(row, col, w=widget):
|
96
|
+
val = self.get_value(w)
|
97
|
+
slot(w, val)
|
98
|
+
|
99
|
+
widget.cellChanged.connect(on_cell_changed)
|
100
|
+
|
75
101
|
|
76
102
|
class SpinBoxHandler(WidgetHandler):
|
77
103
|
"""Handler for QSpinBox and QDoubleSpinBox widgets."""
|
@@ -82,6 +108,9 @@ class SpinBoxHandler(WidgetHandler):
|
|
82
108
|
def set_value(self, widget, value):
|
83
109
|
widget.setValue(value)
|
84
110
|
|
111
|
+
def connect_change_signal(self, widget: QSpinBox | QDoubleSpinBox, slot):
|
112
|
+
widget.valueChanged.connect(lambda val, w=widget: slot(w, val))
|
113
|
+
|
85
114
|
|
86
115
|
class CheckBoxHandler(WidgetHandler):
|
87
116
|
"""Handler for QCheckBox widgets."""
|
@@ -92,6 +121,9 @@ class CheckBoxHandler(WidgetHandler):
|
|
92
121
|
def set_value(self, widget, value):
|
93
122
|
widget.setChecked(value)
|
94
123
|
|
124
|
+
def connect_change_signal(self, widget: QCheckBox, slot):
|
125
|
+
widget.toggled.connect(lambda val, w=widget: slot(w, val))
|
126
|
+
|
95
127
|
|
96
128
|
class LabelHandler(WidgetHandler):
|
97
129
|
"""Handler for QLabel widgets."""
|
@@ -99,12 +131,15 @@ class LabelHandler(WidgetHandler):
|
|
99
131
|
def get_value(self, widget, **kwargs):
|
100
132
|
return widget.text()
|
101
133
|
|
102
|
-
def set_value(self, widget, value):
|
134
|
+
def set_value(self, widget: QLabel, value):
|
103
135
|
widget.setText(value)
|
104
136
|
|
137
|
+
# QLabel typically doesn't have user-editable changes. No signal to connect.
|
138
|
+
# If needed, this can remain empty.
|
139
|
+
|
105
140
|
|
106
141
|
class WidgetIO:
|
107
|
-
"""Public interface for getting
|
142
|
+
"""Public interface for getting, setting values and connecting signals using handler mapping"""
|
108
143
|
|
109
144
|
_handlers = {
|
110
145
|
QLineEdit: LineEditHandler,
|
@@ -148,6 +183,17 @@ class WidgetIO:
|
|
148
183
|
elif not ignore_errors:
|
149
184
|
raise ValueError(f"No handler for widget type: {type(widget)}")
|
150
185
|
|
186
|
+
@staticmethod
|
187
|
+
def connect_widget_change_signal(widget, slot):
|
188
|
+
"""
|
189
|
+
Connect the widget's value-changed signal to a generic slot function (widget, value).
|
190
|
+
This now delegates the logic to the widget's handler.
|
191
|
+
"""
|
192
|
+
handler_class = WidgetIO._find_handler(widget)
|
193
|
+
if handler_class:
|
194
|
+
handler = handler_class()
|
195
|
+
handler.connect_change_signal(widget, slot)
|
196
|
+
|
151
197
|
@staticmethod
|
152
198
|
def check_and_adjust_limits(spin_box: QDoubleSpinBox, number: float):
|
153
199
|
"""
|
@@ -309,8 +355,8 @@ class WidgetHierarchy:
|
|
309
355
|
WidgetHierarchy.import_config_from_dict(child, widget_config, set_values)
|
310
356
|
|
311
357
|
|
312
|
-
# Example
|
313
|
-
|
358
|
+
# Example usage
|
359
|
+
def hierarchy_example(): # pragma: no cover
|
314
360
|
app = QApplication([])
|
315
361
|
|
316
362
|
# Create instance of WidgetHierarchy
|
@@ -365,3 +411,37 @@ if __name__ == "__main__": # pragma: no cover
|
|
365
411
|
print(f"Config dict new REDUCED: {config_dict_new_reduced}")
|
366
412
|
|
367
413
|
app.exec()
|
414
|
+
|
415
|
+
|
416
|
+
def widget_io_signal_example(): # pragma: no cover
|
417
|
+
app = QApplication([])
|
418
|
+
|
419
|
+
main_widget = QWidget()
|
420
|
+
layout = QVBoxLayout(main_widget)
|
421
|
+
line_edit = QLineEdit(main_widget)
|
422
|
+
combo_box = QComboBox(main_widget)
|
423
|
+
spin_box = QSpinBox(main_widget)
|
424
|
+
combo_box.addItems(["Option 1", "Option 2", "Option 3"])
|
425
|
+
|
426
|
+
layout.addWidget(line_edit)
|
427
|
+
layout.addWidget(combo_box)
|
428
|
+
layout.addWidget(spin_box)
|
429
|
+
|
430
|
+
main_widget.show()
|
431
|
+
|
432
|
+
def universal_slot(w, val):
|
433
|
+
print(f"Widget {w.objectName() or w} changed, new value: {val}")
|
434
|
+
|
435
|
+
# Connect all supported widgets through their handlers
|
436
|
+
WidgetIO.connect_widget_change_signal(line_edit, universal_slot)
|
437
|
+
WidgetIO.connect_widget_change_signal(combo_box, universal_slot)
|
438
|
+
WidgetIO.connect_widget_change_signal(spin_box, universal_slot)
|
439
|
+
|
440
|
+
app.exec_()
|
441
|
+
|
442
|
+
|
443
|
+
if __name__ == "__main__": # pragma: no cover
|
444
|
+
# Change example function to test different scenarios
|
445
|
+
|
446
|
+
# hierarchy_example()
|
447
|
+
widget_io_signal_example()
|
@@ -6,7 +6,7 @@ from pydantic import Field
|
|
6
6
|
from pyqtgraph.dockarea import Dock, DockLabel
|
7
7
|
from qtpy import QtCore, QtGui
|
8
8
|
|
9
|
-
from bec_widgets.cli.
|
9
|
+
from bec_widgets.cli.rpc.rpc_widget_handler import widget_handler
|
10
10
|
from bec_widgets.utils import ConnectionConfig, GridLayoutManager
|
11
11
|
from bec_widgets.utils.bec_widget import BECWidget
|
12
12
|
|
@@ -3,11 +3,12 @@ from __future__ import annotations
|
|
3
3
|
from typing import Literal, Optional
|
4
4
|
from weakref import WeakValueDictionary
|
5
5
|
|
6
|
+
from bec_lib.endpoints import MessageEndpoints
|
6
7
|
from pydantic import Field
|
7
8
|
from pyqtgraph.dockarea.DockArea import DockArea
|
8
|
-
from qtpy.QtCore import Qt
|
9
|
+
from qtpy.QtCore import QSize, Qt
|
9
10
|
from qtpy.QtGui import QPainter, QPaintEvent
|
10
|
-
from qtpy.QtWidgets import QSizePolicy, QVBoxLayout, QWidget
|
11
|
+
from qtpy.QtWidgets import QApplication, QSizePolicy, QVBoxLayout, QWidget
|
11
12
|
|
12
13
|
from bec_widgets.qt_utils.error_popups import SafeSlot
|
13
14
|
from bec_widgets.qt_utils.toolbar import (
|
@@ -43,6 +44,7 @@ class BECDockArea(BECWidget, QWidget):
|
|
43
44
|
PLUGIN = True
|
44
45
|
USER_ACCESS = [
|
45
46
|
"_config_dict",
|
47
|
+
"selected_device",
|
46
48
|
"panels",
|
47
49
|
"save_state",
|
48
50
|
"remove_dock",
|
@@ -55,6 +57,7 @@ class BECDockArea(BECWidget, QWidget):
|
|
55
57
|
"temp_areas",
|
56
58
|
"show",
|
57
59
|
"hide",
|
60
|
+
"delete",
|
58
61
|
]
|
59
62
|
|
60
63
|
def __init__(
|
@@ -158,6 +161,9 @@ class BECDockArea(BECWidget, QWidget):
|
|
158
161
|
self.toolbar.addWidget(DarkModeButton(toolbar=True))
|
159
162
|
self._hook_toolbar()
|
160
163
|
|
164
|
+
def minimumSizeHint(self):
|
165
|
+
return QSize(800, 600)
|
166
|
+
|
161
167
|
def _hook_toolbar(self):
|
162
168
|
# Menu Plot
|
163
169
|
self.toolbar.widgets["menu_plots"].widgets["waveform"].triggered.connect(
|
@@ -210,6 +216,17 @@ class BECDockArea(BECWidget, QWidget):
|
|
210
216
|
"Add docks using 'add_dock' method from CLI\n or \n Add widget docks using the toolbar",
|
211
217
|
)
|
212
218
|
|
219
|
+
@property
|
220
|
+
def selected_device(self) -> str:
|
221
|
+
gui_id = QApplication.instance().gui_id
|
222
|
+
auto_update_config = self.client.connector.get(
|
223
|
+
MessageEndpoints.gui_auto_update_config(gui_id)
|
224
|
+
)
|
225
|
+
try:
|
226
|
+
return auto_update_config.selected_device
|
227
|
+
except AttributeError:
|
228
|
+
return None
|
229
|
+
|
213
230
|
@property
|
214
231
|
def panels(self) -> dict[str, BECDock]:
|
215
232
|
"""
|
@@ -406,6 +423,17 @@ class BECDockArea(BECWidget, QWidget):
|
|
406
423
|
self.dock_area.deleteLater()
|
407
424
|
super().cleanup()
|
408
425
|
|
426
|
+
def closeEvent(self, event):
|
427
|
+
if self.parent() is None:
|
428
|
+
# we are at top-level (independent window)
|
429
|
+
if self.isVisible():
|
430
|
+
# we are visible => user clicked on [X]
|
431
|
+
# (when closeEvent is called from shutdown procedure,
|
432
|
+
# everything is hidden first)
|
433
|
+
# so, let's ignore "close", and do hide instead
|
434
|
+
event.ignore()
|
435
|
+
self.setVisible(False)
|
436
|
+
|
409
437
|
def close(self):
|
410
438
|
"""
|
411
439
|
Close the dock area and cleanup.
|
@@ -418,14 +446,24 @@ class BECDockArea(BECWidget, QWidget):
|
|
418
446
|
"""Show all windows including floating docks."""
|
419
447
|
super().show()
|
420
448
|
for docks in self.panels.values():
|
449
|
+
if docks.window() is self:
|
450
|
+
# avoid recursion
|
451
|
+
continue
|
421
452
|
docks.window().show()
|
422
453
|
|
423
454
|
def hide(self):
|
424
455
|
"""Hide all windows including floating docks."""
|
425
456
|
super().hide()
|
426
457
|
for docks in self.panels.values():
|
458
|
+
if docks.window() is self:
|
459
|
+
# avoid recursion
|
460
|
+
continue
|
427
461
|
docks.window().hide()
|
428
462
|
|
463
|
+
def delete(self):
|
464
|
+
self.hide()
|
465
|
+
self.deleteLater()
|
466
|
+
|
429
467
|
|
430
468
|
if __name__ == "__main__":
|
431
469
|
from qtpy.QtWidgets import QApplication
|
@@ -1,9 +1,41 @@
|
|
1
|
-
from qtpy.QtWidgets import QMainWindow
|
1
|
+
from qtpy.QtWidgets import QApplication, QMainWindow
|
2
2
|
|
3
3
|
from bec_widgets.utils import BECConnector
|
4
|
+
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
|
4
5
|
|
5
6
|
|
6
7
|
class BECMainWindow(QMainWindow, BECConnector):
|
7
8
|
def __init__(self, *args, **kwargs):
|
8
9
|
BECConnector.__init__(self, **kwargs)
|
9
10
|
QMainWindow.__init__(self, *args, **kwargs)
|
11
|
+
|
12
|
+
def _dump(self):
|
13
|
+
"""Return a dictionary with informations about the application state, for use in tests"""
|
14
|
+
# TODO: ModularToolBar and something else leak top-level widgets (3 or 4 QMenu + 2 QWidget);
|
15
|
+
# so, a filtering based on title is applied here, but the solution is to not have those widgets
|
16
|
+
# as top-level (so for now, a window with no title does not appear in _dump() result)
|
17
|
+
|
18
|
+
# NOTE: the main window itself is excluded, since we want to dump dock areas
|
19
|
+
info = {
|
20
|
+
tlw.gui_id: {
|
21
|
+
"title": tlw.windowTitle(),
|
22
|
+
"visible": tlw.isVisible(),
|
23
|
+
"class": str(type(tlw)),
|
24
|
+
}
|
25
|
+
for tlw in QApplication.instance().topLevelWidgets()
|
26
|
+
if tlw is not self and tlw.windowTitle()
|
27
|
+
}
|
28
|
+
# Add the main window dock area
|
29
|
+
info[self.centralWidget().gui_id] = {
|
30
|
+
"title": self.windowTitle(),
|
31
|
+
"visible": self.isVisible(),
|
32
|
+
"class": str(type(self.centralWidget())),
|
33
|
+
}
|
34
|
+
return info
|
35
|
+
|
36
|
+
def new_dock_area(self, name):
|
37
|
+
dock_area = BECDockArea()
|
38
|
+
dock_area.resize(dock_area.minimumSizeHint())
|
39
|
+
dock_area.window().setWindowTitle(name)
|
40
|
+
dock_area.show()
|
41
|
+
return dock_area
|