From 9ff170660edd9e03f99eccee60b5e20fc1cf5a8d Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Fri, 20 Jun 2025 18:26:14 +0200 Subject: [PATCH] feat(main_window): timer to show hide scan progress when it is relevant only --- .../containers/main_window/main_window.py | 121 ++++++++++++++++-- .../scan_progressbar/scan_progressbar.py | 8 +- tests/unit_tests/test_main_widnow.py | 41 ++++++ 3 files changed, 159 insertions(+), 11 deletions(-) diff --git a/bec_widgets/widgets/containers/main_window/main_window.py b/bec_widgets/widgets/containers/main_window/main_window.py index dcb062d8..596781f4 100644 --- a/bec_widgets/widgets/containers/main_window/main_window.py +++ b/bec_widgets/widgets/containers/main_window/main_window.py @@ -1,9 +1,28 @@ +from __future__ import annotations + import os from bec_lib.endpoints import MessageEndpoints -from qtpy.QtCore import QEvent, QSize, Qt, QTimer +from qtpy.QtCore import ( + QAbstractAnimation, + QEasingCurve, + QEvent, + QPropertyAnimation, + QSize, + Qt, + QTimer, +) from qtpy.QtGui import QAction, QActionGroup, QIcon -from qtpy.QtWidgets import QApplication, QFrame, QLabel, QMainWindow, QStyle, QVBoxLayout, QWidget +from qtpy.QtWidgets import ( + QApplication, + QFrame, + QHBoxLayout, + QLabel, + QMainWindow, + QStyle, + QVBoxLayout, + QWidget, +) import bec_widgets from bec_widgets.utils import UILoader @@ -21,6 +40,8 @@ MODULE_PATH = os.path.dirname(bec_widgets.__file__) class BECMainWindow(BECWidget, QMainWindow): RPC = False PLUGIN = False + SCAN_PROGRESS_WIDTH = 100 # px + STATUS_BAR_WIDGETS_EXPIRE_TIME = 60_000 # milliseconds def __init__( self, @@ -34,6 +55,7 @@ class BECMainWindow(BECWidget, QMainWindow): super().__init__(parent=parent, gui_id=gui_id, **kwargs) self.app = QApplication.instance() + self.status_bar = self.statusBar() self.setWindowTitle(window_title) self._init_ui() self._connect_to_theme_change() @@ -62,14 +84,13 @@ class BECMainWindow(BECWidget, QMainWindow): """ Prepare the BEC specific widgets in the status bar. """ - status_bar = self.statusBar() # Left: App‑ID label self._app_id_label = QLabel() self._app_id_label.setAlignment( Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter ) - status_bar.addWidget(self._app_id_label) + self.status_bar.addWidget(self._app_id_label) # Add a separator after the app ID label self._add_separator() @@ -79,26 +100,100 @@ class BECMainWindow(BECWidget, QMainWindow): self._client_info_label.setAlignment( Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter ) - status_bar.addWidget(self._client_info_label, 1) + self.status_bar.addWidget(self._client_info_label, 1) # Timer to automatically clear client messages once they expire self._client_info_expire_timer = QTimer(self) self._client_info_expire_timer.setSingleShot(True) self._client_info_expire_timer.timeout.connect(lambda: self._client_info_label.setText("")) - self._add_separator() + # Add scan_progress bar with display logic + self._add_scan_progress_bar() + + ################################################################################ + # Progress‑bar helpers + def _add_scan_progress_bar(self): + + # --- Progress bar ------------------------------------------------- + # Scan progress bar minimalistic design setup self._scan_progress_bar = ScanProgressBar(self, one_line_design=True) self._scan_progress_bar.show_elapsed_time = False self._scan_progress_bar.show_remaining_time = False self._scan_progress_bar.show_source_label = False self._scan_progress_bar.progressbar.label_template = "" - self._scan_progress_bar.progressbar.setFixedWidth(80) self._scan_progress_bar.progressbar.setFixedHeight(8) - status_bar.addWidget(self._scan_progress_bar) + self._scan_progress_bar.progressbar.setFixedWidth(80) - def _add_separator(self): + # Bundle the progress bar with a separator + separator = self._add_separator(separate_object=True) + self._scan_progress_bar_with_separator = QWidget() + self._scan_progress_bar_with_separator.layout = QHBoxLayout( + self._scan_progress_bar_with_separator + ) + self._scan_progress_bar_with_separator.layout.setContentsMargins(0, 0, 0, 0) + self._scan_progress_bar_with_separator.layout.setSpacing(0) + self._scan_progress_bar_with_separator.layout.addWidget(separator) + self._scan_progress_bar_with_separator.layout.addWidget(self._scan_progress_bar) + + # Set Size + self._scan_progress_bar_target_width = self.SCAN_PROGRESS_WIDTH + self._scan_progress_bar_with_separator.setMaximumWidth(self._scan_progress_bar_target_width) + + self.status_bar.addWidget(self._scan_progress_bar_with_separator) + + # Visibility logic + self._scan_progress_bar_with_separator.hide() + self._scan_progress_bar_with_separator.setMaximumWidth(0) + + # Timer for hiding logic + self._scan_progress_hide_timer = QTimer(self) + self._scan_progress_hide_timer.setSingleShot(True) + self._scan_progress_hide_timer.setInterval(self.STATUS_BAR_WIDGETS_EXPIRE_TIME) + self._scan_progress_hide_timer.timeout.connect(self._animate_hide_scan_progress_bar) + + # Show / hide behaviour + self._scan_progress_bar.progress_started.connect(self._show_scan_progress_bar) + self._scan_progress_bar.progress_finished.connect(self._delay_hide_scan_progress_bar) + + def _show_scan_progress_bar(self): + if self._scan_progress_hide_timer.isActive(): + self._scan_progress_hide_timer.stop() + if self._scan_progress_bar_with_separator.isVisible(): + return + + # Make visible and reset width + self._scan_progress_bar_with_separator.show() + self._scan_progress_bar_with_separator.setMaximumWidth(0) + + self._show_container_anim = QPropertyAnimation( + self._scan_progress_bar_with_separator, b"maximumWidth", self + ) + self._show_container_anim.setDuration(300) + self._show_container_anim.setStartValue(0) + self._show_container_anim.setEndValue(self._scan_progress_bar_target_width) + self._show_container_anim.setEasingCurve(QEasingCurve.OutCubic) + self._show_container_anim.start() + + def _delay_hide_scan_progress_bar(self): + """Start the countdown to hide the scan progress bar.""" + if hasattr(self, "_scan_progress_hide_timer"): + self._scan_progress_hide_timer.start() + + def _animate_hide_scan_progress_bar(self): + """Shrink container to the right, then hide.""" + self._hide_container_anim = QPropertyAnimation( + self._scan_progress_bar_with_separator, b"maximumWidth", self + ) + self._hide_container_anim.setDuration(300) + self._hide_container_anim.setStartValue(self._scan_progress_bar_with_separator.width()) + self._hide_container_anim.setEndValue(0) + self._hide_container_anim.setEasingCurve(QEasingCurve.InCubic) + self._hide_container_anim.finished.connect(self._scan_progress_bar_with_separator.hide) + self._hide_container_anim.start() + + def _add_separator(self, separate_object: bool = False) -> QWidget | None: """ - Add a vertically centred separator to the status bar. + Add a vertically centred separator to the status bar or just return it as a separate object. """ status_bar = self.statusBar() @@ -117,6 +212,8 @@ class BECMainWindow(BECWidget, QMainWindow): vbox.addStretch() wrapper.setFixedWidth(line.sizeHint().width()) + if separate_object: + return wrapper status_bar.addWidget(wrapper) def _init_bec_icon(self): @@ -290,8 +387,12 @@ class BECMainWindow(BECWidget, QMainWindow): child.close() child.deleteLater() + # Timer cleanup if hasattr(self, "_client_info_expire_timer") and self._client_info_expire_timer.isActive(): self._client_info_expire_timer.stop() + if hasattr(self, "_scan_progress_hide_timer") and self._scan_progress_hide_timer.isActive(): + self._scan_progress_hide_timer.stop() + # Status bar widgets cleanup self._client_info_label.cleanup() self._scan_progress_bar.close() diff --git a/bec_widgets/widgets/progress/scan_progressbar/scan_progressbar.py b/bec_widgets/widgets/progress/scan_progressbar/scan_progressbar.py index 47ff0bec..996f6012 100644 --- a/bec_widgets/widgets/progress/scan_progressbar/scan_progressbar.py +++ b/bec_widgets/widgets/progress/scan_progressbar/scan_progressbar.py @@ -8,7 +8,7 @@ from typing import Literal import numpy as np from bec_lib.endpoints import MessageEndpoints from bec_lib.logger import bec_logger -from qtpy.QtCore import QObject, QTimer +from qtpy.QtCore import QObject, QTimer, Signal from qtpy.QtWidgets import QVBoxLayout, QWidget from bec_widgets.utils.bec_widget import BECWidget @@ -124,6 +124,8 @@ class ScanProgressBar(BECWidget, QWidget): """ ICON_NAME = "timelapse" + progress_started = Signal() + progress_finished = Signal() def __init__(self, parent=None, client=None, config=None, gui_id=None, one_line_design=False): super().__init__(parent=parent, client=client, config=config, gui_id=gui_id) @@ -145,6 +147,7 @@ class ScanProgressBar(BECWidget, QWidget): self._progress_source = None self.task = None self.scan_number = None + self.progress_started.connect(lambda: print("Scan progress started")) def connect_to_queue(self): """ @@ -178,6 +181,7 @@ class ScanProgressBar(BECWidget, QWidget): ), ) self.update_source_label(source, device=device) + # self.progress_started.emit() def update_source_label(self, source: ProgressSource, device=None): scan_text = f"Scan {self.scan_number}" if self.scan_number is not None else "Scan" @@ -209,6 +213,7 @@ class ScanProgressBar(BECWidget, QWidget): if done: self.task = None + self.progress_finished.emit() return @SafeProperty(bool) @@ -264,6 +269,7 @@ class ScanProgressBar(BECWidget, QWidget): return if scan_info.get("status").lower() == "running" and self.task is None: self.task = ProgressTask(parent=self) + self.progress_started.emit() active_request_block = scan_info.get("active_request_block", {}) if active_request_block is None: diff --git a/tests/unit_tests/test_main_widnow.py b/tests/unit_tests/test_main_widnow.py index fed321c1..ed92935b 100644 --- a/tests/unit_tests/test_main_widnow.py +++ b/tests/unit_tests/test_main_widnow.py @@ -187,3 +187,44 @@ def test_bec_weblinks(monkeypatch): "https://bec.readthedocs.io/projects/bec-widgets/en/latest/", "https://gitlab.psi.ch/groups/bec/-/issues/", ] + + +################################################################# +# Tests for scan‑progress bar animations + + +def test_scan_progress_bar_show_animation(qtbot, bec_main_window): + """ + _show_scan_progress_bar should animate the container's maximumWidth + from 0 to the configured target width. + """ + container = bec_main_window._scan_progress_bar_with_separator + + # Pre‑condition: collapsed + assert container.maximumWidth() == 0 + + bec_main_window._show_scan_progress_bar() + + target = bec_main_window._scan_progress_bar_target_width + qtbot.waitUntil(lambda: container.maximumWidth() == target, timeout=2000) + + assert container.maximumWidth() == target + + +def test_scan_progress_bar_hide_animation(qtbot, bec_main_window): + """ + _animate_hide_scan_progress_bar should collapse the container back to 0 width. + """ + container = bec_main_window._scan_progress_bar_with_separator + + # First expand it + bec_main_window._show_scan_progress_bar() + target = bec_main_window._scan_progress_bar_target_width + qtbot.waitUntil(lambda: container.maximumWidth() == target, timeout=2000) + + # Trigger hide animation + bec_main_window._animate_hide_scan_progress_bar() + + qtbot.waitUntil(lambda: container.maximumWidth() == 0, timeout=2000) + + assert container.maximumWidth() == 0