0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-13 19:21: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
def dock_area(name: str | None = None):
_dock_area = BECDockArea(name=name)
def dock_area(object_name: str | None = None):
_dock_area = BECDockArea(object_name=object_name)
return _dock_area

View File

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

View File

@ -10,7 +10,7 @@ import threading
import time
from contextlib import contextmanager
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.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
if TYPE_CHECKING: # pragma: no cover
from bec_lib.redis_connector import StreamMessage
from bec_lib.messages import GUIRegistryStateMessage
else:
StreamMessage = lazy_import_from("bec_lib.redis_connector", ("StreamMessage",))
GUIRegistryStateMessage = lazy_import_from("bec_lib.messages", "GUIRegistryStateMessage")
logger = bec_logger.logger
@ -71,7 +71,11 @@ def _get_output(process, logger) -> None:
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]:
"""
Start the plot in a new process.
@ -199,7 +203,7 @@ class BECGuiClient(RPCBase):
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self._lock = Lock()
self._default_dock_name = "bec"
self._anchor_widget = "launcher"
self._auto_updates_enabled = True
self._auto_updates = None
self._killed = False
@ -220,7 +224,7 @@ class BECGuiClient(RPCBase):
@property
def launcher(self) -> RPCBase:
"""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:
"""Connect to a GUI server"""
@ -247,7 +251,7 @@ class BECGuiClient(RPCBase):
@property
def windows(self) -> dict:
"""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
def window_list(self) -> list:
@ -365,7 +369,7 @@ class BECGuiClient(RPCBase):
# After 60s timeout. Should this raise an exception on timeout?
while time.time() < time.time() + timeout:
if len(list(self._server_registry.keys())) < 2 or not hasattr(
self, self._default_dock_name
self, self._anchor_widget
):
time.sleep(0.1)
else:
@ -383,7 +387,7 @@ class BECGuiClient(RPCBase):
self._gui_started_event.clear()
self._process, self._process_output_processing_thread = _start_plot_process(
self._gui_id,
gui_class_id=self._default_dock_name,
gui_class_id="bec",
config=self._client._service_config.config, # pylint: disable=protected-access
logger=logger,
)
@ -413,11 +417,13 @@ class BECGuiClient(RPCBase):
return self._start_server(wait=wait)
@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.
# with self._lock:
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)
def _do_show_all(self):
@ -460,12 +466,11 @@ class BECGuiClient(RPCBase):
for gui_id, widget in self._ipython_registry.items():
if gui_id not in server_registry:
remove_from_registry.append(gui_id)
widget._refresh_references()
for gui_id in remove_from_registry:
self._ipython_registry.pop(gui_id)
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:
@ -475,10 +480,13 @@ class BECGuiClient(RPCBase):
delattr(self, widget_name)
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
for widget in self._ipython_registry.values():
widget._refresh_references()
def _add_widget(self, state: dict, parent: object) -> RPCReference | None:
"""Add a widget to the namespace
@ -486,7 +494,7 @@ class BECGuiClient(RPCBase):
state (dict): The state of the widget from the _server_registry.
parent (object): The parent object.
"""
name = state["name"]
object_name = state["object_name"]
gui_id = state["gui_id"]
if state["widget_class"] in IGNORE_WIDGETS:
return
@ -495,7 +503,7 @@ class BECGuiClient(RPCBase):
return
obj = self._ipython_registry.get(gui_id)
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
else:
widget = obj

View File

@ -4,7 +4,7 @@ import inspect
import threading
import uuid
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.endpoints import MessageEndpoints
@ -41,7 +41,7 @@ def rpc_call(func):
def wrapper(self, *args, **kwargs):
# we could rely on a strict type check here, but this is more flexible
# moreover, it would anyway crash for objects...
caller_frame = inspect.currentframe().f_back
caller_frame = inspect.currentframe().f_back # type: ignore
while caller_frame:
if "jedi" in caller_frame.f_globals:
# Jedi module is present, likely tab completion
@ -91,7 +91,7 @@ class RPCReference:
def __init__(self, registry: dict, gui_id: str) -> None:
self._registry = registry
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
def __getattr__(self, name):
@ -134,13 +134,13 @@ class RPCBase:
self,
gui_id: str | None = None,
config: dict | None = None,
name: str | None = None,
object_name: str | None = None,
parent=None,
) -> None:
self._client = BECClient() # BECClient is a singleton; here, we simply get the instance
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._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._msg_wait_event = threading.Event()
self._rpc_response = None
@ -163,7 +163,7 @@ class RPCBase:
"""
Get the widget name.
"""
return self._name
return self.object_name
@property
def _root(self) -> BECGuiClient:
@ -175,7 +175,7 @@ class RPCBase:
# pylint: disable=protected-access
while parent._parent is not None:
parent = parent._parent
return parent
return parent # type: ignore
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(
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:
raise ValueError(self._rpc_response.message["error"])
msg_result = self._rpc_response.message.get("result")
@ -227,8 +231,8 @@ class RPCBase:
return self._create_widget_from_msg_result(msg_result)
@staticmethod
def _on_rpc_response(msg: MessageObject, parent: RPCBase) -> None:
msg = msg.value
def _on_rpc_response(msg_obj: MessageObject, parent: RPCBase) -> None:
msg = cast(messages.RequestResponseMessage, msg_obj.value)
parent._msg_wait_event.set()
parent._rpc_response = msg
@ -283,12 +287,17 @@ class RPCBase:
for key, val in self._root._server_registry.items():
parent_id = val["config"].get("parent_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())
for key in removed_references:
delattr(self, self._rpc_references[key]["name"])
delattr(self, self._rpc_references[key]["object_name"])
self._rpc_references = references
for key, val in references.items():
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,
# i.e. curve and image items are excluded
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):
"""
@ -172,6 +172,6 @@ class RPCRegisterBroadcast:
"""Exit the context manager"""
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.broadcast()

