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

fix(widgets)!: BECConnector resolves hierarchy including objectName, parent, parent_id upon init; all widgets adjusted

This commit is contained in:
2025-04-07 13:19:11 +02:00
parent a2128ad8d6
commit a1bec75115
66 changed files with 467 additions and 279 deletions

View File

@ -1,6 +1,6 @@
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
def dock_area(name: str | None = None): def dock_area(object_name: str | None = None):
_dock_area = BECDockArea(name=name) _dock_area = BECDockArea(object_name=object_name)
return _dock_area return _dock_area

View File

@ -19,9 +19,8 @@ MODULE_PATH = os.path.dirname(bec_widgets.__file__)
class LaunchWindow(BECWidget, QMainWindow): class LaunchWindow(BECWidget, QMainWindow):
def __init__(self, gui_id: str = None, *args, **kwargs): def __init__(self, parent=None, gui_id: str = None, *args, **kwargs):
BECWidget.__init__(self, gui_id=gui_id, **kwargs) super().__init__(parent=parent, gui_id=gui_id, **kwargs)
QMainWindow.__init__(self, *args, **kwargs)
self.app = QApplication.instance() self.app = QApplication.instance()

View File

@ -10,7 +10,7 @@ import threading
import time import time
from contextlib import contextmanager from contextlib import contextmanager
from threading import Lock from threading import Lock
from typing import TYPE_CHECKING, Literal, TypeAlias from typing import TYPE_CHECKING, Literal, TypeAlias, cast
from bec_lib.endpoints import MessageEndpoints from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import bec_logger from bec_lib.logger import bec_logger
@ -22,9 +22,9 @@ import bec_widgets.cli.client as client
from bec_widgets.cli.rpc.rpc_base import RPCBase, RPCReference from bec_widgets.cli.rpc.rpc_base import RPCBase, RPCReference
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from bec_lib.redis_connector import StreamMessage from bec_lib.messages import GUIRegistryStateMessage
else: else:
StreamMessage = lazy_import_from("bec_lib.redis_connector", ("StreamMessage",)) GUIRegistryStateMessage = lazy_import_from("bec_lib.messages", "GUIRegistryStateMessage")
logger = bec_logger.logger logger = bec_logger.logger
@ -71,7 +71,11 @@ def _get_output(process, logger) -> None:
def _start_plot_process( def _start_plot_process(
gui_id: str, gui_class_id: str, config: dict | str, gui_class: str = "launcher", logger=None gui_id: str,
gui_class_id: str,
config: dict | str,
gui_class: str = "dock_area",
logger=None, # FIXME change gui_class back to "launcher" later
) -> tuple[subprocess.Popen[str], threading.Thread | None]: ) -> tuple[subprocess.Popen[str], threading.Thread | None]:
""" """
Start the plot in a new process. Start the plot in a new process.
@ -199,7 +203,7 @@ class BECGuiClient(RPCBase):
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs) -> None:
super().__init__(**kwargs) super().__init__(**kwargs)
self._lock = Lock() self._lock = Lock()
self._default_dock_name = "bec" self._anchor_widget = "launcher"
self._auto_updates_enabled = True self._auto_updates_enabled = True
self._auto_updates = None self._auto_updates = None
self._killed = False self._killed = False
@ -220,7 +224,7 @@ class BECGuiClient(RPCBase):
@property @property
def launcher(self) -> RPCBase: def launcher(self) -> RPCBase:
"""The launcher object.""" """The launcher object."""
return RPCBase(gui_id=f"{self._gui_id}:launcher", parent=self, name="launcher") return RPCBase(gui_id=f"{self._gui_id}:launcher", parent=self, object_name="launcher")
def connect_to_gui_server(self, gui_id: str) -> None: def connect_to_gui_server(self, gui_id: str) -> None:
"""Connect to a GUI server""" """Connect to a GUI server"""
@ -247,7 +251,7 @@ class BECGuiClient(RPCBase):
@property @property
def windows(self) -> dict: def windows(self) -> dict:
"""Dictionary with dock areas in the GUI.""" """Dictionary with dock areas in the GUI."""
return {widget._name: widget for widget in self._top_level.values()} return {widget.object_name: widget for widget in self._top_level.values()}
@property @property
def window_list(self) -> list: def window_list(self) -> list:
@ -365,7 +369,7 @@ class BECGuiClient(RPCBase):
# After 60s timeout. Should this raise an exception on timeout? # After 60s timeout. Should this raise an exception on timeout?
while time.time() < time.time() + timeout: while time.time() < time.time() + timeout:
if len(list(self._server_registry.keys())) < 2 or not hasattr( if len(list(self._server_registry.keys())) < 2 or not hasattr(
self, self._default_dock_name self, self._anchor_widget
): ):
time.sleep(0.1) time.sleep(0.1)
else: else:
@ -383,7 +387,7 @@ class BECGuiClient(RPCBase):
self._gui_started_event.clear() self._gui_started_event.clear()
self._process, self._process_output_processing_thread = _start_plot_process( self._process, self._process_output_processing_thread = _start_plot_process(
self._gui_id, self._gui_id,
gui_class_id=self._default_dock_name, gui_class_id="bec",
config=self._client._service_config.config, # pylint: disable=protected-access config=self._client._service_config.config, # pylint: disable=protected-access
logger=logger, logger=logger,
) )
@ -413,11 +417,13 @@ class BECGuiClient(RPCBase):
return self._start_server(wait=wait) return self._start_server(wait=wait)
@staticmethod @staticmethod
def _handle_registry_update(msg: StreamMessage, parent: BECGuiClient) -> None: def _handle_registry_update(
msg: dict[str, GUIRegistryStateMessage], parent: BECGuiClient
) -> None:
# This was causing a deadlock during shutdown, not sure why. # This was causing a deadlock during shutdown, not sure why.
# with self._lock: # with self._lock:
self = parent self = parent
self._server_registry = msg["data"].state self._server_registry = cast(dict[str, RegistryState], msg["data"].state)
self._update_dynamic_namespace(self._server_registry) self._update_dynamic_namespace(self._server_registry)
def _do_show_all(self): def _do_show_all(self):
@ -460,12 +466,11 @@ class BECGuiClient(RPCBase):
for gui_id, widget in self._ipython_registry.items(): for gui_id, widget in self._ipython_registry.items():
if gui_id not in server_registry: if gui_id not in server_registry:
remove_from_registry.append(gui_id) remove_from_registry.append(gui_id)
widget._refresh_references()
for gui_id in remove_from_registry: for gui_id in remove_from_registry:
self._ipython_registry.pop(gui_id) self._ipython_registry.pop(gui_id)
removed_widgets = [ removed_widgets = [
widget._name for widget in self._top_level.values() if widget._is_deleted() widget.object_name for widget in self._top_level.values() if widget._is_deleted()
] ]
for widget_name in removed_widgets: for widget_name in removed_widgets:
@ -475,10 +480,13 @@ class BECGuiClient(RPCBase):
delattr(self, widget_name) delattr(self, widget_name)
for gui_id, widget_ref in top_level_widgets.items(): for gui_id, widget_ref in top_level_widgets.items():
setattr(self, widget_ref._name, widget_ref) setattr(self, widget_ref.object_name, widget_ref)
self._top_level = top_level_widgets self._top_level = top_level_widgets
for widget in self._ipython_registry.values():
widget._refresh_references()
def _add_widget(self, state: dict, parent: object) -> RPCReference | None: def _add_widget(self, state: dict, parent: object) -> RPCReference | None:
"""Add a widget to the namespace """Add a widget to the namespace
@ -486,7 +494,7 @@ class BECGuiClient(RPCBase):
state (dict): The state of the widget from the _server_registry. state (dict): The state of the widget from the _server_registry.
parent (object): The parent object. parent (object): The parent object.
""" """
name = state["name"] object_name = state["object_name"]
gui_id = state["gui_id"] gui_id = state["gui_id"]
if state["widget_class"] in IGNORE_WIDGETS: if state["widget_class"] in IGNORE_WIDGETS:
return return
@ -495,7 +503,7 @@ class BECGuiClient(RPCBase):
return return
obj = self._ipython_registry.get(gui_id) obj = self._ipython_registry.get(gui_id)
if obj is None: if obj is None:
widget = widget_class(gui_id=gui_id, name=name, parent=parent) widget = widget_class(gui_id=gui_id, object_name=object_name, parent=parent)
self._ipython_registry[gui_id] = widget self._ipython_registry[gui_id] = widget
else: else:
widget = obj widget = obj

View File

