mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-05-18 20:36:57 +02:00
feat: add dynamic name space to gui instance
This commit is contained in:
+150
-71
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)},
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user