diff --git a/bec_widgets/cli/client_utils.py b/bec_widgets/cli/client_utils.py index 06050c15..65203233 100644 --- a/bec_widgets/cli/client_utils.py +++ b/bec_widgets/cli/client_utils.py @@ -179,6 +179,7 @@ class BECGuiClient(RPCBase): self._gui_started_event = threading.Event() self._process = None self._process_output_processing_thread = None + self._exposed_widgets = [] @property def windows(self): @@ -329,19 +330,31 @@ class BECGuiClient(RPCBase): with wait_for_server(self): return self._top_level["main"].widget - def new(self, title): + def new(self, title: str = None) -> BECDockArea: """Ask main window to create a new top-level dock area""" with wait_for_server(self): rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self) widget = rpc_client._run_rpc("new_dock_area", title) - self._top_level[widget._gui_id] = WidgetDesc(title=title, widget=widget) + self._top_level[widget._gui_id] = widget + setattr(self, widget._gui_id, widget) + self._exposed_widgets.append(widget._gui_id) return widget + def _update_top_level_widgets(self): + for widget_id in self._exposed_widgets: + delattr(self, widget_id) + self._exposed_widgets.clear() + + for widget_id, widget in self._top_level.items(): + setattr(self, widget_id, widget) + self._exposed_widgets.append(widget_id) + def close(self) -> None: """ Close the gui window. """ self._top_level.clear() + self._update_top_level_widgets() if self._gui_started_timer is not None: self._gui_started_timer.cancel() diff --git a/bec_widgets/cli/rpc/rpc_register.py b/bec_widgets/cli/rpc/rpc_register.py index 4f926f59..8e94cf02 100644 --- a/bec_widgets/cli/rpc/rpc_register.py +++ b/bec_widgets/cli/rpc/rpc_register.py @@ -1,11 +1,27 @@ from __future__ import annotations +from functools import wraps from threading import Lock +from typing import Callable from weakref import WeakValueDictionary from qtpy.QtCore import QObject +def broadcast_update(func): + """ + Decorator to broadcast updates to the RPCRegister whenever a new RPC object is added or removed. + """ + + @wraps(func) + def wrapper(self, *args, **kwargs): + result = func(self, *args, **kwargs) + self.broadcast() + return result + + return wrapper + + class RPCRegister: """ A singleton class that keeps track of all the RPC objects registered in the system for CLI usage. @@ -26,7 +42,9 @@ class RPCRegister: return self._rpc_register = WeakValueDictionary() self._initialized = True + self.callbacks = [] + @broadcast_update def add_rpc(self, rpc: QObject): """ Add an RPC object to the register. @@ -38,6 +56,7 @@ class RPCRegister: raise ValueError("RPC object must have a 'gui_id' attribute.") self._rpc_register[rpc.gui_id] = rpc + @broadcast_update def remove_rpc(self, rpc: str): """ Remove an RPC object from the register. @@ -73,6 +92,38 @@ class RPCRegister: connections = dict(self._rpc_register) return connections + def get_rpc_by_type(self, type_name) -> list[str]: + """ + Get all RPC objects of a certain type. + + Args: + type_name(str): The type of the RPC object to be retrieved. + + Returns: + list: A list of RPC objects of the given type. + """ + rpc_objects = [rpc for rpc in self._rpc_register if rpc.startswith(type_name)] + return rpc_objects + + def broadcast(self): + """ + Broadcast the update to all the callbacks. + """ + print("Broadcasting") + connections = self.list_all_connections() + for callback in self.callbacks: + callback(connections) + + def add_callback(self, callback: Callable[[dict], None]): + """ + Add a callback that will be called whenever the registry is updated. + + Args: + callback(Callable[[dict], None]): The callback to be added. It should accept a dictionary of all the + registered RPC objects as an argument. + """ + self.callbacks.append(callback) + @classmethod def reset_singleton(cls): """ diff --git a/bec_widgets/cli/server.py b/bec_widgets/cli/server.py index 1f854f25..59e98aa8 100644 --- a/bec_widgets/cli/server.py +++ b/bec_widgets/cli/server.py @@ -152,6 +152,24 @@ class BECWidgetsCLIServer: except RedisError as exc: logger.error(f"Error while emitting heartbeat: {exc}") + def broadcast_registry_update(self, connections: dict): + """ + Broadcast the updated registry to all clients. + """ + + # We only need to broadcast the dock areas + data = { + key: self.serialize_object(val) + for key, val in connections.items() + if val.__class__.__name__ == "BECDockArea" + } + logger.info(f"Broadcasting registry update: {data}") + # self.client.connector.set( + # MessageEndpoints.gui_registry_update(self.gui_id), + # messages.RegistryUpdateMessage(connections=connections), + # expire=10, + # ) + def shutdown(self): # TODO not sure if needed when cleanup is done at level of BECConnector logger.info(f"Shutting down server with gui_id: {self.gui_id}") self.status = messages.BECStatus.IDLE @@ -263,6 +281,7 @@ def main(): win.setWindowTitle("BEC Widgets") RPCRegister().add_rpc(win) + RPCRegister().add_callback(server.broadcast_registry_update) gui = server.gui win.setCentralWidget(gui) diff --git a/bec_widgets/widgets/containers/main_window/main_window.py b/bec_widgets/widgets/containers/main_window/main_window.py index 0a655aac..93eb2c34 100644 --- a/bec_widgets/widgets/containers/main_window/main_window.py +++ b/bec_widgets/widgets/containers/main_window/main_window.py @@ -1,5 +1,6 @@ from qtpy.QtWidgets import QApplication, QMainWindow +from bec_widgets.cli.rpc.rpc_register import RPCRegister from bec_widgets.utils import BECConnector from bec_widgets.widgets.containers.dock.dock_area import BECDockArea @@ -33,8 +34,15 @@ class BECMainWindow(QMainWindow, BECConnector): } return info - def new_dock_area(self, name): - dock_area = BECDockArea() + def new_dock_area(self, name=None): + name = name or "BEC Widgets" + self.rpc_register = RPCRegister() + gui_id = name.replace(" ", "_") + existing_widgets = self.rpc_register.get_rpc_by_type(gui_id) + if existing_widgets: + name = f"{name} {len(existing_widgets) + 1}" + + dock_area = BECDockArea(gui_id=name.replace(" ", "_")) dock_area.resize(dock_area.minimumSizeHint()) dock_area.window().setWindowTitle(name) dock_area.show()