@ -4,7 +4,7 @@ import inspect
import threading import threading
import uuid import uuid
from functools import wraps from functools import wraps
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any, cast
from bec_lib.client import BECClient from bec_lib.client import BECClient
from bec_lib.endpoints import MessageEndpoints from bec_lib.endpoints import MessageEndpoints
@ -41,7 +41,7 @@ def rpc_call(func):
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
# we could rely on a strict type check here, but this is more flexible # we could rely on a strict type check here, but this is more flexible
# moreover, it would anyway crash for objects... # moreover, it would anyway crash for objects...
caller_frame = inspect.currentframe().f_back caller_frame = inspect.currentframe().f_back # type: ignore
while caller_frame: while caller_frame:
if "jedi" in caller_frame.f_globals: if "jedi" in caller_frame.f_globals:
# Jedi module is present, likely tab completion # Jedi module is present, likely tab completion
@ -91,7 +91,7 @@ class RPCReference:
def __init__(self, registry: dict, gui_id: str) -> None: def __init__(self, registry: dict, gui_id: str) -> None:
self._registry = registry self._registry = registry
self._gui_id = gui_id self._gui_id = gui_id
self._name = self._registry[self._gui_id]._name self.object_name = self._registry[self._gui_id].object_name
@check_for_deleted_widget @check_for_deleted_widget
def __getattr__(self, name): def __getattr__(self, name):
@ -134,13 +134,13 @@ class RPCBase:
self, self,
gui_id: str | None = None, gui_id: str | None = None,
config: dict | None = None, config: dict | None = None,
name: str | None = None, object_name: str | None = None,
parent=None, parent=None,
) -> None: ) -> None:
self._client = BECClient() # BECClient is a singleton; here, we simply get the instance self._client = BECClient() # BECClient is a singleton; here, we simply get the instance
self._config = config if config is not None else {} self._config = config if config is not None else {}
self._gui_id = gui_id if gui_id is not None else str(uuid.uuid4())[:5] self._gui_id = gui_id if gui_id is not None else str(uuid.uuid4())[:5]
self._name = name if name is not None else str(uuid.uuid4())[:5] self.object_name = object_name if object_name is not None else str(uuid.uuid4())[:5]
self._parent = parent self._parent = parent
self._msg_wait_event = threading.Event() self._msg_wait_event = threading.Event()
self._rpc_response = None self._rpc_response = None
@ -163,7 +163,7 @@ class RPCBase:
""" """
Get the widget name. Get the widget name.
""" """
return self._name return self.object_name
@property @property
def _root(self) -> BECGuiClient: def _root(self) -> BECGuiClient:
@ -175,7 +175,7 @@ class RPCBase:
# pylint: disable=protected-access # pylint: disable=protected-access
while parent._parent is not None: while parent._parent is not None:
parent = parent._parent parent = parent._parent
return parent return parent # type: ignore
def _run_rpc(self, method, *args, wait_for_rpc_response=True, timeout=300, **kwargs) -> Any: def _run_rpc(self, method, *args, wait_for_rpc_response=True, timeout=300, **kwargs) -> Any:
""" """
@ -219,7 +219,11 @@ class RPCBase:
self._client.connector.unregister( self._client.connector.unregister(
MessageEndpoints.gui_instruction_response(request_id), cb=self._on_rpc_response MessageEndpoints.gui_instruction_response(request_id), cb=self._on_rpc_response
) )
# get class name
# we can assume that the response is a RequestResponseMessage, updated by
# the _on_rpc_response method
assert isinstance(self._rpc_response, messages.RequestResponseMessage)
if not self._rpc_response.accepted: if not self._rpc_response.accepted:
raise ValueError(self._rpc_response.message["error"]) raise ValueError(self._rpc_response.message["error"])
msg_result = self._rpc_response.message.get("result") msg_result = self._rpc_response.message.get("result")
@ -227,8 +231,8 @@ class RPCBase:
return self._create_widget_from_msg_result(msg_result) return self._create_widget_from_msg_result(msg_result)
@staticmethod @staticmethod
def _on_rpc_response(msg: MessageObject, parent: RPCBase) -> None: def _on_rpc_response(msg_obj: MessageObject, parent: RPCBase) -> None:
msg = msg.value msg = cast(messages.RequestResponseMessage, msg_obj.value)
parent._msg_wait_event.set() parent._msg_wait_event.set()
parent._rpc_response = msg parent._rpc_response = msg
@ -283,12 +287,17 @@ class RPCBase:
for key, val in self._root._server_registry.items(): for key, val in self._root._server_registry.items():
parent_id = val["config"].get("parent_id") parent_id = val["config"].get("parent_id")
if parent_id == self._gui_id: if parent_id == self._gui_id:
references[key] = {"gui_id": val["config"]["gui_id"], "name": val["name"]} references[key] = {
"gui_id": val["config"]["gui_id"],
"object_name": val["object_name"],
}
removed_references = set(self._rpc_references.keys()) - set(references.keys()) removed_references = set(self._rpc_references.keys()) - set(references.keys())
for key in removed_references: for key in removed_references:
delattr(self, self._rpc_references[key]["name"]) delattr(self, self._rpc_references[key]["object_name"])
self._rpc_references = references self._rpc_references = references
for key, val in references.items(): for key, val in references.items():
setattr( setattr(
self, val["name"], RPCReference(self._root._ipython_registry, val["gui_id"]) self,
val["object_name"],
RPCReference(self._root._ipython_registry, val["gui_id"]),
) )

View File

@ -123,7 +123,7 @@ class RPCRegister:
# This retrieves any rpc objects that are subclass of BECWidget, # This retrieves any rpc objects that are subclass of BECWidget,
# i.e. curve and image items are excluded # i.e. curve and image items are excluded
widgets = [rpc for rpc in self._rpc_register.values() if isinstance(rpc, cls)] widgets = [rpc for rpc in self._rpc_register.values() if isinstance(rpc, cls)]
return [widget._name for widget in widgets] return [widget.object_name for widget in widgets]
def broadcast(self): def broadcast(self):
""" """
@ -172,6 +172,6 @@ class RPCRegisterBroadcast:
"""Exit the context manager""" """Exit the context manager"""
self._call_depth -= 1 # Remove nested calls self._call_depth -= 1 # Remove nested calls
if self._call_depth == 0: # Last one to exit is repsonsible for broadcasting if self._call_depth == 0: # The Last one to exit is responsible for broadcasting
self.rpc_register._skip_broadcast = False self.rpc_register._skip_broadcast = False
self.rpc_register.broadcast() self.rpc_register.broadcast()

View File

@ -36,7 +36,7 @@ class RPCWidgetHandler:
cls.__name__: cls for cls in clss.widgets if cls.__name__ not in IGNORE_WIDGETS cls.__name__: cls for cls in clss.widgets if cls.__name__ not in IGNORE_WIDGETS
} }
def create_widget(self, widget_type, name: str | None = None, **kwargs) -> BECWidget: def create_widget(self, widget_type, **kwargs) -> BECWidget:
""" """
Create a widget from an RPC message. Create a widget from an RPC message.
@ -50,7 +50,7 @@ class RPCWidgetHandler:
""" """
widget_class = self.widget_classes.get(widget_type) # type: ignore widget_class = self.widget_classes.get(widget_type) # type: ignore
if widget_class: if widget_class:
return widget_class(name=name, **kwargs) return widget_class(**kwargs)
raise ValueError(f"Unknown widget type: {widget_type}") raise ValueError(f"Unknown widget type: {widget_type}")

View File

@ -111,8 +111,8 @@ class GUIServer:
self.setup_bec_icon() self.setup_bec_icon()
service_config = self._get_service_config() service_config = self._get_service_config()
self.dispatcher = BECDispatcher(config=service_config) self.dispatcher = BECDispatcher(config=service_config, gui_id=self.gui_id)
self.dispatcher.start_cli_server(gui_id=self.gui_id) # self.dispatcher.start_cli_server(gui_id=self.gui_id)
self.launcher_window = LaunchWindow(gui_id=f"{self.gui_id}:launcher") self.launcher_window = LaunchWindow(gui_id=f"{self.gui_id}:launcher")
self.launcher_window.setAttribute(Qt.WA_ShowWithoutActivating) # type: ignore self.launcher_window.setAttribute(Qt.WA_ShowWithoutActivating) # type: ignore
@ -139,8 +139,6 @@ class GUIServer:
if self.app: if self.app:
self.app.quit() self.app.quit()
# gui.bec.close()
# win.shutdown()
signal.signal(signal.SIGINT, sigint_handler) signal.signal(signal.SIGINT, sigint_handler)
signal.signal(signal.SIGTERM, sigint_handler) signal.signal(signal.SIGTERM, sigint_handler)

View File

@ -10,21 +10,23 @@ from typing import TYPE_CHECKING, Optional
from bec_lib.logger import bec_logger from bec_lib.logger import bec_logger
from bec_lib.utils.import_utils import lazy_import_from from bec_lib.utils.import_utils import lazy_import_from
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator
from qtpy.QtCore import QObject, QRunnable, QThreadPool, Signal from qtpy.QtCore import QObject, QRunnable, Qt, QThreadPool, Signal
from qtpy.QtWidgets import QApplication 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.utils.container_utils import WidgetContainerUtils from bec_widgets.utils.container_utils import WidgetContainerUtils
from bec_widgets.utils.error_popups import ErrorPopupUtility from bec_widgets.utils.error_popups import ErrorPopupUtility
from bec_widgets.utils.error_popups import SafeSlot as pyqtSlot from bec_widgets.utils.error_popups import SafeSlot as pyqtSlot
from bec_widgets.utils.widget_io import WidgetHierarchy
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: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from bec_widgets.utils.bec_dispatcher import BECDispatcher from bec_widgets.utils.bec_dispatcher import BECDispatcher
from bec_widgets.widgets.containers.dock import BECDock from bec_widgets.widgets.containers.dock import BECDock
else:
BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))
logger = bec_logger.logger logger = bec_logger.logger
BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))
class ConnectionConfig(BaseModel): class ConnectionConfig(BaseModel):
@ -82,14 +84,21 @@ class BECConnector:
client=None, client=None,
config: ConnectionConfig | None = None, config: ConnectionConfig | None = None,
gui_id: str | None = None, gui_id: str | None = None,
name: str | None = None, object_name: str | None = None,
parent_dock: BECDock | None = None, parent_dock: BECDock | None = None, # TODO should go away -> issue created #473
parent_id: str | None = None, parent_id: str | None = None,
**kwargs,
): ):
# Extract object_name from kwargs to not pass it to Qt class
object_name = object_name or kwargs.pop("objectName", None)
# Ensure the parent is always the first argument for QObject
parent = kwargs.pop("parent", None)
# This initializes the QObject or any qt related class
super().__init__(parent=parent, **kwargs)
# 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
self._parent_dock = parent_dock self._parent_dock = parent_dock # TODO also remove at some point -> issue created #473
if not self.client in BECConnector.EXIT_HANDLERS: if not self.client in BECConnector.EXIT_HANDLERS:
# register function to clean connections at exit; # register function to clean connections at exit;
@ -122,12 +131,24 @@ class BECConnector:
self.gui_id: str = gui_id # Keep namespace in sync self.gui_id: str = gui_id # Keep namespace in sync
else: else:
self.gui_id: str = self.config.gui_id # type: ignore self.gui_id: str = self.config.gui_id # type: ignore
if name is None:
name = self.__class__.__name__ # TODO Hierarchy can be refreshed upon creation -> also registry should be notified if objectName changes -> issue #472
else: if object_name is not None:
if not WidgetContainerUtils.has_name_valid_chars(name): self.setObjectName(object_name)
raise ValueError(f"Name {name} contains invalid characters.")
self._name = name if name else self.__class__.__name__ # 1) If no objectName is set, set the initial name
if not self.objectName():
self.setObjectName(self.__class__.__name__)
self.object_name = self.objectName()
# 2) Enforce unique objectName among siblings with the same BECConnector parent
self.setParent(parent)
if parent_id is None:
connector_parent = WidgetHierarchy._get_becwidget_ancestor(self)
if connector_parent is not None:
self.parent_id = connector_parent.gui_id
self._enforce_unique_sibling_name()
self.rpc_register = RPCRegister() self.rpc_register = RPCRegister()
self.rpc_register.add_rpc(self) self.rpc_register.add_rpc(self)
@ -138,6 +159,49 @@ class BECConnector:
# Store references to running workers so they're not garbage collected prematurely. # Store references to running workers so they're not garbage collected prematurely.
self._workers = [] self._workers = []
def _enforce_unique_sibling_name(self):
"""
Enforce that this BECConnector has a unique objectName among its siblings.
Sibling logic:
- If there's a nearest BECConnector parent, only compare with children of that parent.
- If parent is None (i.e., top-level object), compare with all other top-level BECConnectors.
"""
parent_bec = WidgetHierarchy._get_becwidget_ancestor(self)
if parent_bec:
# We have a parent => only compare with siblings under that parent
siblings = parent_bec.findChildren(BECConnector)
else:
# No parent => treat all top-level BECConnectors as siblings
# 1) Gather all BECConnectors from QApplication
all_widgets = QApplication.allWidgets()
all_bec = [w for w in all_widgets if isinstance(w, BECConnector)]
# 2) "Top-level" means closest BECConnector parent is None
top_level_bec = [
w for w in all_bec if WidgetHierarchy._get_becwidget_ancestor(w) is None
]
# 3) We are among these top-level siblings
siblings = top_level_bec
# Collect used names among siblings
used_names = {sib.objectName() for sib in siblings if sib is not self}
base_name = self.objectName()
if base_name not in used_names:
# Name is already unique among siblings
return
# Need a suffix to avoid collision
counter = 0
while True:
trial_name = f"{base_name}_{counter}"
if trial_name not in used_names:
self.setObjectName(trial_name)
self.object_name = trial_name
break
counter += 1
def submit_task(self, fn, *args, on_complete: pyqtSlot = None, **kwargs) -> Worker: def submit_task(self, fn, *args, on_complete: pyqtSlot = None, **kwargs) -> Worker:
""" """
Submit a task to run in a separate thread. The task will run the specified Submit a task to run in a separate thread. The task will run the specified
@ -316,8 +380,9 @@ class BECConnector:
def remove(self): def remove(self):
"""Cleanup the BECConnector""" """Cleanup the BECConnector"""
# If the widget is attached to a dock, remove it from the dock. # If the widget is attached to a dock, remove it from the dock.
# TODO this should be handled by dock and dock are not by BECConnector
if self._parent_dock is not None: if self._parent_dock is not None:
self._parent_dock.delete(self._name) self._parent_dock.delete(self.object_name)
# If the widget is from Qt, trigger its close method. # If the widget is from Qt, trigger its close method.
elif hasattr(self, "close"): elif hasattr(self, "close"):
self.close() self.close()

