From a9b36a4c45f30dcc46489c38e3ee214c5b4846d9 Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 10 Mar 2025 07:02:43 +0100 Subject: [PATCH] refactor: fix cleanup, add remove methods, improve docs --- bec_widgets/cli/client.py | 406 ++++-------------- bec_widgets/cli/client_utils.py | 91 +++- bec_widgets/cli/generate_cli.py | 12 + bec_widgets/cli/rpc/rpc_base.py | 15 +- bec_widgets/cli/rpc/rpc_widget_handler.py | 9 +- bec_widgets/cli/server.py | 2 +- bec_widgets/utils/bec_connector.py | 17 +- bec_widgets/utils/bec_widget.py | 16 +- bec_widgets/widgets/containers/dock/dock.py | 48 ++- .../widgets/containers/dock/dock_area.py | 29 +- .../widgets/containers/figure/figure.py | 13 +- .../widgets/plots_next_gen/plot_base.py | 2 +- 12 files changed, 273 insertions(+), 387 deletions(-) diff --git a/bec_widgets/cli/client.py b/bec_widgets/cli/client.py index 13abe921..21cb7d8e 100644 --- a/bec_widgets/cli/client.py +++ b/bec_widgets/cli/client.py @@ -50,27 +50,12 @@ class Widgets(str, enum.Enum): class AbortButton(RPCBase): - @property - @rpc_call - def _config_dict(self) -> "dict": - """ - Get the configuration of the widget. - - Returns: - dict: The configuration of the widget. - """ + """A button that abort the scan.""" @rpc_call - def _get_all_rpc(self) -> "dict": + def remove(self): """ - Get all registered RPC objects. - """ - - @property - @rpc_call - def _rpc_id(self) -> "str": - """ - Get the RPC ID of the widget. + Cleanup the BECConnector """ @@ -263,7 +248,8 @@ class BECDock(RPCBase): Add a widget to the dock. Args: - widget(QWidget): The widget to add. + widget(QWidget): The widget to add. It can not be BECDock or BECDockArea. + name(str): The name of the widget. row(int): The row to add the widget to. If None, the widget will be added to the next available row. col(int): The column to add the widget to. rowspan(int): The number of rows the widget should span. @@ -313,17 +299,6 @@ class BECDock(RPCBase): list: The list of eligible widgets. """ - @rpc_call - def move_widget(self, widget: "QWidget", new_row: "int", new_col: "int"): - """ - Move a widget to a new position in the layout. - - Args: - widget(QWidget): The widget to move. - new_row(int): The new row to move the widget to. - new_col(int): The new column to move the widget to. - """ - @rpc_call def delete(self, widget_name: "str") -> "None": """ @@ -339,6 +314,12 @@ class BECDock(RPCBase): Remove all widgets from the dock. """ + @rpc_call + def remove(self): + """ + Remove the dock from the parent dock area. + """ + @rpc_call def attach(self): """ @@ -433,6 +414,12 @@ class BECDockArea(RPCBase): Delete all docks. """ + @rpc_call + def remove(self) -> "None": + """ + Remove the dock area. + """ + @rpc_call def detach_dock(self, dock_name: "str") -> "BECDock": """ @@ -2214,6 +2201,8 @@ class BECPlotBase(RPCBase): class BECProgressBar(RPCBase): + """A custom progress bar with smooth transitions. The displayed text can be customized using a template.""" + @rpc_call def set_value(self, value): """ @@ -2265,52 +2254,22 @@ class BECProgressBar(RPCBase): class BECQueue(RPCBase): - @property - @rpc_call - def _config_dict(self) -> "dict": - """ - Get the configuration of the widget. - - Returns: - dict: The configuration of the widget. - """ + """Widget to display the BEC queue.""" @rpc_call - def _get_all_rpc(self) -> "dict": + def remove(self): """ - Get all registered RPC objects. - """ - - @property - @rpc_call - def _rpc_id(self) -> "str": - """ - Get the RPC ID of the widget. + Cleanup the BECConnector """ class BECStatusBox(RPCBase): - @property - @rpc_call - def _config_dict(self) -> "dict": - """ - Get the configuration of the widget. - - Returns: - dict: The configuration of the widget. - """ + """An autonomous widget to display the status of BEC services.""" @rpc_call - def _get_all_rpc(self) -> "dict": + def remove(self): """ - Get all registered RPC objects. - """ - - @property - @rpc_call - def _rpc_id(self) -> "str": - """ - Get the RPC ID of the widget. + Cleanup the BECConnector """ @@ -2829,6 +2788,8 @@ class Curve(RPCBase): class DapComboBox(RPCBase): + """The DAPComboBox widget is an extension to the QComboBox with all avaialble DAP model from BEC.""" + @rpc_call def select_y_axis(self, y_axis: str): """ @@ -2867,156 +2828,66 @@ class DarkModeButton(RPCBase): class DeviceBrowser(RPCBase): - @property @rpc_call - def _config_dict(self) -> "dict": + def remove(self): """ - Get the configuration of the widget. - - Returns: - dict: The configuration of the widget. - """ - - @rpc_call - def _get_all_rpc(self) -> "dict": - """ - Get all registered RPC objects. - """ - - @property - @rpc_call - def _rpc_id(self) -> "str": - """ - Get the RPC ID of the widget. + Cleanup the BECConnector """ class DeviceComboBox(RPCBase): - @property - @rpc_call - def _config_dict(self) -> "dict": - """ - Get the configuration of the widget. - - Returns: - dict: The configuration of the widget. - """ + """Combobox widget for device input with autocomplete for device names.""" @rpc_call - def _get_all_rpc(self) -> "dict": + def remove(self): """ - Get all registered RPC objects. - """ - - @property - @rpc_call - def _rpc_id(self) -> "str": - """ - Get the RPC ID of the widget. + Cleanup the BECConnector """ class DeviceInputBase(RPCBase): - @property - @rpc_call - def _config_dict(self) -> "dict": - """ - Get the configuration of the widget. - - Returns: - dict: The configuration of the widget. - """ + """Mixin base class for device input widgets.""" @rpc_call - def _get_all_rpc(self) -> "dict": + def remove(self): """ - Get all registered RPC objects. - """ - - @property - @rpc_call - def _rpc_id(self) -> "str": - """ - Get the RPC ID of the widget. + Cleanup the BECConnector """ class DeviceLineEdit(RPCBase): - @property - @rpc_call - def _config_dict(self) -> "dict": - """ - Get the configuration of the widget. - - Returns: - dict: The configuration of the widget. - """ + """Line edit widget for device input with autocomplete for device names.""" @rpc_call - def _get_all_rpc(self) -> "dict": + def remove(self): """ - Get all registered RPC objects. - """ - - @property - @rpc_call - def _rpc_id(self) -> "str": - """ - Get the RPC ID of the widget. + Cleanup the BECConnector """ class DeviceSignalInputBase(RPCBase): - @property - @rpc_call - def _config_dict(self) -> "dict": - """ - Get the configuration of the widget. - - Returns: - dict: The configuration of the widget. - """ + """Mixin base class for device signal input widgets.""" @rpc_call - def _get_all_rpc(self) -> "dict": + def remove(self): """ - Get all registered RPC objects. - """ - - @property - @rpc_call - def _rpc_id(self) -> "str": - """ - Get the RPC ID of the widget. + Cleanup the BECConnector """ class LMFitDialog(RPCBase): - @property - @rpc_call - def _config_dict(self) -> "dict": - """ - Get the configuration of the widget. - - Returns: - dict: The configuration of the widget. - """ + """Dialog for displaying the fit summary and params for LMFit DAP processes""" @rpc_call - def _get_all_rpc(self) -> "dict": + def remove(self): """ - Get all registered RPC objects. - """ - - @property - @rpc_call - def _rpc_id(self) -> "str": - """ - Get the RPC ID of the widget. + Cleanup the BECConnector """ class LogPanel(RPCBase): + """Displays a log panel""" + @rpc_call def set_plain_text(self, text: str) -> None: """ @@ -3079,6 +2950,8 @@ class PositionIndicator(RPCBase): class PositionerBox(RPCBase): + """Simple Widget to control a positioner in box form""" + @rpc_call def set_positioner(self, positioner: "str | Positioner"): """ @@ -3090,6 +2963,8 @@ class PositionerBox(RPCBase): class PositionerBox2D(RPCBase): + """Simple Widget to control two positioners in box form""" + @rpc_call def set_positioner_hor(self, positioner: "str | Positioner"): """ @@ -3110,31 +2985,18 @@ class PositionerBox2D(RPCBase): class PositionerBoxBase(RPCBase): - @property - @rpc_call - def _config_dict(self) -> "dict": - """ - Get the configuration of the widget. - - Returns: - dict: The configuration of the widget. - """ + """Contains some core logic for positioner box widgets""" @rpc_call - def _get_all_rpc(self) -> "dict": + def remove(self): """ - Get all registered RPC objects. - """ - - @property - @rpc_call - def _rpc_id(self) -> "str": - """ - Get the RPC ID of the widget. + Cleanup the BECConnector """ class PositionerControlLine(RPCBase): + """A widget that controls a single device.""" + @rpc_call def set_positioner(self, positioner: "str | Positioner"): """ @@ -3146,6 +3008,8 @@ class PositionerControlLine(RPCBase): class PositionerGroup(RPCBase): + """Simple Widget to control a positioner in box form""" + @rpc_call def set_positioners(self, device_names: "str"): """ @@ -3156,52 +3020,22 @@ class PositionerGroup(RPCBase): class ResetButton(RPCBase): - @property - @rpc_call - def _config_dict(self) -> "dict": - """ - Get the configuration of the widget. - - Returns: - dict: The configuration of the widget. - """ + """A button that resets the scan queue.""" @rpc_call - def _get_all_rpc(self) -> "dict": + def remove(self): """ - Get all registered RPC objects. - """ - - @property - @rpc_call - def _rpc_id(self) -> "str": - """ - Get the RPC ID of the widget. + Cleanup the BECConnector """ class ResumeButton(RPCBase): - @property - @rpc_call - def _config_dict(self) -> "dict": - """ - Get the configuration of the widget. - - Returns: - dict: The configuration of the widget. - """ + """A button that continue scan queue.""" @rpc_call - def _get_all_rpc(self) -> "dict": + def remove(self): """ - Get all registered RPC objects. - """ - - @property - @rpc_call - def _rpc_id(self) -> "str": - """ - Get the RPC ID of the widget. + Cleanup the BECConnector """ @@ -3485,131 +3319,56 @@ class RingProgressBar(RPCBase): class ScanControl(RPCBase): - @property @rpc_call - def _config_dict(self) -> "dict": + def remove(self): """ - Get the configuration of the widget. - - Returns: - dict: The configuration of the widget. - """ - - @rpc_call - def _get_all_rpc(self) -> "dict": - """ - Get all registered RPC objects. - """ - - @property - @rpc_call - def _rpc_id(self) -> "str": - """ - Get the RPC ID of the widget. + Cleanup the BECConnector """ class ScanMetadata(RPCBase): - @property - @rpc_call - def _config_dict(self) -> "dict": - """ - Get the configuration of the widget. - - Returns: - dict: The configuration of the widget. - """ + """Dynamically generates a form for inclusion of metadata for a scan. Uses the""" @rpc_call - def _get_all_rpc(self) -> "dict": + def remove(self): """ - Get all registered RPC objects. - """ - - @property - @rpc_call - def _rpc_id(self) -> "str": - """ - Get the RPC ID of the widget. + Cleanup the BECConnector """ class SignalComboBox(RPCBase): - @property - @rpc_call - def _config_dict(self) -> "dict": - """ - Get the configuration of the widget. - - Returns: - dict: The configuration of the widget. - """ + """Line edit widget for device input with autocomplete for device names.""" @rpc_call - def _get_all_rpc(self) -> "dict": + def remove(self): """ - Get all registered RPC objects. - """ - - @property - @rpc_call - def _rpc_id(self) -> "str": - """ - Get the RPC ID of the widget. + Cleanup the BECConnector """ class SignalLineEdit(RPCBase): - @property - @rpc_call - def _config_dict(self) -> "dict": - """ - Get the configuration of the widget. - - Returns: - dict: The configuration of the widget. - """ + """Line edit widget for device input with autocomplete for device names.""" @rpc_call - def _get_all_rpc(self) -> "dict": + def remove(self): """ - Get all registered RPC objects. - """ - - @property - @rpc_call - def _rpc_id(self) -> "str": - """ - Get the RPC ID of the widget. + Cleanup the BECConnector """ class StopButton(RPCBase): - @property - @rpc_call - def _config_dict(self) -> "dict": - """ - Get the configuration of the widget. - - Returns: - dict: The configuration of the widget. - """ + """A button that stops the current scan.""" @rpc_call - def _get_all_rpc(self) -> "dict": + def remove(self): """ - Get all registered RPC objects. - """ - - @property - @rpc_call - def _rpc_id(self) -> "str": - """ - Get the RPC ID of the widget. + Cleanup the BECConnector """ class TextBox(RPCBase): + """A widget that displays text in plain and HTML format""" + @rpc_call def set_plain_text(self, text: str) -> None: """ @@ -3629,7 +3388,10 @@ class TextBox(RPCBase): """ -class VSCodeEditor(RPCBase): ... +class VSCodeEditor(RPCBase): + """A widget to display the VSCode editor.""" + + ... class Waveform(RPCBase): @@ -4044,6 +3806,8 @@ class Waveform(RPCBase): class WebsiteWidget(RPCBase): + """A simple widget to display a website""" + @rpc_call def set_url(self, url: str) -> None: """ diff --git a/bec_widgets/cli/client_utils.py b/bec_widgets/cli/client_utils.py index d4962dfd..d78b41b1 100644 --- a/bec_widgets/cli/client_utils.py +++ b/bec_widgets/cli/client_utils.py @@ -1,4 +1,4 @@ -""" Client utilities for the BEC GUI. """ +"""Client utilities for the BEC GUI.""" from __future__ import annotations @@ -16,6 +16,8 @@ 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 lazy_import, lazy_import_from +from rich.console import Console +from rich.table import Table import bec_widgets.cli.client as client from bec_widgets.cli.auto_updates import AutoUpdates @@ -155,7 +157,40 @@ def wait_for_server(client: BECGuiClient): class WidgetNameSpace: - pass + def __repr__(self): + console = Console() + table = Table(title="Available widgets for BEC CLI usage") + table.add_column("Widget Name", justify="left", style="magenta") + table.add_column("Description", justify="left") + for attr, value in self.__dict__.items(): + docs = value.__doc__ + docs = docs if docs else "No description available" + table.add_row(attr, docs) + console.print(table) + return f"" + + +class AvailableWidgetsNamespace: + """Namespace for available widgets in the BEC GUI.""" + + def __init__(self): + for widget in client.Widgets: + name = widget.value + if name in ["BECDockArea", "BECDock"]: + continue + setattr(self, name, name) + + def __repr__(self): + console = Console() + table = Table(title="Available widgets for BEC CLI usage") + table.add_column("Widget Name", justify="left", style="magenta") + table.add_column("Description", justify="left") + for attr_name, _ in self.__dict__.items(): + docs = getattr(client, attr_name).__doc__ + docs = docs if docs else "No description available" + table.add_row(attr_name, docs if len(docs.strip()) > 0 else "No description available") + console.print(table) + return "" # f"<{self.__class__.__name__}>" class BECDockArea(client.BECDockArea): @@ -166,6 +201,17 @@ class BECDockArea(client.BECDockArea): # Add namespaces for DockArea self.elements = WidgetNameSpace() + def delete(self, dock_name): + # Don't close the bec dock area + if dock_name == "bec": + raise ValueError("Cannot delete the bec dock area") + super().delete(dock_name) + + def remove(self): + if self._name == "bec": + raise ValueError("Cannot delete the bec dock area") + super().remove() + class BECGuiClient(RPCBase): """BEC GUI client class. Container for GUI applications within Python.""" @@ -177,6 +223,7 @@ class BECGuiClient(RPCBase): self._default_dock_name = "bec" self._auto_updates_enabled = True self._auto_updates = None + self._killed = False self._startup_timeout = 0 self._gui_started_timer = None self._gui_started_event = threading.Event() @@ -184,7 +231,7 @@ class BECGuiClient(RPCBase): self._process_output_processing_thread = None self._exposed_dock_areas = [] self._registry_state = {} - self.available_widgets = client.Widgets + self.available_widgets = AvailableWidgetsNamespace() def connect_to_gui_server(self, gui_id: str) -> None: """Connect to a GUI server""" @@ -336,6 +383,7 @@ class BECGuiClient(RPCBase): return rpc_client._run_rpc("_dump") def _start(self): + self._killed = False self._client.connector.register( MessageEndpoints.gui_registry_state(self._gui_id), cb=self._handle_registry_update ) @@ -365,8 +413,12 @@ class BECGuiClient(RPCBase): with wait_for_server(self): rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self) rpc_client._run_rpc("hide") # pylint: disable=protected-access - for window in self._top_level.values(): - window.hide() + # because of the registry callbacks, we may have + # dock areas that are already killed, but not yet + # removed from the registry state + if not self._killed: + for window in self._top_level.values(): + window.hide() def show(self): """Show the GUI window.""" @@ -399,6 +451,22 @@ class BECGuiClient(RPCBase): widget = rpc_client._run_rpc("new_dock_area", name) # pylint: disable=protected-access return widget + def delete(self, name: str) -> None: + """Delete a dock area. + + Args: + name(str): The name of the dock area. + """ + widget = self.windows.get(name) + if widget is None: + raise ValueError(f"Dock area {name} not found.") + widget._run_rpc("close") # pylint: disable=protected-access + + def delete_all(self) -> None: + """Delete all dock areas.""" + for widget_name in self.windows.keys(): + self.delete(widget_name) + def _clear_top_level_widgets(self): self._top_level.clear() for widget_id in self._exposed_dock_areas: @@ -447,19 +515,14 @@ class BECGuiClient(RPCBase): self._add_dock_areas_from_registry() def close(self): - """Deprecated. Use kill() instead.""" + """Deprecated. Use kill_server() instead.""" # FIXME, deprecated in favor of kill, will be removed in the future - self.kill() + self.kill_server() - def kill(self) -> None: + def kill_server(self) -> None: """Kill the GUI server.""" - self._close() - - def _close(self) -> None: - """ - Close the gui window. - """ self._top_level.clear() + self._killed = True if self._gui_started_timer is not None: self._gui_started_timer.cancel() diff --git a/bec_widgets/cli/generate_cli.py b/bec_widgets/cli/generate_cli.py index 71052271..4475a020 100644 --- a/bec_widgets/cli/generate_cli.py +++ b/bec_widgets/cli/generate_cli.py @@ -95,9 +95,21 @@ class {class_name}(RPCBase):""" self.content += f""" class {class_name}(RPCBase):""" + if cls.__doc__: + # We only want the first line of the docstring + # But skip the first line if it's a blank line + first_line = cls.__doc__.split("\n")[0] + if first_line: + class_docs = first_line + else: + class_docs = cls.__doc__.split("\n")[1] + self.content += f""" + \"\"\"{class_docs}\"\"\" + """ if not cls.USER_ACCESS: self.content += """... """ + for method in cls.USER_ACCESS: is_property_setter = False obj = getattr(cls, method, None) diff --git a/bec_widgets/cli/rpc/rpc_base.py b/bec_widgets/cli/rpc/rpc_base.py index 18b5ad4c..60feb98c 100644 --- a/bec_widgets/cli/rpc/rpc_base.py +++ b/bec_widgets/cli/rpc/rpc_base.py @@ -81,7 +81,20 @@ class RPCBase: def __repr__(self): type_ = type(self) qualname = type_.__qualname__ - return f"<{qualname} object at {hex(id(self))}>" + return f"<{qualname} with name: {self.widget_name}>" + + def remove(self): + """ + Remove the widget. + """ + self._run_rpc("remove") + + @property + def widget_name(self): + """ + Get the widget name. + """ + return self._name @property def _root(self): diff --git a/bec_widgets/cli/rpc/rpc_widget_handler.py b/bec_widgets/cli/rpc/rpc_widget_handler.py index c58c11a8..fad20de9 100644 --- a/bec_widgets/cli/rpc/rpc_widget_handler.py +++ b/bec_widgets/cli/rpc/rpc_widget_handler.py @@ -33,9 +33,14 @@ class RPCWidgetHandler: from bec_widgets.utils.plugin_utils import get_custom_classes clss = get_custom_classes("bec_widgets") - self._widget_classes = {cls.__name__: cls for cls in clss.widgets} + self._widget_classes = { + cls.__name__: cls + for cls in clss.widgets + if cls.__name__ + not in ["BECDockArea", "BECDock"] # Exclude these classes due to hierarchy + } - def create_widget(self, widget_type, name: str, **kwargs) -> BECWidget: + def create_widget(self, widget_type, name: str | None = None, **kwargs) -> BECWidget: """ Create a widget from an RPC message. diff --git a/bec_widgets/cli/server.py b/bec_widgets/cli/server.py index a0a2e34d..0b2a85d7 100644 --- a/bec_widgets/cli/server.py +++ b/bec_widgets/cli/server.py @@ -171,7 +171,7 @@ class BECWidgetsCLIServer: for key, val in connections.items() if val.__class__.__name__ == "BECDockArea" } - logger.info(f"Broadcasting registry update: {data}") + logger.info(f"All registered connections: {list(connections.keys())}") self.client.connector.xadd( MessageEndpoints.gui_registry_state(self.gui_id), msg_dict={"data": messages.GUIRegistryStateMessage(state=data)}, diff --git a/bec_widgets/utils/bec_connector.py b/bec_widgets/utils/bec_connector.py index 71302744..e15cbd6c 100644 --- a/bec_widgets/utils/bec_connector.py +++ b/bec_widgets/utils/bec_connector.py @@ -209,6 +209,7 @@ class BECConnector: """ self.config = config + # FIXME some thoughts are required to decide how thhis should work with rpc registry def apply_config(self, config: dict, generate_new_id: bool = True) -> None: """ Apply the configuration to the widget. @@ -221,11 +222,12 @@ class BECConnector: if generate_new_id is True: gui_id = str(uuid.uuid4()) self.rpc_register.remove_rpc(self) - self.set_gui_id(gui_id) + self._set_gui_id(gui_id) self.rpc_register.add_rpc(self) else: self.gui_id = self.config.gui_id + # FIXME some thoughts are required to decide how thhis should work with rpc registry def load_config(self, path: str | None = None, gui: bool = False): """ Load the configuration of the widget from YAML. @@ -262,8 +264,8 @@ class BECConnector: file_path = os.path.join(path, f"{self.__class__.__name__}_config.yaml") save_yaml(file_path, self._config_dict) - @pyqtSlot(str) - def set_gui_id(self, gui_id: str) -> None: + # @pyqtSlot(str) + def _set_gui_id(self, gui_id: str) -> None: """ Set the GUI ID for the widget. @@ -309,6 +311,15 @@ class BECConnector: if gui_id and config.gui_id != gui_id: # Recreating config should not overwrite the gui_id self.config.gui_id = gui_id + def remove(self): + """Cleanup the BECConnector""" + if hasattr(self, "close"): + self.close() + if hasattr(self, "deleteLater"): + self.deleteLater() + else: + self.rpc_register.remove_rpc(self) + def get_config(self, dict_output: bool = True) -> dict | BaseModel: """ Get the configuration of the widget. diff --git a/bec_widgets/utils/bec_widget.py b/bec_widgets/utils/bec_widget.py index 71cef843..00d6deb5 100644 --- a/bec_widgets/utils/bec_widget.py +++ b/bec_widgets/utils/bec_widget.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import darkdetect from bec_lib.logger import bec_logger from qtpy.QtCore import Slot @@ -9,6 +11,9 @@ from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig from bec_widgets.utils.colors import set_theme from bec_widgets.utils.container_utils import WidgetContainerUtils +if TYPE_CHECKING: + from bec_widgets.widgets.containers.dock import BECDock + logger = bec_logger.logger @@ -18,7 +23,9 @@ class BECWidget(BECConnector): # The icon name is the name of the icon in the icon theme, typically a name taken # from fonts.google.com/icons. Override this in subclasses to set the icon name. ICON_NAME = "widgets" + USER_ACCESS = ["remove"] + # pylint: disable=too-many-arguments def __init__( self, client=None, @@ -26,6 +33,7 @@ class BECWidget(BECConnector): gui_id: str | None = None, theme_update: bool = False, name: str | None = None, + parent_dock: BECDock | None = None, **kwargs, ): """ @@ -55,6 +63,7 @@ class BECWidget(BECConnector): if not WidgetContainerUtils.has_name_valid_chars(name): raise ValueError(f"Name {name} contains invalid characters.") super().__init__(client=client, config=config, gui_id=gui_id, name=name) + self._parent_dock = parent_dock app = QApplication.instance() if not hasattr(app, "theme"): # DO NOT SET THE THEME TO AUTO! Otherwise, the qwebengineview will segfault @@ -95,10 +104,13 @@ class BECWidget(BECConnector): def cleanup(self): """Cleanup the widget.""" + # needed here instead of closeEvent, to be checked why + # However, all widgets need to call super().cleanup() in their cleanup method + self.rpc_register.remove_rpc(self) def closeEvent(self, event): - self.rpc_register.remove_rpc(self) + """Wrap the close even to ensure the rpc_register is cleaned up.""" try: self.cleanup() finally: - super().closeEvent(event) + super().closeEvent(event) # pylint: disable=no-member diff --git a/bec_widgets/widgets/containers/dock/dock.py b/bec_widgets/widgets/containers/dock/dock.py index bb547c2e..0d35337e 100644 --- a/bec_widgets/widgets/containers/dock/dock.py +++ b/bec_widgets/widgets/containers/dock/dock.py @@ -121,6 +121,7 @@ class BECDock(BECWidget, Dock): "available_widgets", "delete", "delete_all", + "remove", "attach", "detach", ] @@ -136,9 +137,11 @@ class BECDock(BECWidget, Dock): closable: bool = True, **kwargs, ) -> None: + if config is None: config = DockConfig( - widget_class=self.__class__.__name__, parent_dock_area=parent_dock_area.gui_id + widget_class=self.__class__.__name__, + parent_dock_area=parent_dock_area.gui_id if parent_dock_area else None, ) else: if isinstance(config, dict): @@ -148,7 +151,7 @@ class BECDock(BECWidget, Dock): client=client, config=config, gui_id=gui_id, name=name ) # Name was checked and created in BEC Widget label = CustomDockLabel(text=name, closable=closable) - Dock.__init__(self, name=name, label=label, **kwargs) + Dock.__init__(self, name=name, label=label, parent=self, **kwargs) # Dock.__init__(self, name=name, **kwargs) self.parent_dock_area = parent_dock_area @@ -282,7 +285,8 @@ class BECDock(BECWidget, Dock): Add a widget to the dock. Args: - widget(QWidget): The widget to add. + widget(QWidget): The widget to add. It can not be BECDock or BECDockArea. + name(str): The name of the widget. row(int): The row to add the widget to. If None, the widget will be added to the next available row. col(int): The column to add the widget to. rowspan(int): The number of rows the widget should span. @@ -311,8 +315,16 @@ class BECDock(BECWidget, Dock): name = WidgetContainerUtils.generate_unique_name( name=widget_class_name, list_of_names=existing_widgets_parent_dock ) + # Check that Widget is not BECDock or BECDockArea + widget_class_name = widget if isinstance(widget, str) else widget.__class__.__name__ + if widget_class_name in ["BECDock", "BECDockArea"]: + raise ValueError(f"Widget {widget} can not be added to dock.") + if isinstance(widget, str): - widget = cast(BECWidget, widget_handler.create_widget(widget_type=widget, name=name)) + widget = cast( + BECWidget, + widget_handler.create_widget(widget_type=widget, name=name, parent_dock=self), + ) else: widget._name = name # pylint: disable=protected-access @@ -354,7 +366,7 @@ class BECDock(BECWidget, Dock): """ Remove the dock from the parent dock area. """ - self.parent_dock_area.delete(self.gui_id) + self.parent_dock_area.delete(self._name) def delete(self, widget_name: str) -> None: """ @@ -364,20 +376,21 @@ class BECDock(BECWidget, Dock): 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: + widgets = [widget for widget in self.widgets if widget._name == widget_name] + if len(widgets) == 0: logger.warning( f"Widget with name {widget_name} not found in dock {self.name()}. " f"Checking if gui_id was passed as widget_name." ) - # Try to find the widget in the RPC register + # Try to find the widget in the RPC register, maybe the gui_id was passed as widget_name widget = self.rpc_register.get_rpc_by_id(widget_name) if widget is None: logger.warning( f"Widget not found for name or gui_id: {widget_name} in dock {self.name()}" ) return - widget = widget[0] + else: + widget = widgets[0] self.layout.removeWidget(widget) self.config.widgets.pop(widget._name, None) if widget in self.widgets: @@ -396,12 +409,25 @@ class BECDock(BECWidget, Dock): """ Clean up the dock, including all its widgets. """ + # Remove the dock from the parent dock area + if self.parent_dock_area: + self.parent_dock_area.dock_area.docks.pop(self.name(), None) + self.parent_dock_area.config.docks.pop(self.name(), None) self.delete_all() self.widgets.clear() self.label.close() self.label.deleteLater() super().cleanup() + # def closeEvent(self, event): # pylint: disable=uselsess-parent-delegation + # """Close Event for dock and cleanup. + + # This wrapper ensures that the BECWidget close event is triggered. + # If removed, the closeEvent from pyqtgraph will be triggered, which + # is not calling super().closeEvent(event) and will not trigger the BECWidget close event. + # """ + # return super().closeEvent(event) + def close(self): """ Close the dock area and cleanup. @@ -409,13 +435,15 @@ class BECDock(BECWidget, Dock): """ self.cleanup() super().close() - self.parent_dock_area.dock_area.docks.pop(self.name(), None) if __name__ == "__main__": + import sys + from qtpy.QtWidgets import QApplication app = QApplication([]) dock = BECDock(name="dock") dock.show() app.exec_() + sys.exit(app.exec_()) diff --git a/bec_widgets/widgets/containers/dock/dock_area.py b/bec_widgets/widgets/containers/dock/dock_area.py index 69b87b30..962a4091 100644 --- a/bec_widgets/widgets/containers/dock/dock_area.py +++ b/bec_widgets/widgets/containers/dock/dock_area.py @@ -55,6 +55,7 @@ class BECDockArea(BECWidget, QWidget): "panel_list", "delete", "delete_all", + "remove", "detach_dock", "attach_all", "selected_device", @@ -79,6 +80,7 @@ class BECDockArea(BECWidget, QWidget): self.config = config super().__init__(client=client, config=config, gui_id=gui_id, name=name, **kwargs) QWidget.__init__(self, parent=parent) + self._parent = parent self.layout = QVBoxLayout(self) self.layout.setSpacing(5) self.layout.setContentsMargins(0, 0, 0, 0) @@ -346,9 +348,7 @@ class BECDockArea(BECWidget, QWidget): f"with name: {self._name} and id {self.gui_id}." ) else: # Name is not provided - name = WidgetContainerUtils.generate_unique_name( - name=BECDock.__name__, list_of_names=dock_names - ) + name = WidgetContainerUtils.generate_unique_name(name="dock", list_of_names=dock_names) dock = BECDock(name=name, parent_dock_area=self, closable=closable) dock.config.position = position @@ -430,25 +430,6 @@ class BECDockArea(BECWidget, QWidget): self.dock_area.deleteLater() super().cleanup() - def closeEvent(self, event): - if self.parent() is None: - # we are at top-level (independent window) - if self.isVisible(): - # we are visible => user clicked on [X] - # (when closeEvent is called from shutdown procedure, - # everything is hidden first) - # so, let's ignore "close", and do hide instead - event.ignore() - self.setVisible(False) - - def close(self): - """ - Close the dock area and cleanup. - Has to be implemented to overwrite pyqtgraph event accept in Container close. - """ - self.cleanup() - super().close() - def show(self): """Show all windows including floating docks.""" super().show() @@ -494,6 +475,10 @@ class BECDockArea(BECWidget, QWidget): raise ValueError(f"Dock with name {dock_name} does not exist.") self._broadcast_update() + def remove(self) -> None: + """Remove the dock area.""" + self.close() + if __name__ == "__main__": # pragma: no cover diff --git a/bec_widgets/widgets/containers/figure/figure.py b/bec_widgets/widgets/containers/figure/figure.py index c9262add..ae838ae2 100644 --- a/bec_widgets/widgets/containers/figure/figure.py +++ b/bec_widgets/widgets/containers/figure/figure.py @@ -78,13 +78,7 @@ class WidgetHandler: } def create_widget( - self, - widget_type: str, - widget_id: str, - parent_figure, - parent_id: str, - config: dict = None, - **axis_kwargs, + self, widget_type: str, parent_figure, parent_id: str, config: dict = None, **axis_kwargs ) -> BECPlotBase: """ Create and configure a widget based on its type. @@ -109,7 +103,6 @@ class WidgetHandler: widget_config_dict = { "widget_class": widget_class.__name__, "parent_id": parent_id, - "gui_id": widget_id, **(config if config is not None else {}), } widget_config = config_class(**widget_config_dict) @@ -568,13 +561,13 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget): widget = self.widget_handler.create_widget( widget_type=widget_type, - widget_id=widget_id, parent_figure=self, parent_id=self.gui_id, config=config, **axis_kwargs, ) - widget.set_gui_id(widget_id) + widget_id = widget.gui_id + widget.config.row = row widget.config.col = col diff --git a/bec_widgets/widgets/plots_next_gen/plot_base.py b/bec_widgets/widgets/plots_next_gen/plot_base.py index c515c094..6d618860 100644 --- a/bec_widgets/widgets/plots_next_gen/plot_base.py +++ b/bec_widgets/widgets/plots_next_gen/plot_base.py @@ -942,7 +942,7 @@ class PlotBase(BECWidget, QWidget): self.axis_settings_dialog.close() self.axis_settings_dialog = None self.cleanup_pyqtgraph() - self.rpc_register.remove_rpc(self) + super().cleanup() def cleanup_pyqtgraph(self): """Cleanup pyqtgraph items."""