0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 03:31:50 +02:00

wip - namespace

This commit is contained in:
2025-03-13 10:17:43 +01:00
parent 5f0dd62f25
commit bf060b3aba
6 changed files with 237 additions and 143 deletions

View File

@ -1,6 +1,9 @@
from __future__ import annotations 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: class RPCWidgetHandler:
@ -10,7 +13,7 @@ class RPCWidgetHandler:
self._widget_classes = None self._widget_classes = None
@property @property
def widget_classes(self): def widget_classes(self) -> dict[str, Any]:
""" """
Get the available widget classes. Get the available widget classes.
@ -19,7 +22,7 @@ class RPCWidgetHandler:
""" """
if self._widget_classes is None: if self._widget_classes is None:
self.update_available_widgets() self.update_available_widgets()
return self._widget_classes return self._widget_classes # type: ignore
def update_available_widgets(self): def update_available_widgets(self):
""" """
@ -31,24 +34,27 @@ class RPCWidgetHandler:
from bec_widgets.utils.plugin_utils import get_custom_classes from bec_widgets.utils.plugin_utils import get_custom_classes
clss = get_custom_classes("bec_widgets") 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. Create a widget from an RPC message.
Args: Args:
widget_type(str): The type of the widget. widget_type(str): The type of the widget.
name (str): The name of the widget.
**kwargs: The keyword arguments for the widget. **kwargs: The keyword arguments for the widget.
Returns: Returns:
widget(BECConnector): The created widget. widget(BECWidget): The created widget.
""" """
if self._widget_classes is None: if self._widget_classes is None:
self.update_available_widgets() 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: if widget_class:
return widget_class(**kwargs) return widget_class(name=name, **kwargs)
raise ValueError(f"Unknown widget type: {widget_type}") raise ValueError(f"Unknown widget type: {widget_type}")

View File

@ -198,14 +198,18 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
def _init_dock(self): def _init_dock(self):
self.d0 = self.dock.add_dock(name="dock_0") self.d0 = self.dock.new(name="dock_0")
self.mm = self.d0.add_widget("BECMotorMapWidget") self.mm = self.d0.new("BECMotorMapWidget")
self.mm.change_motors("samx", "samy") self.mm.change_motors("samx", "samy")
self.d1 = self.dock.add_dock(name="dock_1", position="right") self.d1 = self.dock.new(name="dock_1", position="right")
self.im = self.d1.add_widget("BECImageWidget") self.im = self.d1.new("BECImageWidget")
self.im.image("waveform", "1d") 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.mw = None # self.wf.multi_waveform(monitor="waveform") # , config=config)
self.dock.save_state() self.dock.save_state()

View File