View File

@ -36,7 +36,7 @@ class RPCWidgetHandler:
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.
@ -50,7 +50,7 @@ class RPCWidgetHandler:
"""
widget_class = self.widget_classes.get(widget_type) # type: ignore
if widget_class:
return widget_class(name=name, **kwargs)
return widget_class(**kwargs)
raise ValueError(f"Unknown widget type: {widget_type}")

View File

@ -111,8 +111,8 @@ class GUIServer:
self.setup_bec_icon()
service_config = self._get_service_config()
self.dispatcher = BECDispatcher(config=service_config)
self.dispatcher.start_cli_server(gui_id=self.gui_id)
self.dispatcher = BECDispatcher(config=service_config, 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.setAttribute(Qt.WA_ShowWithoutActivating) # type: ignore
@ -139,8 +139,6 @@ class GUIServer:
if self.app:
self.app.quit()
# gui.bec.close()
# win.shutdown()
signal.signal(signal.SIGINT, 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.utils.import_utils import lazy_import_from
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 bec_widgets.cli.rpc.rpc_register import RPCRegister
from bec_widgets.utils.container_utils import WidgetContainerUtils
from bec_widgets.utils.error_popups import ErrorPopupUtility
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
if TYPE_CHECKING: # pragma: no cover
from bec_widgets.utils.bec_dispatcher import BECDispatcher
from bec_widgets.widgets.containers.dock import BECDock
else:
BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))
logger = bec_logger.logger
BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))
class ConnectionConfig(BaseModel):
@ -82,14 +84,21 @@ class BECConnector:
client=None,
config: ConnectionConfig | None = None,
gui_id: str | None = None,
name: str | None = None,
parent_dock: BECDock | None = None,
object_name: str | None = None,
parent_dock: BECDock | None = None, # TODO should go away -> issue created #473
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
self.bec_dispatcher = BECDispatcher(client=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:
# register function to clean connections at exit;
@ -122,12 +131,24 @@ class BECConnector:
self.gui_id: str = gui_id # Keep namespace in sync
else:
self.gui_id: str = self.config.gui_id # type: ignore
if name is None:
name = self.__class__.__name__
else:
if not WidgetContainerUtils.has_name_valid_chars(name):
raise ValueError(f"Name {name} contains invalid characters.")
self._name = name if name else self.__class__.__name__
# TODO Hierarchy can be refreshed upon creation -> also registry should be notified if objectName changes -> issue #472
if object_name is not None:
self.setObjectName(object_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.add_rpc(self)
@ -138,6 +159,49 @@ class BECConnector:
# Store references to running workers so they're not garbage collected prematurely.
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:
"""
Submit a task to run in a separate thread. The task will run the specified
@ -316,8 +380,9 @@ class BECConnector:
def remove(self):
"""Cleanup the BECConnector"""
# 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:
self._parent_dock.delete(self._name)
self._parent_dock.delete(self.object_name)
# If the widget is from Qt, trigger its close method.
elif hasattr(self, "close"):
self.close()

