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.
@@ -35,7 +35,7 @@ from __future__ import annotations
35
35
  import enum
36
36
  from typing import Literal, Optional, overload
37
37
 
38
- from bec_widgets.cli.client_utils import RPCBase, rpc_call, BECGuiClientMixin
38
+ from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call
39
39
 
40
40
  # pylint: skip-file"""
41
41
 
@@ -84,7 +84,7 @@ class Widgets(str, enum.Enum):
84
84
  # Generate the content
85
85
  if cls.__name__ == "BECDockArea":
86
86
  self.content += f"""
87
- class {class_name}(RPCBase, BECGuiClientMixin):"""
87
+ class {class_name}(RPCBase):"""
88
88
  else:
89
89
  self.content += f"""
90
90
  class {class_name}(RPCBase):"""
File without changes
@@ -0,0 +1,177 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ import uuid
5
+ from functools import wraps
6
+ from typing import TYPE_CHECKING
7
+
8
+ from bec_lib.client import BECClient
9
+ from bec_lib.endpoints import MessageEndpoints
10
+ from bec_lib.utils.import_utils import lazy_import, lazy_import_from
11
+
12
+ import bec_widgets.cli.client as client
13
+
14
+ if TYPE_CHECKING:
15
+ from bec_lib import messages
16
+ from bec_lib.connector import MessageObject
17
+ else:
18
+ messages = lazy_import("bec_lib.messages")
19
+ # from bec_lib.connector import MessageObject
20
+ MessageObject = lazy_import_from("bec_lib.connector", ("MessageObject",))
21
+
22
+
23
+ def rpc_call(func):
24
+ """
25
+ A decorator for calling a function on the server.
26
+
27
+ Args:
28
+ func: The function to call.
29
+
30
+ Returns:
31
+ The result of the function call.
32
+ """
33
+
34
+ @wraps(func)
35
+ def wrapper(self, *args, **kwargs):
36
+ # we could rely on a strict type check here, but this is more flexible
37
+ # moreover, it would anyway crash for objects...
38
+ out = []
39
+ for arg in args:
40
+ if hasattr(arg, "name"):
41
+ arg = arg.name
42
+ out.append(arg)
43
+ args = tuple(out)
44
+ for key, val in kwargs.items():
45
+ if hasattr(val, "name"):
46
+ kwargs[key] = val.name
47
+ if not self.gui_is_alive():
48
+ raise RuntimeError("GUI is not alive")
49
+ return self._run_rpc(func.__name__, *args, **kwargs)
50
+
51
+ return wrapper
52
+
53
+
54
+ class RPCResponseTimeoutError(Exception):
55
+ """Exception raised when an RPC response is not received within the expected time."""
56
+
57
+ def __init__(self, request_id, timeout):
58
+ super().__init__(
59
+ f"RPC response not received within {timeout} seconds for request ID {request_id}"
60
+ )
61
+
62
+
63
+ class RPCBase:
64
+ def __init__(self, gui_id: str = None, config: dict = None, parent=None) -> None:
65
+ self._client = BECClient() # BECClient is a singleton; here, we simply get the instance
66
+ self._config = config if config is not None else {}
67
+ self._gui_id = gui_id if gui_id is not None else str(uuid.uuid4())[:5]
68
+ self._parent = parent
69
+ self._msg_wait_event = threading.Event()
70
+ self._rpc_response = None
71
+ super().__init__()
72
+ # print(f"RPCBase: {self._gui_id}")
73
+
74
+ def __repr__(self):
75
+ type_ = type(self)
76
+ qualname = type_.__qualname__
77
+ return f"<{qualname} object at {hex(id(self))}>"
78
+
79
+ @property
80
+ def _root(self):
81
+ """
82
+ Get the root widget. This is the BECFigure widget that holds
83
+ the anchor gui_id.
84
+ """
85
+ parent = self
86
+ # pylint: disable=protected-access
87
+ while parent._parent is not None:
88
+ parent = parent._parent
89
+ return parent
90
+
91
+ def _run_rpc(self, method, *args, wait_for_rpc_response=True, timeout=3, **kwargs):
92
+ """
93
+ Run the RPC call.
94
+
95
+ Args:
96
+ method: The method to call.
97
+ args: The arguments to pass to the method.
98
+ wait_for_rpc_response: Whether to wait for the RPC response.
99
+ kwargs: The keyword arguments to pass to the method.
100
+
101
+ Returns:
102
+ The result of the RPC call.
103
+ """
104
+ request_id = str(uuid.uuid4())
105
+ rpc_msg = messages.GUIInstructionMessage(
106
+ action=method,
107
+ parameter={"args": args, "kwargs": kwargs, "gui_id": self._gui_id},
108
+ metadata={"request_id": request_id},
109
+ )
110
+
111
+ # pylint: disable=protected-access
112
+ receiver = self._root._gui_id
113
+ if wait_for_rpc_response:
114
+ self._rpc_response = None
115
+ self._msg_wait_event.clear()
116
+ self._client.connector.register(
117
+ MessageEndpoints.gui_instruction_response(request_id),
118
+ cb=self._on_rpc_response,
119
+ parent=self,
120
+ )
121
+
122
+ self._client.connector.set_and_publish(MessageEndpoints.gui_instructions(receiver), rpc_msg)
123
+
124
+ if wait_for_rpc_response:
125
+ try:
126
+ finished = self._msg_wait_event.wait(timeout)
127
+ if not finished:
128
+ raise RPCResponseTimeoutError(request_id, timeout)
129
+ finally:
130
+ self._msg_wait_event.clear()
131
+ self._client.connector.unregister(
132
+ MessageEndpoints.gui_instruction_response(request_id), cb=self._on_rpc_response
133
+ )
134
+ # get class name
135
+ if not self._rpc_response.accepted:
136
+ raise ValueError(self._rpc_response.message["error"])
137
+ msg_result = self._rpc_response.message.get("result")
138
+ self._rpc_response = None
139
+ return self._create_widget_from_msg_result(msg_result)
140
+
141
+ @staticmethod
142
+ def _on_rpc_response(msg: MessageObject, parent: RPCBase) -> None:
143
+ msg = msg.value
144
+ parent._msg_wait_event.set()
145
+ parent._rpc_response = msg
146
+
147
+ def _create_widget_from_msg_result(self, msg_result):
148
+ if msg_result is None:
149
+ return None
150
+ if isinstance(msg_result, list):
151
+ return [self._create_widget_from_msg_result(res) for res in msg_result]
152
+ if isinstance(msg_result, dict):
153
+ if "__rpc__" not in msg_result:
154
+ return {
155
+ key: self._create_widget_from_msg_result(val) for key, val in msg_result.items()
156
+ }
157
+ cls = msg_result.pop("widget_class", None)
158
+ msg_result.pop("__rpc__", None)
159
+
160
+ if not cls:
161
+ return msg_result
162
+
163
+ cls = getattr(client, cls)
164
+ # print(msg_result)
165
+ return cls(parent=self, **msg_result)
166
+ return msg_result
167
+
168
+ def gui_is_alive(self):
169
+ """
170
+ Check if the GUI is alive.
171
+ """
172
+ heart = self._client.connector.get(MessageEndpoints.gui_heartbeat(self._root._gui_id))
173
+ if heart is None:
174
+ return False
175
+ if heart.status == messages.BECStatus.RUNNING:
176
+ return True
177
+ return False
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
- from contextlib import redirect_stderr, redirect_stdout
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
- try:
61
- obj = self.get_object_from_config(msg["parameter"])
62
- method = msg["action"]
63
- args = msg["parameter"].get("args", [])
64
- kwargs = msg["parameter"].get("kwargs", {})
65
- res = self.run_rpc(obj, method, args, kwargs)
66
- except Exception as e:
67
- logger.error(f"Error while executing RPC instruction: {e}")
68
- self.send_response(request_id, False, {"error": str(e)})
69
- else:
70
- logger.debug(f"RPC instruction executed successfully: {res}")
71
- self.send_response(request_id, True, {"result": res})
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.gui_class == "BECFigure":
203
- gui_class = BECFigure
204
- elif args.gui_class == "BECDockArea":
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
- app.setQuitOnLastWindowClosed(False)
217
- app.setApplicationName("BEC Figure")
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__": # pragma: no cover
254
- sys.argv = ["bec_widgets.cli.server", "--id", "e2860", "--gui_class", "BECDockArea"]
291
+ if __name__ == "__main__":
255
292
  main()
@@ -2,10 +2,46 @@ import functools
2
2
  import sys
3
3
  import traceback
4
4
 
5
- from qtpy.QtCore import QObject, Qt, Signal, Slot
5
+ from qtpy.QtCore import Property, QObject, Qt, Signal, Slot
6
6
  from qtpy.QtWidgets import QApplication, QMessageBox, QPushButton, QVBoxLayout, QWidget
7
7
 
8
8
 
9
+ def SafeProperty(prop_type, *prop_args, popup_error: bool = False, **prop_kwargs):
10
+ """
11
+ Decorator to create a Qt Property with a safe setter that won't crash Designer on errors.
12
+ Behaves similarly to SafeSlot, but for properties.
13
+
14
+ Args:
15
+ prop_type: The property type (e.g., str, bool, "QStringList", etc.)
16
+ popup_error (bool): If True, show popup on error, otherwise just handle it silently.
17
+ *prop_args, **prop_kwargs: Additional arguments and keyword arguments accepted by Property.
18
+ """
19
+
20
+ def decorator(getter):
21
+ class PropertyWrapper:
22
+ def __init__(self, getter_func):
23
+ self.getter_func = getter_func
24
+
25
+ def setter(self, setter_func):
26
+ @functools.wraps(setter_func)
27
+ def safe_setter(self_, value):
28
+ try:
29
+ return setter_func(self_, value)
30
+ except Exception:
31
+ if popup_error:
32
+ ErrorPopupUtility().custom_exception_hook(
33
+ *sys.exc_info(), popup_error=True
34
+ )
35
+ else:
36
+ return
37
+
38
+ return Property(prop_type, self.getter_func, safe_setter, *prop_args, **prop_kwargs)
39
+
40
+ return PropertyWrapper(getter)
41
+
42
+ return decorator
43
+
44
+
9
45
  def SafeSlot(*slot_args, **slot_kwargs): # pylint: disable=invalid-name
10
46
  """Function with args, acting like a decorator, applying "error_managed" decorator + Qt Slot
