1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-03-09 18:27:52 +01:00

refactor: fix cleanup, add remove methods, improve docs

This commit is contained in:
2025-03-10 07:02:43 +01:00
parent d90a383e29
commit a9b36a4c45
12 changed files with 273 additions and 387 deletions

View File

@@ -50,27 +50,12 @@ class Widgets(str, enum.Enum):
class AbortButton(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
"""A button that abort the scan."""
@rpc_call
def _get_all_rpc(self) -> "dict":
def remove(self):
"""
Get all registered RPC objects.
"""
@property
@rpc_call
def _rpc_id(self) -> "str":
"""
Get the RPC ID of the widget.
Cleanup the BECConnector
"""
@@ -263,7 +248,8 @@ class BECDock(RPCBase):
Add a widget to the dock.
Args:
widget(QWidget): The widget to add.
widget(QWidget): The widget to add. It can not be BECDock or BECDockArea.
name(str): The name of the widget.
row(int): The row to add the widget to. If None, the widget will be added to the next available row.
col(int): The column to add the widget to.
rowspan(int): The number of rows the widget should span.
@@ -313,17 +299,6 @@ class BECDock(RPCBase):
list: The list of eligible widgets.
"""
@rpc_call
def move_widget(self, widget: "QWidget", new_row: "int", new_col: "int"):
"""
Move a widget to a new position in the layout.
Args:
widget(QWidget): The widget to move.
new_row(int): The new row to move the widget to.
new_col(int): The new column to move the widget to.
"""
@rpc_call
def delete(self, widget_name: "str") -> "None":
"""
@@ -339,6 +314,12 @@ class BECDock(RPCBase):
Remove all widgets from the dock.
"""
@rpc_call
def remove(self):
"""
Remove the dock from the parent dock area.
"""
@rpc_call
def attach(self):
"""
@@ -433,6 +414,12 @@ class BECDockArea(RPCBase):
Delete all docks.
"""
@rpc_call
def remove(self) -> "None":
"""
Remove the dock area.
"""
@rpc_call
def detach_dock(self, dock_name: "str") -> "BECDock":
"""
@@ -2214,6 +2201,8 @@ class BECPlotBase(RPCBase):
class BECProgressBar(RPCBase):
"""A custom progress bar with smooth transitions. The displayed text can be customized using a template."""
@rpc_call
def set_value(self, value):
"""
@@ -2265,52 +2254,22 @@ class BECProgressBar(RPCBase):
class BECQueue(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
"""Widget to display the BEC queue."""
@rpc_call
def _get_all_rpc(self) -> "dict":
def remove(self):
"""
Get all registered RPC objects.
"""
@property
@rpc_call
def _rpc_id(self) -> "str":
"""
Get the RPC ID of the widget.
Cleanup the BECConnector
"""
class BECStatusBox(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
"""An autonomous widget to display the status of BEC services."""
@rpc_call
def _get_all_rpc(self) -> "dict":
def remove(self):
"""
Get all registered RPC objects.
"""
@property
@rpc_call
def _rpc_id(self) -> "str":
"""
Get the RPC ID of the widget.
Cleanup the BECConnector
"""
@@ -2829,6 +2788,8 @@ class Curve(RPCBase):
class DapComboBox(RPCBase):
"""The DAPComboBox widget is an extension to the QComboBox with all avaialble DAP model from BEC."""
@rpc_call
def select_y_axis(self, y_axis: str):
"""
@@ -2867,156 +2828,66 @@ class DarkModeButton(RPCBase):
class DeviceBrowser(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
def remove(self):
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
@rpc_call
def _get_all_rpc(self) -> "dict":
"""
Get all registered RPC objects.
"""
@property
@rpc_call
def _rpc_id(self) -> "str":
"""
Get the RPC ID of the widget.
Cleanup the BECConnector
"""
class DeviceComboBox(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
"""Combobox widget for device input with autocomplete for device names."""
@rpc_call
def _get_all_rpc(self) -> "dict":
def remove(self):
"""
Get all registered RPC objects.
"""
@property
@rpc_call
def _rpc_id(self) -> "str":
"""
Get the RPC ID of the widget.
Cleanup the BECConnector
"""
class DeviceInputBase(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
"""Mixin base class for device input widgets."""
@rpc_call
def _get_all_rpc(self) -> "dict":
def remove(self):
"""
Get all registered RPC objects.
"""
@property
@rpc_call
def _rpc_id(self) -> "str":
"""
Get the RPC ID of the widget.
Cleanup the BECConnector
"""
class DeviceLineEdit(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
"""Line edit widget for device input with autocomplete for device names."""
@rpc_call
def _get_all_rpc(self) -> "dict":
def remove(self):
"""
Get all registered RPC objects.
"""
@property
@rpc_call
def _rpc_id(self) -> "str":
"""
Get the RPC ID of the widget.
Cleanup the BECConnector
"""
class DeviceSignalInputBase(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
"""Mixin base class for device signal input widgets."""
@rpc_call
def _get_all_rpc(self) -> "dict":
def remove(self):
"""
Get all registered RPC objects.
"""
@property
@rpc_call
def _rpc_id(self) -> "str":
"""
Get the RPC ID of the widget.
Cleanup the BECConnector
"""
class LMFitDialog(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
"""Dialog for displaying the fit summary and params for LMFit DAP processes"""
@rpc_call
def _get_all_rpc(self) -> "dict":
def remove(self):
"""
Get all registered RPC objects.
"""
@property
@rpc_call
def _rpc_id(self) -> "str":
"""
Get the RPC ID of the widget.
Cleanup the BECConnector
"""
class LogPanel(RPCBase):
"""Displays a log panel"""
@rpc_call
def set_plain_text(self, text: str) -> None:
"""
@@ -3079,6 +2950,8 @@ class PositionIndicator(RPCBase):
class PositionerBox(RPCBase):
"""Simple Widget to control a positioner in box form"""
@rpc_call
def set_positioner(self, positioner: "str | Positioner"):
"""
@@ -3090,6 +2963,8 @@ class PositionerBox(RPCBase):
class PositionerBox2D(RPCBase):
"""Simple Widget to control two positioners in box form"""
@rpc_call
def set_positioner_hor(self, positioner: "str | Positioner"):
"""
@@ -3110,31 +2985,18 @@ class PositionerBox2D(RPCBase):
class PositionerBoxBase(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
"""Contains some core logic for positioner box widgets"""
@rpc_call
def _get_all_rpc(self) -> "dict":
def remove(self):
"""
Get all registered RPC objects.
"""
@property
@rpc_call
def _rpc_id(self) -> "str":
"""
Get the RPC ID of the widget.
Cleanup the BECConnector
"""
class PositionerControlLine(RPCBase):
"""A widget that controls a single device."""
@rpc_call
def set_positioner(self, positioner: "str | Positioner"):
"""
@@ -3146,6 +3008,8 @@ class PositionerControlLine(RPCBase):
class PositionerGroup(RPCBase):
"""Simple Widget to control a positioner in box form"""
@rpc_call
def set_positioners(self, device_names: "str"):
"""
@@ -3156,52 +3020,22 @@ class PositionerGroup(RPCBase):
class ResetButton(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
"""A button that resets the scan queue."""
@rpc_call
def _get_all_rpc(self) -> "dict":
def remove(self):
"""
Get all registered RPC objects.
"""
@property
@rpc_call
def _rpc_id(self) -> "str":
"""
Get the RPC ID of the widget.
Cleanup the BECConnector
"""
class ResumeButton(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
"""A button that continue scan queue."""
@rpc_call
def _get_all_rpc(self) -> "dict":
def remove(self):
"""
Get all registered RPC objects.
"""
@property
@rpc_call
def _rpc_id(self) -> "str":
"""
Get the RPC ID of the widget.
Cleanup the BECConnector
"""
@@ -3485,131 +3319,56 @@ class RingProgressBar(RPCBase):
class ScanControl(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
def remove(self):
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
@rpc_call
def _get_all_rpc(self) -> "dict":
"""
Get all registered RPC objects.
"""
@property
@rpc_call
def _rpc_id(self) -> "str":
"""
Get the RPC ID of the widget.
Cleanup the BECConnector
"""
class ScanMetadata(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
"""Dynamically generates a form for inclusion of metadata for a scan. Uses the"""
@rpc_call
def _get_all_rpc(self) -> "dict":
def remove(self):
"""
Get all registered RPC objects.
"""
@property
@rpc_call
def _rpc_id(self) -> "str":
"""
Get the RPC ID of the widget.
Cleanup the BECConnector
"""
class SignalComboBox(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
"""Line edit widget for device input with autocomplete for device names."""
@rpc_call
def _get_all_rpc(self) -> "dict":
def remove(self):
"""
Get all registered RPC objects.
"""
@property
@rpc_call
def _rpc_id(self) -> "str":
"""
Get the RPC ID of the widget.
Cleanup the BECConnector
"""
class SignalLineEdit(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
"""Line edit widget for device input with autocomplete for device names."""
@rpc_call
def _get_all_rpc(self) -> "dict":
def remove(self):
"""
Get all registered RPC objects.
"""
@property
@rpc_call
def _rpc_id(self) -> "str":
"""
Get the RPC ID of the widget.
Cleanup the BECConnector
"""
class StopButton(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
"""A button that stops the current scan."""
@rpc_call
def _get_all_rpc(self) -> "dict":
def remove(self):
"""
Get all registered RPC objects.
"""
@property
@rpc_call
def _rpc_id(self) -> "str":
"""
Get the RPC ID of the widget.
Cleanup the BECConnector
"""
class TextBox(RPCBase):
"""A widget that displays text in plain and HTML format"""
@rpc_call
def set_plain_text(self, text: str) -> None:
"""
@@ -3629,7 +3388,10 @@ class TextBox(RPCBase):
"""
class VSCodeEditor(RPCBase): ...
class VSCodeEditor(RPCBase):
"""A widget to display the VSCode editor."""
...
class Waveform(RPCBase):
@@ -4044,6 +3806,8 @@ class Waveform(RPCBase):
class WebsiteWidget(RPCBase):
"""A simple widget to display a website"""
@rpc_call
def set_url(self, url: str) -> None:
"""

View File

@@ -1,4 +1,4 @@
""" Client utilities for the BEC GUI. """
"""Client utilities for the BEC GUI."""
from __future__ import annotations
@@ -16,6 +16,8 @@ from typing import TYPE_CHECKING
from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import bec_logger
from bec_lib.utils.import_utils import lazy_import, lazy_import_from
from rich.console import Console
from rich.table import Table
import bec_widgets.cli.client as client
from bec_widgets.cli.auto_updates import AutoUpdates
@@ -155,7 +157,40 @@ def wait_for_server(client: BECGuiClient):
class WidgetNameSpace:
pass
def __repr__(self):
console = Console()
table = Table(title="Available widgets for BEC CLI usage")
table.add_column("Widget Name", justify="left", style="magenta")
table.add_column("Description", justify="left")
for attr, value in self.__dict__.items():
docs = value.__doc__
docs = docs if docs else "No description available"
table.add_row(attr, docs)
console.print(table)
return f""
class AvailableWidgetsNamespace:
"""Namespace for available widgets in the BEC GUI."""
def __init__(self):
for widget in client.Widgets:
name = widget.value
if name in ["BECDockArea", "BECDock"]:
continue
setattr(self, name, name)
def __repr__(self):
console = Console()
table = Table(title="Available widgets for BEC CLI usage")
table.add_column("Widget Name", justify="left", style="magenta")
table.add_column("Description", justify="left")
for attr_name, _ in self.__dict__.items():
docs = getattr(client, attr_name).__doc__
docs = docs if docs else "No description available"
table.add_row(attr_name, docs if len(docs.strip()) > 0 else "No description available")
console.print(table)
return "" # f"<{self.__class__.__name__}>"
class BECDockArea(client.BECDockArea):
@@ -166,6 +201,17 @@ class BECDockArea(client.BECDockArea):
# Add namespaces for DockArea
self.elements = WidgetNameSpace()
def delete(self, dock_name):
# Don't close the bec dock area
if dock_name == "bec":
raise ValueError("Cannot delete the bec dock area")
super().delete(dock_name)
def remove(self):
if self._name == "bec":
raise ValueError("Cannot delete the bec dock area")
super().remove()
class BECGuiClient(RPCBase):
"""BEC GUI client class. Container for GUI applications within Python."""
@@ -177,6 +223,7 @@ class BECGuiClient(RPCBase):
self._default_dock_name = "bec"
self._auto_updates_enabled = True
self._auto_updates = None
self._killed = False
self._startup_timeout = 0
self._gui_started_timer = None
self._gui_started_event = threading.Event()
@@ -184,7 +231,7 @@ class BECGuiClient(RPCBase):
self._process_output_processing_thread = None
self._exposed_dock_areas = []
self._registry_state = {}
self.available_widgets = client.Widgets
self.available_widgets = AvailableWidgetsNamespace()
def connect_to_gui_server(self, gui_id: str) -> None:
"""Connect to a GUI server"""
@@ -336,6 +383,7 @@ class BECGuiClient(RPCBase):
return rpc_client._run_rpc("_dump")
def _start(self):
self._killed = False
self._client.connector.register(
MessageEndpoints.gui_registry_state(self._gui_id), cb=self._handle_registry_update
)
@@ -365,8 +413,12 @@ class BECGuiClient(RPCBase):
with wait_for_server(self):
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
rpc_client._run_rpc("hide") # pylint: disable=protected-access
for window in self._top_level.values():
window.hide()
# because of the registry callbacks, we may have
# dock areas that are already killed, but not yet
# removed from the registry state
if not self._killed:
for window in self._top_level.values():
window.hide()
def show(self):
"""Show the GUI window."""
@@ -399,6 +451,22 @@ class BECGuiClient(RPCBase):
widget = rpc_client._run_rpc("new_dock_area", name) # pylint: disable=protected-access
return widget
def delete(self, name: str) -> None:
"""Delete a dock area.
Args:
name(str): The name of the dock area.
"""
widget = self.windows.get(name)
if widget is None:
raise ValueError(f"Dock area {name} not found.")
widget._run_rpc("close") # pylint: disable=protected-access
def delete_all(self) -> None:
"""Delete all dock areas."""
for widget_name in self.windows.keys():
self.delete(widget_name)
def _clear_top_level_widgets(self):
self._top_level.clear()
for widget_id in self._exposed_dock_areas:
@@ -447,19 +515,14 @@ class BECGuiClient(RPCBase):
self._add_dock_areas_from_registry()
def close(self):
"""Deprecated. Use kill() instead."""
"""Deprecated. Use kill_server() instead."""
# FIXME, deprecated in favor of kill, will be removed in the future
self.kill()
self.kill_server()
def kill(self) -> None:
def kill_server(self) -> None:
"""Kill the GUI server."""
self._close()
def _close(self) -> None:
"""
Close the gui window.
"""
self._top_level.clear()
self._killed = True
if self._gui_started_timer is not None:
self._gui_started_timer.cancel()

View File

@@ -95,9 +95,21 @@ class {class_name}(RPCBase):"""
self.content += f"""
class {class_name}(RPCBase):"""
if cls.__doc__:
# We only want the first line of the docstring
# But skip the first line if it's a blank line
first_line = cls.__doc__.split("\n")[0]
if first_line:
class_docs = first_line
else:
class_docs = cls.__doc__.split("\n")[1]
self.content += f"""
\"\"\"{class_docs}\"\"\"
"""
if not cls.USER_ACCESS:
self.content += """...
"""
for method in cls.USER_ACCESS:
is_property_setter = False
obj = getattr(cls, method, None)

View File

@@ -81,7 +81,20 @@ class RPCBase:
def __repr__(self):
type_ = type(self)
qualname = type_.__qualname__
return f"<{qualname} object at {hex(id(self))}>"
return f"<{qualname} with name: {self.widget_name}>"
def remove(self):
"""
Remove the widget.
"""
self._run_rpc("remove")
@property
def widget_name(self):
"""
Get the widget name.
"""
return self._name
@property
def _root(self):

View File

@@ -33,9 +33,14 @@ class RPCWidgetHandler:
from bec_widgets.utils.plugin_utils import get_custom_classes
clss = get_custom_classes("bec_widgets")
self._widget_classes = {cls.__name__: cls for cls in clss.widgets}
self._widget_classes = {
cls.__name__: cls
for cls in clss.widgets
if cls.__name__
not in ["BECDockArea", "BECDock"] # Exclude these classes due to hierarchy
}
def create_widget(self, widget_type, name: str, **kwargs) -> BECWidget:
def create_widget(self, widget_type, name: str | None = None, **kwargs) -> BECWidget:
"""
Create a widget from an RPC message.

View File

@@ -171,7 +171,7 @@ class BECWidgetsCLIServer:
for key, val in connections.items()
if val.__class__.__name__ == "BECDockArea"
}
logger.info(f"Broadcasting registry update: {data}")
logger.info(f"All registered connections: {list(connections.keys())}")
self.client.connector.xadd(
MessageEndpoints.gui_registry_state(self.gui_id),
msg_dict={"data": messages.GUIRegistryStateMessage(state=data)},

View File

@@ -209,6 +209,7 @@ class BECConnector:
"""
self.config = config
# FIXME some thoughts are required to decide how thhis should work with rpc registry
def apply_config(self, config: dict, generate_new_id: bool = True) -> None:
"""
Apply the configuration to the widget.
@@ -221,11 +222,12 @@ class BECConnector:
if generate_new_id is True:
gui_id = str(uuid.uuid4())
self.rpc_register.remove_rpc(self)
self.set_gui_id(gui_id)
self._set_gui_id(gui_id)
self.rpc_register.add_rpc(self)
else:
self.gui_id = self.config.gui_id
# FIXME some thoughts are required to decide how thhis should work with rpc registry
def load_config(self, path: str | None = None, gui: bool = False):
"""
Load the configuration of the widget from YAML.
@@ -262,8 +264,8 @@ class BECConnector:
file_path = os.path.join(path, f"{self.__class__.__name__}_config.yaml")
save_yaml(file_path, self._config_dict)
@pyqtSlot(str)
def set_gui_id(self, gui_id: str) -> None:
# @pyqtSlot(str)
def _set_gui_id(self, gui_id: str) -> None:
"""
Set the GUI ID for the widget.
@@ -309,6 +311,15 @@ class BECConnector:
if gui_id and config.gui_id != gui_id: # Recreating config should not overwrite the gui_id
self.config.gui_id = gui_id
def remove(self):
"""Cleanup the BECConnector"""
if hasattr(self, "close"):
self.close()
if hasattr(self, "deleteLater"):
self.deleteLater()
else:
self.rpc_register.remove_rpc(self)
def get_config(self, dict_output: bool = True) -> dict | BaseModel:
"""
Get the configuration of the widget.

View File

@@ -1,5 +1,7 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import darkdetect
from bec_lib.logger import bec_logger
from qtpy.QtCore import Slot
@@ -9,6 +11,9 @@ from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
from bec_widgets.utils.colors import set_theme
from bec_widgets.utils.container_utils import WidgetContainerUtils
if TYPE_CHECKING:
from bec_widgets.widgets.containers.dock import BECDock
logger = bec_logger.logger
@@ -18,7 +23,9 @@ class BECWidget(BECConnector):
# The icon name is the name of the icon in the icon theme, typically a name taken
# from fonts.google.com/icons. Override this in subclasses to set the icon name.
ICON_NAME = "widgets"
USER_ACCESS = ["remove"]
# pylint: disable=too-many-arguments
def __init__(
self,
client=None,
@@ -26,6 +33,7 @@ class BECWidget(BECConnector):
gui_id: str | None = None,
theme_update: bool = False,
name: str | None = None,
parent_dock: BECDock | None = None,
**kwargs,
):
"""
@@ -55,6 +63,7 @@ class BECWidget(BECConnector):
if not WidgetContainerUtils.has_name_valid_chars(name):
raise ValueError(f"Name {name} contains invalid characters.")
super().__init__(client=client, config=config, gui_id=gui_id, name=name)
self._parent_dock = parent_dock
app = QApplication.instance()
if not hasattr(app, "theme"):
# DO NOT SET THE THEME TO AUTO! Otherwise, the qwebengineview will segfault
@@ -95,10 +104,13 @@ class BECWidget(BECConnector):
def cleanup(self):
"""Cleanup the widget."""
# needed here instead of closeEvent, to be checked why
# However, all widgets need to call super().cleanup() in their cleanup method
self.rpc_register.remove_rpc(self)
def closeEvent(self, event):
self.rpc_register.remove_rpc(self)
"""Wrap the close even to ensure the rpc_register is cleaned up."""
try:
self.cleanup()
finally:
super().closeEvent(event)
super().closeEvent(event) # pylint: disable=no-member

View File

@@ -121,6 +121,7 @@ class BECDock(BECWidget, Dock):
"available_widgets",
"delete",
"delete_all",
"remove",
"attach",
"detach",
]
@@ -136,9 +137,11 @@ class BECDock(BECWidget, Dock):
closable: bool = True,
**kwargs,
) -> None:
if config is None:
config = DockConfig(
widget_class=self.__class__.__name__, parent_dock_area=parent_dock_area.gui_id
widget_class=self.__class__.__name__,
parent_dock_area=parent_dock_area.gui_id if parent_dock_area else None,
)
else:
if isinstance(config, dict):
@@ -148,7 +151,7 @@ class BECDock(BECWidget, Dock):
client=client, config=config, gui_id=gui_id, name=name
) # Name was checked and created in BEC Widget
label = CustomDockLabel(text=name, closable=closable)
Dock.__init__(self, name=name, label=label, **kwargs)
Dock.__init__(self, name=name, label=label, parent=self, **kwargs)
# Dock.__init__(self, name=name, **kwargs)
self.parent_dock_area = parent_dock_area
@@ -282,7 +285,8 @@ class BECDock(BECWidget, Dock):
Add a widget to the dock.
Args:
widget(QWidget): The widget to add.
widget(QWidget): The widget to add. It can not be BECDock or BECDockArea.
name(str): The name of the widget.
row(int): The row to add the widget to. If None, the widget will be added to the next available row.
col(int): The column to add the widget to.
rowspan(int): The number of rows the widget should span.
@@ -311,8 +315,16 @@ class BECDock(BECWidget, Dock):
name = WidgetContainerUtils.generate_unique_name(
name=widget_class_name, list_of_names=existing_widgets_parent_dock
)
# Check that Widget is not BECDock or BECDockArea
widget_class_name = widget if isinstance(widget, str) else widget.__class__.__name__
if widget_class_name in ["BECDock", "BECDockArea"]:
raise ValueError(f"Widget {widget} can not be added to dock.")
if isinstance(widget, str):
widget = cast(BECWidget, widget_handler.create_widget(widget_type=widget, name=name))
widget = cast(
BECWidget,
widget_handler.create_widget(widget_type=widget, name=name, parent_dock=self),
)
else:
widget._name = name # pylint: disable=protected-access
@@ -354,7 +366,7 @@ class BECDock(BECWidget, Dock):
"""
Remove the dock from the parent dock area.
"""
self.parent_dock_area.delete(self.gui_id)
self.parent_dock_area.delete(self._name)
def delete(self, widget_name: str) -> None:
"""
@@ -364,20 +376,21 @@ class BECDock(BECWidget, Dock):
widget_name(str): Delete the widget with the given name.
"""
# pylint: disable=protected-access
widget = [widget for widget in self.widgets if widget._name == widget_name]
if not widget:
widgets = [widget for widget in self.widgets if widget._name == widget_name]
if len(widgets) == 0:
logger.warning(
f"Widget with name {widget_name} not found in dock {self.name()}. "
f"Checking if gui_id was passed as widget_name."
)
# Try to find the widget in the RPC register
# Try to find the widget in the RPC register, maybe the gui_id was passed as widget_name
widget = self.rpc_register.get_rpc_by_id(widget_name)
if widget is None:
logger.warning(
f"Widget not found for name or gui_id: {widget_name} in dock {self.name()}"
)
return
widget = widget[0]
else:
widget = widgets[0]
self.layout.removeWidget(widget)
self.config.widgets.pop(widget._name, None)
if widget in self.widgets:
@@ -396,12 +409,25 @@ class BECDock(BECWidget, Dock):
"""
Clean up the dock, including all its widgets.
"""
# Remove the dock from the parent dock area
if self.parent_dock_area:
self.parent_dock_area.dock_area.docks.pop(self.name(), None)
self.parent_dock_area.config.docks.pop(self.name(), None)
self.delete_all()
self.widgets.clear()
self.label.close()
self.label.deleteLater()
super().cleanup()
# def closeEvent(self, event): # pylint: disable=uselsess-parent-delegation
# """Close Event for dock and cleanup.
# This wrapper ensures that the BECWidget close event is triggered.
# If removed, the closeEvent from pyqtgraph will be triggered, which
# is not calling super().closeEvent(event) and will not trigger the BECWidget close event.
# """
# return super().closeEvent(event)
def close(self):
"""
Close the dock area and cleanup.
@@ -409,13 +435,15 @@ class BECDock(BECWidget, Dock):
"""
self.cleanup()
super().close()
self.parent_dock_area.dock_area.docks.pop(self.name(), None)
if __name__ == "__main__":
import sys
from qtpy.QtWidgets import QApplication
app = QApplication([])
dock = BECDock(name="dock")
dock.show()
app.exec_()
sys.exit(app.exec_())

View File

@@ -55,6 +55,7 @@ class BECDockArea(BECWidget, QWidget):
"panel_list",
"delete",
"delete_all",
"remove",
"detach_dock",
"attach_all",
"selected_device",
@@ -79,6 +80,7 @@ class BECDockArea(BECWidget, QWidget):
self.config = config
super().__init__(client=client, config=config, gui_id=gui_id, name=name, **kwargs)
QWidget.__init__(self, parent=parent)
self._parent = parent
self.layout = QVBoxLayout(self)
self.layout.setSpacing(5)
self.layout.setContentsMargins(0, 0, 0, 0)
@@ -346,9 +348,7 @@ class BECDockArea(BECWidget, QWidget):
f"with name: {self._name} and id {self.gui_id}."
)
else: # Name is not provided
name = WidgetContainerUtils.generate_unique_name(
name=BECDock.__name__, list_of_names=dock_names
)
name = WidgetContainerUtils.generate_unique_name(name="dock", list_of_names=dock_names)
dock = BECDock(name=name, parent_dock_area=self, closable=closable)
dock.config.position = position
@@ -430,25 +430,6 @@ class BECDockArea(BECWidget, QWidget):
self.dock_area.deleteLater()
super().cleanup()
def closeEvent(self, event):
if self.parent() is None:
# we are at top-level (independent window)
if self.isVisible():
# we are visible => user clicked on [X]
# (when closeEvent is called from shutdown procedure,
# everything is hidden first)
# so, let's ignore "close", and do hide instead
event.ignore()
self.setVisible(False)
def close(self):
"""
Close the dock area and cleanup.
Has to be implemented to overwrite pyqtgraph event accept in Container close.
"""
self.cleanup()
super().close()
def show(self):
"""Show all windows including floating docks."""
super().show()
@@ -494,6 +475,10 @@ class BECDockArea(BECWidget, QWidget):
raise ValueError(f"Dock with name {dock_name} does not exist.")
self._broadcast_update()
def remove(self) -> None:
"""Remove the dock area."""
self.close()
if __name__ == "__main__": # pragma: no cover

View File

@@ -78,13 +78,7 @@ class WidgetHandler:
}
def create_widget(
self,
widget_type: str,
widget_id: str,
parent_figure,
parent_id: str,
config: dict = None,
**axis_kwargs,
self, widget_type: str, parent_figure, parent_id: str, config: dict = None, **axis_kwargs
) -> BECPlotBase:
"""
Create and configure a widget based on its type.
@@ -109,7 +103,6 @@ class WidgetHandler:
widget_config_dict = {
"widget_class": widget_class.__name__,
"parent_id": parent_id,
"gui_id": widget_id,
**(config if config is not None else {}),
}
widget_config = config_class(**widget_config_dict)
@@ -568,13 +561,13 @@ class BECFigure(BECWidget, pg.GraphicsLayoutWidget):
widget = self.widget_handler.create_widget(
widget_type=widget_type,
widget_id=widget_id,
parent_figure=self,
parent_id=self.gui_id,
config=config,
**axis_kwargs,
)
widget.set_gui_id(widget_id)
widget_id = widget.gui_id
widget.config.row = row
widget.config.col = col

View File

@@ -942,7 +942,7 @@ class PlotBase(BECWidget, QWidget):
self.axis_settings_dialog.close()
self.axis_settings_dialog = None
self.cleanup_pyqtgraph()
self.rpc_register.remove_rpc(self)
super().cleanup()
def cleanup_pyqtgraph(self):
"""Cleanup pyqtgraph items."""