View File

@ -80,13 +80,21 @@ class BECDispatcher:
client: BECClient
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:
cls._instance = super(BECDispatcher, cls).__new__(cls)
cls._initialized = False
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:
return
@ -114,6 +122,8 @@ class BECDispatcher:
logger.warning("Could not connect to Redis, skipping start of BECClient.")
logger.success("Initialized BECDispatcher")
self.start_cli_server(gui_id=gui_id)
self._initialized = True
@classmethod
@ -207,7 +217,7 @@ class BECDispatcher:
logger.error("Cannot start CLI server without a running client")
return
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):
"""

View File

@ -1,11 +1,11 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING
import darkdetect
from bec_lib.logger import bec_logger
from qtpy.QtCore import Slot
from qtpy.QtWidgets import QApplication, QWidget
from qtpy.QtCore import QObject, Slot
from qtpy.QtWidgets import QApplication
from bec_widgets.cli.rpc.rpc_register import RPCRegister
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
@ -32,8 +32,7 @@ class BECWidget(BECConnector):
config: ConnectionConfig = None,
gui_id: str | None = None,
theme_update: bool = False,
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,
**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
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__(
client=client,
config=config,
gui_id=gui_id,
name=name,
parent_dock=parent_dock,
parent_id=parent_id,
**kwargs,
)
if not isinstance(self, QObject):
raise RuntimeError(f"{repr(self)} is not a subclass of QWidget")
app = QApplication.instance()
if not hasattr(app, "theme"):
# 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.utils.import_utils import lazy_import
from qtpy.QtCore import QTimer
from qtpy.QtWidgets import QApplication
from redis.exceptions import RedisError
from bec_widgets.cli.rpc.rpc_register import RPCRegister
from bec_widgets.utils import BECDispatcher
from bec_widgets.utils.bec_connector import BECConnector
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:
from bec_lib import messages
@ -77,6 +80,7 @@ class CLIServer:
self._heartbeat_timer = QTimer()
self._heartbeat_timer.timeout.connect(self.emit_heartbeat)
self._heartbeat_timer.start(200)
self._registry_update_callbacks = []
self.status = messages.BECStatus.RUNNING
logger.success(f"Server started with gui_id: {self.gui_id}")
@ -141,17 +145,10 @@ class CLIServer:
def serialize_object(self, obj):
if isinstance(obj, BECConnector):
config = obj.config.model_dump()
config["parent_id"] = obj.parent_id # add parent_id to config
return {
"gui_id": obj.gui_id,
"name": (
obj._name if hasattr(obj, "_name") else obj.__class__.__name__
), # pylint: disable=protected-access
"widget_class": obj.__class__.__name__,
"config": config,
"__rpc__": True,
}
# Respect RPC = False
if hasattr(obj, "RPC") and obj.RPC is False:
return None
return self._serialize_bec_connector(obj)
return obj
def emit_heartbeat(self):
@ -166,19 +163,57 @@ class CLIServer:
logger.error(f"Error while emitting heartbeat: {exc}")
def broadcast_registry_update(self, connections: dict):
"""
Broadcast the updated registry to all clients.
"""
data = {}
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}")
self.client.connector.xadd(
MessageEndpoints.gui_registry_state(self.gui_id),
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
self.status = messages.BECStatus.IDLE
self._heartbeat_timer.stop()

View File

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

View File

@ -35,7 +35,6 @@ class SidePanel(QWidget):
super().__init__(parent=parent)
self.setProperty("skip_settings", True)
self.setObjectName("SidePanel")
self._orientation = orientation
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. "
)
widget = self.custom_widgets[class_name](self.baseinstance)
widget.setObjectName(name)
return widget
return super().createWidget(class_name, self.baseinstance, name)

View File

@ -18,7 +18,6 @@ from qtpy.QtWidgets import (
QWidget,
)
from bec_widgets.utils.bec_widget import BECWidget
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.
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
is_bec = isinstance(widget, BECWidget)
print_this = (not only_bec_widgets) or is_bec
from bec_widgets.utils import BECConnector
from bec_widgets.widgets.plots.waveform.waveform import Waveform
# 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:
ancestor = WidgetHierarchy._get_becwidget_ancestor(widget)
if ancestor is not None:
parent_info = f" parent={ancestor.__class__.__name__}"
if ancestor:
parent_label = ancestor.objectName() or ancestor.__class__.__name__
parent_info = f" parent={parent_label}"
else:
parent_info = " parent=None"
else:
parent_info = ""
if print_this:
widget_info = f"{widget.__class__.__name__} ({widget.objectName()}){parent_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)
widget_info = f"{widget.__class__.__name__} ({widget.objectName()}){parent_info}"
print(prefix + widget_info)
# Always recurse so we can discover deeper BECWidgets even if the current widget is not a BECWidget
children = widget.children()
for i, child in enumerate(children):
# Possibly skip known internal child widgets of a QComboBox
if (
exclude_internal_widgets
and isinstance(widget, QComboBox)
and child.__class__.__name__ in ["QFrame", "QBoxLayout", "QListView"]
):
# 3) If it's a Waveform, explicitly print the curves
if isinstance(widget, Waveform):
for curve in widget.curves:
curve_prefix = prefix + " └─ "
print(
f"{curve_prefix}{curve.__class__.__name__} ({curve.objectName()}) "
f"parent={widget.objectName()}"
)
# 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
child_prefix = prefix + " "
arrow = "├─ " if child != children[-1] else "└─ "
# Regardless of whether child is BECWidget or not, keep recursing, or we might miss deeper BECWidgets
# if WidgetHierarchy._get_becwidget_ancestor(child) == widget:
child_prefix = prefix + " └─ "
WidgetHierarchy.print_widget_hierarchy(
child,
indent + 1,
indent=indent + 1,
grab_values=grab_values,
prefix=child_prefix + arrow,
prefix=child_prefix,
exclude_internal_widgets=exclude_internal_widgets,
only_bec_widgets=only_bec_widgets,
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
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.
"""
from bec_widgets.utils import BECConnector
parent = widget.parent()
while parent is not None:
if isinstance(parent, BECWidget):
if isinstance(parent, BECConnector):
return parent
parent = parent.parent()
return None