View File

@ -80,13 +80,21 @@ class BECDispatcher:
client: BECClient client: BECClient
cli_server: CLIServer | None = None cli_server: CLIServer | None = None
def __new__(cls, client=None, config: str | ServiceConfig | None = None, *args, **kwargs): # TODO add custom gui id for server
def __new__(
cls,
client=None,
config: str | ServiceConfig | None = None,
gui_id: str = None,
*args,
**kwargs,
):
if cls._instance is None: if cls._instance is None:
cls._instance = super(BECDispatcher, cls).__new__(cls) cls._instance = super(BECDispatcher, cls).__new__(cls)
cls._initialized = False cls._initialized = False
return cls._instance return cls._instance
def __init__(self, client=None, config: str | ServiceConfig | None = None): def __init__(self, client=None, config: str | ServiceConfig | None = None, gui_id: str = None):
if self._initialized: if self._initialized:
return return
@ -114,6 +122,8 @@ class BECDispatcher:
logger.warning("Could not connect to Redis, skipping start of BECClient.") logger.warning("Could not connect to Redis, skipping start of BECClient.")
logger.success("Initialized BECDispatcher") logger.success("Initialized BECDispatcher")
self.start_cli_server(gui_id=gui_id)
self._initialized = True self._initialized = True
@classmethod @classmethod
@ -207,7 +217,7 @@ class BECDispatcher:
logger.error("Cannot start CLI server without a running client") logger.error("Cannot start CLI server without a running client")
return return
self.cli_server = CLIServer(gui_id, dispatcher=self, client=self.client) self.cli_server = CLIServer(gui_id, dispatcher=self, client=self.client)
logger.success("Started CLI server with gui_id: {gui_id}") logger.success(f"Started CLI server with gui_id: {gui_id}")
def stop_cli_server(self): def stop_cli_server(self):
""" """

View File

@ -1,11 +1,11 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Any 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 QObject, Slot
from qtpy.QtWidgets import QApplication, QWidget 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.utils.bec_connector import BECConnector, ConnectionConfig from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
@ -32,8 +32,7 @@ class BECWidget(BECConnector):
config: ConnectionConfig = None, config: ConnectionConfig = None,
gui_id: str | None = None, gui_id: str | None = None,
theme_update: bool = False, theme_update: bool = False,
name: str | None = None, parent_dock: BECDock | None = None, # TODO should go away -> issue created #473
parent_dock: BECDock | None = None,
parent_id: str | None = None, parent_id: str | None = None,
**kwargs, **kwargs,
): ):
@ -54,16 +53,17 @@ class BECWidget(BECConnector):
theme_update(bool, optional): Whether to subscribe to theme updates. Defaults to False. When set to True, the theme_update(bool, optional): Whether to subscribe to theme updates. Defaults to False. When set to True, the
widget's apply_theme method will be called when the theme changes. widget's apply_theme method will be called when the theme changes.
""" """
if not isinstance(self, QWidget):
raise RuntimeError(f"{repr(self)} is not a subclass of QWidget")
super().__init__( super().__init__(
client=client, client=client,
config=config, config=config,
gui_id=gui_id, gui_id=gui_id,
name=name,
parent_dock=parent_dock, parent_dock=parent_dock,
parent_id=parent_id, parent_id=parent_id,
**kwargs,
) )
if not isinstance(self, QObject):
raise RuntimeError(f"{repr(self)} is not a subclass of QWidget")
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

View File

@ -11,12 +11,15 @@ from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import bec_logger from bec_lib.logger import bec_logger
from bec_lib.utils.import_utils import lazy_import from bec_lib.utils.import_utils import lazy_import
from qtpy.QtCore import QTimer from qtpy.QtCore import QTimer
from qtpy.QtWidgets import QApplication
from redis.exceptions import RedisError from redis.exceptions import RedisError
from bec_widgets.cli.rpc.rpc_register import RPCRegister from bec_widgets.cli.rpc.rpc_register import RPCRegister
from bec_widgets.utils import BECDispatcher from bec_widgets.utils import BECDispatcher
from bec_widgets.utils.bec_connector import BECConnector from bec_widgets.utils.bec_connector import BECConnector
from bec_widgets.utils.error_popups import ErrorPopupUtility from bec_widgets.utils.error_popups import ErrorPopupUtility
from bec_widgets.utils.widget_io import WidgetHierarchy
from bec_widgets.widgets.plots.plot_base import PlotBase
if TYPE_CHECKING: if TYPE_CHECKING:
from bec_lib import messages from bec_lib import messages
@ -77,6 +80,7 @@ class CLIServer:
self._heartbeat_timer = QTimer() self._heartbeat_timer = QTimer()
self._heartbeat_timer.timeout.connect(self.emit_heartbeat) self._heartbeat_timer.timeout.connect(self.emit_heartbeat)
self._heartbeat_timer.start(200) self._heartbeat_timer.start(200)
self._registry_update_callbacks = []
self.status = messages.BECStatus.RUNNING self.status = messages.BECStatus.RUNNING
logger.success(f"Server started with gui_id: {self.gui_id}") logger.success(f"Server started with gui_id: {self.gui_id}")
@ -141,17 +145,10 @@ class CLIServer:
def serialize_object(self, obj): def serialize_object(self, obj):
if isinstance(obj, BECConnector): if isinstance(obj, BECConnector):
config = obj.config.model_dump() # Respect RPC = False
config["parent_id"] = obj.parent_id # add parent_id to config if hasattr(obj, "RPC") and obj.RPC is False:
return { return None
"gui_id": obj.gui_id, return self._serialize_bec_connector(obj)
"name": (
obj._name if hasattr(obj, "_name") else obj.__class__.__name__
), # pylint: disable=protected-access
"widget_class": obj.__class__.__name__,
"config": config,
"__rpc__": True,
}
return obj return obj
def emit_heartbeat(self): def emit_heartbeat(self):
@ -166,19 +163,57 @@ class CLIServer:
logger.error(f"Error while emitting heartbeat: {exc}") logger.error(f"Error while emitting heartbeat: {exc}")
def broadcast_registry_update(self, connections: dict): def broadcast_registry_update(self, connections: dict):
""" data = {}
Broadcast the updated registry to all clients. for key, val in connections.items():
""" if not isinstance(val, BECConnector):
continue
if not getattr(val, "RPC", True):
continue
data[key] = self._serialize_bec_connector(val)
# We only need to broadcast the dock areas
data = {key: self.serialize_object(val) for key, val in connections.items()}
logger.info(f"Broadcasting registry update: {data} for {self.gui_id}") logger.info(f"Broadcasting registry update: {data} for {self.gui_id}")
self.client.connector.xadd( self.client.connector.xadd(
MessageEndpoints.gui_registry_state(self.gui_id), MessageEndpoints.gui_registry_state(self.gui_id),
msg_dict={"data": messages.GUIRegistryStateMessage(state=data)}, msg_dict={"data": messages.GUIRegistryStateMessage(state=data)},
max_size=1, # only single message in stream max_size=1,
) )
def _serialize_bec_connector(self, connector: BECConnector) -> dict:
"""
Create the serialization dict for a single BECConnector,
setting 'parent_id' via the real nearest BECConnector parent.
"""
config_dict = connector.config.model_dump()
config_dict["parent_id"] = getattr(connector, "parent_id", None)
return {
"gui_id": connector.gui_id,
"object_name": connector.object_name or connector.__class__.__name__,
"widget_class": connector.__class__.__name__,
"config": config_dict,
"__rpc__": True,
}
@staticmethod
def _get_becwidget_ancestor(widget):
"""
Traverse up the parent chain to find the nearest BECConnector.
Returns None if none is found.
"""
from bec_widgets.utils import BECConnector
parent = widget.parent()
while parent is not None:
if isinstance(parent, BECConnector):
return parent
parent = parent.parent()
return None
# Suppose clients register callbacks to receive updates
def add_registry_update_callback(self, cb):
self._registry_update_callbacks.append(cb)
def shutdown(self): # TODO not sure if needed when cleanup is done at level of BECConnector def shutdown(self): # TODO not sure if needed when cleanup is done at level of BECConnector
self.status = messages.BECStatus.IDLE self.status = messages.BECStatus.IDLE
self._heartbeat_timer.stop() self._heartbeat_timer.stop()

