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:
@ -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}")
|
||||||
|
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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_())
|
||||||
|
Reference in New Issue
Block a user