View File

@ -133,6 +133,7 @@ class BECDock(BECWidget, Dock):
parent_id: str | None = None,
config: DockConfig | None = None,
name: str | None = None,
object_name: str | None = None,
client=None,
gui_id: str | None = None,
closable: bool = True,
@ -148,12 +149,17 @@ class BECDock(BECWidget, Dock):
if isinstance(config, dict):
config = DockConfig(**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)
Dock.__init__(self, name=name, label=label, parent=self, **kwargs)
# Dock.__init__(self, name=name, **kwargs)
super().__init__(
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
# Layout Manager
@ -193,7 +199,7 @@ class BECDock(BECWidget, Dock):
widgets(dict): The widgets in the dock.
"""
# 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
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.
"""
if row is None:
# row = cast(int, self.layout.rowCount()) # type:ignore
row = self.layout.rowCount()
# row = cast(int, row)
if self.layout_manager.is_position_occupied(row, col):
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
widget_class_name = widget if isinstance(widget, str) else widget.__class__.__name__
if widget_class_name in IGNORE_WIDGETS:
@ -326,16 +316,20 @@ class BECDock(BECWidget, Dock):
widget = cast(
BECWidget,
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:
widget._name = name # pylint: disable=protected-access
widget.object_name = name
self.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan)
if hasattr(widget, "config"):
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
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.
"""
self.parent_dock_area.delete(self._name)
self.parent_dock_area.delete(self.object_name)
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.
"""
# 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:
logger.warning(
f"Widget with name {widget_name} not found in dock {self.name()}. "
@ -391,7 +385,7 @@ class BECDock(BECWidget, Dock):
else:
widget = widgets[0]
self.layout.removeWidget(widget)
self.config.widgets.pop(widget._name, None)
self.config.widgets.pop(widget.object_name, None)
if widget in self.widgets:
self.widgets.remove(widget)
widget.close()
@ -401,7 +395,7 @@ class BECDock(BECWidget, Dock):
Remove all widgets from the dock.
"""
for widget in self.widgets:
self.delete(widget._name) # pylint: disable=protected-access
self.delete(widget.object_name)
def cleanup(self):
"""

View File

@ -21,6 +21,7 @@ from bec_widgets.utils.toolbar import (
ModularToolBar,
SeparatorAction,
)
from bec_widgets.utils.widget_io import WidgetHierarchy
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.scan_control.scan_control import ScanControl
@ -73,7 +74,7 @@ class BECDockArea(BECWidget, QWidget):
config: DockAreaConfig | None = None,
client=None,
gui_id: str = None,
name: str | None = None,
object_name: str = None,
**kwargs,
) -> None:
if config is None:
@ -82,9 +83,15 @@ class BECDockArea(BECWidget, QWidget):
if isinstance(config, dict):
config = DockAreaConfig(**config)
self.config = config
super().__init__(client=client, config=config, gui_id=gui_id, name=name, **kwargs)
QWidget.__init__(self, parent=parent)
self._parent = parent
super().__init__(
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.setSpacing(5)
self.layout.setContentsMargins(0, 0, 0, 0)
@ -354,17 +361,26 @@ class BECDockArea(BECWidget, QWidget):
Returns:
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 in dock_names:
raise ValueError(
f"Name {name} must be unique for docks, but already exists in DockArea "
f"with name: {self._name} and id {self.gui_id}."
f"with name: {self.object_name} and id {self.gui_id}."
)
else: # Name is not provided
name = WidgetContainerUtils.generate_unique_name(name="dock", list_of_names=dock_names)
dock = BECDock(name=name, parent_dock_area=self, 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
self.config.docks[dock.name()] = dock.config
# The dock.name is equal to the name passed to BECDock
@ -499,11 +515,13 @@ if __name__ == "__main__": # pragma: no cover
app = QApplication([])
set_theme("auto")
dock_area = BECDockArea()
dock_1 = dock_area.new(name="dock_0", widget="Waveform")
dock_1 = dock_area.new(name="dock_0", widget="DarkModeButton")
dock_1.new(widget="DarkModeButton")
# 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.setGeometry(100, 100, 800, 600)
app.topLevelWidgets()
WidgetHierarchy.print_becconnector_hierarchy_from_app()
app.exec_()
sys.exit(app.exec_())

View File

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

View File

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

View File

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

View File

@ -11,11 +11,10 @@ class ResetButton(BECWidget, QWidget):
PLUGIN = True
ICON_NAME = "restart_alt"
RPC = False
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)
QWidget.__init__(self, parent=parent)
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
self.get_bec_shortcuts()
self.layout = QHBoxLayout(self)

View File

@ -11,10 +11,10 @@ class ResumeButton(BECWidget, QWidget):
PLUGIN = True
ICON_NAME = "resume"
RPC = False
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)
QWidget.__init__(self, parent=parent)
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
self.get_bec_shortcuts()

View File

@ -11,10 +11,10 @@ class StopButton(BECWidget, QWidget):
PLUGIN = True
ICON_NAME = "dangerous"
RPC = False
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)
QWidget.__init__(self, parent=parent)
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
self.get_bec_shortcuts()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,6 +17,7 @@ class LMFitDialog(BECWidget, QWidget):
PLUGIN = True
ICON_NAME = "monitoring"
RPC = False
# Signal to emit the currently selected fit curve_id
selected_fit = Signal(str)
# 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.
ui_file (str): The UI file to be loaded.
"""
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
QWidget.__init__(self, parent=parent)
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
self.setProperty("skip_settings", True)
self.setObjectName("LMFitDialog")
self._ui_file = ui_file
self.target_widget = target_widget

View File

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

View File

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

View File

@ -26,8 +26,7 @@ class WebsiteWidget(BECWidget, QWidget):
def __init__(
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)
QWidget.__init__(self, parent=parent)
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
self.website = QWebEngineView()

View File

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

View File

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

View File

@ -82,10 +82,12 @@ class ImageItem(BECConnector, pg.ImageItem):
self.config = config
else:
self.config = config
super().__init__(config=config, gui_id=gui_id)
pg.ImageItem.__init__(self)
self.parent_image = parent_image
if parent_image is not None:
self.set_parent(parent_image)
else:
self.parent_image = None
self.parent_id = None
super().__init__(config=config, gui_id=gui_id, **kwargs)
self.raw_data = None
self.buffer = []
@ -94,6 +96,13 @@ class ImageItem(BECConnector, pg.ImageItem):
# Image processor will handle any setting of data
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):
self.raw_data = data
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
)
# For PropertyManager identification
self.setObjectName("MotorMap")
# Default values for PlotBase
self.x_grid = True
self.y_grid = True

View File

@ -20,7 +20,6 @@ class MotorMapSettings(SettingWidget):
super().__init__(parent=parent, *args, **kwargs)
self.setProperty("skip_settings", True)
self.setObjectName("MotorMapSettings")
current_path = os.path.dirname(__file__)
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
)
# For PropertyManager identification
self.setObjectName("MultiWaveform")
# Scan Data
self.old_scan_id = None
self.scan_id = None

View File

@ -20,7 +20,6 @@ class MultiWaveformControlPanel(SettingWidget):
super().__init__(parent=parent, *args, **kwargs)
self.setProperty("skip_settings", True)
self.setObjectName("MultiWaveformControlPanel")
current_path = os.path.dirname(__file__)
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:
if config is None:
config = ConnectionConfig(widget_class=self.__class__.__name__)
super().__init__(client=client, gui_id=gui_id, config=config, **kwargs)
QWidget.__init__(self, parent=parent)
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
# For PropertyManager identification
self.setObjectName("PlotBase")
self.get_bec_shortcuts()
# Layout Management
@ -1018,7 +1016,7 @@ if __name__ == "__main__": # pragma: no cover:
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
window = DemoPlotBase()
window = PlotBase()
window.show()
sys.exit(app.exec_())

View File

@ -77,13 +77,16 @@ class ScatterCurve(BECConnector, pg.PlotDataItem):
else:
self.config = config
name = config.label
super().__init__(config=config, gui_id=gui_id)
pg.PlotDataItem.__init__(self, name=name)
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.apply_config()
def parent(self):
return self.parent_item
def apply_config(self, config: dict | ScatterCurveConfig | None = None, **kwargs) -> None:
"""
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
)
self._main_curve = ScatterCurve(parent_item=self)
# For PropertyManager identification
self.setObjectName("ScatterWaveform")
# Specific GUI elements
self.scatter_dialog = None