View File

@ -22,10 +22,10 @@ class PaletteViewer(BECWidget, QWidget):
""" """
ICON_NAME = "palette" ICON_NAME = "palette"
RPC = False
def __init__(self, *args, parent=None, **kwargs): def __init__(self, *args, parent=None, **kwargs):
super().__init__(*args, theme_update=True, **kwargs) super().__init__(parent=parent, theme_update=True, **kwargs)
QWidget.__init__(self, parent=parent)
self.setFixedSize(400, 600) self.setFixedSize(400, 600)
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
dark_mode_button = DarkModeButton(self) dark_mode_button = DarkModeButton(self)

View File

@ -35,7 +35,6 @@ class SidePanel(QWidget):
super().__init__(parent=parent) super().__init__(parent=parent)
self.setProperty("skip_settings", True) self.setProperty("skip_settings", True)
self.setObjectName("SidePanel")
self._orientation = orientation self._orientation = orientation
self._panel_max_width = panel_max_width self._panel_max_width = panel_max_width

View File

@ -31,7 +31,6 @@ if PYSIDE6:
f"Custom widget {class_name} does not have a parent_id argument. " f"Custom widget {class_name} does not have a parent_id argument. "
) )
widget = self.custom_widgets[class_name](self.baseinstance) widget = self.custom_widgets[class_name](self.baseinstance)
widget.setObjectName(name)
return widget return widget
return super().createWidget(class_name, self.baseinstance, name) return super().createWidget(class_name, self.baseinstance, name)

View File

@ -18,7 +18,6 @@ from qtpy.QtWidgets import (
QWidget, QWidget,
) )
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.widgets.utility.toggle.toggle import ToggleSwitch from bec_widgets.widgets.utility.toggle.toggle import ToggleSwitch
@ -291,62 +290,141 @@ class WidgetHierarchy:
only_bec_widgets(bool, optional): Whether to print only widgets that are instances of BECWidget. only_bec_widgets(bool, optional): Whether to print only widgets that are instances of BECWidget.
show_parent(bool, optional): Whether to display which BECWidget is the parent of each discovered BECWidget. show_parent(bool, optional): Whether to display which BECWidget is the parent of each discovered BECWidget.
""" """
# Decide if this particular widget is to be printed from bec_widgets.utils import BECConnector
is_bec = isinstance(widget, BECWidget) from bec_widgets.widgets.plots.waveform.waveform import Waveform
print_this = (not only_bec_widgets) or is_bec
# If it is a BECWidget and we're showing the parent, climb the chain to find the nearest BECWidget ancestor # 1) Filter out widgets that are not BECConnectors (if 'only_bec_widgets' is True)
is_bec = isinstance(widget, BECConnector)
if only_bec_widgets and not is_bec:
return
# 2) Determine and print the parent's info (closest BECConnector)
parent_info = ""
if show_parent and is_bec: if show_parent and is_bec:
ancestor = WidgetHierarchy._get_becwidget_ancestor(widget) ancestor = WidgetHierarchy._get_becwidget_ancestor(widget)
if ancestor is not None: if ancestor:
parent_info = f" parent={ancestor.__class__.__name__}" parent_label = ancestor.objectName() or ancestor.__class__.__name__
parent_info = f" parent={parent_label}"
else: else:
parent_info = " parent=None" parent_info = " parent=None"
else:
parent_info = ""
if print_this: widget_info = f"{widget.__class__.__name__} ({widget.objectName()}){parent_info}"
widget_info = f"{widget.__class__.__name__} ({widget.objectName()}){parent_info}" print(prefix + widget_info)
if grab_values:
value = WidgetIO.get_value(widget, ignore_errors=True)
value_str = f" [value: {value}]" if value is not None else ""
widget_info += value_str
print(prefix + widget_info)
# Always recurse so we can discover deeper BECWidgets even if the current widget is not a BECWidget # 3) If it's a Waveform, explicitly print the curves
children = widget.children() if isinstance(widget, Waveform):
for i, child in enumerate(children): for curve in widget.curves:
# Possibly skip known internal child widgets of a QComboBox curve_prefix = prefix + " └─ "
if ( print(
exclude_internal_widgets f"{curve_prefix}{curve.__class__.__name__} ({curve.objectName()}) "
and isinstance(widget, QComboBox) f"parent={widget.objectName()}"
and child.__class__.__name__ in ["QFrame", "QBoxLayout", "QListView"] )
):
# 4) Recursively handle each child if:
# - It's a QWidget
# - It is a BECConnector (or we don't care about filtering)
# - Its closest BECConnector parent is the current widget
for child in widget.findChildren(QWidget):
if only_bec_widgets and not isinstance(child, BECConnector):
continue continue
child_prefix = prefix + " " # if WidgetHierarchy._get_becwidget_ancestor(child) == widget:
arrow = "├─ " if child != children[-1] else "└─ " child_prefix = prefix + " └─ "
# Regardless of whether child is BECWidget or not, keep recursing, or we might miss deeper BECWidgets
WidgetHierarchy.print_widget_hierarchy( WidgetHierarchy.print_widget_hierarchy(
child, child,
indent + 1, indent=indent + 1,
grab_values=grab_values, grab_values=grab_values,
prefix=child_prefix + arrow, prefix=child_prefix,
exclude_internal_widgets=exclude_internal_widgets, exclude_internal_widgets=exclude_internal_widgets,
only_bec_widgets=only_bec_widgets, only_bec_widgets=only_bec_widgets,
show_parent=show_parent, show_parent=show_parent,
) )
@staticmethod
def print_becconnector_hierarchy_from_app():
"""
Enumerate ALL BECConnector objects in the QApplication.
Also detect if a widget is a PlotBase, and add any data items
(PlotDataItem-like) that are also BECConnector objects.
Build a parent->children graph where each child's 'parent'
is its closest BECConnector ancestor. Print the entire hierarchy
from the root(s).
The result is a single, consolidated tree for your entire
running GUI, including PlotBase data items that are BECConnector.
"""
import sys
from collections import defaultdict
from qtpy.QtWidgets import QApplication
from bec_widgets.utils import BECConnector
from bec_widgets.widgets.plots.plot_base import PlotBase
# 1) Gather ALL QWidget-based BECConnector objects
all_qwidgets = QApplication.allWidgets()
bec_widgets = set(w for w in all_qwidgets if isinstance(w, BECConnector))
# 2) Also gather any BECConnector-based data items from PlotBase widgets
for w in all_qwidgets:
if isinstance(w, PlotBase) and hasattr(w, "plot_item"):
plot_item = w.plot_item
if hasattr(plot_item, "listDataItems"):
for data_item in plot_item.listDataItems():
if isinstance(data_item, BECConnector):
bec_widgets.add(data_item)
# 3) Build a map of (closest BECConnector parent) -> list of children
parent_map = defaultdict(list)
for w in bec_widgets:
parent_bec = WidgetHierarchy._get_becwidget_ancestor(w)
parent_map[parent_bec].append(w)
# 4) Define a recursive printer to show each object's children
def print_tree(parent, prefix=""):
children = parent_map[parent]
for i, child in enumerate(children):
connector_class = child.__class__.__name__
connector_name = child.objectName() or connector_class
if parent is None:
parent_label = "None"
else:
parent_label = parent.objectName() or parent.__class__.__name__
line = f"{connector_class} ({connector_name}) parent={parent_label}"
# Determine tree-branch symbols
is_last = i == len(children) - 1
branch_str = "└─ " if is_last else "├─ "
print(prefix + branch_str + line)
# Recurse deeper
next_prefix = prefix + (" " if is_last else "")
print_tree(child, prefix=next_prefix)
# 5) Print top-level items (roots) whose BECConnector parent is None
roots = parent_map[None]
for r_i, root in enumerate(roots):
root_class = root.__class__.__name__
root_name = root.objectName() or root_class
line = f"{root_class} ({root_name}) parent=None"
is_last_root = r_i == len(roots) - 1
print(line)
# Recurse into its children
print_tree(root, prefix=" ")
@staticmethod @staticmethod
def _get_becwidget_ancestor(widget): def _get_becwidget_ancestor(widget):
""" """
Climb the parent chain to find the nearest BECWidget above this widget. Traverse up the parent chain to find the nearest BECConnector.
Returns None if none is found. Returns None if none is found.
""" """
from bec_widgets.utils import BECConnector
parent = widget.parent() parent = widget.parent()
while parent is not None: while parent is not None:
if isinstance(parent, BECWidget): if isinstance(parent, BECConnector):
return parent return parent
parent = parent.parent() parent = parent.parent()
return None return None

View File

