bec-widgets 1.11.0__py3-none-any.whl → 1.13.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 +5712 -0
- PKG-INFO +4 -3
- bec_widgets/cli/auto_updates.py +45 -61
- bec_widgets/cli/client.py +15 -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 +47 -3
- 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-1.11.0.dist-info → bec_widgets-1.13.0.dist-info}/METADATA +4 -3
- {bec_widgets-1.11.0.dist-info → bec_widgets-1.13.0.dist-info}/RECORD +26 -24
- {bec_widgets-1.11.0.dist-info → bec_widgets-1.13.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.11.0.dist-info → bec_widgets-1.13.0.dist-info}/entry_points.txt +0 -0
- {bec_widgets-1.11.0.dist-info → bec_widgets-1.13.0.dist-info}/licenses/LICENSE +0 -0
PKG-INFO
CHANGED
@@ -1,9 +1,10 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: bec_widgets
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.13.0
|
4
4
|
Summary: BEC Widgets
|
5
5
|
Project-URL: Bug Tracker, https://gitlab.psi.ch/bec/bec_widgets/issues
|
6
6
|
Project-URL: Homepage, https://gitlab.psi.ch/bec/bec_widgets
|
7
|
+
License-File: LICENSE
|
7
8
|
Classifier: Development Status :: 3 - Alpha
|
8
9
|
Classifier: Programming Language :: Python :: 3
|
9
10
|
Classifier: Topic :: Scientific/Engineering
|
@@ -21,7 +22,7 @@ Requires-Dist: qtpy~=2.4
|
|
21
22
|
Provides-Extra: dev
|
22
23
|
Requires-Dist: coverage~=7.0; extra == 'dev'
|
23
24
|
Requires-Dist: fakeredis>=2.23.2,~=2.23; extra == 'dev'
|
24
|
-
Requires-Dist: pytest-bec-e2e
|
25
|
+
Requires-Dist: pytest-bec-e2e<=4.0,>=2.21.4; extra == 'dev'
|
25
26
|
Requires-Dist: pytest-qt~=4.4; extra == 'dev'
|
26
27
|
Requires-Dist: pytest-random-order~=1.1; extra == 'dev'
|
27
28
|
Requires-Dist: pytest-timeout~=2.2; extra == 'dev'
|
bec_widgets/cli/auto_updates.py
CHANGED
@@ -27,25 +27,17 @@ class AutoUpdates:
|
|
27
27
|
|
28
28
|
def __init__(self, gui: BECDockArea):
|
29
29
|
self.gui = gui
|
30
|
-
self.
|
31
|
-
self.
|
32
|
-
self._shutdown_sentinel = object()
|
33
|
-
self.start()
|
34
|
-
|
35
|
-
def start(self):
|
36
|
-
"""
|
37
|
-
Start the auto update thread.
|
38
|
-
"""
|
39
|
-
self.auto_update_thread = threading.Thread(target=self.process_queue)
|
40
|
-
self.auto_update_thread.start()
|
30
|
+
self._default_dock = None
|
31
|
+
self._default_fig = None
|
41
32
|
|
42
33
|
def start_default_dock(self):
|
43
34
|
"""
|
44
35
|
Create a default dock for the auto updates.
|
45
36
|
"""
|
46
|
-
dock = self.gui.add_dock("default_figure")
|
47
|
-
dock.add_widget("BECFigure")
|
48
37
|
self.dock_name = "default_figure"
|
38
|
+
self._default_dock = self.gui.add_dock(self.dock_name)
|
39
|
+
self._default_dock.add_widget("BECFigure")
|
40
|
+
self._default_fig = self._default_dock.widget_list[0]
|
49
41
|
|
50
42
|
@staticmethod
|
51
43
|
def get_scan_info(msg) -> ScanInfo:
|
@@ -73,15 +65,9 @@ class AutoUpdates:
|
|
73
65
|
"""
|
74
66
|
Get the default figure from the GUI.
|
75
67
|
"""
|
76
|
-
|
77
|
-
if not dock:
|
78
|
-
return None
|
79
|
-
widgets = dock.widget_list
|
80
|
-
if not widgets:
|
81
|
-
return None
|
82
|
-
return widgets[0]
|
68
|
+
return self._default_fig
|
83
69
|
|
84
|
-
def
|
70
|
+
def do_update(self, msg):
|
85
71
|
"""
|
86
72
|
Run the update function if enabled.
|
87
73
|
"""
|
@@ -90,20 +76,9 @@ class AutoUpdates:
|
|
90
76
|
if msg.status != "open":
|
91
77
|
return
|
92
78
|
info = self.get_scan_info(msg)
|
93
|
-
self.handler(info)
|
94
|
-
|
95
|
-
def process_queue(self):
|
96
|
-
"""
|
97
|
-
Process the message queue.
|
98
|
-
"""
|
99
|
-
while True:
|
100
|
-
msg = self.msg_queue.get()
|
101
|
-
if msg is self._shutdown_sentinel:
|
102
|
-
break
|
103
|
-
self.run(msg)
|
79
|
+
return self.handler(info)
|
104
80
|
|
105
|
-
|
106
|
-
def get_selected_device(monitored_devices, selected_device):
|
81
|
+
def get_selected_device(self, monitored_devices, selected_device):
|
107
82
|
"""
|
108
83
|
Get the selected device for the plot. If no device is selected, the first
|
109
84
|
device in the monitored devices list is selected.
|
@@ -120,14 +95,11 @@ class AutoUpdates:
|
|
120
95
|
Default update function.
|
121
96
|
"""
|
122
97
|
if info.scan_name == "line_scan" and info.scan_report_devices:
|
123
|
-
self.simple_line_scan(info)
|
124
|
-
return
|
98
|
+
return self.simple_line_scan(info)
|
125
99
|
if info.scan_name == "grid_scan" and info.scan_report_devices:
|
126
|
-
self.simple_grid_scan(info)
|
127
|
-
return
|
100
|
+
return self.simple_grid_scan(info)
|
128
101
|
if info.scan_report_devices:
|
129
|
-
self.best_effort(info)
|
130
|
-
return
|
102
|
+
return self.best_effort(info)
|
131
103
|
|
132
104
|
def simple_line_scan(self, info: ScanInfo) -> None:
|
133
105
|
"""
|
@@ -137,12 +109,19 @@ class AutoUpdates:
|
|
137
109
|
if not fig:
|
138
110
|
return
|
139
111
|
dev_x = info.scan_report_devices[0]
|
140
|
-
|
112
|
+
selected_device = yield self.gui.selected_device
|
113
|
+
dev_y = self.get_selected_device(info.monitored_devices, selected_device)
|
141
114
|
if not dev_y:
|
142
115
|
return
|
143
|
-
fig.clear_all()
|
144
|
-
|
145
|
-
|
116
|
+
yield fig.clear_all()
|
117
|
+
yield fig.plot(
|
118
|
+
x_name=dev_x,
|
119
|
+
y_name=dev_y,
|
120
|
+
label=f"Scan {info.scan_number} - {dev_y}",
|
121
|
+
title=f"Scan {info.scan_number}",
|
122
|
+
x_label=dev_x,
|
123
|
+
y_label=dev_y,
|
124
|
+
)
|
146
125
|
|
147
126
|
def simple_grid_scan(self, info: ScanInfo) -> None:
|
148
127
|
"""
|
@@ -153,12 +132,18 @@ class AutoUpdates:
|
|
153
132
|
return
|
154
133
|
dev_x = info.scan_report_devices[0]
|
155
134
|
dev_y = info.scan_report_devices[1]
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
135
|
+
selected_device = yield self.gui.selected_device
|
136
|
+
dev_z = self.get_selected_device(info.monitored_devices, selected_device)
|
137
|
+
yield fig.clear_all()
|
138
|
+
yield fig.plot(
|
139
|
+
x_name=dev_x,
|
140
|
+
y_name=dev_y,
|
141
|
+
z_name=dev_z,
|
142
|
+
label=f"Scan {info.scan_number} - {dev_z}",
|
143
|
+
title=f"Scan {info.scan_number}",
|
144
|
+
x_label=dev_x,
|
145
|
+
y_label=dev_y,
|
160
146
|
)
|
161
|
-
plt.set(title=f"Scan {info.scan_number}", x_label=dev_x, y_label=dev_y)
|
162
147
|
|
163
148
|
def best_effort(self, info: ScanInfo) -> None:
|
164
149
|
"""
|
@@ -168,17 +153,16 @@ class AutoUpdates:
|
|
168
153
|
if not fig:
|
169
154
|
return
|
170
155
|
dev_x = info.scan_report_devices[0]
|
171
|
-
|
156
|
+
selected_device = yield self.gui.selected_device
|
157
|
+
dev_y = self.get_selected_device(info.monitored_devices, selected_device)
|
172
158
|
if not dev_y:
|
173
159
|
return
|
174
|
-
fig.clear_all()
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
if self.auto_update_thread:
|
184
|
-
self.auto_update_thread.join()
|
160
|
+
yield fig.clear_all()
|
161
|
+
yield fig.plot(
|
162
|
+
x_name=dev_x,
|
163
|
+
y_name=dev_y,
|
164
|
+
label=f"Scan {info.scan_number} - {dev_y}",
|
165
|
+
title=f"Scan {info.scan_number}",
|
166
|
+
x_label=dev_x,
|
167
|
+
y_label=dev_y,
|
168
|
+
)
|
bec_widgets/cli/client.py
CHANGED
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
5
5
|
import enum
|
6
6
|
from typing import Literal, Optional, overload
|
7
7
|
|
8
|
-
from bec_widgets.cli.
|
8
|
+
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call
|
9
9
|
|
10
10
|
# pylint: skip-file
|
11
11
|
|
@@ -342,7 +342,7 @@ class BECDock(RPCBase):
|
|
342
342
|
"""
|
343
343
|
|
344
344
|
|
345
|
-
class BECDockArea(RPCBase
|
345
|
+
class BECDockArea(RPCBase):
|
346
346
|
@property
|
347
347
|
@rpc_call
|
348
348
|
def _config_dict(self) -> "dict":
|
@@ -353,6 +353,13 @@ class BECDockArea(RPCBase, BECGuiClientMixin):
|
|
353
353
|
dict: The configuration of the widget.
|
354
354
|
"""
|
355
355
|
|
356
|
+
@property
|
357
|
+
@rpc_call
|
358
|
+
def selected_device(self) -> "str":
|
359
|
+
"""
|
360
|
+
None
|
361
|
+
"""
|
362
|
+
|
356
363
|
@property
|
357
364
|
@rpc_call
|
358
365
|
def panels(self) -> "dict[str, BECDock]":
|
@@ -480,6 +487,12 @@ class BECDockArea(RPCBase, BECGuiClientMixin):
|
|
480
487
|
Hide all windows including floating docks.
|
481
488
|
"""
|
482
489
|
|
490
|
+
@rpc_call
|
491
|
+
def delete(self):
|
492
|
+
"""
|
493
|
+
None
|
494
|
+
"""
|
495
|
+
|
483
496
|
|
484
497
|
class BECFigure(RPCBase):
|
485
498
|
@property
|
bec_widgets/cli/client_utils.py
CHANGED
@@ -7,61 +7,33 @@ import os
|
|
7
7
|
import select
|
8
8
|
import subprocess
|
9
9
|
import threading
|
10
|
-
import
|
11
|
-
import
|
12
|
-
from functools import wraps
|
10
|
+
from contextlib import contextmanager
|
11
|
+
from dataclasses import dataclass
|
13
12
|
from typing import TYPE_CHECKING
|
14
13
|
|
15
|
-
from bec_lib.client import BECClient
|
16
14
|
from bec_lib.endpoints import MessageEndpoints
|
17
15
|
from bec_lib.logger import bec_logger
|
18
16
|
from bec_lib.utils.import_utils import isinstance_based_on_class_name, lazy_import, lazy_import_from
|
19
17
|
|
20
18
|
import bec_widgets.cli.client as client
|
21
19
|
from bec_widgets.cli.auto_updates import AutoUpdates
|
20
|
+
from bec_widgets.cli.rpc.rpc_base import RPCBase
|
22
21
|
|
23
22
|
if TYPE_CHECKING:
|
23
|
+
from bec_lib import messages
|
24
|
+
from bec_lib.connector import MessageObject
|
24
25
|
from bec_lib.device import DeviceBase
|
25
26
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
27
|
+
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
28
|
+
else:
|
29
|
+
messages = lazy_import("bec_lib.messages")
|
30
|
+
# from bec_lib.connector import MessageObject
|
31
|
+
MessageObject = lazy_import_from("bec_lib.connector", ("MessageObject",))
|
32
|
+
BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))
|
30
33
|
|
31
34
|
logger = bec_logger.logger
|
32
35
|
|
33
36
|
|
34
|
-
def rpc_call(func):
|
35
|
-
"""
|
36
|
-
A decorator for calling a function on the server.
|
37
|
-
|
38
|
-
Args:
|
39
|
-
func: The function to call.
|
40
|
-
|
41
|
-
Returns:
|
42
|
-
The result of the function call.
|
43
|
-
"""
|
44
|
-
|
45
|
-
@wraps(func)
|
46
|
-
def wrapper(self, *args, **kwargs):
|
47
|
-
# we could rely on a strict type check here, but this is more flexible
|
48
|
-
# moreover, it would anyway crash for objects...
|
49
|
-
out = []
|
50
|
-
for arg in args:
|
51
|
-
if hasattr(arg, "name"):
|
52
|
-
arg = arg.name
|
53
|
-
out.append(arg)
|
54
|
-
args = tuple(out)
|
55
|
-
for key, val in kwargs.items():
|
56
|
-
if hasattr(val, "name"):
|
57
|
-
kwargs[key] = val.name
|
58
|
-
if not self.gui_is_alive():
|
59
|
-
raise RuntimeError("GUI is not alive")
|
60
|
-
return self._run_rpc(func.__name__, *args, **kwargs)
|
61
|
-
|
62
|
-
return wrapper
|
63
|
-
|
64
|
-
|
65
37
|
def _get_output(process, logger) -> None:
|
66
38
|
log_func = {process.stdout: logger.debug, process.stderr: logger.error}
|
67
39
|
stream_buffer = {process.stdout: [], process.stderr: []}
|
@@ -132,29 +104,79 @@ class RepeatTimer(threading.Timer):
|
|
132
104
|
self.function(*self.args, **self.kwargs)
|
133
105
|
|
134
106
|
|
135
|
-
|
107
|
+
@contextmanager
|
108
|
+
def wait_for_server(client):
|
109
|
+
timeout = client._startup_timeout
|
110
|
+
if not timeout:
|
111
|
+
if client.gui_is_alive():
|
112
|
+
# there is hope, let's wait a bit
|
113
|
+
timeout = 1
|
114
|
+
else:
|
115
|
+
raise RuntimeError("GUI is not alive")
|
116
|
+
try:
|
117
|
+
if client._gui_started_event.wait(timeout=timeout):
|
118
|
+
client._gui_started_timer.cancel()
|
119
|
+
client._gui_started_timer.join()
|
120
|
+
else:
|
121
|
+
raise TimeoutError("Could not connect to GUI server")
|
122
|
+
finally:
|
123
|
+
# after initial waiting period, do not wait so much any more
|
124
|
+
# (only relevant if GUI didn't start)
|
125
|
+
client._startup_timeout = 0
|
126
|
+
yield
|
127
|
+
|
128
|
+
|
129
|
+
### ----------------------------
|
130
|
+
### NOTE
|
131
|
+
### it is far easier to extend the 'delete' method on the client side,
|
132
|
+
### to know when the client is deleted, rather than listening to server
|
133
|
+
### to get notified. However, 'generate_cli.py' cannot add extra stuff
|
134
|
+
### in the generated client module. So, here a class with the same name
|
135
|
+
### is created, and client module is patched.
|
136
|
+
class BECDockArea(client.BECDockArea):
|
137
|
+
def delete(self):
|
138
|
+
if self is BECGuiClient._top_level["main"].widget:
|
139
|
+
raise RuntimeError("Cannot delete main window")
|
140
|
+
super().delete()
|
141
|
+
try:
|
142
|
+
del BECGuiClient._top_level[self._gui_id]
|
143
|
+
except KeyError:
|
144
|
+
# if a dock area is not at top level
|
145
|
+
pass
|
146
|
+
|
147
|
+
|
148
|
+
client.BECDockArea = BECDockArea
|
149
|
+
### ----------------------------
|
150
|
+
|
151
|
+
|
152
|
+
@dataclass
|
153
|
+
class WidgetDesc:
|
154
|
+
title: str
|
155
|
+
widget: BECDockArea
|
156
|
+
|
157
|
+
|
158
|
+
class BECGuiClient(RPCBase):
|
159
|
+
_top_level = {}
|
160
|
+
|
136
161
|
def __init__(self, **kwargs) -> None:
|
137
162
|
super().__init__(**kwargs)
|
138
163
|
self._auto_updates_enabled = True
|
139
164
|
self._auto_updates = None
|
165
|
+
self._startup_timeout = 0
|
140
166
|
self._gui_started_timer = None
|
141
167
|
self._gui_started_event = threading.Event()
|
142
168
|
self._process = None
|
143
169
|
self._process_output_processing_thread = None
|
144
|
-
self._target_endpoint = MessageEndpoints.scan_status()
|
145
|
-
self._selected_device = None
|
146
170
|
|
147
171
|
@property
|
148
|
-
def
|
149
|
-
|
150
|
-
self._gui_started_event.wait()
|
151
|
-
return self._auto_updates
|
172
|
+
def windows(self):
|
173
|
+
return self._top_level
|
152
174
|
|
153
|
-
|
175
|
+
@property
|
176
|
+
def auto_updates(self):
|
154
177
|
if self._auto_updates_enabled:
|
155
|
-
|
156
|
-
self._auto_updates
|
157
|
-
self._auto_updates = None
|
178
|
+
with wait_for_server(self):
|
179
|
+
return self._auto_updates
|
158
180
|
|
159
181
|
def _get_update_script(self) -> AutoUpdates | None:
|
160
182
|
eps = imd.entry_points(group="bec.widgets.auto_updates")
|
@@ -175,49 +197,59 @@ class BECGuiClientMixin:
|
|
175
197
|
"""
|
176
198
|
Selected device for the plot.
|
177
199
|
"""
|
178
|
-
|
200
|
+
auto_update_config_ep = MessageEndpoints.gui_auto_update_config(self._gui_id)
|
201
|
+
auto_update_config = self._client.connector.get(auto_update_config_ep)
|
202
|
+
if auto_update_config:
|
203
|
+
return auto_update_config.selected_device
|
204
|
+
return None
|
179
205
|
|
180
206
|
@selected_device.setter
|
181
207
|
def selected_device(self, device: str | DeviceBase):
|
182
208
|
if isinstance_based_on_class_name(device, "bec_lib.device.DeviceBase"):
|
183
|
-
self.
|
209
|
+
self._client.connector.set_and_publish(
|
210
|
+
MessageEndpoints.gui_auto_update_config(self._gui_id),
|
211
|
+
messages.GUIAutoUpdateConfigMessage(selected_device=device.name),
|
212
|
+
)
|
184
213
|
elif isinstance(device, str):
|
185
|
-
self.
|
214
|
+
self._client.connector.set_and_publish(
|
215
|
+
MessageEndpoints.gui_auto_update_config(self._gui_id),
|
216
|
+
messages.GUIAutoUpdateConfigMessage(selected_device=device),
|
217
|
+
)
|
186
218
|
else:
|
187
219
|
raise ValueError("Device must be a string or a device object")
|
188
220
|
|
189
221
|
def _start_update_script(self) -> None:
|
190
|
-
self._client.connector.register(
|
191
|
-
self._target_endpoint, cb=self._handle_msg_update, parent=self
|
192
|
-
)
|
222
|
+
self._client.connector.register(MessageEndpoints.scan_status(), cb=self._handle_msg_update)
|
193
223
|
|
194
|
-
|
195
|
-
|
196
|
-
if parent.auto_updates is not None:
|
224
|
+
def _handle_msg_update(self, msg: MessageObject) -> None:
|
225
|
+
if self.auto_updates is not None:
|
197
226
|
# pylint: disable=protected-access
|
198
|
-
|
227
|
+
return self._update_script_msg_parser(msg.value)
|
199
228
|
|
200
229
|
def _update_script_msg_parser(self, msg: messages.BECMessage) -> None:
|
201
230
|
if isinstance(msg, messages.ScanStatusMessage):
|
202
231
|
if not self.gui_is_alive():
|
203
232
|
return
|
204
233
|
if self._auto_updates_enabled:
|
205
|
-
self.auto_updates.
|
234
|
+
return self.auto_updates.do_update(msg)
|
206
235
|
|
207
236
|
def _gui_post_startup(self):
|
237
|
+
self._top_level["main"] = WidgetDesc(
|
238
|
+
title="BEC Widgets", widget=BECDockArea(gui_id=self._gui_id)
|
239
|
+
)
|
208
240
|
if self._auto_updates_enabled:
|
209
241
|
if self._auto_updates is None:
|
210
242
|
auto_updates = self._get_update_script()
|
211
243
|
if auto_updates is None:
|
212
244
|
AutoUpdates.create_default_dock = True
|
213
245
|
AutoUpdates.enabled = True
|
214
|
-
auto_updates = AutoUpdates(
|
246
|
+
auto_updates = AutoUpdates(self._top_level["main"].widget)
|
215
247
|
if auto_updates.create_default_dock:
|
216
248
|
auto_updates.start_default_dock()
|
217
|
-
|
249
|
+
self._start_update_script()
|
218
250
|
self._auto_updates = auto_updates
|
251
|
+
self._do_show_all()
|
219
252
|
self._gui_started_event.set()
|
220
|
-
self.show_all()
|
221
253
|
|
222
254
|
def start_server(self, wait=False) -> None:
|
223
255
|
"""
|
@@ -225,8 +257,8 @@ class BECGuiClientMixin:
|
|
225
257
|
"""
|
226
258
|
if self._process is None or self._process.poll() is not None:
|
227
259
|
logger.success("GUI starting...")
|
260
|
+
self._startup_timeout = 5
|
228
261
|
self._gui_started_event.clear()
|
229
|
-
self._start_update_script()
|
230
262
|
self._process, self._process_output_processing_thread = _start_plot_process(
|
231
263
|
self._gui_id, self.__class__, self._client._service_config.config, logger=logger
|
232
264
|
)
|
@@ -239,27 +271,66 @@ class BECGuiClientMixin:
|
|
239
271
|
threading.current_thread().cancel()
|
240
272
|
|
241
273
|
self._gui_started_timer = RepeatTimer(
|
242
|
-
|
274
|
+
0.5, lambda: self.gui_is_alive() and gui_started_callback(self._gui_post_startup)
|
243
275
|
)
|
244
276
|
self._gui_started_timer.start()
|
245
277
|
|
246
278
|
if wait:
|
247
279
|
self._gui_started_event.wait()
|
248
280
|
|
249
|
-
def
|
250
|
-
self.
|
281
|
+
def _dump(self):
|
282
|
+
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
|
283
|
+
return rpc_client._run_rpc("_dump")
|
284
|
+
|
285
|
+
def start(self):
|
286
|
+
return self.start_server()
|
287
|
+
|
288
|
+
def _do_show_all(self):
|
251
289
|
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
|
252
290
|
rpc_client._run_rpc("show")
|
291
|
+
for window in self._top_level.values():
|
292
|
+
window.widget.show()
|
293
|
+
|
294
|
+
def show_all(self):
|
295
|
+
with wait_for_server(self):
|
296
|
+
return self._do_show_all()
|
253
297
|
|
254
298
|
def hide_all(self):
|
255
|
-
self
|
256
|
-
|
257
|
-
|
299
|
+
with wait_for_server(self):
|
300
|
+
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
|
301
|
+
rpc_client._run_rpc("hide")
|
302
|
+
for window in self._top_level.values():
|
303
|
+
window.widget.hide()
|
304
|
+
|
305
|
+
def show(self):
|
306
|
+
if self._process is not None:
|
307
|
+
return self.show_all()
|
308
|
+
# backward compatibility: show() was also starting server
|
309
|
+
return self.start_server(wait=True)
|
310
|
+
|
311
|
+
def hide(self):
|
312
|
+
return self.hide_all()
|
313
|
+
|
314
|
+
@property
|
315
|
+
def main(self):
|
316
|
+
"""Return client to main dock area (in main window)"""
|
317
|
+
with wait_for_server(self):
|
318
|
+
return self._top_level["main"].widget
|
319
|
+
|
320
|
+
def new(self, title):
|
321
|
+
"""Ask main window to create a new top-level dock area"""
|
322
|
+
with wait_for_server(self):
|
323
|
+
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
|
324
|
+
widget = rpc_client._run_rpc("new_dock_area", title)
|
325
|
+
self._top_level[widget._gui_id] = WidgetDesc(title=title, widget=widget)
|
326
|
+
return widget
|
258
327
|
|
259
328
|
def close(self) -> None:
|
260
329
|
"""
|
261
330
|
Close the gui window.
|
262
331
|
"""
|
332
|
+
self._top_level.clear()
|
333
|
+
|
263
334
|
if self._gui_started_timer is not None:
|
264
335
|
self._gui_started_timer.cancel()
|
265
336
|
self._gui_started_timer.join()
|
@@ -274,130 +345,3 @@ class BECGuiClientMixin:
|
|
274
345
|
self._process_output_processing_thread.join()
|
275
346
|
self._process.wait()
|
276
347
|
self._process = None
|
277
|
-
self.shutdown_auto_updates()
|
278
|
-
|
279
|
-
|
280
|
-
class RPCResponseTimeoutError(Exception):
|
281
|
-
"""Exception raised when an RPC response is not received within the expected time."""
|
282
|
-
|
283
|
-
def __init__(self, request_id, timeout):
|
284
|
-
super().__init__(
|
285
|
-
f"RPC response not received within {timeout} seconds for request ID {request_id}"
|
286
|
-
)
|
287
|
-
|
288
|
-
|
289
|
-
class RPCBase:
|
290
|
-
def __init__(self, gui_id: str = None, config: dict = None, parent=None) -> None:
|
291
|
-
self._client = BECClient() # BECClient is a singleton; here, we simply get the instance
|
292
|
-
self._config = config if config is not None else {}
|
293
|
-
self._gui_id = gui_id if gui_id is not None else str(uuid.uuid4())[:5]
|
294
|
-
self._parent = parent
|
295
|
-
self._msg_wait_event = threading.Event()
|
296
|
-
self._rpc_response = None
|
297
|
-
super().__init__()
|
298
|
-
# print(f"RPCBase: {self._gui_id}")
|
299
|
-
|
300
|
-
def __repr__(self):
|
301
|
-
type_ = type(self)
|
302
|
-
qualname = type_.__qualname__
|
303
|
-
return f"<{qualname} object at {hex(id(self))}>"
|
304
|
-
|
305
|
-
@property
|
306
|
-
def _root(self):
|
307
|
-
"""
|
308
|
-
Get the root widget. This is the BECFigure widget that holds
|
309
|
-
the anchor gui_id.
|
310
|
-
"""
|
311
|
-
parent = self
|
312
|
-
# pylint: disable=protected-access
|
313
|
-
while parent._parent is not None:
|
314
|
-
parent = parent._parent
|
315
|
-
return parent
|
316
|
-
|
317
|
-
def _run_rpc(self, method, *args, wait_for_rpc_response=True, timeout=3, **kwargs):
|
318
|
-
"""
|
319
|
-
Run the RPC call.
|
320
|
-
|
321
|
-
Args:
|
322
|
-
method: The method to call.
|
323
|
-
args: The arguments to pass to the method.
|
324
|
-
wait_for_rpc_response: Whether to wait for the RPC response.
|
325
|
-
kwargs: The keyword arguments to pass to the method.
|
326
|
-
|
327
|
-
Returns:
|
328
|
-
The result of the RPC call.
|
329
|
-
"""
|
330
|
-
request_id = str(uuid.uuid4())
|
331
|
-
rpc_msg = messages.GUIInstructionMessage(
|
332
|
-
action=method,
|
333
|
-
parameter={"args": args, "kwargs": kwargs, "gui_id": self._gui_id},
|
334
|
-
metadata={"request_id": request_id},
|
335
|
-
)
|
336
|
-
|
337
|
-
# pylint: disable=protected-access
|
338
|
-
receiver = self._root._gui_id
|
339
|
-
if wait_for_rpc_response:
|
340
|
-
self._rpc_response = None
|
341
|
-
self._msg_wait_event.clear()
|
342
|
-
self._client.connector.register(
|
343
|
-
MessageEndpoints.gui_instruction_response(request_id),
|
344
|
-
cb=self._on_rpc_response,
|
345
|
-
parent=self,
|
346
|
-
)
|
347
|
-
|
348
|
-
self._client.connector.set_and_publish(MessageEndpoints.gui_instructions(receiver), rpc_msg)
|
349
|
-
|
350
|
-
if wait_for_rpc_response:
|
351
|
-
try:
|
352
|
-
finished = self._msg_wait_event.wait(10)
|
353
|
-
if not finished:
|
354
|
-
raise RPCResponseTimeoutError(request_id, timeout)
|
355
|
-
finally:
|
356
|
-
self._msg_wait_event.clear()
|
357
|
-
self._client.connector.unregister(
|
358
|
-
MessageEndpoints.gui_instruction_response(request_id), cb=self._on_rpc_response
|
359
|
-
)
|
360
|
-
# get class name
|
361
|
-
if not self._rpc_response.accepted:
|
362
|
-
raise ValueError(self._rpc_response.message["error"])
|
363
|
-
msg_result = self._rpc_response.message.get("result")
|
364
|
-
self._rpc_response = None
|
365
|
-
return self._create_widget_from_msg_result(msg_result)
|
366
|
-
|
367
|
-
@staticmethod
|
368
|
-
def _on_rpc_response(msg: MessageObject, parent: RPCBase) -> None:
|
369
|
-
msg = msg.value
|
370
|
-
parent._msg_wait_event.set()
|
371
|
-
parent._rpc_response = msg
|
372
|
-
|
373
|
-
def _create_widget_from_msg_result(self, msg_result):
|
374
|
-
if msg_result is None:
|
375
|
-
return None
|
376
|
-
if isinstance(msg_result, list):
|
377
|
-
return [self._create_widget_from_msg_result(res) for res in msg_result]
|
378
|
-
if isinstance(msg_result, dict):
|
379
|
-
if "__rpc__" not in msg_result:
|
380
|
-
return {
|
381
|
-
key: self._create_widget_from_msg_result(val) for key, val in msg_result.items()
|
382
|
-
}
|
383
|
-
cls = msg_result.pop("widget_class", None)
|
384
|
-
msg_result.pop("__rpc__", None)
|
385
|
-
|
386
|
-
if not cls:
|
387
|
-
return msg_result
|
388
|
-
|
389
|
-
cls = getattr(client, cls)
|
390
|
-
# print(msg_result)
|
391
|
-
return cls(parent=self, **msg_result)
|
392
|
-
return msg_result
|
393
|
-
|
394
|
-
def gui_is_alive(self):
|
395
|
-
"""
|
396
|
-
Check if the GUI is alive.
|
397
|
-
"""
|
398
|
-
heart = self._client.connector.get(MessageEndpoints.gui_heartbeat(self._root._gui_id))
|
399
|
-
if heart is None:
|
400
|
-
return False
|
401
|
-
if heart.status == messages.BECStatus.RUNNING:
|
402
|
-
return True
|
403
|
-
return False
|