feat: add dynamic name space to gui instance

This commit is contained in:
2025-03-05 13:19:00 +01:00
committed by wyzula-jan
parent 3029433740
commit 5f11a44f27
4 changed files with 175 additions and 84 deletions
+150 -71
View File
@@ -1,3 +1,5 @@
""" Client utilities for the BEC GUI. """
from __future__ import annotations
import importlib
@@ -13,11 +15,10 @@ from typing import TYPE_CHECKING
from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import bec_logger
from bec_lib.utils.import_utils import isinstance_based_on_class_name, lazy_import, lazy_import_from
from bec_lib.utils.import_utils import lazy_import, lazy_import_from
import bec_widgets.cli.client as client
from bec_widgets.cli.auto_updates import AutoUpdates
from bec_widgets.cli.client import BECDockArea
from bec_widgets.cli.rpc.rpc_base import RPCBase
if TYPE_CHECKING:
@@ -122,13 +123,17 @@ def _start_plot_process(
class RepeatTimer(threading.Timer):
"""RepeatTimer class."""
def run(self):
while not self.finished.wait(self.interval):
self.function(*self.args, **self.kwargs)
# pylint: disable=protected-access
@contextmanager
def wait_for_server(client):
def wait_for_server(client: BECGuiClient):
"""Context manager to wait for the server to start."""
timeout = client._startup_timeout
if not timeout:
if client._gui_is_alive():
@@ -149,7 +154,22 @@ def wait_for_server(client):
yield
class WidgetNameSpace:
pass
class BECDockArea(client.BECDockArea):
"""Extend the BECDockArea class and add namespaces to access widgets of docks."""
def __init__(self, gui_id=None, config=None, name=None, parent=None):
super().__init__(gui_id, config, name, parent)
# Add namespaces for DockArea
self.elements = WidgetNameSpace()
class BECGuiClient(RPCBase):
"""BEC GUI client class. Container for GUI applications within Python."""
_top_level = {}
def __init__(self, **kwargs) -> None:
@@ -162,22 +182,43 @@ class BECGuiClient(RPCBase):
self._gui_started_event = threading.Event()
self._process = None
self._process_output_processing_thread = None
self._exposed_widgets = []
self._exposed_dock_areas = []
self._registry_state = {}
def connect_to_gui_server(self, gui_id: str) -> None:
"""Connect to a GUI server"""
# Unregister the old callback
self._client.connector.unregister(
MessageEndpoints.gui_registry_state(self._gui_id), cb=self._handle_registry_update
)
self._gui_id = gui_id
# Get the registry state
msgs = self._client.connector.xread(
MessageEndpoints.gui_registry_state(self._gui_id), count=1
)
if msgs:
self._handle_registry_update(msgs[0])
# Register the new callback
self._client.connector.register(
MessageEndpoints.gui_registry_state(self._gui_id), cb=self._handle_registry_update
)
@property
def windows(self):
def windows(self) -> dict:
"""Dictionary with dock ares in the GUI."""
return self._top_level
@property
def window_list(self):
def window_list(self) -> list:
"""List with dock areas in the GUI."""
return list(self._top_level.values())
@property
def auto_updates(self):
if self._auto_updates_enabled:
with wait_for_server(self):
return self._auto_updates
# FIXME AUTO UPDATES
# @property
# def auto_updates(self):
# if self._auto_updates_enabled:
# with wait_for_server(self):
# return self._auto_updates
def _get_update_script(self) -> AutoUpdates | None:
eps = imd.entry_points(group="bec.widgets.auto_updates")
@@ -193,46 +234,48 @@ class BECGuiClient(RPCBase):
logger.error(f"Error loading auto update script from plugin: {str(e)}")
return None
@property
def selected_device(self) -> str | None:
"""
Selected device for the plot.
"""
auto_update_config_ep = MessageEndpoints.gui_auto_update_config(self._gui_id)
auto_update_config = self._client.connector.get(auto_update_config_ep)
if auto_update_config:
return auto_update_config.selected_device
return None
# FIME AUTO UPDATES
# @property
# def selected_device(self) -> str | None:
# """
# Selected device for the plot.
# """
# auto_update_config_ep = MessageEndpoints.gui_auto_update_config(self._gui_id)
# auto_update_config = self._client.connector.get(auto_update_config_ep)
# if auto_update_config:
# return auto_update_config.selected_device
# return None
@selected_device.setter
def selected_device(self, device: str | DeviceBase):
if isinstance_based_on_class_name(device, "bec_lib.device.DeviceBase"):
self._client.connector.set_and_publish(
MessageEndpoints.gui_auto_update_config(self._gui_id),
messages.GUIAutoUpdateConfigMessage(selected_device=device.name),
)
elif isinstance(device, str):
self._client.connector.set_and_publish(
MessageEndpoints.gui_auto_update_config(self._gui_id),
messages.GUIAutoUpdateConfigMessage(selected_device=device),
)
else:
raise ValueError("Device must be a string or a device object")
# @selected_device.setter
# def selected_device(self, device: str | DeviceBase):
# if isinstance_based_on_class_name(device, "bec_lib.device.DeviceBase"):
# self._client.connector.set_and_publish(
# MessageEndpoints.gui_auto_update_config(self._gui_id),
# messages.GUIAutoUpdateConfigMessage(selected_device=device.name),
# )
# elif isinstance(device, str):
# self._client.connector.set_and_publish(
# MessageEndpoints.gui_auto_update_config(self._gui_id),
# messages.GUIAutoUpdateConfigMessage(selected_device=device),
# )
# else:
# raise ValueError("Device must be a string or a device object")
def _start_update_script(self) -> None:
self._client.connector.register(MessageEndpoints.scan_status(), cb=self._handle_msg_update)
# FIXME AUTO UPDATES
# def _start_update_script(self) -> None:
# self._client.connector.register(MessageEndpoints.scan_status(), cb=self._handle_msg_update)
def _handle_msg_update(self, msg: StreamMessage) -> None:
if self.auto_updates is not None:
# pylint: disable=protected-access
return self._update_script_msg_parser(msg.value)
# def _handle_msg_update(self, msg: StreamMessage) -> None:
# if self.auto_updates is not None:
# # pylint: disable=protected-access
# return self._update_script_msg_parser(msg.value)
def _update_script_msg_parser(self, msg: messages.BECMessage) -> None:
if isinstance(msg, messages.ScanStatusMessage):
if not self._gui_is_alive():
return
if self._auto_updates_enabled:
return self.auto_updates.do_update(msg)
# def _update_script_msg_parser(self, msg: messages.BECMessage) -> None:
# if isinstance(msg, messages.ScanStatusMessage):
# if not self._gui_is_alive():
# return
# if self._auto_updates_enabled:
# return self.auto_updates.do_update(msg)
def _gui_post_startup(self):
timeout = 10
@@ -241,11 +284,7 @@ class BECGuiClient(RPCBase):
time.sleep(0.1)
else:
break
key = list(self._registry_state.keys())[0]
gui_id = self._registry_state[key]["gui_id"]
name = self._registry_state[key]["name"]
widget = BECDockArea(gui_id=gui_id, name=name, parent=self)
self._add_widget_to_top_level(name, widget)
# FIXME AUTO UPDATES
# if self._auto_updates_enabled:
# if self._auto_updates is None:
# auto_updates = self._get_update_script()
@@ -272,7 +311,7 @@ class BECGuiClient(RPCBase):
self._gui_id,
self.__class__,
gui_class_id=self._default_dock_name,
config=self._client._service_config.config,
config=self._client._service_config.config, # pylint: disable=protected-access
logger=logger,
)
@@ -302,15 +341,18 @@ class BECGuiClient(RPCBase):
return self._start_server()
def start(self):
# FIXME keeping backwards compatibility for now
"""Start the GUI server."""
return self._start()
def _handle_registry_update(self, msg: StreamMessage) -> None:
self._registry_state = msg["data"].state
self._update_dynamic_namespace()
# self._update_dynamic_namespace()
# FIXME logic to update namespace
def _do_show_all(self):
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
rpc_client._run_rpc("show")
rpc_client._run_rpc("show") # pylint: disable=protected-access
for window in self._top_level.values():
window.show()
@@ -321,17 +363,19 @@ class BECGuiClient(RPCBase):
def _hide_all(self):
with wait_for_server(self):
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
rpc_client._run_rpc("hide")
rpc_client._run_rpc("hide") # pylint: disable=protected-access
for window in self._top_level.values():
window.hide()
def show(self):
"""Show the GUI window."""
if self._process is not None:
return self._show_all()
# backward compatibility: show() was also starting server
return self._start_server(wait=True)
def hide(self):
"""Hide the GUI window."""
return self._hide_all()
def new(self, name: str | None = None, wait: bool = True) -> BECDockArea:
@@ -349,31 +393,63 @@ class BECGuiClient(RPCBase):
widget = rpc_client._run_rpc(
"new_dock_area", name
) # pylint: disable=protected-access
self._add_widget_to_top_level(widget._name, widget)
return widget
widget = rpc_client._run_rpc("new_dock_area", name) # pylint: disable=protected-access
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
self._add_widget_to_top_level(widget._name, widget)
widget = rpc_client._run_rpc("new_dock_area", name) # pylint: disable=protected-access
return widget
def _add_widget_to_top_level(self, widget_id: str, widget: BECDockArea) -> None:
self._top_level[widget_id] = widget
self._update_top_level_widgets()
def _update_top_level_widgets(self):
for widget_id in self._exposed_widgets:
def _clear_top_level_widgets(self):
self._top_level.clear()
for widget_id in self._exposed_dock_areas:
delattr(self, widget_id)
self._exposed_widgets.clear()
self._exposed_dock_areas.clear()
for widget_id, widget in self._top_level.items():
setattr(self, widget_id, widget)
self._exposed_widgets.append(widget_id)
def _add_dock_areas_from_registry(self):
for dock_area_info in self._registry_state.values():
name = dock_area_info["name"]
gui_id = dock_area_info["gui_id"]
dock_area = BECDockArea(gui_id=gui_id, name=name, parent=self)
self._top_level[name] = dock_area
self._exposed_dock_areas.append(name)
setattr(self, name, dock_area)
dock_info = dock_area_info["config"].get("docks", None)
if dock_info:
self._add_docks_from_registry(dock_info, dock_area)
def _add_docks_from_registry(self, dock_info: dict[str, dict], dock_area: BECDockArea):
for dock_name, info in dock_info.items():
dock = client.BECDock(gui_id=info["gui_id"], name=dock_name, parent=dock_area)
setattr(dock_area, dock_name, dock)
widget_info = info["widgets"]
if widget_info:
self._add_widgets_from_registry(
widget_info=widget_info, dock_area=dock_area, dock=dock
)
def _add_widgets_from_registry(
self, widget_info: dict[str, dict], dock_area: client.BECDockArea, dock: client.BECDock
):
for widget_name, info in widget_info.items():
widget_class = getattr(client, info["widget_class"])
widget = widget_class(gui_id=info["gui_id"], name=widget_name, parent=dock)
obj = getattr(dock_area, "elements")
setattr(obj, widget_name, widget)
setattr(dock, widget_name, widget)
def _update_dynamic_namespace(self):
"""Update the dynamic name space"""
self._clear_top_level_widgets()
self._add_dock_areas_from_registry()
def close(self):
# Needed to shut down gui for IPythonClient, will be remove in future
"""Deprecated. Use kill() instead."""
# FIXME, deprecated in favor of kill, will be removed in the future
self.kill()
def kill(self) -> None:
"""Kill the GUI server."""
self._close()
def _close(self) -> None:
@@ -381,7 +457,6 @@ class BECGuiClient(RPCBase):
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()
@@ -397,3 +472,7 @@ class BECGuiClient(RPCBase):
self._process_output_processing_thread.join()
self._process.wait()
self._process = None
# Unregister the registry state
self._client.connector.unregister(
MessageEndpoints.gui_registry_state(self._gui_id), cb=self._handle_registry_update
)
-3
View File
@@ -172,9 +172,6 @@ class BECWidgetsCLIServer:
if val.__class__.__name__ == "BECDockArea"
}
logger.info(f"Broadcasting registry update: {data}")
for key, val in data.items():
logger.info(f"DockArea: {key} - docks: {len(val['config']['docks'])}")
logger.warning(f"Broadcasting registry update: {data}")
self.client.connector.xadd(
MessageEndpoints.gui_registry_state(self.gui_id),
msg_dict={"data": messages.GUIRegistryStateMessage(state=data)},
+14 -8
View File
@@ -1,13 +1,13 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Literal, Optional, cast
from weakref import WeakValueDictionary
from bec_lib.logger import bec_logger
from pydantic import Field
from pyqtgraph.dockarea import Dock, DockLabel
from qtpy import QtCore, QtGui
from bec_widgets.cli.rpc.rpc_register import RPCRegister
from bec_widgets.cli.rpc.rpc_widget_handler import widget_handler
from bec_widgets.utils import ConnectionConfig, GridLayoutManager
from bec_widgets.utils.bec_widget import BECWidget
@@ -140,7 +140,7 @@ class BECDock(BECWidget, Dock):
) -> None:
if config is None:
config = DockConfig(
widget_class=self.__class__.__name__, parent_dock_area=parent_dock_area._name
widget_class=self.__class__.__name__, parent_dock_area=parent_dock_area.gui_id
)
else:
if isinstance(config, dict):
@@ -309,11 +309,9 @@ class BECDock(BECWidget, Dock):
f"with name: {self.parent_dock_area._name} and id {self.parent_dock_area.gui_id}."
)
else: # Name is not provided
widget_class_name = widget if isinstance(widget, str) else widget.__class__.__name__
name = WidgetContainerUtils.generate_unique_name(
name=(
widget if isinstance(widget, str) else widget._name
), # pylint: disable=protected-access
list_of_names=existing_widgets_parent_dock,
name=widget_class_name, list_of_names=existing_widgets_parent_dock
)
if isinstance(widget, str):
widget = cast(BECWidget, widget_handler.create_widget(widget_type=widget, name=name))
@@ -322,9 +320,15 @@ class BECDock(BECWidget, Dock):
self.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan)
if hasattr(widget, "config"):
self.config.widgets[widget._name] = widget.config
widget.config.gui_id = widget.gui_id
self.config.widgets[widget._name] = widget.config # pylint: disable=protected-access
self._broadcast_update()
return widget
def _broadcast_update(self):
rpc_register = RPCRegister()
rpc_register.broadcast()
def move_widget(self, widget: QWidget, new_row: int, new_col: int):
"""
Move a widget to a new position in the layout.
@@ -361,6 +365,7 @@ class BECDock(BECWidget, Dock):
Args:
widget_name(str): Delete the widget with the given name.
"""
# pylint: disable=protected-access
widget = [widget for widget in self.widgets if widget._name == widget_name]
if not widget:
logger.warning(
@@ -380,13 +385,14 @@ class BECDock(BECWidget, Dock):
if widget in self.widgets:
self.widgets.remove(widget)
widget.close()
self._broadcast_update()
def delete_all(self):
"""
Remove all widgets from the dock.
"""
for widget in self.widgets:
self.delete(widget._name)
self.delete(widget._name) # pylint: disable=protected-access
def cleanup(self):
"""
@@ -1,10 +1,10 @@
from __future__ import annotations
from typing import Literal, Optional
from unittest.mock import NonCallableMagicMock
from weakref import WeakValueDictionary
from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import bec_logger
from pydantic import Field
from pyqtgraph.dockarea.DockArea import DockArea
from qtpy.QtCore import QSize, Qt
@@ -35,6 +35,8 @@ from bec_widgets.widgets.services.bec_status_box.bec_status_box import BECStatus
from bec_widgets.widgets.utility.logpanel.logpanel import LogPanel
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
logger = bec_logger.logger
class DockAreaConfig(ConnectionConfig):
docks: dict[str, DockConfig] = Field({}, description="The docks in the dock area.")
@@ -345,7 +347,7 @@ class BECDockArea(BECWidget, QWidget):
)
else: # Name is not provided
name = WidgetContainerUtils.generate_unique_name(
name=self.__class__.__name__, list_of_names=dock_names
name=BECDock.__name__, list_of_names=dock_names
)
dock = BECDock(name=name, parent_dock_area=self, closable=closable)
@@ -372,8 +374,14 @@ class BECDockArea(BECWidget, QWidget):
self.update()
if floating:
dock.detach()
# Run broadcast update
self._broadcast_update()
return dock
def _broadcast_update(self):
rpc_register = RPCRegister()
rpc_register.broadcast()
def detach_dock(self, dock_name: str) -> BECDock:
"""
Undock a dock from the dock area.
@@ -479,6 +487,7 @@ class BECDockArea(BECWidget, QWidget):
dock.hide_title_bar()
else:
raise ValueError(f"Dock with name {dock_name} does not exist.")
self._broadcast_update()
if __name__ == "__main__": # pragma: no cover