@ -133,6 +133,7 @@ class BECDock(BECWidget, Dock):
parent_id: str | None = None, parent_id: str | None = None,
config: DockConfig | None = None, config: DockConfig | None = None,
name: str | None = None, name: str | None = None,
object_name: str | None = None,
client=None, client=None,
gui_id: str | None = None, gui_id: str | None = None,
closable: bool = True, closable: bool = True,
@ -148,12 +149,17 @@ class BECDock(BECWidget, Dock):
if isinstance(config, dict): if isinstance(config, dict):
config = DockConfig(**config) config = DockConfig(**config)
self.config = config self.config = config
super().__init__(
client=client, config=config, gui_id=gui_id, name=name, parent_id=parent_id
) # Name was checked and created in BEC Widget
label = CustomDockLabel(text=name, closable=closable) label = CustomDockLabel(text=name, closable=closable)
Dock.__init__(self, name=name, label=label, parent=self, **kwargs) super().__init__(
# Dock.__init__(self, name=name, **kwargs) parent=parent_dock_area,
name=name,
object_name=object_name,
client=client,
gui_id=gui_id,
config=config,
label=label,
**kwargs,
)
self.parent_dock_area = parent_dock_area self.parent_dock_area = parent_dock_area
# Layout Manager # Layout Manager
@ -193,7 +199,7 @@ class BECDock(BECWidget, Dock):
widgets(dict): The widgets in the dock. widgets(dict): The widgets in the dock.
""" """
# pylint: disable=protected-access # pylint: disable=protected-access
return dict((widget._name, widget) for widget in self.element_list) return dict((widget.object_name, widget) for widget in self.element_list)
@property @property
def element_list(self) -> list[BECWidget]: def element_list(self) -> list[BECWidget]:
@ -296,27 +302,11 @@ class BECDock(BECWidget, Dock):
shift(Literal["down", "up", "left", "right"]): The direction to shift the widgets if the position is occupied. shift(Literal["down", "up", "left", "right"]): The direction to shift the widgets if the position is occupied.
""" """
if row is None: if row is None:
# row = cast(int, self.layout.rowCount()) # type:ignore
row = self.layout.rowCount() row = self.layout.rowCount()
# row = cast(int, row)
if self.layout_manager.is_position_occupied(row, col): if self.layout_manager.is_position_occupied(row, col):
self.layout_manager.shift_widgets(shift, start_row=row) self.layout_manager.shift_widgets(shift, start_row=row)
existing_widgets_parent_dock = self._get_list_of_widget_name_of_parent_dock_area()
if name is not None: # Name is provided
if name in existing_widgets_parent_dock:
# pylint: disable=protected-access
raise ValueError(
f"Name {name} must be unique for widgets, but already exists in DockArea "
f"with name: {self.parent_dock_area._name} and id {self.parent_dock_area.gui_id}."
)
else: # Name is not provided
widget_class_name = widget if isinstance(widget, str) else widget.__class__.__name__
name = WidgetContainerUtils.generate_unique_name(
name=widget_class_name, list_of_names=existing_widgets_parent_dock
)
# Check that Widget is not BECDock or BECDockArea # Check that Widget is not BECDock or BECDockArea
widget_class_name = widget if isinstance(widget, str) else widget.__class__.__name__ widget_class_name = widget if isinstance(widget, str) else widget.__class__.__name__
if widget_class_name in IGNORE_WIDGETS: if widget_class_name in IGNORE_WIDGETS:
@ -326,16 +316,20 @@ class BECDock(BECWidget, Dock):
widget = cast( widget = cast(
BECWidget, BECWidget,
widget_handler.create_widget( widget_handler.create_widget(
widget_type=widget, name=name, parent_dock=self, parent_id=self.gui_id widget_type=widget,
object_name=name,
parent_dock=self,
parent_id=self.gui_id,
parent=self,
), ),
) )
else: else:
widget._name = name # pylint: disable=protected-access widget.object_name = name
self.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan) self.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan)
if hasattr(widget, "config"): if hasattr(widget, "config"):
widget.config.gui_id = widget.gui_id widget.config.gui_id = widget.gui_id
self.config.widgets[widget._name] = widget.config # pylint: disable=protected-access self.config.widgets[widget.object_name] = widget.config
return widget return widget
def move_widget(self, widget: QWidget, new_row: int, new_col: int): def move_widget(self, widget: QWidget, new_row: int, new_col: int):
@ -365,7 +359,7 @@ class BECDock(BECWidget, Dock):
""" """
Remove the dock from the parent dock area. Remove the dock from the parent dock area.
""" """
self.parent_dock_area.delete(self._name) self.parent_dock_area.delete(self.object_name)
def delete(self, widget_name: str) -> None: def delete(self, widget_name: str) -> None:
""" """
@ -375,7 +369,7 @@ class BECDock(BECWidget, Dock):
widget_name(str): Delete the widget with the given name. widget_name(str): Delete the widget with the given name.
""" """
# pylint: disable=protected-access # pylint: disable=protected-access
widgets = [widget for widget in self.widgets if widget._name == widget_name] widgets = [widget for widget in self.widgets if widget.object_name == widget_name]
if len(widgets) == 0: if len(widgets) == 0:
logger.warning( logger.warning(
f"Widget with name {widget_name} not found in dock {self.name()}. " f"Widget with name {widget_name} not found in dock {self.name()}. "
@ -391,7 +385,7 @@ class BECDock(BECWidget, Dock):
else: else:
widget = widgets[0] widget = widgets[0]
self.layout.removeWidget(widget) self.layout.removeWidget(widget)
self.config.widgets.pop(widget._name, None) self.config.widgets.pop(widget.object_name, None)
if widget in self.widgets: if widget in self.widgets:
self.widgets.remove(widget) self.widgets.remove(widget)
widget.close() widget.close()
@ -401,7 +395,7 @@ class BECDock(BECWidget, Dock):
Remove all widgets from the dock. Remove all widgets from the dock.
""" """
for widget in self.widgets: for widget in self.widgets:
self.delete(widget._name) # pylint: disable=protected-access self.delete(widget.object_name)
def cleanup(self): def cleanup(self):
""" """

View File

@ -21,6 +21,7 @@ from bec_widgets.utils.toolbar import (
ModularToolBar, ModularToolBar,
SeparatorAction, SeparatorAction,
) )
from bec_widgets.utils.widget_io import WidgetHierarchy
from bec_widgets.widgets.containers.dock.dock import BECDock, DockConfig from bec_widgets.widgets.containers.dock.dock import BECDock, DockConfig
from bec_widgets.widgets.control.device_control.positioner_box import PositionerBox from bec_widgets.widgets.control.device_control.positioner_box import PositionerBox
from bec_widgets.widgets.control.scan_control.scan_control import ScanControl from bec_widgets.widgets.control.scan_control.scan_control import ScanControl
@ -73,7 +74,7 @@ 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, object_name: str = None,
**kwargs, **kwargs,
) -> None: ) -> None:
if config is None: if config is None:
@ -82,9 +83,15 @@ 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, name=name, **kwargs) super().__init__(
QWidget.__init__(self, parent=parent) parent=parent,
self._parent = parent object_name=object_name,
client=client,
gui_id=gui_id,
config=config,
**kwargs,
)
self._parent = parent # TODO probably not needed
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)
@ -354,17 +361,26 @@ class BECDockArea(BECWidget, QWidget):
Returns: Returns:
BECDock: The created dock. BECDock: The created dock.
""" """
dock_names = [dock._name for dock in self.panel_list] # pylint: disable=protected-access dock_names = [
dock.object_name for dock in self.panel_list
] # pylint: disable=protected-access
if name is not None: # Name is provided if name is not None: # Name is provided
if name in dock_names: if name in dock_names:
raise ValueError( raise ValueError(
f"Name {name} must be unique for docks, but already exists in DockArea " f"Name {name} must be unique for docks, but already exists in DockArea "
f"with name: {self._name} and id {self.gui_id}." f"with name: {self.object_name} and id {self.gui_id}."
) )
else: # Name is not provided else: # Name is not provided
name = WidgetContainerUtils.generate_unique_name(name="dock", 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, parent_id=self.gui_id, closable=closable) dock = BECDock(
parent=self,
name=name, # this is dock name pyqtgraph property, this is displayed on label
object_name=name, # this is a real qt object name passed to BECConnector
parent_dock_area=self,
parent_id=self.gui_id,
closable=closable,
)
dock.config.position = position dock.config.position = position
self.config.docks[dock.name()] = dock.config self.config.docks[dock.name()] = dock.config
# The dock.name is equal to the name passed to BECDock # The dock.name is equal to the name passed to BECDock
@ -499,11 +515,13 @@ if __name__ == "__main__": # pragma: no cover
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="DarkModeButton")
dock_1.new(widget="DarkModeButton")
# 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.new(widget="DarkModeButton")
dock_area.show() dock_area.show()
dock_area.setGeometry(100, 100, 800, 600) dock_area.setGeometry(100, 100, 800, 600)
app.topLevelWidgets() app.topLevelWidgets()
WidgetHierarchy.print_becconnector_hierarchy_from_app()
app.exec_() app.exec_()
sys.exit(app.exec_()) sys.exit(app.exec_())

View File

@ -34,7 +34,6 @@ class LayoutManagerWidget(QWidget):
def __init__(self, parent=None, auto_reindex=True): def __init__(self, parent=None, auto_reindex=True):
super().__init__(parent) super().__init__(parent)
self.setObjectName("LayoutManagerWidget")
self.layout = QGridLayout(self) self.layout = QGridLayout(self)
self.auto_reindex = auto_reindex self.auto_reindex = auto_reindex

View File

@ -20,9 +20,8 @@ logger = bec_logger.logger
class BECMainWindow(BECWidget, QMainWindow): class BECMainWindow(BECWidget, QMainWindow):
def __init__(self, gui_id: str = None, *args, **kwargs): def __init__(self, parent=None, gui_id: str = None, *args, **kwargs):
BECWidget.__init__(self, gui_id=gui_id, **kwargs) super().__init__(parent=parent, gui_id=gui_id, **kwargs)
QMainWindow.__init__(self, *args, **kwargs)
self.app = QApplication.instance() self.app = QApplication.instance()

View File

@ -11,6 +11,7 @@ class AbortButton(BECWidget, QWidget):
PLUGIN = True PLUGIN = True
ICON_NAME = "cancel" ICON_NAME = "cancel"
RPC = False
def __init__( def __init__(
self, self,
@ -22,9 +23,7 @@ class AbortButton(BECWidget, QWidget):
scan_id=None, scan_id=None,
**kwargs, **kwargs,
): ):
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs) super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
QWidget.__init__(self, parent=parent)
self.get_bec_shortcuts() self.get_bec_shortcuts()
self.layout = QHBoxLayout(self) self.layout = QHBoxLayout(self)

View File

@ -11,11 +11,10 @@ class ResetButton(BECWidget, QWidget):
PLUGIN = True PLUGIN = True
ICON_NAME = "restart_alt" ICON_NAME = "restart_alt"
RPC = False
def __init__(self, parent=None, client=None, config=None, gui_id=None, toolbar=False, **kwargs): def __init__(self, parent=None, client=None, config=None, gui_id=None, toolbar=False, **kwargs):
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs) super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
QWidget.__init__(self, parent=parent)
self.get_bec_shortcuts() self.get_bec_shortcuts()
self.layout = QHBoxLayout(self) self.layout = QHBoxLayout(self)

View File

@ -11,10 +11,10 @@ class ResumeButton(BECWidget, QWidget):
PLUGIN = True PLUGIN = True
ICON_NAME = "resume" ICON_NAME = "resume"
RPC = False
def __init__(self, parent=None, client=None, config=None, gui_id=None, toolbar=False, **kwargs): def __init__(self, parent=None, client=None, config=None, gui_id=None, toolbar=False, **kwargs):
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs) super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
QWidget.__init__(self, parent=parent)
self.get_bec_shortcuts() self.get_bec_shortcuts()

View File

@ -11,10 +11,10 @@ class StopButton(BECWidget, QWidget):
PLUGIN = True PLUGIN = True
ICON_NAME = "dangerous" ICON_NAME = "dangerous"
RPC = False
def __init__(self, parent=None, client=None, config=None, gui_id=None, toolbar=False, **kwargs): def __init__(self, parent=None, client=None, config=None, gui_id=None, toolbar=False, **kwargs):
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs) super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
QWidget.__init__(self, parent=parent)
self.get_bec_shortcuts() self.get_bec_shortcuts()

View File

@ -13,8 +13,8 @@ class PositionIndicator(BECWidget, QWidget):
ICON_NAME = "horizontal_distribute" ICON_NAME = "horizontal_distribute"
def __init__(self, parent=None, client=None, config=None, gui_id=None, **kwargs): def __init__(self, parent=None, client=None, config=None, gui_id=None, **kwargs):
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs) super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
QWidget.__init__(self, parent=parent)
self.position = 50 self.position = 50
self.min_value = 0 self.min_value = 0
self.max_value = 100 self.max_value = 100

View File

@ -47,6 +47,7 @@ class PositionerBoxBase(BECWidget, CompactPopupWidget):
current_path = "" current_path = ""
ICON_NAME = "switch_right" ICON_NAME = "switch_right"
RPC = False
def __init__(self, parent=None, **kwargs): def __init__(self, parent=None, **kwargs):
"""Initialize the PositionerBox widget. """Initialize the PositionerBox widget.
@ -55,8 +56,7 @@ class PositionerBoxBase(BECWidget, CompactPopupWidget):
parent: The parent widget. parent: The parent widget.
device (Positioner): The device to control. device (Positioner): The device to control.
""" """
super().__init__(**kwargs) super().__init__(parent=parent, layout=QVBoxLayout, **kwargs)
CompactPopupWidget.__init__(self, parent=parent, layout=QVBoxLayout)
self._dialog = None self._dialog = None
self.get_bec_shortcuts() self.get_bec_shortcuts()

View File

@ -69,8 +69,7 @@ class PositionerGroup(BECWidget, QWidget):
Args: Args:
parent: The parent widget. parent: The parent widget.
""" """
super().__init__(**kwargs) super().__init__(parent=parent, **kwargs)
QWidget.__init__(self, parent)
self.get_bec_shortcuts() self.get_bec_shortcuts()

View File

@ -29,6 +29,7 @@ class DeviceSignalInputBase(BECWidget):
signal object based on the current text of the widget. signal object based on the current text of the widget.
""" """
RPC = False
_filter_handler = { _filter_handler = {
Kind.hinted: "include_hinted_signals", Kind.hinted: "include_hinted_signals",
Kind.normal: "include_normal_signals", Kind.normal: "include_normal_signals",

View File

@ -47,8 +47,7 @@ class DeviceComboBox(DeviceInputBase, QComboBox):
arg_name: str | None = None, arg_name: str | None = None,
**kwargs, **kwargs,
): ):
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs) super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
QComboBox.__init__(self, parent=parent)
if arg_name is not None: if arg_name is not None:
self.config.arg_name = arg_name self.config.arg_name = arg_name
self.arg_name = arg_name self.arg_name = arg_name

View File

@ -53,8 +53,7 @@ class DeviceLineEdit(DeviceInputBase, QLineEdit):
self._callback_id = None self._callback_id = None
self._is_valid_input = False self._is_valid_input = False
self._accent_colors = get_accent_colors() self._accent_colors = get_accent_colors()
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs) super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
QLineEdit.__init__(self, parent=parent)
self.completer = QCompleter(self) self.completer = QCompleter(self)
self.setCompleter(self.completer) self.setCompleter(self.completer)

View File

@ -40,8 +40,7 @@ class SignalComboBox(DeviceSignalInputBase, QComboBox):
arg_name: str | None = None, arg_name: str | None = None,
**kwargs, **kwargs,
): ):
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs) super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
QComboBox.__init__(self, parent=parent)
if arg_name is not None: if arg_name is not None:
self.config.arg_name = arg_name self.config.arg_name = arg_name
self.arg_name = arg_name self.arg_name = arg_name

View File

@ -42,8 +42,7 @@ class SignalLineEdit(DeviceSignalInputBase, QLineEdit):
**kwargs, **kwargs,
): ):
self._is_valid_input = False self._is_valid_input = False
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs) super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
QLineEdit.__init__(self, parent=parent)
self._accent_colors = get_accent_colors() self._accent_colors = get_accent_colors()
self.completer = QCompleter(self) self.completer = QCompleter(self)
self.setCompleter(self.completer) self.setCompleter(self.completer)

View File

@ -65,8 +65,7 @@ class ScanControl(BECWidget, QWidget):
config = ScanControlConfig( config = ScanControlConfig(
widget_class=self.__class__.__name__, allowed_scans=allowed_scans widget_class=self.__class__.__name__, allowed_scans=allowed_scans
) )
super().__init__(client=client, gui_id=gui_id, config=config, **kwargs) super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
QWidget.__init__(self, parent=parent)
self._hide_add_remove_buttons = False self._hide_add_remove_buttons = False

View File

@ -44,8 +44,7 @@ class DapComboBox(BECWidget, QWidget):
default_fit: str | None = None, default_fit: str | None = None,
**kwargs, **kwargs,
): ):
super().__init__(client=client, gui_id=gui_id, **kwargs) super().__init__(parent=parent, client=client, gui_id=gui_id, **kwargs)
QWidget.__init__(self, parent=parent)
self.layout = QVBoxLayout(self) self.layout = QVBoxLayout(self)
self.fit_model_combobox = QComboBox(self) self.fit_model_combobox = QComboBox(self)
self.layout.addWidget(self.fit_model_combobox) self.layout.addWidget(self.fit_model_combobox)

View File

@ -17,6 +17,7 @@ class LMFitDialog(BECWidget, QWidget):
PLUGIN = True PLUGIN = True
ICON_NAME = "monitoring" ICON_NAME = "monitoring"
RPC = False
# Signal to emit the currently selected fit curve_id # Signal to emit the currently selected fit curve_id
selected_fit = Signal(str) selected_fit = Signal(str)
# Signal to emit a move action in form of a tuple (param_name, value) # Signal to emit a move action in form of a tuple (param_name, value)
@ -43,10 +44,8 @@ class LMFitDialog(BECWidget, QWidget):
gui_id (str): GUI ID. gui_id (str): GUI ID.
ui_file (str): The UI file to be loaded. ui_file (str): The UI file to be loaded.
""" """
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs) super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
QWidget.__init__(self, parent=parent)
self.setProperty("skip_settings", True) self.setProperty("skip_settings", True)
self.setObjectName("LMFitDialog")
self._ui_file = ui_file self._ui_file = ui_file
self.target_widget = target_widget self.target_widget = target_widget

View File

@ -45,6 +45,7 @@ class ScanMetadata(BECWidget, QWidget):
metadata_updated = Signal(dict) metadata_updated = Signal(dict)
metadata_cleared = Signal(NoneType) metadata_cleared = Signal(NoneType)
RPC = False
def __init__( def __init__(
self, self,
@ -54,8 +55,7 @@ class ScanMetadata(BECWidget, QWidget):
initial_extras: list[list[str]] | None = None, initial_extras: list[list[str]] | None = None,
**kwargs, **kwargs,
): ):
super().__init__(client=client, **kwargs) super().__init__(parent=parent, client=client, **kwargs)
QWidget.__init__(self, parent=parent)
self.set_schema(scan_name) self.set_schema(scan_name)

View File

@ -49,8 +49,7 @@ class TextBox(BECWidget, QWidget):
if isinstance(config, dict): if isinstance(config, dict):
config = TextBoxConfig(**config) config = TextBoxConfig(**config)
self.config = config self.config = config
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs) super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
QWidget.__init__(self, parent)
self.layout = QVBoxLayout(self) self.layout = QVBoxLayout(self)
self.text_box_text_edit = QTextEdit(parent=self) self.text_box_text_edit = QTextEdit(parent=self)
self.layout.addWidget(self.text_box_text_edit) self.layout.addWidget(self.text_box_text_edit)

View File

@ -26,8 +26,7 @@ class WebsiteWidget(BECWidget, QWidget):
def __init__( def __init__(
self, parent=None, url: str = None, config=None, client=None, gui_id=None, **kwargs self, parent=None, url: str = None, config=None, client=None, gui_id=None, **kwargs
): ):
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs) super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
QWidget.__init__(self, parent=parent)
layout = QVBoxLayout() layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0) layout.setContentsMargins(0, 0, 0, 0)
self.website = QWebEngineView() self.website = QWebEngineView()

View File

@ -144,10 +144,10 @@ class Minesweeper(BECWidget, QWidget):
PLUGIN = True PLUGIN = True
ICON_NAME = "videogame_asset" ICON_NAME = "videogame_asset"
USER_ACCESS = [] USER_ACCESS = []
RPC = False
def __init__(self, parent=None, *args, **kwargs): def __init__(self, parent=None, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(parent=parent, *args, **kwargs)
QWidget.__init__(self, parent=parent)
self._ui_initialised = False self._ui_initialised = False
self._timer_start_num_seconds = 0 self._timer_start_num_seconds = 0

View File

@ -125,16 +125,15 @@ class Image(PlotBase):
popups: bool = True, popups: bool = True,
**kwargs, **kwargs,
): ):
self._main_image = ImageItem(parent_image=self)
self._color_bar = None
if config is None: if config is None:
config = ImageConfig(widget_class=self.__class__.__name__) config = ImageConfig(widget_class=self.__class__.__name__)
self.gui_id = config.gui_id
self._color_bar = None
self._main_image = ImageItem()
super().__init__( super().__init__(
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
) )
self._main_image.parent_image = self
# For PropertyManager identification
self.setObjectName("Image")
self.plot_item.addItem(self._main_image) self.plot_item.addItem(self._main_image)
self.scan_id = None self.scan_id = None

View File

