From b1a3403cd3e3fdd0d5efe60f9e0576bf7e9f6041 Mon Sep 17 00:00:00 2001 From: appel_c Date: Fri, 16 Jan 2026 16:38:12 +0100 Subject: [PATCH] fix(busy-loader): adjust busy loader and tests --- .../device_manager_display_widget.py | 1 + bec_widgets/utils/bec_widget.py | 11 +- bec_widgets/utils/busy_loader.py | 118 +++++++++--------- tests/unit_tests/test_busy_loader.py | 19 +++ tests/unit_tests/test_device_manager_view.py | 9 ++ 5 files changed, 93 insertions(+), 65 deletions(-) diff --git a/bec_widgets/applications/views/device_manager_view/device_manager_display_widget.py b/bec_widgets/applications/views/device_manager_view/device_manager_display_widget.py index d90e72d0..3ae9c1bb 100644 --- a/bec_widgets/applications/views/device_manager_view/device_manager_display_widget.py +++ b/bec_widgets/applications/views/device_manager_view/device_manager_display_widget.py @@ -248,6 +248,7 @@ class DeviceManagerDisplayWidget(DockAreaWidget): def _set_busy_wrapper(self, enabled: bool): """Thin wrapper around set_busy to flip the state variable.""" + self._busy_overlay.set_opacity(0.8) self._config_upload_active = enabled self.set_busy(enabled=enabled) diff --git a/bec_widgets/utils/bec_widget.py b/bec_widgets/utils/bec_widget.py index 79553a29..bb48b527 100644 --- a/bec_widgets/utils/bec_widget.py +++ b/bec_widgets/utils/bec_widget.py @@ -12,6 +12,7 @@ from qtpy.QtWidgets import QApplication, QFileDialog, QLabel, QVBoxLayout, QWidg import bec_widgets.widgets.containers.qt_ads as QtAds from bec_widgets.cli.rpc.rpc_register import RPCRegister from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig +from bec_widgets.utils.busy_loader import install_busy_loader from bec_widgets.utils.error_popups import SafeConnect, SafeSlot from bec_widgets.utils.rpc_decorator import rpc_timeout from bec_widgets.utils.widget_io import WidgetHierarchy @@ -70,9 +71,9 @@ class BECWidget(BECConnector): self._busy_state_widget: QWidget | None = None self._loading = False + self._busy_overlay = self._install_busy_loader() if start_busy and isinstance(self, QWidget): - self._busy_overlay = self._install_busy_loader() - self._adjust_busy_overlay() + self._show_busy_overlay() self._loading = True def _connect_to_theme_change(self): @@ -152,7 +153,6 @@ class BECWidget(BECConnector): child.stop() widget = BusyStateWidget(self) - return widget def _install_busy_loader(self) -> "BusyLoaderOverlay" | None: @@ -164,7 +164,6 @@ class BECWidget(BECConnector): return None overlay = getattr(self, "_busy_overlay", None) if overlay is None: - from bec_widgets.utils.busy_loader import install_busy_loader overlay = install_busy_loader(self, start_loading=False) self._busy_overlay = overlay @@ -174,7 +173,7 @@ class BECWidget(BECConnector): self._busy_overlay.set_widget(self._busy_state_widget) return overlay - def _adjust_busy_overlay(self) -> None: + def _show_busy_overlay(self) -> None: """Create and attach the loading overlay to this widget if QWidget is present.""" if not isinstance(self, QWidget): return @@ -198,7 +197,7 @@ class BECWidget(BECConnector): if self._busy_overlay is None: self._busy_overlay = self._install_busy_loader() if enabled: - self._adjust_busy_overlay() + self._show_busy_overlay() else: self._busy_overlay.hide() self._loading = bool(enabled) diff --git a/bec_widgets/utils/busy_loader.py b/bec_widgets/utils/busy_loader.py index 76af94d3..4df122ef 100644 --- a/bec_widgets/utils/busy_loader.py +++ b/bec_widgets/utils/busy_loader.py @@ -13,10 +13,8 @@ from qtpy.QtWidgets import ( QWidget, ) -from bec_widgets import BECWidget from bec_widgets.utils.colors import apply_theme from bec_widgets.utils.error_popups import SafeProperty -from bec_widgets.widgets.plots.waveform.waveform import Waveform class _OverlayEventFilter(QObject): @@ -56,14 +54,14 @@ class BusyLoaderOverlay(QWidget): foreground_color_changed = Signal(QColor) scrim_color_changed = Signal(QColor) - def __init__(self, parent: QWidget, opacity: float = 0.85, **kwargs): + def __init__(self, parent: QWidget, opacity: float = 0.35, **kwargs): super().__init__(parent=parent, **kwargs) self.setAttribute(Qt.WA_StyledBackground, True) self.setAutoFillBackground(False) self.setAttribute(Qt.WA_TranslucentBackground, True) self._opacity = opacity - self._scrim_color = QColor(0, 0, 0, 110) + self._scrim_color = QColor(128, 128, 128, 110) self._label_color = QColor(240, 240, 240) self._filter: QObject | None = None @@ -165,7 +163,7 @@ class BusyLoaderOverlay(QWidget): base = self.scrim_color base.setAlpha(int(255 * self._opacity)) self.scrim_color = base - self.update() + self._update_palette() ########################## ### Internal methods ### @@ -193,8 +191,9 @@ class BusyLoaderOverlay(QWidget): self.foreground_color = fg # Set the frame style with updated foreground colors + r, g, b, a = base.getRgb() self._frame.setStyleSheet( - f"#busyFrame {{ border: 2px dashed {self.foreground_color.name()}; border-radius: 9px; background-color: rgba(128, 128, 128, 110); }}" + f"#busyFrame {{ border: 2px dashed {self.foreground_color.name()}; border-radius: 9px; background-color: rgba({r}, {g}, {b}, {a}); }}" ) self.update() @@ -255,62 +254,63 @@ def install_busy_loader( # -------------------------- # Launchable demo # -------------------------- -class DemoWidget(BECWidget, QWidget): # pragma: no cover - def __init__(self, parent=None, start_busy: bool = False): - super().__init__(parent=parent, theme_update=True, start_busy=start_busy) - - self._title = QLabel("Demo Content", self) - self._title.setAlignment(Qt.AlignCenter) - self._title.setFrameStyle(QFrame.Panel | QFrame.Sunken) - lay = QVBoxLayout(self) - lay.addWidget(self._title) - waveform = Waveform(self) - waveform.plot([1, 2, 3, 4, 5]) - lay.addWidget(waveform, 1) - - QTimer.singleShot(5000, self._ready) - - def _ready(self): - self._title.setText("Ready ✓") - self.set_busy(False) - - -class DemoWindow(QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("Busy Loader — BECWidget demo") - - left = DemoWidget(start_busy=True) - right = DemoWidget() - - btn_on = QPushButton("Right → Loading") - btn_off = QPushButton("Right → Ready") - btn_text = QPushButton("Set custom text") - btn_on.clicked.connect(lambda: right.set_busy(True)) - btn_off.clicked.connect(lambda: right.set_busy(False)) - - panel = QWidget() - prow = QVBoxLayout(panel) - prow.addWidget(btn_on) - prow.addWidget(btn_off) - prow.addWidget(btn_text) - prow.addStretch(1) - - central = QWidget() - row = QHBoxLayout(central) - row.setContentsMargins(12, 12, 12, 12) - row.setSpacing(12) - row.addWidget(left, 1) - row.addWidget(right, 1) - row.addWidget(panel, 0) - - self.setCentralWidget(central) - self.resize(900, 420) - - if __name__ == "__main__": # pragma: no cover import sys + from bec_widgets.utils.bec_widget import BECWidget + from bec_widgets.widgets.plots.waveform.waveform import Waveform + + class DemoWidget(BECWidget, QWidget): # pragma: no cover + def __init__(self, parent=None, start_busy: bool = False): + super().__init__(parent=parent, theme_update=True, start_busy=start_busy) + + self._title = QLabel("Demo Content", self) + self._title.setAlignment(Qt.AlignCenter) + self._title.setFrameStyle(QFrame.Panel | QFrame.Sunken) + lay = QVBoxLayout(self) + lay.addWidget(self._title) + waveform = Waveform(self) + waveform.plot([1, 2, 3, 4, 5]) + lay.addWidget(waveform, 1) + + QTimer.singleShot(5000, self._ready) + + def _ready(self): + self._title.setText("Ready ✓") + self.set_busy(False) + + class DemoWindow(QMainWindow): # pragma: no cover + def __init__(self): + super().__init__() + self.setWindowTitle("Busy Loader — BECWidget demo") + + left = DemoWidget(start_busy=True) + right = DemoWidget() + + btn_on = QPushButton("Right → Loading") + btn_off = QPushButton("Right → Ready") + btn_text = QPushButton("Set custom text") + btn_on.clicked.connect(lambda: right.set_busy(True)) + btn_off.clicked.connect(lambda: right.set_busy(False)) + + panel = QWidget() + prow = QVBoxLayout(panel) + prow.addWidget(btn_on) + prow.addWidget(btn_off) + prow.addWidget(btn_text) + prow.addStretch(1) + + central = QWidget() + row = QHBoxLayout(central) + row.setContentsMargins(12, 12, 12, 12) + row.setSpacing(12) + row.addWidget(left, 1) + row.addWidget(right, 1) + row.addWidget(panel, 0) + + self.setCentralWidget(central) + self.resize(900, 420) + app = QApplication(sys.argv) apply_theme("light") w = DemoWindow() diff --git a/tests/unit_tests/test_busy_loader.py b/tests/unit_tests/test_busy_loader.py index 466ea03a..d7fdc68a 100644 --- a/tests/unit_tests/test_busy_loader.py +++ b/tests/unit_tests/test_busy_loader.py @@ -1,3 +1,4 @@ +import numpy as np import pytest from qtpy.QtWidgets import QLabel, QVBoxLayout, QWidget @@ -68,6 +69,24 @@ def test_becwidget_set_busy_toggle_and_text(qtbot, widget_idle): qtbot.waitUntil(lambda: overlay.isHidden()) +def test_becwidget_busy_overlay_set_opacity(qtbot, widget_busy): + overlay = getattr(widget_busy, "_busy_overlay") + qtbot.waitUntil(lambda: overlay.isVisible()) + + # Default opacity is 0.7 + frame = getattr(overlay, "_frame", None) + assert frame is not None + sheet = frame.styleSheet() + _, _, _, a = overlay.scrim_color.getRgb() + assert np.isclose(a / 255, 0.35, atol=0.02) + + # Change opacity + overlay.set_opacity(0.7) + qtbot.waitUntil(lambda: overlay.isVisible()) + _, _, _, a = overlay.scrim_color.getRgb() + assert np.isclose(a / 255, 0.7, atol=0.02) + + def test_becwidget_overlay_tracks_resize(qtbot, widget_busy): overlay = getattr(widget_busy, "_busy_overlay") qtbot.waitUntil(lambda: overlay.geometry() == widget_busy.rect()) diff --git a/tests/unit_tests/test_device_manager_view.py b/tests/unit_tests/test_device_manager_view.py index 12aa2219..0ca779fb 100644 --- a/tests/unit_tests/test_device_manager_view.py +++ b/tests/unit_tests/test_device_manager_view.py @@ -23,6 +23,7 @@ from bec_widgets.applications.views.device_manager_view.device_manager_dialogs.u ValidationSection, ) from bec_widgets.applications.views.device_manager_view.device_manager_display_widget import ( + CustomBusyWidget, DeviceManagerDisplayWidget, ) from bec_widgets.applications.views.device_manager_view.device_manager_view import ( @@ -592,6 +593,14 @@ class TestDeviceManagerView: qtbot.waitExposed(widget) yield widget + @pytest.fixture + def custom_busy(self, qtbot, mocked_client): + """Fixture for the custom busy widget of the DeviceManagerDisplayWidget.""" + widget = CustomBusyWidget(client=mocked_client) + qtbot.addWidget(widget) + qtbot.waitExposed(widget) + yield widget + @pytest.fixture def device_configs(self, device_config: dict): """Fixture for multiple device configurations."""