bec-widgets 0.51.0__py3-none-any.whl → 0.52.1__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.
- bec_widgets/cli/__init__.py +1 -1
- bec_widgets/cli/bec_widgets_icon.png +0 -0
- bec_widgets/cli/client.py +328 -5
- bec_widgets/cli/client_utils.py +17 -5
- bec_widgets/cli/generate_cli.py +7 -3
- bec_widgets/cli/rpc_register.py +4 -0
- bec_widgets/cli/rpc_wigdet_handler.py +27 -0
- bec_widgets/cli/server.py +34 -9
- bec_widgets/examples/jupyter_console/jupyter_console_window.py +54 -2
- bec_widgets/examples/jupyter_console/jupyter_console_window.ui +26 -2
- bec_widgets/examples/jupyter_console/terminal_icon.png +0 -0
- bec_widgets/utils/__init__.py +1 -0
- bec_widgets/utils/bec_connector.py +18 -3
- bec_widgets/utils/bec_table.py +1 -0
- bec_widgets/utils/container_utils.py +3 -0
- bec_widgets/utils/crosshair.py +1 -0
- bec_widgets/utils/entry_validator.py +2 -0
- bec_widgets/utils/layout_manager.py +121 -0
- bec_widgets/utils/widget_io.py +5 -0
- bec_widgets/utils/yaml_dialog.py +2 -0
- bec_widgets/validation/monitor_config_validator.py +2 -1
- bec_widgets/widgets/__init__.py +1 -0
- bec_widgets/widgets/dock/__init__.py +2 -0
- bec_widgets/widgets/dock/dock.py +269 -0
- bec_widgets/widgets/dock/dock_area.py +225 -0
- bec_widgets/widgets/figure/figure.py +45 -16
- bec_widgets/widgets/plots/__init__.py +1 -1
- bec_widgets/widgets/plots/image.py +46 -9
- bec_widgets/widgets/plots/motor_map.py +17 -3
- bec_widgets/widgets/plots/plot_base.py +14 -3
- bec_widgets/widgets/plots/waveform.py +37 -10
- bec_widgets/widgets/scan_control/scan_control.py +10 -2
- bec_widgets/widgets/toolbar/toolbar.py +1 -0
- {bec_widgets-0.51.0.dist-info → bec_widgets-0.52.1.dist-info}/METADATA +1 -1
- {bec_widgets-0.51.0.dist-info → bec_widgets-0.52.1.dist-info}/RECORD +46 -40
- tests/end-2-end/conftest.py +19 -4
- tests/end-2-end/test_bec_dock_rpc_e2e.py +145 -0
- tests/end-2-end/test_bec_figure_rpc_e2e.py +17 -17
- tests/end-2-end/test_rpc_register_e2e.py +3 -3
- tests/unit_tests/test_bec_dock.py +114 -0
- tests/unit_tests/test_bec_figure.py +20 -22
- tests/unit_tests/test_generate_cli_client.py +1 -1
- tests/unit_tests/test_waveform1d.py +1 -1
- bec_widgets/simulations/__init__.py +0 -0
- bec_widgets/utils/ctrl_c.py +0 -39
- {bec_widgets-0.51.0.dist-info → bec_widgets-0.52.1.dist-info}/LICENSE +0 -0
- {bec_widgets-0.51.0.dist-info → bec_widgets-0.52.1.dist-info}/WHEEL +0 -0
- {bec_widgets-0.51.0.dist-info → bec_widgets-0.52.1.dist-info}/top_level.txt +0 -0
bec_widgets/cli/client_utils.py
CHANGED
@@ -22,7 +22,7 @@ from bec_widgets.cli.auto_updates import AutoUpdates
|
|
22
22
|
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
23
23
|
|
24
24
|
if TYPE_CHECKING:
|
25
|
-
from bec_widgets.cli.client import BECFigure
|
25
|
+
from bec_widgets.cli.client import BECDockArea, BECFigure
|
26
26
|
|
27
27
|
|
28
28
|
def rpc_call(func):
|
@@ -56,7 +56,7 @@ def rpc_call(func):
|
|
56
56
|
return wrapper
|
57
57
|
|
58
58
|
|
59
|
-
class
|
59
|
+
class BECGuiClientMixin:
|
60
60
|
def __init__(self, **kwargs) -> None:
|
61
61
|
super().__init__(**kwargs)
|
62
62
|
self._process = None
|
@@ -94,7 +94,7 @@ class BECFigureClientMixin:
|
|
94
94
|
)
|
95
95
|
|
96
96
|
@staticmethod
|
97
|
-
def _handle_msg_update(msg: MessageObject, parent:
|
97
|
+
def _handle_msg_update(msg: MessageObject, parent: BECGuiClientMixin) -> None:
|
98
98
|
if parent.update_script is not None:
|
99
99
|
# pylint: disable=protected-access
|
100
100
|
parent._update_script_msg_parser(msg.value)
|
@@ -139,8 +139,19 @@ class BECFigureClientMixin:
|
|
139
139
|
config = self._client._service_config.redis
|
140
140
|
monitor_module = importlib.import_module("bec_widgets.cli.server")
|
141
141
|
monitor_path = monitor_module.__file__
|
142
|
-
|
143
|
-
|
142
|
+
gui_class = self.__class__.__name__
|
143
|
+
|
144
|
+
command = [
|
145
|
+
sys.executable,
|
146
|
+
"-u",
|
147
|
+
monitor_path,
|
148
|
+
"--id",
|
149
|
+
self._gui_id,
|
150
|
+
"--config",
|
151
|
+
config,
|
152
|
+
"--gui_class",
|
153
|
+
gui_class,
|
154
|
+
]
|
144
155
|
self._process = subprocess.Popen(
|
145
156
|
command, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
146
157
|
)
|
@@ -270,6 +281,7 @@ class RPCBase:
|
|
270
281
|
def _wait_for_response(self, request_id: str, timeout: int = 5):
|
271
282
|
"""
|
272
283
|
Wait for the response from the server.
|
284
|
+
|
273
285
|
Args:
|
274
286
|
request_id(str): The request ID.
|
275
287
|
timeout(int): The timeout in seconds.
|
bec_widgets/cli/generate_cli.py
CHANGED
@@ -22,7 +22,7 @@ else:
|
|
22
22
|
class ClientGenerator:
|
23
23
|
def __init__(self):
|
24
24
|
self.header = """# This file was automatically generated by generate_cli.py\n
|
25
|
-
from bec_widgets.cli.client_utils import rpc_call, RPCBase,
|
25
|
+
from bec_widgets.cli.client_utils import rpc_call, RPCBase, BECGuiClientMixin
|
26
26
|
from typing import Literal, Optional, overload"""
|
27
27
|
|
28
28
|
self.content = ""
|
@@ -41,6 +41,7 @@ from typing import Literal, Optional, overload"""
|
|
41
41
|
def generate_content_for_class(self, cls):
|
42
42
|
"""
|
43
43
|
Generate the content for the class.
|
44
|
+
|
44
45
|
Args:
|
45
46
|
cls: The class for which to generate the content.
|
46
47
|
"""
|
@@ -53,9 +54,9 @@ from typing import Literal, Optional, overload"""
|
|
53
54
|
# from {module} import {class_name}"""
|
54
55
|
|
55
56
|
# Generate the content
|
56
|
-
if cls.__name__ == "
|
57
|
+
if cls.__name__ == "BECDockArea":
|
57
58
|
self.content += f"""
|
58
|
-
class {class_name}(RPCBase,
|
59
|
+
class {class_name}(RPCBase, BECGuiClientMixin):"""
|
59
60
|
else:
|
60
61
|
self.content += f"""
|
61
62
|
class {class_name}(RPCBase):"""
|
@@ -108,6 +109,7 @@ if __name__ == "__main__": # pragma: no cover
|
|
108
109
|
import os
|
109
110
|
|
110
111
|
from bec_widgets.utils import BECConnector
|
112
|
+
from bec_widgets.widgets.dock import BECDock, BECDockArea
|
111
113
|
from bec_widgets.widgets.figure import BECFigure
|
112
114
|
from bec_widgets.widgets.plots import BECImageShow, BECMotorMap, BECPlotBase, BECWaveform
|
113
115
|
from bec_widgets.widgets.plots.image import BECImageItem
|
@@ -124,6 +126,8 @@ if __name__ == "__main__": # pragma: no cover
|
|
124
126
|
BECConnector,
|
125
127
|
BECImageItem,
|
126
128
|
BECMotorMap,
|
129
|
+
BECDock,
|
130
|
+
BECDockArea,
|
127
131
|
]
|
128
132
|
generator = ClientGenerator()
|
129
133
|
generator.generate_client(clss)
|
bec_widgets/cli/rpc_register.py
CHANGED
@@ -28,6 +28,7 @@ class RPCRegister:
|
|
28
28
|
def add_rpc(self, rpc: QObject):
|
29
29
|
"""
|
30
30
|
Add an RPC object to the register.
|
31
|
+
|
31
32
|
Args:
|
32
33
|
rpc(QObject): The RPC object to be added to the register.
|
33
34
|
"""
|
@@ -38,6 +39,7 @@ class RPCRegister:
|
|
38
39
|
def remove_rpc(self, rpc: str):
|
39
40
|
"""
|
40
41
|
Remove an RPC object from the register.
|
42
|
+
|
41
43
|
Args:
|
42
44
|
rpc(str): The RPC object to be removed from the register.
|
43
45
|
"""
|
@@ -48,6 +50,7 @@ class RPCRegister:
|
|
48
50
|
def get_rpc_by_id(self, gui_id: str) -> QObject:
|
49
51
|
"""
|
50
52
|
Get an RPC object by its ID.
|
53
|
+
|
51
54
|
Args:
|
52
55
|
gui_id(str): The ID of the RPC object to be retrieved.
|
53
56
|
|
@@ -60,6 +63,7 @@ class RPCRegister:
|
|
60
63
|
def list_all_connections(self) -> dict:
|
61
64
|
"""
|
62
65
|
List all the registered RPC objects.
|
66
|
+
|
63
67
|
Returns:
|
64
68
|
dict: A dictionary containing all the registered RPC objects.
|
65
69
|
"""
|
@@ -0,0 +1,27 @@
|
|
1
|
+
from bec_widgets.utils import BECConnector
|
2
|
+
from bec_widgets.widgets.figure import BECFigure
|
3
|
+
|
4
|
+
|
5
|
+
class RPCWidgetHandler:
|
6
|
+
"""Handler class for creating widgets from RPC messages."""
|
7
|
+
|
8
|
+
widget_classes = {
|
9
|
+
"BECFigure": BECFigure,
|
10
|
+
}
|
11
|
+
|
12
|
+
@staticmethod
|
13
|
+
def create_widget(widget_type, **kwargs) -> BECConnector:
|
14
|
+
"""
|
15
|
+
Create a widget from an RPC message.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
widget_type(str): The type of the widget.
|
19
|
+
**kwargs: The keyword arguments for the widget.
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
widget(BECConnector): The created widget.
|
23
|
+
"""
|
24
|
+
widget_class = RPCWidgetHandler.widget_classes.get(widget_type)
|
25
|
+
if widget_class:
|
26
|
+
return widget_class(**kwargs)
|
27
|
+
raise ValueError(f"Unknown widget type: {widget_type}")
|
bec_widgets/cli/server.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import inspect
|
2
2
|
import threading
|
3
3
|
import time
|
4
|
+
from typing import Literal, Union
|
4
5
|
|
5
6
|
from bec_lib import MessageEndpoints, messages
|
6
7
|
from qtpy.QtCore import QTimer
|
@@ -8,6 +9,7 @@ from qtpy.QtCore import QTimer
|
|
8
9
|
from bec_widgets.cli.rpc_register import RPCRegister
|
9
10
|
from bec_widgets.utils import BECDispatcher
|
10
11
|
from bec_widgets.utils.bec_connector import BECConnector
|
12
|
+
from bec_widgets.widgets.dock.dock_area import BECDockArea
|
11
13
|
from bec_widgets.widgets.figure import BECFigure
|
12
14
|
from bec_widgets.widgets.plots import BECCurve, BECImageShow, BECWaveform
|
13
15
|
|
@@ -16,15 +18,20 @@ class BECWidgetsCLIServer:
|
|
16
18
|
WIDGETS = [BECWaveform, BECFigure, BECCurve, BECImageShow]
|
17
19
|
|
18
20
|
def __init__(
|
19
|
-
self,
|
21
|
+
self,
|
22
|
+
gui_id: str = None,
|
23
|
+
dispatcher: BECDispatcher = None,
|
24
|
+
client=None,
|
25
|
+
config=None,
|
26
|
+
gui_class: Union["BECFigure", "BECDockArea"] = BECFigure,
|
20
27
|
) -> None:
|
21
28
|
self.dispatcher = BECDispatcher(config=config) if dispatcher is None else dispatcher
|
22
29
|
self.client = self.dispatcher.client if client is None else client
|
23
30
|
self.client.start()
|
24
31
|
self.gui_id = gui_id
|
25
|
-
self.
|
32
|
+
self.gui = gui_class(gui_id=self.gui_id)
|
26
33
|
self.rpc_register = RPCRegister()
|
27
|
-
self.rpc_register.add_rpc(self.
|
34
|
+
self.rpc_register.add_rpc(self.gui)
|
28
35
|
|
29
36
|
self.dispatcher.connect_slot(
|
30
37
|
self.on_rpc_update, MessageEndpoints.gui_instructions(self.gui_id)
|
@@ -102,7 +109,7 @@ class BECWidgetsCLIServer:
|
|
102
109
|
expire=10,
|
103
110
|
)
|
104
111
|
|
105
|
-
def shutdown(self):
|
112
|
+
def shutdown(self): # TODO not sure if needed when cleanup is done at level of BECConnector
|
106
113
|
self._shutdown_event = True
|
107
114
|
self._heartbeat_timer.stop()
|
108
115
|
self.client.shutdown()
|
@@ -110,6 +117,7 @@ class BECWidgetsCLIServer:
|
|
110
117
|
|
111
118
|
if __name__ == "__main__": # pragma: no cover
|
112
119
|
import argparse
|
120
|
+
import os
|
113
121
|
import sys
|
114
122
|
|
115
123
|
from qtpy.QtCore import QSize
|
@@ -118,8 +126,9 @@ if __name__ == "__main__": # pragma: no cover
|
|
118
126
|
|
119
127
|
app = QApplication(sys.argv)
|
120
128
|
app.setApplicationName("BEC Figure")
|
129
|
+
current_path = os.path.dirname(__file__)
|
121
130
|
icon = QIcon()
|
122
|
-
icon.addFile("bec_widgets_icon.png", size=QSize(48, 48))
|
131
|
+
icon.addFile(os.path.join(current_path, "bec_widgets_icon.png"), size=QSize(48, 48))
|
123
132
|
app.setWindowIcon(icon)
|
124
133
|
|
125
134
|
win = QMainWindow()
|
@@ -127,15 +136,31 @@ if __name__ == "__main__": # pragma: no cover
|
|
127
136
|
|
128
137
|
parser = argparse.ArgumentParser(description="BEC Widgets CLI Server")
|
129
138
|
parser.add_argument("--id", type=str, help="The id of the server")
|
139
|
+
parser.add_argument(
|
140
|
+
"--gui_class",
|
141
|
+
type=str,
|
142
|
+
help="Name of the gui class to be rendered. Possible values: \n- BECFigure\n- BECDockArea",
|
143
|
+
)
|
130
144
|
parser.add_argument("--config", type=str, help="Config to connect to redis.")
|
131
145
|
|
132
146
|
args = parser.parse_args()
|
133
147
|
|
134
|
-
|
135
|
-
|
148
|
+
if args.gui_class == "BECFigure":
|
149
|
+
gui_class = BECFigure
|
150
|
+
elif args.gui_class == "BECDockArea":
|
151
|
+
gui_class = BECDockArea
|
152
|
+
else:
|
153
|
+
print(
|
154
|
+
"Please specify a valid gui_class to run. Use -h for help."
|
155
|
+
"\n Starting with default gui_class BECFigure."
|
156
|
+
)
|
157
|
+
gui_class = BECFigure
|
158
|
+
|
159
|
+
server = BECWidgetsCLIServer(gui_id=args.id, config=args.config, gui_class=gui_class)
|
136
160
|
|
137
|
-
|
138
|
-
win.setCentralWidget(
|
161
|
+
gui = server.gui
|
162
|
+
win.setCentralWidget(gui)
|
163
|
+
win.resize(800, 600)
|
139
164
|
win.show()
|
140
165
|
|
141
166
|
app.aboutToQuit.connect(server.shutdown)
|
@@ -2,14 +2,17 @@ import os
|
|
2
2
|
|
3
3
|
import numpy as np
|
4
4
|
import pyqtgraph as pg
|
5
|
-
from pyqtgraph.Qt import uic
|
5
|
+
from pyqtgraph.Qt import QtWidgets, uic
|
6
6
|
from qtconsole.inprocess import QtInProcessKernelManager
|
7
7
|
from qtconsole.rich_jupyter_widget import RichJupyterWidget
|
8
|
+
from qtpy.QtCore import QSize
|
9
|
+
from qtpy.QtGui import QIcon
|
8
10
|
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
9
11
|
|
10
12
|
from bec_widgets.cli.rpc_register import RPCRegister
|
11
13
|
from bec_widgets.utils import BECDispatcher
|
12
14
|
from bec_widgets.widgets import BECFigure
|
15
|
+
from bec_widgets.widgets.dock.dock_area import BECDockArea
|
13
16
|
|
14
17
|
|
15
18
|
class JupyterConsoleWidget(RichJupyterWidget): # pragma: no cover:
|
@@ -46,15 +49,22 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
|
46
49
|
|
47
50
|
self.register = RPCRegister()
|
48
51
|
self.register.add_rpc(self.figure)
|
49
|
-
|
52
|
+
|
50
53
|
# console push
|
51
54
|
self.console.kernel_manager.kernel.shell.push(
|
52
55
|
{
|
53
56
|
"fig": self.figure,
|
54
57
|
"register": self.register,
|
58
|
+
"dock": self.dock,
|
55
59
|
"w1": self.w1,
|
56
60
|
"w2": self.w2,
|
57
61
|
"w3": self.w3,
|
62
|
+
"d1": self.d1,
|
63
|
+
"d2": self.d2,
|
64
|
+
"d3": self.d3,
|
65
|
+
"b2a": self.button_2_a,
|
66
|
+
"b2b": self.button_2_b,
|
67
|
+
"b2c": self.button_2_c,
|
58
68
|
"bec": self.figure.client,
|
59
69
|
"scans": self.figure.client.scans,
|
60
70
|
"dev": self.figure.client.device_manager.devices,
|
@@ -67,9 +77,16 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
|
67
77
|
self.figure = BECFigure(parent=self, gui_id="remote") # Create a new BECDeviceMonitor
|
68
78
|
self.glw_1_layout.addWidget(self.figure) # Add BECDeviceMonitor to the layout
|
69
79
|
|
80
|
+
self.dock_layout = QVBoxLayout(self.dock_placeholder)
|
81
|
+
self.dock = BECDockArea(gui_id="remote")
|
82
|
+
self.dock_layout.addWidget(self.dock)
|
83
|
+
|
70
84
|
# add stuff to figure
|
71
85
|
self._init_figure()
|
72
86
|
|
87
|
+
# init dock for testing
|
88
|
+
self._init_dock()
|
89
|
+
|
73
90
|
self.console_layout = QVBoxLayout(self.widget_console)
|
74
91
|
self.console = JupyterConsoleWidget()
|
75
92
|
self.console_layout.addWidget(self.console)
|
@@ -91,6 +108,36 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
|
91
108
|
self.w1.add_curve_scan("samx", "samy", "bpm3a", pen_style="dash")
|
92
109
|
self.c1 = self.w1.get_config()
|
93
110
|
|
111
|
+
def _init_dock(self):
|
112
|
+
self.button_1 = QtWidgets.QPushButton("Button 1 ")
|
113
|
+
self.button_2_a = QtWidgets.QPushButton("Button to be added at place 0,0 in d3")
|
114
|
+
self.button_2_b = QtWidgets.QPushButton("button after without postions specified")
|
115
|
+
self.button_2_c = QtWidgets.QPushButton("button super late")
|
116
|
+
self.button_3 = QtWidgets.QPushButton("Button above Figure ")
|
117
|
+
self.label_1 = QtWidgets.QLabel("some scan info label with useful information")
|
118
|
+
|
119
|
+
self.label_2 = QtWidgets.QLabel("label which is added separately")
|
120
|
+
self.label_3 = QtWidgets.QLabel("Label above figure")
|
121
|
+
|
122
|
+
self.d1 = self.dock.add_dock(widget=self.button_1, position="left")
|
123
|
+
self.d1.addWidget(self.label_2)
|
124
|
+
self.d2 = self.dock.add_dock(widget=self.label_1, position="right")
|
125
|
+
self.d3 = self.dock.add_dock(name="figure")
|
126
|
+
self.fig_dock3 = BECFigure()
|
127
|
+
self.fig_dock3.plot("samx", "bpm4d")
|
128
|
+
self.d3.add_widget(self.label_3)
|
129
|
+
self.d3.add_widget(self.button_3)
|
130
|
+
self.d3.add_widget(self.fig_dock3)
|
131
|
+
|
132
|
+
self.dock.save_state()
|
133
|
+
|
134
|
+
def closeEvent(self, event):
|
135
|
+
"""Override to handle things when main window is closed."""
|
136
|
+
self.dock.cleanup()
|
137
|
+
self.figure.clear_all()
|
138
|
+
self.figure.client.shutdown()
|
139
|
+
super().closeEvent(event)
|
140
|
+
|
94
141
|
|
95
142
|
if __name__ == "__main__": # pragma: no cover
|
96
143
|
import sys
|
@@ -101,7 +148,12 @@ if __name__ == "__main__": # pragma: no cover
|
|
101
148
|
|
102
149
|
app = QApplication(sys.argv)
|
103
150
|
app.setApplicationName("Jupyter Console")
|
151
|
+
app.setApplicationDisplayName("Jupyter Console")
|
152
|
+
icon = QIcon()
|
153
|
+
icon.addFile("terminal_icon.png", size=QSize(48, 48))
|
154
|
+
app.setWindowIcon(icon)
|
104
155
|
win = JupyterConsoleWindow()
|
105
156
|
win.show()
|
106
157
|
|
158
|
+
app.aboutToQuit.connect(win.close)
|
107
159
|
sys.exit(app.exec_())
|
@@ -13,13 +13,37 @@
|
|
13
13
|
<property name="windowTitle">
|
14
14
|
<string>Plotting Console</string>
|
15
15
|
</property>
|
16
|
-
<layout class="QVBoxLayout" name="
|
16
|
+
<layout class="QVBoxLayout" name="verticalLayout_4">
|
17
17
|
<item>
|
18
18
|
<widget class="QSplitter" name="splitter">
|
19
19
|
<property name="orientation">
|
20
20
|
<enum>Qt::Horizontal</enum>
|
21
21
|
</property>
|
22
|
-
<widget class="
|
22
|
+
<widget class="QTabWidget" name="tabWidget">
|
23
|
+
<property name="currentIndex">
|
24
|
+
<number>0</number>
|
25
|
+
</property>
|
26
|
+
<widget class="QWidget" name="tab_1">
|
27
|
+
<attribute name="title">
|
28
|
+
<string>BECDock</string>
|
29
|
+
</attribute>
|
30
|
+
<layout class="QVBoxLayout" name="verticalLayout">
|
31
|
+
<item>
|
32
|
+
<widget class="QWidget" name="dock_placeholder" native="true"/>
|
33
|
+
</item>
|
34
|
+
</layout>
|
35
|
+
</widget>
|
36
|
+
<widget class="QWidget" name="tab_2">
|
37
|
+
<attribute name="title">
|
38
|
+
<string>BECFigure</string>
|
39
|
+
</attribute>
|
40
|
+
<layout class="QVBoxLayout" name="verticalLayout_3">
|
41
|
+
<item>
|
42
|
+
<widget class="QWidget" name="glw" native="true"/>
|
43
|
+
</item>
|
44
|
+
</layout>
|
45
|
+
</widget>
|
46
|
+
</widget>
|
23
47
|
<widget class="QWidget" name="widget_console" native="true"/>
|
24
48
|
</widget>
|
25
49
|
</item>
|
Binary file
|
bec_widgets/utils/__init__.py
CHANGED
@@ -5,5 +5,6 @@ from .colors import Colors
|
|
5
5
|
from .container_utils import WidgetContainerUtils
|
6
6
|
from .crosshair import Crosshair
|
7
7
|
from .entry_validator import EntryValidator
|
8
|
+
from .layout_manager import GridLayoutManager
|
8
9
|
from .rpc_decorator import register_rpc_methods, rpc_public
|
9
10
|
from .validator_delegate import DoubleValidationDelegate
|
@@ -78,6 +78,7 @@ class BECConnector:
|
|
78
78
|
def config_dict(self) -> dict:
|
79
79
|
"""
|
80
80
|
Get the configuration of the widget.
|
81
|
+
|
81
82
|
Returns:
|
82
83
|
dict: The configuration of the widget.
|
83
84
|
"""
|
@@ -87,6 +88,7 @@ class BECConnector:
|
|
87
88
|
def config_dict(self, config: BaseModel) -> None:
|
88
89
|
"""
|
89
90
|
Get the configuration of the widget.
|
91
|
+
|
90
92
|
Returns:
|
91
93
|
dict: The configuration of the widget.
|
92
94
|
"""
|
@@ -96,6 +98,7 @@ class BECConnector:
|
|
96
98
|
def set_gui_id(self, gui_id: str) -> None:
|
97
99
|
"""
|
98
100
|
Set the GUI ID for the widget.
|
101
|
+
|
99
102
|
Args:
|
100
103
|
gui_id(str): GUI ID
|
101
104
|
"""
|
@@ -116,6 +119,7 @@ class BECConnector:
|
|
116
119
|
|
117
120
|
def update_client(self, client) -> None:
|
118
121
|
"""Update the client and device manager from BEC and create object for BEC shortcuts.
|
122
|
+
|
119
123
|
Args:
|
120
124
|
client: BEC client
|
121
125
|
"""
|
@@ -126,6 +130,7 @@ class BECConnector:
|
|
126
130
|
def on_config_update(self, config: ConnectionConfig | dict) -> None:
|
127
131
|
"""
|
128
132
|
Update the configuration for the widget.
|
133
|
+
|
129
134
|
Args:
|
130
135
|
config(ConnectionConfig): Configuration settings.
|
131
136
|
"""
|
@@ -138,8 +143,10 @@ class BECConnector:
|
|
138
143
|
def get_config(self, dict_output: bool = True) -> dict | BaseModel:
|
139
144
|
"""
|
140
145
|
Get the configuration of the widget.
|
146
|
+
|
141
147
|
Args:
|
142
148
|
dict_output(bool): If True, return the configuration as a dictionary. If False, return the configuration as a pydantic model.
|
149
|
+
|
143
150
|
Returns:
|
144
151
|
dict: The configuration of the plot widget.
|
145
152
|
"""
|
@@ -148,6 +155,14 @@ class BECConnector:
|
|
148
155
|
else:
|
149
156
|
return self.config
|
150
157
|
|
151
|
-
def
|
152
|
-
|
153
|
-
|
158
|
+
def cleanup(self):
|
159
|
+
"""Cleanup the widget."""
|
160
|
+
self.rpc_register.remove_rpc(self)
|
161
|
+
all_connections = self.rpc_register.list_all_connections()
|
162
|
+
if len(all_connections) == 0:
|
163
|
+
print("No more connections. Shutting down GUI BEC client.")
|
164
|
+
self.client.shutdown()
|
165
|
+
|
166
|
+
# def closeEvent(self, event):
|
167
|
+
# self.cleanup()
|
168
|
+
# super().closeEvent(event)
|
bec_widgets/utils/bec_table.py
CHANGED
@@ -10,6 +10,7 @@ class WidgetContainerUtils:
|
|
10
10
|
def generate_unique_widget_id(container: dict, prefix: str = "widget") -> str:
|
11
11
|
"""
|
12
12
|
Generate a unique widget ID.
|
13
|
+
|
13
14
|
Args:
|
14
15
|
container(dict): The container of widgets.
|
15
16
|
prefix(str): The prefix of the widget ID.
|
@@ -29,10 +30,12 @@ class WidgetContainerUtils:
|
|
29
30
|
) -> QWidget | None:
|
30
31
|
"""
|
31
32
|
Find the first widget of a given class in the figure.
|
33
|
+
|
32
34
|
Args:
|
33
35
|
container(dict): The container of widgets.
|
34
36
|
widget_class(Type): The class of the widget to find.
|
35
37
|
can_fail(bool): If True, the method will return None if no widget is found. If False, it will raise an error.
|
38
|
+
|
36
39
|
Returns:
|
37
40
|
widget: The widget of the given class.
|
38
41
|
"""
|
bec_widgets/utils/crosshair.py
CHANGED
@@ -17,6 +17,7 @@ class Crosshair(QObject):
|
|
17
17
|
def __init__(self, plot_item: pg.PlotItem, precision: int = None, parent=None):
|
18
18
|
"""
|
19
19
|
Crosshair for 1D and 2D plots.
|
20
|
+
|
20
21
|
Args:
|
21
22
|
plot_item (pyqtgraph.PlotItem): The plot item to which the crosshair will be attached.
|
22
23
|
precision (int, optional): Number of decimal places to round the coordinates to. Defaults to None.
|
@@ -5,6 +5,7 @@ class EntryValidator:
|
|
5
5
|
def validate_signal(self, name: str, entry: str = None) -> str:
|
6
6
|
"""
|
7
7
|
Validate a signal entry for a given device. If the entry is not provided, the first signal entry will be used from the device hints.
|
8
|
+
|
8
9
|
Args:
|
9
10
|
name(str): Device name
|
10
11
|
entry(str): Signal entry
|
@@ -28,6 +29,7 @@ class EntryValidator:
|
|
28
29
|
def validate_monitor(self, monitor: str) -> str:
|
29
30
|
"""
|
30
31
|
Validate a monitor entry for a given device.
|
32
|
+
|
31
33
|
Args:
|
32
34
|
monitor(str): Monitor entry
|
33
35
|
|
@@ -0,0 +1,121 @@
|
|
1
|
+
from collections import OrderedDict
|
2
|
+
from typing import Literal
|
3
|
+
|
4
|
+
from qtpy.QtWidgets import QGridLayout, QWidget
|
5
|
+
|
6
|
+
|
7
|
+
class GridLayoutManager:
|
8
|
+
"""
|
9
|
+
GridLayoutManager class is used to manage widgets in a QGridLayout and extend its functionality.
|
10
|
+
|
11
|
+
The GridLayoutManager class provides methods to add, move, and check the position of widgets in a QGridLayout.
|
12
|
+
It also provides a method to get the positions of all widgets in the layout.
|
13
|
+
|
14
|
+
Args:
|
15
|
+
layout(QGridLayout): The layout to manage.
|
16
|
+
"""
|
17
|
+
|
18
|
+
def __init__(self, layout: QGridLayout):
|
19
|
+
self.layout = layout
|
20
|
+
|
21
|
+
def is_position_occupied(self, row: int, col: int) -> bool:
|
22
|
+
"""
|
23
|
+
Check if the position in the layout is occupied by a widget.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
row(int): The row to check.
|
27
|
+
col(int): The column to check.
|
28
|
+
|
29
|
+
Returns:
|
30
|
+
bool: True if the position is occupied, False otherwise.
|
31
|
+
"""
|
32
|
+
for i in range(self.layout.count()):
|
33
|
+
widget_row, widget_col, _, _ = self.layout.getItemPosition(i)
|
34
|
+
if widget_row == row and widget_col == col:
|
35
|
+
return True
|
36
|
+
return False
|
37
|
+
|
38
|
+
def shift_widgets(
|
39
|
+
self,
|
40
|
+
direction: Literal["down", "up", "left", "right"] = "down",
|
41
|
+
start_row: int = 0,
|
42
|
+
start_col: int = 0,
|
43
|
+
):
|
44
|
+
"""
|
45
|
+
Shift widgets in the layout in the specified direction starting from the specified position.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
direction(str): The direction to shift the widgets. Can be "down", "up", "left", or "right".
|
49
|
+
start_row(int): The row to start shifting from. Default is 0.
|
50
|
+
start_col(int): The column to start shifting from. Default is 0.
|
51
|
+
"""
|
52
|
+
for i in reversed(range(self.layout.count())):
|
53
|
+
widget_item = self.layout.itemAt(i)
|
54
|
+
widget = widget_item.widget()
|
55
|
+
row, col, rowspan, colspan = self.layout.getItemPosition(i)
|
56
|
+
if direction == "down" and row >= start_row:
|
57
|
+
self.layout.addWidget(widget, row + 1, col, rowspan, colspan)
|
58
|
+
elif direction == "up" and row > start_row:
|
59
|
+
self.layout.addWidget(widget, row - 1, col, rowspan, colspan)
|
60
|
+
elif direction == "right" and col >= start_col:
|
61
|
+
self.layout.addWidget(widget, row, col + 1, rowspan, colspan)
|
62
|
+
elif direction == "left" and col > start_col:
|
63
|
+
self.layout.addWidget(widget, row, col - 1, rowspan, colspan)
|
64
|
+
|
65
|
+
def move_widget(self, widget: QWidget, new_row: int, new_col: int):
|
66
|
+
"""
|
67
|
+
Move a widget to a new position in the layout.
|
68
|
+
|
69
|
+
Args:
|
70
|
+
widget(QWidget): The widget to move.
|
71
|
+
new_row(int): The new row to move the widget to.
|
72
|
+
new_col(int): The new column to move the widget to.
|
73
|
+
"""
|
74
|
+
self.layout.removeWidget(widget)
|
75
|
+
self.layout.addWidget(widget, new_row, new_col)
|
76
|
+
|
77
|
+
def add_widget(
|
78
|
+
self,
|
79
|
+
widget: QWidget,
|
80
|
+
row=None,
|
81
|
+
col=0,
|
82
|
+
rowspan=1,
|
83
|
+
colspan=1,
|
84
|
+
shift: Literal["down", "up", "left", "right"] = "down",
|
85
|
+
):
|
86
|
+
"""
|
87
|
+
Add a widget to the layout at the specified position.
|
88
|
+
|
89
|
+
Args:
|
90
|
+
widget(QWidget): The widget to add.
|
91
|
+
row(int): The row to add the widget to. If None, the widget will be added to the next available row.
|
92
|
+
col(int): The column to add the widget to. Default is 0.
|
93
|
+
rowspan(int): The number of rows the widget will span. Default is 1.
|
94
|
+
colspan(int): The number of columns the widget will span. Default is 1.
|
95
|
+
shift(str): The direction to shift the widgets if the position is occupied. Can be "down", "up", "left", or "right".
|
96
|
+
"""
|
97
|
+
if row is None:
|
98
|
+
row = self.layout.rowCount()
|
99
|
+
if self.is_position_occupied(row, col):
|
100
|
+
self.shift_widgets(shift, start_row=row)
|
101
|
+
self.layout.addWidget(widget, row, col, rowspan, colspan)
|
102
|
+
|
103
|
+
def get_widgets_positions(self) -> dict:
|
104
|
+
"""
|
105
|
+
Get the positions of all widgets in the layout.
|
106
|
+
Returns:
|
107
|
+
dict: A dictionary with the positions of the widgets in the layout.
|
108
|
+
|
109
|
+
"""
|
110
|
+
positions = []
|
111
|
+
for i in range(self.layout.count()):
|
112
|
+
widget_item = self.layout.itemAt(i)
|
113
|
+
widget = widget_item.widget()
|
114
|
+
if widget:
|
115
|
+
position = self.layout.getItemPosition(i)
|
116
|
+
positions.append((position, widget))
|
117
|
+
positions.sort(key=lambda x: (x[0][0], x[0][1], x[0][2], x[0][3]))
|
118
|
+
ordered_positions = OrderedDict()
|
119
|
+
for pos, widget in positions:
|
120
|
+
ordered_positions[pos] = widget
|
121
|
+
return ordered_positions
|