@ -82,10 +82,12 @@ class ImageItem(BECConnector, pg.ImageItem):
self.config = config self.config = config
else: else:
self.config = config self.config = config
super().__init__(config=config, gui_id=gui_id) if parent_image is not None:
pg.ImageItem.__init__(self) self.set_parent(parent_image)
else:
self.parent_image = parent_image self.parent_image = None
self.parent_id = None
super().__init__(config=config, gui_id=gui_id, **kwargs)
self.raw_data = None self.raw_data = None
self.buffer = [] self.buffer = []
@ -94,6 +96,13 @@ class ImageItem(BECConnector, pg.ImageItem):
# Image processor will handle any setting of data # Image processor will handle any setting of data
self._image_processor = ImageProcessor(config=self.config.processing) self._image_processor = ImageProcessor(config=self.config.processing)
def set_parent(self, parent: BECConnector):
self.parent_image = parent
self.parent_id = parent.gui_id
def parent(self):
return self.parent_image
def set_data(self, data: np.ndarray): def set_data(self, data: np.ndarray):
self.raw_data = data self.raw_data = data
self._process_image() self._process_image()

View File

@ -161,9 +161,6 @@ class MotorMap(PlotBase):
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
) )
# For PropertyManager identification
self.setObjectName("MotorMap")
# Default values for PlotBase # Default values for PlotBase
self.x_grid = True self.x_grid = True
self.y_grid = True self.y_grid = True

View File

@ -20,7 +20,6 @@ class MotorMapSettings(SettingWidget):
super().__init__(parent=parent, *args, **kwargs) super().__init__(parent=parent, *args, **kwargs)
self.setProperty("skip_settings", True) self.setProperty("skip_settings", True)
self.setObjectName("MotorMapSettings")
current_path = os.path.dirname(__file__) current_path = os.path.dirname(__file__)
form = UILoader().load_ui(os.path.join(current_path, "motor_map_settings.ui"), self) form = UILoader().load_ui(os.path.join(current_path, "motor_map_settings.ui"), self)

View File

@ -126,9 +126,6 @@ class MultiWaveform(PlotBase):
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
) )
# For PropertyManager identification
self.setObjectName("MultiWaveform")
# Scan Data # Scan Data
self.old_scan_id = None self.old_scan_id = None
self.scan_id = None self.scan_id = None

View File

@ -20,7 +20,6 @@ class MultiWaveformControlPanel(SettingWidget):
super().__init__(parent=parent, *args, **kwargs) super().__init__(parent=parent, *args, **kwargs)
self.setProperty("skip_settings", True) self.setProperty("skip_settings", True)
self.setObjectName("MultiWaveformControlPanel")
current_path = os.path.dirname(__file__) current_path = os.path.dirname(__file__)
form = UILoader().load_ui(os.path.join(current_path, "multi_waveform_controls.ui"), self) form = UILoader().load_ui(os.path.join(current_path, "multi_waveform_controls.ui"), self)

View File

@ -74,11 +74,9 @@ class PlotBase(BECWidget, QWidget):
) -> None: ) -> None:
if config is None: if config is None:
config = ConnectionConfig(widget_class=self.__class__.__name__) config = ConnectionConfig(widget_class=self.__class__.__name__)
super().__init__(client=client, gui_id=gui_id, config=config, **kwargs) super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
QWidget.__init__(self, parent=parent)
# For PropertyManager identification # For PropertyManager identification
self.setObjectName("PlotBase")
self.get_bec_shortcuts() self.get_bec_shortcuts()
# Layout Management # Layout Management
@ -1018,7 +1016,7 @@ if __name__ == "__main__": # pragma: no cover:
from qtpy.QtWidgets import QApplication from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv) app = QApplication(sys.argv)
window = DemoPlotBase() window = PlotBase()
window.show() window.show()
sys.exit(app.exec_()) sys.exit(app.exec_())

View File

@ -77,13 +77,16 @@ class ScatterCurve(BECConnector, pg.PlotDataItem):
else: else:
self.config = config self.config = config
name = config.label name = config.label
super().__init__(config=config, gui_id=gui_id)
pg.PlotDataItem.__init__(self, name=name)
self.parent_item = parent_item self.parent_item = parent_item
self.parent_id = self.parent_item.gui_id
super().__init__(name=name, config=config, gui_id=gui_id, **kwargs)
self.data_z = None # color scaling needs to be cashed for changing colormap self.data_z = None # color scaling needs to be cashed for changing colormap
self.apply_config() self.apply_config()
def parent(self):
return self.parent_item
def apply_config(self, config: dict | ScatterCurveConfig | None = None, **kwargs) -> None: def apply_config(self, config: dict | ScatterCurveConfig | None = None, **kwargs) -> None:
""" """
Apply the configuration to the curve. Apply the configuration to the curve.

View File

@ -111,8 +111,6 @@ class ScatterWaveform(PlotBase):
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
) )
self._main_curve = ScatterCurve(parent_item=self) self._main_curve = ScatterCurve(parent_item=self)
# For PropertyManager identification
self.setObjectName("ScatterWaveform")
# Specific GUI elements # Specific GUI elements
self.scatter_dialog = None self.scatter_dialog = None

View File

@ -15,7 +15,6 @@ class ScatterCurveSettings(SettingWidget):
# and should mirror what is in the target widget. # and should mirror what is in the target widget.
# Saving settings for this widget could result in recursively setting the target widget. # Saving settings for this widget could result in recursively setting the target widget.
self.setProperty("skip_settings", True) self.setProperty("skip_settings", True)
self.setObjectName("ScatterCurveSettings")
current_path = os.path.dirname(__file__) current_path = os.path.dirname(__file__)
if popup: if popup:

View File

@ -16,7 +16,6 @@ class AxisSettings(SettingWidget):
# and should mirror what is in the target widget. # and should mirror what is in the target widget.
# Saving settings for this widget could result in recursively setting the target widget. # Saving settings for this widget could result in recursively setting the target widget.
self.setProperty("skip_settings", True) self.setProperty("skip_settings", True)
self.setObjectName("AxisSettings")
current_path = os.path.dirname(__file__) current_path = os.path.dirname(__file__)
if popup: if popup:
form = UILoader().load_ui( form = UILoader().load_ui(

View File

@ -91,10 +91,10 @@ class Curve(BECConnector, pg.PlotDataItem):
self.config = config self.config = config
else: else:
self.config = config self.config = config
super().__init__(config=config, gui_id=gui_id)
pg.PlotDataItem.__init__(self, name=name)
self.parent_item = parent_item self.parent_item = parent_item
self.parent_id = self.parent_item.gui_id
super().__init__(name=name, config=config, gui_id=gui_id, **kwargs)
self.apply_config() self.apply_config()
self.dap_params = None self.dap_params = None
self.dap_summary = None self.dap_summary = None
@ -104,6 +104,9 @@ class Curve(BECConnector, pg.PlotDataItem):
# Activate setClipToView, to boost performance for large datasets per default # Activate setClipToView, to boost performance for large datasets per default
self.setClipToView(True) self.setClipToView(True)
def parent(self):
return self.parent_item
def apply_config(self, config: dict | CurveConfig | None = None, **kwargs) -> None: def apply_config(self, config: dict | CurveConfig | None = None, **kwargs) -> None:
""" """
Apply the configuration to the curve. Apply the configuration to the curve.

View File

@ -334,11 +334,11 @@ class CurveTree(BECWidget, QWidget):
client=None, client=None,
gui_id: str | None = None, gui_id: str | None = None,
waveform: Waveform | None = None, waveform: Waveform | None = None,
**kwargs,
) -> None: ) -> None:
if config is None: if config is None:
config = ConnectionConfig(widget_class=self.__class__.__name__) config = ConnectionConfig(widget_class=self.__class__.__name__)
super().__init__(client=client, gui_id=gui_id, config=config) super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
QWidget.__init__(self, parent=parent)
self.waveform = waveform self.waveform = waveform
if self.waveform and hasattr(self.waveform, "color_palette"): if self.waveform and hasattr(self.waveform, "color_palette"):

View File

@ -9,8 +9,15 @@ import pyqtgraph as pg
from bec_lib import bec_logger, messages from bec_lib import bec_logger, messages
from bec_lib.endpoints import MessageEndpoints from bec_lib.endpoints import MessageEndpoints
from pydantic import Field, ValidationError, field_validator from pydantic import Field, ValidationError, field_validator
from qtpy.QtCore import QTimer, Signal, Slot from qtpy.QtCore import QTimer, Signal
from qtpy.QtWidgets import QDialog, QHBoxLayout, QMainWindow, QVBoxLayout, QWidget from qtpy.QtWidgets import (
QApplication,
QDialog,
QHBoxLayout,
QMainWindow,
QVBoxLayout,
QWidget,
)
from bec_widgets.utils import ConnectionConfig from bec_widgets.utils import ConnectionConfig
from bec_widgets.utils.bec_signal_proxy import BECSignalProxy from bec_widgets.utils.bec_signal_proxy import BECSignalProxy
@ -18,7 +25,6 @@ from bec_widgets.utils.colors import Colors, set_theme
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.utils.settings_dialog import SettingsDialog from bec_widgets.utils.settings_dialog import SettingsDialog
from bec_widgets.utils.toolbar import MaterialIconAction from bec_widgets.utils.toolbar import MaterialIconAction
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
from bec_widgets.widgets.dap.lmfit_dialog.lmfit_dialog import LMFitDialog from bec_widgets.widgets.dap.lmfit_dialog.lmfit_dialog import LMFitDialog
from bec_widgets.widgets.plots.plot_base import PlotBase from bec_widgets.widgets.plots.plot_base import PlotBase
from bec_widgets.widgets.plots.waveform.curve import Curve, CurveConfig, DeviceSignal from bec_widgets.widgets.plots.waveform.curve import Curve, CurveConfig, DeviceSignal
@ -129,9 +135,6 @@ class Waveform(PlotBase):
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
) )
# For PropertyManager identification
self.setObjectName("Waveform")
# Curve data # Curve data
self._sync_curves = [] self._sync_curves = []
self._async_curves = [] self._async_curves = []
@ -1737,12 +1740,12 @@ class Waveform(PlotBase):
super().cleanup() super().cleanup()
class DemoApp(BECMainWindow): # pragma: no cover class DemoApp(QMainWindow): # pragma: no cover
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.setWindowTitle("Waveform Demo") self.setWindowTitle("Waveform Demo")
self.resize(800, 600) self.resize(800, 600)
self.main_widget = QWidget() self.main_widget = QWidget(self)
self.layout = QHBoxLayout(self.main_widget) self.layout = QHBoxLayout(self.main_widget)
self.setCentralWidget(self.main_widget) self.setCentralWidget(self.main_widget)
@ -1760,9 +1763,7 @@ class DemoApp(BECMainWindow): # pragma: no cover
if __name__ == "__main__": # pragma: no cover if __name__ == "__main__": # pragma: no cover
import sys import sys
from bec_widgets.utils.bec_qapp import BECApplication app = QApplication(sys.argv)
app = BECApplication(sys.argv)
set_theme("dark") set_theme("dark")
widget = DemoApp() widget = DemoApp()
widget.show() widget.show()