@ -4,6 +4,7 @@ from __future__ import annotations
import os import os
import time import time
import uuid import uuid
from datetime import datetime
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from bec_lib.logger import bec_logger 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.cli.rpc.rpc_register import RPCRegister
from bec_widgets.qt_utils.error_popups import ErrorPopupUtility from bec_widgets.qt_utils.error_popups import ErrorPopupUtility
from bec_widgets.qt_utils.error_popups import SafeSlot as pyqtSlot 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 from bec_widgets.utils.yaml_dialog import load_yaml, load_yaml_gui, save_yaml, save_yaml_gui
if TYPE_CHECKING: if TYPE_CHECKING:
@ -39,8 +41,7 @@ class ConnectionConfig(BaseModel):
"""Generate a GUI ID if none is provided.""" """Generate a GUI ID if none is provided."""
if v is None: if v is None:
widget_class = values.data["widget_class"] widget_class = values.data["widget_class"]
v = f"{widget_class}_{str(time.time())}" v = f"{widget_class}_{datetime.now().strftime('%Y_%m_%d_%H_%M_%S_%f')}"
return v
return v return v
@ -75,7 +76,13 @@ class BECConnector:
USER_ACCESS = ["_config_dict", "_get_all_rpc", "_rpc_id"] USER_ACCESS = ["_config_dict", "_get_all_rpc", "_rpc_id"]
EXIT_HANDLERS = {} 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 # BEC related connections
self.bec_dispatcher = BECDispatcher(client=client) self.bec_dispatcher = BECDispatcher(client=client)
self.client = self.bec_dispatcher.client if client is None else 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__) 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: if gui_id:
self.config.gui_id = gui_id self.config.gui_id = gui_id
self.gui_id = gui_id self.gui_id: str = gui_id
else: else:
self.gui_id = self.config.gui_id self.gui_id: str = self.config.gui_id # type: ignore
if name is None:
# register widget to rpc register name = self.__class__.__name__
# be careful: when registering, and the object is not a BECWidget, else:
# cleanup has to be called manually since there is no 'closeEvent' 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 = RPCRegister()
self.rpc_register.add_rpc(self) self.rpc_register.add_rpc(self)
@ -195,6 +209,7 @@ class BECConnector:
""" """
self.config = config 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: def apply_config(self, config: dict, generate_new_id: bool = True) -> None:
""" """
Apply the configuration to the widget. Apply the configuration to the widget.
@ -207,11 +222,12 @@ class BECConnector:
if generate_new_id is True: if generate_new_id is True:
gui_id = str(uuid.uuid4()) gui_id = str(uuid.uuid4())
self.rpc_register.remove_rpc(self) self.rpc_register.remove_rpc(self)
self.set_gui_id(gui_id) self._set_gui_id(gui_id)
self.rpc_register.add_rpc(self) self.rpc_register.add_rpc(self)
else: else:
self.gui_id = self.config.gui_id 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): def load_config(self, path: str | None = None, gui: bool = False):
""" """
Load the configuration of the widget from YAML. 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") file_path = os.path.join(path, f"{self.__class__.__name__}_config.yaml")
save_yaml(file_path, self._config_dict) save_yaml(file_path, self._config_dict)
@pyqtSlot(str) # @pyqtSlot(str)
def set_gui_id(self, gui_id: str) -> None: def _set_gui_id(self, gui_id: str) -> None:
""" """
Set the GUI ID for the widget. Set the GUI ID for the widget.
@ -288,9 +304,21 @@ class BECConnector:
Args: Args:
config (ConnectionConfig | dict): Configuration settings. config (ConnectionConfig | dict): Configuration settings.
""" """
gui_id = getattr(config, "gui_id", None)
if isinstance(config, dict): if isinstance(config, dict):
config = ConnectionConfig(**config) config = ConnectionConfig(**config)
self.config = 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: def get_config(self, dict_output: bool = True) -> dict | BaseModel:
""" """

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING
import darkdetect import darkdetect
from bec_lib.logger import bec_logger from bec_lib.logger import bec_logger
from qtpy.QtCore import Slot 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.bec_connector import BECConnector, ConnectionConfig
from bec_widgets.utils.colors import set_theme 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 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 # 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. # from fonts.google.com/icons. Override this in subclasses to set the icon name.
ICON_NAME = "widgets" ICON_NAME = "widgets"
USER_ACCESS = ["remove"]
# pylint: disable=too-many-arguments
def __init__( def __init__(
self, self,
client=None, client=None,
config: ConnectionConfig = None, config: ConnectionConfig = None,
gui_id: str = None, gui_id: str | None = None,
theme_update: bool = False, theme_update: bool = False,
name: str | None = None,
parent_dock: BECDock | None = None,
**kwargs, **kwargs,
): ):
""" """
@ -45,9 +55,15 @@ class BECWidget(BECConnector):
""" """
if not isinstance(self, QWidget): if not isinstance(self, QWidget):
raise RuntimeError(f"{repr(self)} is not a subclass of QWidget") raise RuntimeError(f"{repr(self)} is not a subclass of QWidget")
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs) # Create a default name if None is provided
if name is None:
# Set the theme to auto if it is not set yet 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() app = QApplication.instance()
if not hasattr(app, "theme"): if not hasattr(app, "theme"):
# DO NOT SET THE THEME TO AUTO! Otherwise, the qwebengineview will segfault # DO NOT SET THE THEME TO AUTO! Otherwise, the qwebengineview will segfault
@ -88,10 +104,13 @@ class BECWidget(BECConnector):
def cleanup(self): def cleanup(self):
"""Cleanup the widget.""" """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): def closeEvent(self, event):
self.rpc_register.remove_rpc(self) """Wrap the close even to ensure the rpc_register is cleaned up."""
try: try:
self.cleanup() self.cleanup()
finally: finally:
super().closeEvent(event) super().closeEvent(event) # pylint: disable=no-member

View File

@ -1,30 +1,55 @@
from __future__ import annotations from __future__ import annotations
import itertools import itertools
from typing import Type from typing import Literal, Type
from qtpy.QtWidgets import QWidget from qtpy.QtWidgets import QWidget
from bec_widgets.cli.rpc.rpc_register import RPCRegister
class WidgetContainerUtils: 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 @staticmethod
def generate_unique_widget_id(container: dict, prefix: str = "widget") -> str: def has_name_valid_chars(name: str) -> bool:
""" """Check if the name is valid.
Generate a unique widget ID.
Args: Args:
container(dict): The container of widgets. name(str): The name to be checked.
prefix(str): The prefix of the widget ID.
Returns: Returns:
widget_id(str): The unique widget ID. bool: True if the name is valid, False otherwise.
""" """
existing_ids = set(container.keys()) if not name or len(name) > 256:
for i in itertools.count(1): return False # Don't accept empty names or names longer than 256 characters
widget_id = f"{prefix}_{i}" check_value = name.replace("_", "").replace("-", "")
if widget_id not in existing_ids: if not check_value.isalnum() or not check_value.isascii():
return widget_id 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 @staticmethod
def find_first_widget_by_class( def find_first_widget_by_class(

View File

@ -4,12 +4,14 @@ from typing import Literal, Optional
from weakref import WeakValueDictionary from weakref import WeakValueDictionary
from bec_lib.endpoints import MessageEndpoints from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import bec_logger
from pydantic import Field from pydantic import Field
from pyqtgraph.dockarea.DockArea import DockArea from pyqtgraph.dockarea.DockArea import DockArea
from qtpy.QtCore import QSize, Qt from qtpy.QtCore import QSize, Qt
from qtpy.QtGui import QPainter, QPaintEvent from qtpy.QtGui import QPainter, QPaintEvent
from qtpy.QtWidgets import QApplication, QSizePolicy, QVBoxLayout, QWidget 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.error_popups import SafeSlot
from bec_widgets.qt_utils.toolbar import ( from bec_widgets.qt_utils.toolbar import (
ExpandableMenuAction, 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.logpanel.logpanel import LogPanel
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
logger = bec_logger.logger
class DockAreaConfig(ConnectionConfig): class DockAreaConfig(ConnectionConfig):
docks: dict[str, DockConfig] = Field({}, description="The docks in the dock area.") docks: dict[str, DockConfig] = Field({}, description="The docks in the dock area.")
@ -44,21 +48,19 @@ class DockAreaConfig(ConnectionConfig):
class BECDockArea(BECWidget, QWidget): class BECDockArea(BECWidget, QWidget):
PLUGIN = True PLUGIN = True
USER_ACCESS = [ USER_ACCESS = [
"_config_dict", "new",
"selected_device",
"panels",
"save_state",
"remove_dock",
"restore_state",
"add_dock",
"clear_all",
"detach_dock",
"attach_all",
"_get_all_rpc",
"temp_areas",
"show", "show",
"hide", "hide",
"panels",
"panel_list",
"delete", "delete",
"delete_all",
"remove",
"detach_dock",
"attach_all",
"selected_device",
"save_state",
"restore_state",
] ]
def __init__( def __init__(
@ -67,6 +69,8 @@ class BECDockArea(BECWidget, QWidget):
config: DockAreaConfig | None = None, config: DockAreaConfig | None = None,
client=None, client=None,
gui_id: str = None, gui_id: str = None,
name: str | None = None,
**kwargs,
) -> None: ) -> None:
if config is None: if config is None:
config = DockAreaConfig(widget_class=self.__class__.__name__) config = DockAreaConfig(widget_class=self.__class__.__name__)
@ -74,8 +78,9 @@ class BECDockArea(BECWidget, QWidget):
if isinstance(config, dict): if isinstance(config, dict):
config = DockAreaConfig(**config) config = DockAreaConfig(**config)
self.config = 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) QWidget.__init__(self, parent=parent)
self._parent = parent
self.layout = QVBoxLayout(self) self.layout = QVBoxLayout(self)
self.layout.setSpacing(5) self.layout.setSpacing(5)
self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setContentsMargins(0, 0, 0, 0)
@ -169,41 +174,41 @@ class BECDockArea(BECWidget, QWidget):
def _hook_toolbar(self): def _hook_toolbar(self):
# Menu Plot # Menu Plot
self.toolbar.widgets["menu_plots"].widgets["waveform"].triggered.connect( 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( 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( 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( 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 # Menu Devices
self.toolbar.widgets["menu_devices"].widgets["scan_control"].triggered.connect( 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( 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 # Menu Utils
self.toolbar.widgets["menu_utils"].widgets["queue"].triggered.connect( 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( 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( 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( 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( 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 # Icons
@ -211,6 +216,11 @@ class BECDockArea(BECWidget, QWidget):
self.toolbar.widgets["save_state"].action.triggered.connect(self.save_state) self.toolbar.widgets["save_state"].action.triggered.connect(self.save_state)
self.toolbar.widgets["restore_state"].action.triggered.connect(self.restore_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 def paintEvent(self, event: QPaintEvent): # TODO decide if we want any default instructions
super().paintEvent(event) super().paintEvent(event)
if self._instructions_visible: if self._instructions_visible:
@ -218,7 +228,7 @@ class BECDockArea(BECWidget, QWidget):
painter.drawText( painter.drawText(
self.rect(), self.rect(),
Qt.AlignCenter, 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 @property
@ -243,7 +253,17 @@ class BECDockArea(BECWidget, QWidget):
@panels.setter @panels.setter
def panels(self, value: dict[str, BECDock]): 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 @property
def temp_areas(self) -> list: def temp_areas(self) -> list:
@ -287,36 +307,17 @@ class BECDockArea(BECWidget, QWidget):
self.config.docks_state = last_state self.config.docks_state = last_state
return 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) @SafeSlot(popup_error=True)
def add_dock( def new(
self, self,
name: str = None, name: str | None = None,
position: Literal["bottom", "top", "left", "right", "above", "below"] = 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, relative_to: BECDock | None = None,
closable: bool = True, closable: bool = True,
floating: bool = False, floating: bool = False,
prefix: str = "dock", row: int | None = None,
widget: str | QWidget | None = None,
row: int = None,
col: int = 0, col: int = 0,
rowspan: int = 1, rowspan: int = 1,
colspan: int = 1, colspan: int = 1,
@ -326,12 +327,11 @@ class BECDockArea(BECWidget, QWidget):
Args: Args:
name(str): The name of the dock to be displayed and for further references. Has to be unique. 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. 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. relative_to(BECDock): The dock to which the new dock should be added relative to.
closable(bool): Whether the dock is closable. closable(bool): Whether the dock is closable.
floating(bool): Whether the dock is detached after creating. 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. row(int): The row of the added widget.
col(int): The column of the added widget. col(int): The column of the added widget.
rowspan(int): The rowspan of the added widget. rowspan(int): The rowspan of the added widget.
@ -340,21 +340,20 @@ class BECDockArea(BECWidget, QWidget):
Returns: Returns:
BECDock: The created dock. BECDock: The created dock.
""" """
if name is None: dock_names = [dock._name for dock in self.panel_list] # pylint: disable=protected-access
name = WidgetContainerUtils.generate_unique_widget_id( if name is not None: # Name is provided
container=self.dock_area.docks, prefix=prefix if name in dock_names:
) raise ValueError(
f"Name {name} must be unique for docks, but already exists in DockArea "
if name in set(self.dock_area.docks.keys()): f"with name: {self._name} and id {self.gui_id}."
raise ValueError(f"Dock with name {name} already exists.") )
else: # Name is not provided
if position is None: name = WidgetContainerUtils.generate_unique_name(name="dock", list_of_names=dock_names)
position = "bottom"
dock = BECDock(name=name, parent_dock_area=self, closable=closable) dock = BECDock(name=name, parent_dock_area=self, closable=closable)
dock.config.position = position 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) self.dock_area.addDock(dock=dock, position=position, relativeTo=relative_to)
if len(self.dock_area.docks) <= 1: if len(self.dock_area.docks) <= 1:
@ -363,10 +362,11 @@ class BECDockArea(BECWidget, QWidget):
for dock in self.dock_area.docks.values(): for dock in self.dock_area.docks.values():
dock.show_title_bar() dock.show_title_bar()
if widget is not None and isinstance(widget, str): if widget is not None:
dock.add_widget(widget=widget, row=row, col=col, rowspan=rowspan, colspan=colspan) # Check if widget name exists.
elif widget is not None and isinstance(widget, QWidget): dock.new(
dock.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan) widget=widget, name=widget_name, row=row, col=col, rowspan=rowspan, colspan=colspan
)
if ( if (
self._instructions_visible self._instructions_visible
): # TODO still decide how initial instructions should be handled ): # 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. Remove a temporary area from the dock area.
This is a patched method of pyqtgraph's removeTempArea 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) self.dock_area.tempAreas.remove(area)
area.window().close() area.window().close()
area.window().deleteLater() 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): def cleanup(self):
""" """
Cleanup the dock area. Cleanup the dock area.
""" """
self.clear_all() self.delete_all()
self.toolbar.close() self.toolbar.close()
self.toolbar.deleteLater() self.toolbar.deleteLater()
self.dock_area.close() self.dock_area.close()
self.dock_area.deleteLater() self.dock_area.deleteLater()
super().cleanup() 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): def show(self):
"""Show all windows including floating docks.""" """Show all windows including floating docks."""
super().show() super().show()
@ -465,17 +442,52 @@ class BECDockArea(BECWidget, QWidget):
continue continue
docks.window().hide() docks.window().hide()
def delete(self): def delete_all(self) -> None:
self.hide() """
self.deleteLater() 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 if __name__ == "__main__": # pragma: no cover
import sys
from bec_widgets.utils.colors import set_theme from bec_widgets.utils.colors import set_theme
app = QApplication([]) app = QApplication([])
set_theme("auto") set_theme("auto")
dock_area = BECDockArea() 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.show()
dock_area.setGeometry(100, 100, 800, 600)
app.topLevelWidgets()
app.exec_() app.exec_()
sys.exit(app.exec_())