11
47
  to the passed function, to display errors instead of potentially raising an exception
@@ -91,6 +127,12 @@ class _ErrorPopupUtility(QObject):
91
127
  msg.setMinimumHeight(400)
92
128
  msg.exec_()
93
129
 
130
+ def show_property_error(self, title, message, widget):
131
+ """
132
+ Show a property-specific error message.
133
+ """
134
+ self.error_occurred.emit(title, message, widget)
135
+
94
136
  def format_traceback(self, traceback_message: str) -> str:
95
137
  """
96
138
  Format the traceback message to be displayed in the error popup by adding indentation to each line.
@@ -127,12 +169,14 @@ class _ErrorPopupUtility(QObject):
127
169
  error_message = " ".join(captured_message)
128
170
  return error_message
129
171
 
172
+ def get_error_message(self, exctype, value, tb):
173
+ return "".join(traceback.format_exception(exctype, value, tb))
174
+
130
175
  def custom_exception_hook(self, exctype, value, tb, popup_error=False):
131
176
  if popup_error or self.enable_error_popup:
132
- error_message = traceback.format_exception(exctype, value, tb)
133
177
  self.error_occurred.emit(
134
178
  "Method error" if popup_error else "Application Error",
135
- "".join(error_message),
179
+ self.get_error_message(exctype, value, tb),
136
180
  self.parent(),
137
181
  )
138
182
  else:
@@ -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
@@ -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 and setting values using handler mapping"""
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 application to demonstrate the usage of the functions
313
- if __name__ == "__main__": # pragma: no cover
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.rpc_wigdet_handler import widget_handler
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