From bab70012f9ed7ac3ec714e1d38c46a4b1adc486e Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Wed, 20 May 2026 17:30:17 +0200 Subject: [PATCH 1/2] fix(bec-dispatcher): add disconnect_owner method to manage widget slot disconnections --- bec_widgets/utils/bec_dispatcher.py | 18 ++++++++++++++++++ bec_widgets/utils/bec_widget.py | 1 + 2 files changed, 19 insertions(+) diff --git a/bec_widgets/utils/bec_dispatcher.py b/bec_widgets/utils/bec_dispatcher.py index 6f1453155..c62cb521c 100644 --- a/bec_widgets/utils/bec_dispatcher.py +++ b/bec_widgets/utils/bec_dispatcher.py @@ -24,6 +24,7 @@ if TYPE_CHECKING: # pragma: no cover from bec_lib.endpoints import EndpointInfo + from bec_widgets.utils.bec_widget import BECWidget from bec_widgets.utils.rpc_server import RPCServer @@ -77,6 +78,7 @@ def __init__(self, cb: Callable, cb_info: dict | None = None): self.cb_info = cb_info self.cb = cb + self.cb_owner = louie.saferef.safe_ref(cb.__self__) if hasattr(cb, "__self__") else None self.cb_ref = louie.saferef.safe_ref(cb) self.cb_signal.connect(self.cb) self.topics = set() @@ -277,6 +279,22 @@ def disconnect_all(self, *args, **kwargs): # pylint: disable=protected-access self.disconnect_topics(self.client.connector._topics_cb) + def disconnect_owner(self, owner: BECWidget): + """ + Disconnect all slots owned by a particular widget. + + Args: + owner(BECWidget): The owner widget whose slots should be disconnected + """ + slots_to_disconnect = [] + for connected_slot in self._registered_slots.values(): + if connected_slot.cb_owner is not None and connected_slot.cb_owner() == owner: + slots_to_disconnect.append(connected_slot) + for slot in slots_to_disconnect: + topics = slot.topics.copy() + for topic in topics: + self.disconnect_slot(slot.cb, topic) + def start_cli_server(self, gui_id: str | None = None): """ Start the CLI server. diff --git a/bec_widgets/utils/bec_widget.py b/bec_widgets/utils/bec_widget.py index 6f254aab5..543fde76e 100644 --- a/bec_widgets/utils/bec_widget.py +++ b/bec_widgets/utils/bec_widget.py @@ -364,6 +364,7 @@ def closeEvent(self, event): """Wrap the close even to ensure the rpc_register is cleaned up.""" try: if not self._destroyed: + self.bec_dispatcher.disconnect_owner(self) self.cleanup() self._destroyed = True finally: From fb07d495dba64af1ff634aa4cf8b029baddb3631 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Wed, 20 May 2026 20:40:02 +0200 Subject: [PATCH 2/2] wip --- bec_widgets/utils/ui_loader.py | 46 +++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/bec_widgets/utils/ui_loader.py b/bec_widgets/utils/ui_loader.py index 17c3bdb94..857601e2b 100644 --- a/bec_widgets/utils/ui_loader.py +++ b/bec_widgets/utils/ui_loader.py @@ -1,6 +1,6 @@ from bec_lib.logger import bec_logger from qtpy import PYSIDE6 -from qtpy.QtCore import QFile, QIODevice +from qtpy.QtCore import QEvent, QFile, QIODevice, QObject from bec_widgets.utils.plugin_utils import get_designer_plugin @@ -9,16 +9,56 @@ if PYSIDE6: from qtpy.QtUiTools import QUiLoader + class _LoadedUiCloser(QObject): + """Forward root close events to widgets instantiated by ``QUiLoader``. + + Destroying a parent widget does not guarantee ``closeEvent`` is delivered to + every child widget. Some of our designer plugins rely on ``closeEvent`` / + ``cleanup`` to unregister callbacks, so explicitly close loaded descendants + when the loaded form itself is closed. + """ + + def __init__(self, root_widget): + super().__init__(root_widget) + self._root_widget = root_widget + self._widgets = [] + root_widget.installEventFilter(self) + + def register_widget(self, widget): + if widget is None or widget is self._root_widget: + return + self._widgets.append(widget) + + def eventFilter(self, watched, event): + if watched is self._root_widget and event.type() == QEvent.Close: + for widget in reversed(self._widgets): + try: + widget.close() + except RuntimeError: + continue + return super().eventFilter(watched, event) + class CustomUiLoader(QUiLoader): def __init__(self, baseinstance): super().__init__(baseinstance) self.baseinstance = baseinstance + self._closer = _LoadedUiCloser(baseinstance) if baseinstance is not None else None def createWidget(self, class_name, parent=None, name=""): + if parent is None and self.baseinstance is not None: + return self.baseinstance + + widget_parent = parent if parent is not None else self.baseinstance widget = get_designer_plugin(class_name, raise_on_missing=False) if widget is not None: - return widget(self.baseinstance) - return super().createWidget(class_name, self.baseinstance, name) + created_widget = widget(widget_parent) + created_widget.setObjectName(name) + else: + created_widget = super().createWidget(class_name, widget_parent, name) + + if self._closer is not None: + self._closer.register_widget(created_widget) + return created_widget class UILoader: