diff --git a/bec_widgets/cli/rpc/rpc_widget_handler.py b/bec_widgets/cli/rpc/rpc_widget_handler.py index d1cdb401..cf15ba60 100644 --- a/bec_widgets/cli/rpc/rpc_widget_handler.py +++ b/bec_widgets/cli/rpc/rpc_widget_handler.py @@ -1,6 +1,9 @@ from __future__ import annotations -from bec_widgets.utils import BECConnector +from typing import Any + +from bec_widgets.cli.client_utils import IGNORE_WIDGETS +from bec_widgets.utils.bec_widget import BECWidget class RPCWidgetHandler: @@ -10,7 +13,7 @@ class RPCWidgetHandler: self._widget_classes = None @property - def widget_classes(self): + def widget_classes(self) -> dict[str, Any]: """ Get the available widget classes. @@ -19,7 +22,7 @@ class RPCWidgetHandler: """ if self._widget_classes is None: self.update_available_widgets() - return self._widget_classes + return self._widget_classes # type: ignore def update_available_widgets(self): """ @@ -31,24 +34,27 @@ 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 IGNORE_WIDGETS + } - def create_widget(self, widget_type, **kwargs) -> BECConnector: + def create_widget(self, widget_type, name: str | None = None, **kwargs) -> BECWidget: """ Create a widget from an RPC message. Args: widget_type(str): The type of the widget. + name (str): The name of the widget. **kwargs: The keyword arguments for the widget. Returns: - widget(BECConnector): The created widget. + widget(BECWidget): The created widget. """ if self._widget_classes is None: self.update_available_widgets() - widget_class = self._widget_classes.get(widget_type) + widget_class = self._widget_classes.get(widget_type) # type: ignore if widget_class: - return widget_class(**kwargs) + return widget_class(name=name, **kwargs) raise ValueError(f"Unknown widget type: {widget_type}") diff --git a/bec_widgets/examples/jupyter_console/jupyter_console_window.py b/bec_widgets/examples/jupyter_console/jupyter_console_window.py index f10f1d43..2b5dbc67 100644 --- a/bec_widgets/examples/jupyter_console/jupyter_console_window.py +++ b/bec_widgets/examples/jupyter_console/jupyter_console_window.py @@ -198,14 +198,18 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover: def _init_dock(self): - self.d0 = self.dock.add_dock(name="dock_0") - self.mm = self.d0.add_widget("BECMotorMapWidget") + self.d0 = self.dock.new(name="dock_0") + self.mm = self.d0.new("BECMotorMapWidget") self.mm.change_motors("samx", "samy") - self.d1 = self.dock.add_dock(name="dock_1", position="right") - self.im = self.d1.add_widget("BECImageWidget") + self.d1 = self.dock.new(name="dock_1", position="right") + self.im = self.d1.new("BECImageWidget") self.im.image("waveform", "1d") + self.d2 = self.dock.new(name="dock_2", position="bottom") + self.wf = self.d2.new("BECFigure", row=0, col=0) + + self.mw = self.wf.multi_waveform(monitor="waveform") # , config=config) self.mw = None # self.wf.multi_waveform(monitor="waveform") # , config=config) self.dock.save_state() diff --git a/bec_widgets/utils/bec_connector.py b/bec_widgets/utils/bec_connector.py index db450d37..e15cbd6c 100644 --- a/bec_widgets/utils/bec_connector.py +++ b/bec_widgets/utils/bec_connector.py @@ -4,6 +4,7 @@ from __future__ import annotations import os import time import uuid +from datetime import datetime from typing import TYPE_CHECKING, Optional from bec_lib.logger import bec_logger @@ -15,6 +16,7 @@ from qtpy.QtWidgets import QApplication from bec_widgets.cli.rpc.rpc_register import RPCRegister from bec_widgets.qt_utils.error_popups import ErrorPopupUtility from bec_widgets.qt_utils.error_popups import SafeSlot as pyqtSlot +from bec_widgets.utils.container_utils import WidgetContainerUtils from bec_widgets.utils.yaml_dialog import load_yaml, load_yaml_gui, save_yaml, save_yaml_gui if TYPE_CHECKING: @@ -39,8 +41,7 @@ class ConnectionConfig(BaseModel): """Generate a GUI ID if none is provided.""" if v is None: widget_class = values.data["widget_class"] - v = f"{widget_class}_{str(time.time())}" - return v + v = f"{widget_class}_{datetime.now().strftime('%Y_%m_%d_%H_%M_%S_%f')}" return v @@ -75,7 +76,13 @@ class BECConnector: USER_ACCESS = ["_config_dict", "_get_all_rpc", "_rpc_id"] EXIT_HANDLERS = {} - def __init__(self, client=None, config: ConnectionConfig = None, gui_id: str = None): + def __init__( + self, + client=None, + config: ConnectionConfig | None = None, + gui_id: str | None = None, + name: str | None = None, + ): # BEC related connections self.bec_dispatcher = BECDispatcher(client=client) self.client = self.bec_dispatcher.client if client is None else client @@ -103,15 +110,22 @@ class BECConnector: ) self.config = ConnectionConfig(widget_class=self.__class__.__name__) + # I feel that we should not allow BECConnector to be created with a custom gui_id + # because this would break with the logic in the RPCRegister of retrieving widgets by type + # iterating over all widgets and checkinf if the register widget starts with the string that is passsed. + # If the gui_id is randomly generated, this would break since that widget would have a + # gui_id that is generated in a different way. if gui_id: self.config.gui_id = gui_id - self.gui_id = gui_id + self.gui_id: str = gui_id else: - self.gui_id = self.config.gui_id - - # register widget to rpc register - # be careful: when registering, and the object is not a BECWidget, - # cleanup has to be called manually since there is no 'closeEvent' + self.gui_id: str = self.config.gui_id # type: ignore + if name is None: + name = self.__class__.__name__ + else: + if not WidgetContainerUtils.has_name_valid_chars(name): + raise ValueError(f"Name {name} contains invalid characters.") + self._name = name if name else self.__class__.__name__ self.rpc_register = RPCRegister() self.rpc_register.add_rpc(self) @@ -195,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. @@ -207,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. @@ -248,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. @@ -288,9 +304,21 @@ class BECConnector: Args: config (ConnectionConfig | dict): Configuration settings. """ + gui_id = getattr(config, "gui_id", None) if isinstance(config, dict): config = ConnectionConfig(**config) self.config = config + 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: """ diff --git a/bec_widgets/utils/bec_widget.py b/bec_widgets/utils/bec_widget.py index 7dbd8805..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 @@ -7,6 +9,10 @@ from qtpy.QtWidgets import QApplication, QWidget 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 @@ -17,13 +23,17 @@ 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, config: ConnectionConfig = None, - gui_id: str = None, + gui_id: str | None = None, theme_update: bool = False, + name: str | None = None, + parent_dock: BECDock | None = None, **kwargs, ): """ @@ -45,9 +55,15 @@ class BECWidget(BECConnector): """ if not isinstance(self, QWidget): raise RuntimeError(f"{repr(self)} is not a subclass of QWidget") - super().__init__(client=client, config=config, gui_id=gui_id, **kwargs) - - # Set the theme to auto if it is not set yet + # Create a default name if None is provided + if name is None: + name = "bec_widget_init_without_name" + # name = self.__class__.__name__ + # Check for invalid chars in the name + 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 @@ -88,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/utils/container_utils.py b/bec_widgets/utils/container_utils.py index 9b4875d5..f2c2f950 100644 --- a/bec_widgets/utils/container_utils.py +++ b/bec_widgets/utils/container_utils.py @@ -1,30 +1,55 @@ from __future__ import annotations import itertools -from typing import Type +from typing import Literal, Type from qtpy.QtWidgets import QWidget +from bec_widgets.cli.rpc.rpc_register import RPCRegister + class WidgetContainerUtils: + # We need one handler that checks if a WIDGET of a given name is already created for that DOCKAREA + # 1. If the name exists, then it depends whether the name was auto-generated -> add _1 to the name + # or alternatively raise an error that it can't be added again ( just raise an error) + # 2. Dock names in between docks should also be unique + @staticmethod - def generate_unique_widget_id(container: dict, prefix: str = "widget") -> str: - """ - Generate a unique widget ID. + def has_name_valid_chars(name: str) -> bool: + """Check if the name is valid. Args: - container(dict): The container of widgets. - prefix(str): The prefix of the widget ID. + name(str): The name to be checked. Returns: - widget_id(str): The unique widget ID. + bool: True if the name is valid, False otherwise. """ - existing_ids = set(container.keys()) - for i in itertools.count(1): - widget_id = f"{prefix}_{i}" - if widget_id not in existing_ids: - return widget_id + if not name or len(name) > 256: + return False # Don't accept empty names or names longer than 256 characters + check_value = name.replace("_", "").replace("-", "") + if not check_value.isalnum() or not check_value.isascii(): + return False + return True + + @staticmethod + def generate_unique_name(name: str, list_of_names: list[str] | None = None) -> str: + """Generate a unique ID. + + Args: + name(str): The name of the widget. + Returns: + tuple (str): The unique name + """ + if list_of_names is None: + list_of_names = [] + ii = 0 + while ii < 1000: # 1000 is arbritrary! + name_candidate = f"{name}_{ii}" + if name_candidate not in list_of_names: + return name_candidate + ii += 1 + raise ValueError("Could not generate a unique name after within 1000 attempts.") @staticmethod def find_first_widget_by_class( diff --git a/bec_widgets/widgets/containers/dock/dock_area.py b/bec_widgets/widgets/containers/dock/dock_area.py index e0616d7a..1a06fc09 100644 --- a/bec_widgets/widgets/containers/dock/dock_area.py +++ b/bec_widgets/widgets/containers/dock/dock_area.py @@ -4,12 +4,14 @@ from typing import Literal, Optional 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 from qtpy.QtGui import QPainter, QPaintEvent from qtpy.QtWidgets import QApplication, QSizePolicy, QVBoxLayout, QWidget +from bec_widgets.cli.rpc.rpc_register import RPCRegister from bec_widgets.qt_utils.error_popups import SafeSlot from bec_widgets.qt_utils.toolbar import ( ExpandableMenuAction, @@ -33,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.") @@ -44,21 +48,19 @@ class DockAreaConfig(ConnectionConfig): class BECDockArea(BECWidget, QWidget): PLUGIN = True USER_ACCESS = [ - "_config_dict", - "selected_device", - "panels", - "save_state", - "remove_dock", - "restore_state", - "add_dock", - "clear_all", - "detach_dock", - "attach_all", - "_get_all_rpc", - "temp_areas", + "new", "show", "hide", + "panels", + "panel_list", "delete", + "delete_all", + "remove", + "detach_dock", + "attach_all", + "selected_device", + "save_state", + "restore_state", ] def __init__( @@ -67,6 +69,8 @@ class BECDockArea(BECWidget, QWidget): config: DockAreaConfig | None = None, client=None, gui_id: str = None, + name: str | None = None, + **kwargs, ) -> None: if config is None: config = DockAreaConfig(widget_class=self.__class__.__name__) @@ -74,8 +78,9 @@ class BECDockArea(BECWidget, QWidget): if isinstance(config, dict): config = DockAreaConfig(**config) self.config = config - super().__init__(client=client, config=config, gui_id=gui_id) + 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) @@ -169,41 +174,41 @@ class BECDockArea(BECWidget, QWidget): def _hook_toolbar(self): # Menu Plot self.toolbar.widgets["menu_plots"].widgets["waveform"].triggered.connect( - lambda: self.add_dock(widget="Waveform", prefix="waveform") + lambda: self._create_widget_from_toolbar(widget_name="Waveform") ) self.toolbar.widgets["menu_plots"].widgets["multi_waveform"].triggered.connect( - lambda: self.add_dock(widget="BECMultiWaveformWidget", prefix="multi_waveform") + lambda: self._create_widget_from_toolbar(widget_name="BECMultiWaveformWidget") ) self.toolbar.widgets["menu_plots"].widgets["image"].triggered.connect( - lambda: self.add_dock(widget="BECImageWidget", prefix="image") + lambda: self._create_widget_from_toolbar(widget_name="BECImageWidget") ) self.toolbar.widgets["menu_plots"].widgets["motor_map"].triggered.connect( - lambda: self.add_dock(widget="BECMotorMapWidget", prefix="motor_map") + lambda: self._create_widget_from_toolbar(widget_name="BECMotorMapWidget") ) # Menu Devices self.toolbar.widgets["menu_devices"].widgets["scan_control"].triggered.connect( - lambda: self.add_dock(widget="ScanControl", prefix="scan_control") + lambda: self._create_widget_from_toolbar(widget_name="ScanControl") ) self.toolbar.widgets["menu_devices"].widgets["positioner_box"].triggered.connect( - lambda: self.add_dock(widget="PositionerBox", prefix="positioner_box") + lambda: self._create_widget_from_toolbar(widget_name="PositionerBox") ) # Menu Utils self.toolbar.widgets["menu_utils"].widgets["queue"].triggered.connect( - lambda: self.add_dock(widget="BECQueue", prefix="queue") + lambda: self._create_widget_from_toolbar(widget_name="BECQueue") ) self.toolbar.widgets["menu_utils"].widgets["status"].triggered.connect( - lambda: self.add_dock(widget="BECStatusBox", prefix="status") + lambda: self._create_widget_from_toolbar(widget_name="BECStatusBox") ) self.toolbar.widgets["menu_utils"].widgets["vs_code"].triggered.connect( - lambda: self.add_dock(widget="VSCodeEditor", prefix="vs_code") + lambda: self._create_widget_from_toolbar(widget_name="VSCodeEditor") ) self.toolbar.widgets["menu_utils"].widgets["progress_bar"].triggered.connect( - lambda: self.add_dock(widget="RingProgressBar", prefix="progress_bar") + lambda: self._create_widget_from_toolbar(widget_name="RingProgressBar") ) self.toolbar.widgets["menu_utils"].widgets["log_panel"].triggered.connect( - lambda: self.add_dock(widget="LogPanel", prefix="log_panel") + lambda: self._create_widget_from_toolbar(widget_name="LogPanel") ) # Icons @@ -211,6 +216,11 @@ class BECDockArea(BECWidget, QWidget): self.toolbar.widgets["save_state"].action.triggered.connect(self.save_state) self.toolbar.widgets["restore_state"].action.triggered.connect(self.restore_state) + @SafeSlot() + def _create_widget_from_toolbar(self, widget_name: str) -> None: + dock_name = WidgetContainerUtils.generate_unique_name(widget_name, self.panels.keys()) + self.new(name=dock_name, widget=widget_name) + def paintEvent(self, event: QPaintEvent): # TODO decide if we want any default instructions super().paintEvent(event) if self._instructions_visible: @@ -218,7 +228,7 @@ class BECDockArea(BECWidget, QWidget): painter.drawText( self.rect(), Qt.AlignCenter, - "Add docks using 'add_dock' method from CLI\n or \n Add widget docks using the toolbar", + "Add docks using 'new' method from CLI\n or \n Add widget docks using the toolbar", ) @property @@ -243,7 +253,17 @@ class BECDockArea(BECWidget, QWidget): @panels.setter def panels(self, value: dict[str, BECDock]): - self.dock_area.docks = WeakValueDictionary(value) + self.dock_area.docks = WeakValueDictionary(value) # This can not work can it? + + @property + def panel_list(self) -> list[BECDock]: + """ + Get the docks in the dock area. + + Returns: + list: The docks in the dock area. + """ + return list(self.dock_area.docks.values()) @property def temp_areas(self) -> list: @@ -287,36 +307,17 @@ class BECDockArea(BECWidget, QWidget): self.config.docks_state = last_state return last_state - def remove_dock(self, name: str): - """ - Remove a dock by name and ensure it is properly closed and cleaned up. - - Args: - name(str): The name of the dock to remove. - """ - dock = self.dock_area.docks.pop(name, None) - self.config.docks.pop(name, None) - if dock: - dock.close() - dock.deleteLater() - if len(self.dock_area.docks) <= 1: - for dock in self.dock_area.docks.values(): - dock.hide_title_bar() - - else: - raise ValueError(f"Dock with name {name} does not exist.") - @SafeSlot(popup_error=True) - def add_dock( + def new( self, - name: str = None, - position: Literal["bottom", "top", "left", "right", "above", "below"] = None, + name: str | None = None, + widget: str | QWidget | None = None, + widget_name: str | None = None, + position: Literal["bottom", "top", "left", "right", "above", "below"] = "bottom", relative_to: BECDock | None = None, closable: bool = True, floating: bool = False, - prefix: str = "dock", - widget: str | QWidget | None = None, - row: int = None, + row: int | None = None, col: int = 0, rowspan: int = 1, colspan: int = 1, @@ -326,12 +327,11 @@ class BECDockArea(BECWidget, QWidget): Args: name(str): The name of the dock to be displayed and for further references. Has to be unique. + widget(str|QWidget|None): The widget to be added to the dock. While using RPC, only BEC RPC widgets from RPCWidgetHandler are allowed. position(Literal["bottom", "top", "left", "right", "above", "below"]): The position of the dock. relative_to(BECDock): The dock to which the new dock should be added relative to. closable(bool): Whether the dock is closable. floating(bool): Whether the dock is detached after creating. - prefix(str): The prefix for the dock name if no name is provided. - widget(str|QWidget|None): The widget to be added to the dock. While using RPC, only BEC RPC widgets from RPCWidgetHandler are allowed. row(int): The row of the added widget. col(int): The column of the added widget. rowspan(int): The rowspan of the added widget. @@ -340,21 +340,20 @@ class BECDockArea(BECWidget, QWidget): Returns: BECDock: The created dock. """ - if name is None: - name = WidgetContainerUtils.generate_unique_widget_id( - container=self.dock_area.docks, prefix=prefix - ) - - if name in set(self.dock_area.docks.keys()): - raise ValueError(f"Dock with name {name} already exists.") - - if position is None: - position = "bottom" + dock_names = [dock._name for dock in self.panel_list] # pylint: disable=protected-access + if name is not None: # Name is provided + if name in dock_names: + raise ValueError( + f"Name {name} must be unique for docks, but already exists in DockArea " + f"with name: {self._name} and id {self.gui_id}." + ) + else: # Name is not provided + 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 - self.config.docks[name] = dock.config - + self.config.docks[dock.name()] = dock.config + # The dock.name is equal to the name passed to BECDock self.dock_area.addDock(dock=dock, position=position, relativeTo=relative_to) if len(self.dock_area.docks) <= 1: @@ -363,10 +362,11 @@ class BECDockArea(BECWidget, QWidget): for dock in self.dock_area.docks.values(): dock.show_title_bar() - if widget is not None and isinstance(widget, str): - dock.add_widget(widget=widget, row=row, col=col, rowspan=rowspan, colspan=colspan) - elif widget is not None and isinstance(widget, QWidget): - dock.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan) + if widget is not None: + # Check if widget name exists. + dock.new( + widget=widget, name=widget_name, row=row, col=col, rowspan=rowspan, colspan=colspan + ) if ( self._instructions_visible ): # TODO still decide how initial instructions should be handled @@ -404,49 +404,26 @@ class BECDockArea(BECWidget, QWidget): Remove a temporary area from the dock area. This is a patched method of pyqtgraph's removeTempArea """ + if area not in self.dock_area.tempAreas: + # FIXME add some context for the logging, I am not sure which object is passed. + # It looks like a pyqtgraph.DockArea + logger.info(f"Attempted to remove dock_area, but was not floating.") + return self.dock_area.tempAreas.remove(area) area.window().close() area.window().deleteLater() - def clear_all(self): - """ - Close all docks and remove all temp areas. - """ - self.attach_all() - for dock in dict(self.dock_area.docks).values(): - dock.remove() - self.dock_area.docks.clear() - def cleanup(self): """ Cleanup the dock area. """ - self.clear_all() + self.delete_all() self.toolbar.close() self.toolbar.deleteLater() self.dock_area.close() 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() @@ -465,17 +442,52 @@ class BECDockArea(BECWidget, QWidget): continue docks.window().hide() - def delete(self): - self.hide() - self.deleteLater() + def delete_all(self) -> None: + """ + Delete all docks. + """ + self.attach_all() + for dock_name in self.panels.keys(): + self.delete(dock_name) + + def delete(self, dock_name: str): + """ + Delete a dock by name. + + Args: + dock_name(str): The name of the dock to delete. + """ + dock = self.dock_area.docks.pop(dock_name, None) + self.config.docks.pop(dock_name, None) + if dock: + dock.close() + dock.deleteLater() + if len(self.dock_area.docks) <= 1: + for dock in self.dock_area.docks.values(): + dock.hide_title_bar() + else: + 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 + import sys + from bec_widgets.utils.colors import set_theme app = QApplication([]) set_theme("auto") dock_area = BECDockArea() + dock_1 = dock_area.new(name="dock_0", widget="Waveform") + # dock_1 = dock_area.new(name="dock_0", widget="Waveform") + dock_area.new(widget="Waveform") dock_area.show() + dock_area.setGeometry(100, 100, 800, 600) + app.topLevelWidgets() app.exec_() + sys.exit(app.exec_())