View File

@ -25,8 +25,7 @@ class BECProgressBar(BECWidget, QWidget):
ICON_NAME = "page_control" ICON_NAME = "page_control"
def __init__(self, parent=None, client=None, config=None, gui_id=None, **kwargs): def __init__(self, parent=None, client=None, config=None, gui_id=None, **kwargs):
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs) super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
QWidget.__init__(self, parent=parent)
accent_colors = get_accent_colors() accent_colors = get_accent_colors()

View File

@ -6,6 +6,7 @@ from bec_lib.endpoints import EndpointInfo, MessageEndpoints
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator
from pydantic_core import PydanticCustomError from pydantic_core import PydanticCustomError
from qtpy import QtGui from qtpy import QtGui
from qtpy.QtCore import QObject
from bec_widgets.utils import BECConnector, ConnectionConfig from bec_widgets.utils import BECConnector, ConnectionConfig
@ -77,7 +78,7 @@ class RingConfig(ProgressbarConfig):
) )
class Ring(BECConnector): class Ring(BECConnector, QObject):
USER_ACCESS = [ USER_ACCESS = [
"_get_all_rpc", "_get_all_rpc",
"_rpc_id", "_rpc_id",
@ -108,7 +109,7 @@ class Ring(BECConnector):
if isinstance(config, dict): if isinstance(config, dict):
config = RingConfig(**config) config = RingConfig(**config)
self.config = config self.config = config
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs) super().__init__(parent=parent, client=client, config=config, gui_id=gui_id, **kwargs)
self.parent_progress_widget = parent_progress_widget self.parent_progress_widget = parent_progress_widget
self.color = None self.color = None

View File

@ -110,8 +110,7 @@ class RingProgressBar(BECWidget, QWidget):
if isinstance(config, dict): if isinstance(config, dict):
config = RingProgressBarConfig(**config, widget_class=self.__class__.__name__) config = RingProgressBarConfig(**config, widget_class=self.__class__.__name__)
self.config = config self.config = config
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs) super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
QWidget.__init__(self, parent=parent)
self.get_bec_shortcuts() self.get_bec_shortcuts()
self.entry_validator = EntryValidator(self.dev) self.entry_validator = EntryValidator(self.dev)

View File

@ -44,8 +44,9 @@ class BECQueue(BECWidget, CompactPopupWidget):
refresh_upon_start: bool = True, refresh_upon_start: bool = True,
**kwargs, **kwargs,
): ):
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs) super().__init__(
CompactPopupWidget.__init__(self, parent=parent, layout=QVBoxLayout) parent=parent, layout=QVBoxLayout, client=client, gui_id=gui_id, config=config, **kwargs
)
self.layout.setSpacing(0) self.layout.setSpacing(0)
self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setContentsMargins(0, 0, 0, 0)

View File

@ -89,8 +89,7 @@ class BECStatusBox(BECWidget, CompactPopupWidget):
gui_id: str = None, gui_id: str = None,
**kwargs, **kwargs,
): ):
super().__init__(client=client, gui_id=gui_id, **kwargs) super().__init__(parent=parent, layout=QHBoxLayout, client=client, gui_id=gui_id, **kwargs)
CompactPopupWidget.__init__(self, parent=parent, layout=QHBoxLayout)
self.box_name = box_name self.box_name = box_name
self.status_container = defaultdict(lambda: {"info": None, "item": None, "widget": None}) self.status_container = defaultdict(lambda: {"info": None, "item": None, "widget": None})

View File

@ -25,8 +25,7 @@ class DeviceBrowser(BECWidget, QWidget):
gui_id: Optional[str] = None, gui_id: Optional[str] = None,
**kwargs, **kwargs,
) -> None: ) -> None:
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs) super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
QWidget.__init__(self, parent)
self.get_bec_shortcuts() self.get_bec_shortcuts()
self.ui = None self.ui = None

View File

@ -103,8 +103,8 @@ class BecLogsQueue:
self._display_queue.append(self._line_formatter(_msg)) self._display_queue.append(self._line_formatter(_msg))
if self._new_message_signal: if self._new_message_signal:
self._new_message_signal.emit() self._new_message_signal.emit()
except Exception as e: except Exception:
logger.warning(f"Error in LogPanel incoming message callback: {e}") pass
def _set_formatter_and_update_filter(self, line_formatter: LineFormatter = noop_format): def _set_formatter_and_update_filter(self, line_formatter: LineFormatter = noop_format):
self._line_formatter: LineFormatter = line_formatter self._line_formatter: LineFormatter = line_formatter

View File

@ -30,10 +30,8 @@ class BECSpinBox(BECWidget, QDoubleSpinBox):
) -> None: ) -> None:
if config is None: if config is None:
config = ConnectionConfig(widget_class=self.__class__.__name__) config = ConnectionConfig(widget_class=self.__class__.__name__)
super().__init__(client=client, gui_id=gui_id, config=config, **kwargs) super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
QDoubleSpinBox.__init__(self, parent=parent)
self.setObjectName("BECSpinBox")
# Make the widget as compact as possible horizontally. # Make the widget as compact as possible horizontally.
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
self.setAlignment(Qt.AlignHCenter) self.setAlignment(Qt.AlignHCenter)

View File

@ -9,13 +9,11 @@ from bec_widgets.utils.bec_widget import BECWidget
class BECColorMapWidget(BECWidget, QWidget): class BECColorMapWidget(BECWidget, QWidget):
colormap_changed_signal = Signal(str) colormap_changed_signal = Signal(str)
ICON_NAME = "palette" ICON_NAME = "palette"
USER_ACCESS = ["colormap"]
PLUGIN = True PLUGIN = True
RPC = False
def __init__(self, parent=None, cmap: str = "magma", **kwargs): def __init__(self, parent=None, cmap: str = "magma", **kwargs):
super().__init__(**kwargs) super().__init__(parent=parent, **kwargs)
QWidget.__init__(self, parent=parent)
# Create the ColorMapButton # Create the ColorMapButton
self.button = ColorMapButton() self.button = ColorMapButton()

View File

@ -9,10 +9,10 @@ from bec_widgets.utils.colors import set_theme
class DarkModeButton(BECWidget, QWidget): class DarkModeButton(BECWidget, QWidget):
USER_ACCESS = ["toggle_dark_mode"]
ICON_NAME = "dark_mode" ICON_NAME = "dark_mode"
PLUGIN = True PLUGIN = True
RPC = False
def __init__( def __init__(
self, self,
@ -22,8 +22,7 @@ class DarkModeButton(BECWidget, QWidget):
toolbar: bool = False, toolbar: bool = False,
**kwargs, **kwargs,
) -> None: ) -> None:
super().__init__(client=client, gui_id=gui_id, theme_update=True, **kwargs) super().__init__(parent=parent, client=client, gui_id=gui_id, theme_update=True, **kwargs)
QWidget.__init__(self, parent)
self._dark_mode_enabled = False self._dark_mode_enabled = False
self.layout = QHBoxLayout(self) self.layout = QHBoxLayout(self)
@ -99,9 +98,6 @@ class DarkModeButton(BECWidget, QWidget):
if __name__ == "__main__": if __name__ == "__main__":
from qtpy.QtWidgets import QApplication
from bec_widgets.utils.colors import set_theme
app = QApplication([]) app = QApplication([])
set_theme("auto") set_theme("auto")

View File

@ -130,11 +130,11 @@ class ExamplePlotWidget(BECWidget, QWidget):
config: ConnectionConfig | None = None, config: ConnectionConfig | None = None,
client=None, client=None,
gui_id: str | None = None, gui_id: str | None = None,
**kwargs,
) -> None: ) -> None:
if config is None: if config is None:
config = ConnectionConfig(widget_class=self.__class__.__name__) config = ConnectionConfig(widget_class=self.__class__.__name__)
super().__init__(client=client, gui_id=gui_id, config=config) super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
QWidget.__init__(self, parent=parent)
self.layout = QVBoxLayout(self) self.layout = QVBoxLayout(self)
self.glw = pg.GraphicsLayoutWidget() self.glw = pg.GraphicsLayoutWidget()

View File

@ -17,9 +17,8 @@ from .conftest import create_widget
class DeviceInputWidget(DeviceInputBase, QWidget): class DeviceInputWidget(DeviceInputBase, QWidget):
"""Thin wrapper around DeviceInputBase to make it a QWidget""" """Thin wrapper around DeviceInputBase to make it a QWidget"""
def __init__(self, parent=None, client=None, config=None, gui_id=None): def __init__(self, parent=None, client=None, config=None, gui_id=None, **kwargs):
super().__init__(client=client, config=config, gui_id=gui_id) super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
QWidget.__init__(self, parent=parent)
@pytest.fixture @pytest.fixture

View File

@ -22,8 +22,7 @@ class DeviceInputWidget(DeviceSignalInputBase, QWidget):
"""Thin wrapper around DeviceInputBase to make it a QWidget""" """Thin wrapper around DeviceInputBase to make it a QWidget"""
def __init__(self, parent=None, client=None, config=None, gui_id=None): def __init__(self, parent=None, client=None, config=None, gui_id=None):
super().__init__(client=client, config=config, gui_id=gui_id) super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
QWidget.__init__(self, parent=parent)
@pytest.fixture @pytest.fixture

View File

@ -5,7 +5,7 @@ from bec_widgets.cli.rpc.rpc_base import DeletedWidgetError, RPCBase, RPCReferen
@pytest.fixture @pytest.fixture
def rpc_base(): def rpc_base():
yield RPCBase(gui_id="rpc_base_test", name="test") yield RPCBase(gui_id="rpc_base_test", object_name="test")
def test_rpc_base(rpc_base): def test_rpc_base(rpc_base):