mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-13 03:01:50 +02:00
feat(main_window): timer to show hide scan progress when it is relevant only
This commit is contained in:
@ -1,9 +1,28 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from bec_lib.endpoints import MessageEndpoints
|
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.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
|
import bec_widgets
|
||||||
from bec_widgets.utils import UILoader
|
from bec_widgets.utils import UILoader
|
||||||
@ -21,6 +40,8 @@ MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
|||||||
class BECMainWindow(BECWidget, QMainWindow):
|
class BECMainWindow(BECWidget, QMainWindow):
|
||||||
RPC = False
|
RPC = False
|
||||||
PLUGIN = False
|
PLUGIN = False
|
||||||
|
SCAN_PROGRESS_WIDTH = 100 # px
|
||||||
|
STATUS_BAR_WIDGETS_EXPIRE_TIME = 60_000 # milliseconds
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -34,6 +55,7 @@ class BECMainWindow(BECWidget, QMainWindow):
|
|||||||
super().__init__(parent=parent, gui_id=gui_id, **kwargs)
|
super().__init__(parent=parent, gui_id=gui_id, **kwargs)
|
||||||
|
|
||||||
self.app = QApplication.instance()
|
self.app = QApplication.instance()
|
||||||
|
self.status_bar = self.statusBar()
|
||||||
self.setWindowTitle(window_title)
|
self.setWindowTitle(window_title)
|
||||||
self._init_ui()
|
self._init_ui()
|
||||||
self._connect_to_theme_change()
|
self._connect_to_theme_change()
|
||||||
@ -62,14 +84,13 @@ class BECMainWindow(BECWidget, QMainWindow):
|
|||||||
"""
|
"""
|
||||||
Prepare the BEC specific widgets in the status bar.
|
Prepare the BEC specific widgets in the status bar.
|
||||||
"""
|
"""
|
||||||
status_bar = self.statusBar()
|
|
||||||
|
|
||||||
# Left: App‑ID label
|
# Left: App‑ID label
|
||||||
self._app_id_label = QLabel()
|
self._app_id_label = QLabel()
|
||||||
self._app_id_label.setAlignment(
|
self._app_id_label.setAlignment(
|
||||||
Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter
|
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
|
# Add a separator after the app ID label
|
||||||
self._add_separator()
|
self._add_separator()
|
||||||
@ -79,26 +100,100 @@ class BECMainWindow(BECWidget, QMainWindow):
|
|||||||
self._client_info_label.setAlignment(
|
self._client_info_label.setAlignment(
|
||||||
Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter
|
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
|
# Timer to automatically clear client messages once they expire
|
||||||
self._client_info_expire_timer = QTimer(self)
|
self._client_info_expire_timer = QTimer(self)
|
||||||
self._client_info_expire_timer.setSingleShot(True)
|
self._client_info_expire_timer.setSingleShot(True)
|
||||||
self._client_info_expire_timer.timeout.connect(lambda: self._client_info_label.setText(""))
|
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 = ScanProgressBar(self, one_line_design=True)
|
||||||
self._scan_progress_bar.show_elapsed_time = False
|
self._scan_progress_bar.show_elapsed_time = False
|
||||||
self._scan_progress_bar.show_remaining_time = False
|
self._scan_progress_bar.show_remaining_time = False
|
||||||
self._scan_progress_bar.show_source_label = False
|
self._scan_progress_bar.show_source_label = False
|
||||||
self._scan_progress_bar.progressbar.label_template = ""
|
self._scan_progress_bar.progressbar.label_template = ""
|
||||||
self._scan_progress_bar.progressbar.setFixedWidth(80)
|
|
||||||
self._scan_progress_bar.progressbar.setFixedHeight(8)
|
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()
|
status_bar = self.statusBar()
|
||||||
|
|
||||||
@ -117,6 +212,8 @@ class BECMainWindow(BECWidget, QMainWindow):
|
|||||||
vbox.addStretch()
|
vbox.addStretch()
|
||||||
wrapper.setFixedWidth(line.sizeHint().width())
|
wrapper.setFixedWidth(line.sizeHint().width())
|
||||||
|
|
||||||
|
if separate_object:
|
||||||
|
return wrapper
|
||||||
status_bar.addWidget(wrapper)
|
status_bar.addWidget(wrapper)
|
||||||
|
|
||||||
def _init_bec_icon(self):
|
def _init_bec_icon(self):
|
||||||
@ -290,8 +387,12 @@ class BECMainWindow(BECWidget, QMainWindow):
|
|||||||
child.close()
|
child.close()
|
||||||
child.deleteLater()
|
child.deleteLater()
|
||||||
|
|
||||||
|
# Timer cleanup
|
||||||
if hasattr(self, "_client_info_expire_timer") and self._client_info_expire_timer.isActive():
|
if hasattr(self, "_client_info_expire_timer") and self._client_info_expire_timer.isActive():
|
||||||
self._client_info_expire_timer.stop()
|
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
|
# Status bar widgets cleanup
|
||||||
self._client_info_label.cleanup()
|
self._client_info_label.cleanup()
|
||||||
self._scan_progress_bar.close()
|
self._scan_progress_bar.close()
|
||||||
|
@ -8,7 +8,7 @@ from typing import Literal
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
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
|
||||||
from qtpy.QtCore import QObject, QTimer
|
from qtpy.QtCore import QObject, QTimer, Signal
|
||||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_widget import BECWidget
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
@ -124,6 +124,8 @@ class ScanProgressBar(BECWidget, QWidget):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
ICON_NAME = "timelapse"
|
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):
|
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)
|
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._progress_source = None
|
||||||
self.task = None
|
self.task = None
|
||||||
self.scan_number = None
|
self.scan_number = None
|
||||||
|
self.progress_started.connect(lambda: print("Scan progress started"))
|
||||||
|
|
||||||
def connect_to_queue(self):
|
def connect_to_queue(self):
|
||||||
"""
|
"""
|
||||||
@ -178,6 +181,7 @@ class ScanProgressBar(BECWidget, QWidget):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.update_source_label(source, device=device)
|
self.update_source_label(source, device=device)
|
||||||
|
# self.progress_started.emit()
|
||||||
|
|
||||||
def update_source_label(self, source: ProgressSource, device=None):
|
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"
|
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:
|
if done:
|
||||||
self.task = None
|
self.task = None
|
||||||
|
self.progress_finished.emit()
|
||||||
return
|
return
|
||||||
|
|
||||||
@SafeProperty(bool)
|
@SafeProperty(bool)
|
||||||
@ -264,6 +269,7 @@ class ScanProgressBar(BECWidget, QWidget):
|
|||||||
return
|
return
|
||||||
if scan_info.get("status").lower() == "running" and self.task is None:
|
if scan_info.get("status").lower() == "running" and self.task is None:
|
||||||
self.task = ProgressTask(parent=self)
|
self.task = ProgressTask(parent=self)
|
||||||
|
self.progress_started.emit()
|
||||||
|
|
||||||
active_request_block = scan_info.get("active_request_block", {})
|
active_request_block = scan_info.get("active_request_block", {})
|
||||||
if active_request_block is None:
|
if active_request_block is None:
|
||||||
|
@ -187,3 +187,44 @@ def test_bec_weblinks(monkeypatch):
|
|||||||
"https://bec.readthedocs.io/projects/bec-widgets/en/latest/",
|
"https://bec.readthedocs.io/projects/bec-widgets/en/latest/",
|
||||||
"https://gitlab.psi.ch/groups/bec/-/issues/",
|
"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
|
||||||
|
Reference in New Issue
Block a user