View File

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

View File

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

View File

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

View File

@ -334,11 +334,11 @@ class CurveTree(BECWidget, QWidget):
client=None,
gui_id: str | None = None,
waveform: Waveform | None = None,
**kwargs,
) -> None:
if config is None:
config = ConnectionConfig(widget_class=self.__class__.__name__)
super().__init__(client=client, gui_id=gui_id, config=config)
QWidget.__init__(self, parent=parent)
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
self.waveform = waveform
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.endpoints import MessageEndpoints
from pydantic import Field, ValidationError, field_validator
from qtpy.QtCore import QTimer, Signal, Slot
from qtpy.QtWidgets import QDialog, QHBoxLayout, QMainWindow, QVBoxLayout, QWidget
from qtpy.QtCore import QTimer, Signal
from qtpy.QtWidgets import (
QApplication,
QDialog,
QHBoxLayout,
QMainWindow,
QVBoxLayout,
QWidget,
)
from bec_widgets.utils import ConnectionConfig
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.settings_dialog import SettingsDialog
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.plots.plot_base import PlotBase
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
)
# For PropertyManager identification
self.setObjectName("Waveform")
# Curve data
self._sync_curves = []
self._async_curves = []
@ -1737,12 +1740,12 @@ class Waveform(PlotBase):
super().cleanup()
class DemoApp(BECMainWindow): # pragma: no cover
class DemoApp(QMainWindow): # pragma: no cover
def __init__(self):
super().__init__()
self.setWindowTitle("Waveform Demo")
self.resize(800, 600)
self.main_widget = QWidget()
self.main_widget = QWidget(self)
self.layout = QHBoxLayout(self.main_widget)
self.setCentralWidget(self.main_widget)
@ -1760,9 +1763,7 @@ class DemoApp(BECMainWindow): # pragma: no cover
if __name__ == "__main__": # pragma: no cover
import sys
from bec_widgets.utils.bec_qapp import BECApplication
app = BECApplication(sys.argv)
app = QApplication(sys.argv)
set_theme("dark")
widget = DemoApp()
widget.show()

View File

@ -25,8 +25,7 @@ class BECProgressBar(BECWidget, QWidget):
ICON_NAME = "page_control"
def __init__(self, parent=None, client=None, config=None, gui_id=None, **kwargs):
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
QWidget.__init__(self, parent=parent)
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
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_core import PydanticCustomError
from qtpy import QtGui
from qtpy.QtCore import QObject
from bec_widgets.utils import BECConnector, ConnectionConfig
@ -77,7 +78,7 @@ class RingConfig(ProgressbarConfig):
)
class Ring(BECConnector):
class Ring(BECConnector, QObject):
USER_ACCESS = [
"_get_all_rpc",
"_rpc_id",
@ -108,7 +109,7 @@ class Ring(BECConnector):
if isinstance(config, dict):
config = RingConfig(**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.color = None

View File

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

View File

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

View File

@ -89,8 +89,7 @@ class BECStatusBox(BECWidget, CompactPopupWidget):
gui_id: str = None,
**kwargs,
):
super().__init__(client=client, gui_id=gui_id, **kwargs)
CompactPopupWidget.__init__(self, parent=parent, layout=QHBoxLayout)
super().__init__(parent=parent, layout=QHBoxLayout, client=client, gui_id=gui_id, **kwargs)
self.box_name = box_name
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,
**kwargs,
) -> None:
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
QWidget.__init__(self, parent)
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
self.get_bec_shortcuts()
self.ui = None

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ from bec_widgets.cli.rpc.rpc_base import DeletedWidgetError, RPCBase, RPCReferen
@pytest.fixture
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):