From dcbd94505397fde063ed56dbeda5c2c15c24ca57 Mon Sep 17 00:00:00 2001 From: appleb_m Date: Mon, 8 Jun 2026 14:40:26 +0200 Subject: [PATCH] busy_ovoerlay: added a busy_overlay widget so that both axis cameras and the sam cam source the same busy overlay settings --- src/aare/gui/main_window.py | 43 ++++++--- src/aare/gui/panels/axis_video_panel.py | 122 +++++++++++++++++++----- src/aare/gui/widgets/busy_overlay.py | 108 +++++++++++++++++++++ src/aare/gui/widgets/camera_image.py | 40 +++++--- src/aare/gui/widgets/video_image.py | 47 +++++---- 5 files changed, 288 insertions(+), 72 deletions(-) create mode 100644 src/aare/gui/widgets/busy_overlay.py diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py index 3aee036d..5d877667 100644 --- a/src/aare/gui/main_window.py +++ b/src/aare/gui/main_window.py @@ -12,15 +12,19 @@ from PySide6.QtWidgets import ( QDockWidget, QTabWidget) +#Common imports from aare.common.auth_models import BatonStatus from aare.common.coordinate import Coordinate, SmargonCoordinate from aare.common.diffraction_geometry import DiffractionGeometry from aare.common.logger_config import setup_logger from aare.common.models import SampleShortInfoList, TokenData, DAQStatusModel, BeamlineStateEnum from aare.common.sample_geometry import SampleGeometryModel -from aare.gui.models.gui_state_manager import UIStateManager -from aare.gui.panels.LogPanel import LogDock +#Gui Models +from aare.gui.models.gui_state_manager import UIStateManager + +#panels +from aare.gui.panels.LogPanel import LogDock from aare.gui.panels.beamline_controls import BeamlineControls from aare.gui.panels.beamline_state_panel import BeamlineStatePanel from aare.gui.panels.data_collection_settings import DataCollectionSettings @@ -37,33 +41,38 @@ from aare.gui.panels.tell_sample_panel import TellSamplePanel from aare.gui.panels.face_detection_panel import FaceDetectionPanel from aare.gui.panels.smargon_trace_panel import SmargonTracePanel from aare.gui.panels.axis_video_panel import AxisVideoPanel +from aare.gui.panels.fluorescence_panel import FluorescencePanel +from aare.gui.panels.automation_panel import AutomationProgressWidget + +#Scan Logic from aare.gui.scan_logic.raster_grid_manager import RasterGridManager from aare.gui.scan_logic.rotation_scan_manager import RotationScanManager from aare.gui.scan_logic.sample_mount_logic import SampleMountLogic -from aare.gui.threads.axis_video_thread import VideoThread +#Threads +from aare.gui.threads.axis_video_thread import VideoThread +from aare.gui.threads.prediction_subscriber import PredictionSubscriber +from aare.gui.threads.daq_worker import DAQWorker +from aare.gui.threads.jfjoch_viewer import JFJochDBusClient + +#Tutorials from aare.gui.tutorials.tutorial_actions import TutorialActionExecutor from aare.gui.tutorials.tutorial_manager import TutorialManager from aare.gui.tutorials.tutorial_runtime import DictionaryTextResolver, TutorialEventBus from aare.gui.tutorials.tutorial_targets import MainWindowTutorialTargetResolver from aare.gui.tutorials.tutorial_registration import register_tutorials from aare.gui.tutorials.tutroial_texts import MANUAL_MOUNT_TUTORIAL - from aare.gui.tutorials.controls_help_dialog import ControlsHelpDialog - -from aare.gui.threads.prediction_subscriber import PredictionSubscriber -from aare.gui.threads.daq_worker import DAQWorker -from aare.gui.threads.jfjoch_viewer import JFJochDBusClient from aare.gui.tutorials.tutorial_registration import register_tutorials + +#Widgets from aare.gui.widgets.alert_banner import AlertBanner from aare.gui.widgets.baton_request_dialog import BatonRequestDialog, BatonPendingDialog from aare.gui.widgets.camera_image import SampleCameraImageLabel from aare.gui.widgets.no_wheel_scroll_area import NoWheelScrollArea from aare.gui.widgets.status_bar import StatusBar from aare.gui.widgets.video_image import VideoGraphicsView -from aare.gui.panels.fluorescence_panel import FluorescencePanel - -from aare.gui.panels.automation_panel import AutomationProgressWidget +from aare.gui.widgets.busy_overlay import build_busy_overlay_style logger = setup_logger("aareGUI") @@ -1299,14 +1308,18 @@ class MainWindow(QMainWindow): if hasattr(self, "gonio_camera_thread") and self.gonio_camera_thread is not None: self.gonio_camera_thread.set_busy(s.busy) - axis_busy_text = self._axis_busy_text_from_status(s) + busy_style = build_busy_overlay_style( + is_busy=bool(s.busy), + tell_state=s.tell_state, + session_state=getattr(getattr(s, "session", None), "session", None), + ) if hasattr(self, "beamline_view_panel") and self.beamline_view_panel is not None: - self.beamline_view_panel.set_busy_state(bool(s.busy), axis_busy_text) + self.beamline_view_panel.set_busy_style(busy_style) if hasattr(self, "gonio_view_panel") and self.gonio_view_panel is not None: - self.gonio_view_panel.set_busy_state(bool(s.busy), axis_busy_text) + self.gonio_view_panel.set_busy_style(busy_style) if hasattr(self, "beamline_combined_panel") and self.beamline_combined_panel is not None: - self.beamline_combined_panel.set_busy_state(bool(s.busy), axis_busy_text) + self.beamline_combined_panel.set_busy_style(busy_style) self.target_stability_panel.set_beam_center( s.geom.beam_location_pxl.x, diff --git a/src/aare/gui/panels/axis_video_panel.py b/src/aare/gui/panels/axis_video_panel.py index a7999fbb..f1fa36ea 100644 --- a/src/aare/gui/panels/axis_video_panel.py +++ b/src/aare/gui/panels/axis_video_panel.py @@ -1,6 +1,7 @@ from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton +from aare.gui.widgets.busy_overlay import BusyOverlayStyle from aare.gui.widgets.video_image import VideoGraphicsView @@ -13,18 +14,46 @@ class AxisVideoPanel(QWidget): self._title_label = QLabel(title, self) self._title_label.setStyleSheet("font-weight: bold;") - self._status_label = QLabel("", self) - self._status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - self._status_label.setMinimumWidth(140) - self._status_label.setStyleSheet( - "QLabel {" - " padding: 4px 10px;" - " border-radius: 10px;" + self._status_container = QWidget(self) + self._status_container.setStyleSheet( + "QWidget {" + " border-radius: 12px;" " background-color: #d9e2f2;" - " color: #2f3b52;" "}" ) + self._status_dot = QLabel(self._status_container) + self._status_dot.setFixedSize(10, 10) + self._status_dot.setStyleSheet( + "QLabel {" + " min-width: 10px;" + " max-width: 10px;" + " min-height: 10px;" + " max-height: 10px;" + " border-radius: 5px;" + " background-color: transparent;" + "}" + ) + + self._status_label = QLabel("", self._status_container) + self._status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self._status_label.setStyleSheet( + "QLabel {" + " background-color: transparent;" + " color: #2f3b52;" + " font-weight: bold;" + "}" + ) + + status_layout = QHBoxLayout(self._status_container) + status_layout.setContentsMargins(10, 6, 12, 6) + status_layout.setSpacing(8) + status_layout.addWidget(self._status_dot) + status_layout.addWidget(self._status_label) + + self._status_container.setMinimumWidth(190) + self._status_container.hide() + self._refresh_button = QPushButton("Refresh Axis Cameras", self) self._refresh_button.clicked.connect(self.refresh_requested.emit) @@ -34,7 +63,7 @@ class AxisVideoPanel(QWidget): controls_layout.setContentsMargins(0, 0, 0, 0) controls_layout.addWidget(self._title_label) controls_layout.addStretch() - controls_layout.addWidget(self._status_label) + controls_layout.addWidget(self._status_container) controls_layout.addWidget(self._refresh_button) root_layout = QVBoxLayout(self) @@ -43,33 +72,74 @@ class AxisVideoPanel(QWidget): root_layout.addLayout(controls_layout) root_layout.addWidget(self.view) - def set_busy_state(self, busy: bool, text: str | None = None) -> None: - status_text = (text or "BUSY").upper() if busy else "" + def _all_video_views(self) -> list[VideoGraphicsView]: + views: list[VideoGraphicsView] = [] + if isinstance(self.view, VideoGraphicsView): + views.append(self.view) + views.extend(self.findChildren(VideoGraphicsView)) + unique_views: list[VideoGraphicsView] = [] + for view in views: + if view not in unique_views: + unique_views.append(view) + return unique_views - if busy: - self._status_label.setText(status_text) + def set_busy_style(self, style: BusyOverlayStyle | None) -> None: + if style is None: + self._status_label.setText("") + self._status_container.setStyleSheet( + "QWidget {" + " border-radius: 12px;" + " background-color: #d9e2f2;" + "}" + ) + self._status_dot.setStyleSheet( + "QLabel {" + " min-width: 10px;" + " max-width: 10px;" + " min-height: 10px;" + " max-height: 10px;" + " border-radius: 5px;" + " background-color: transparent;" + "}" + ) self._status_label.setStyleSheet( "QLabel {" - " padding: 5px 12px;" - " border-radius: 11px;" - " background-color: #f7c948;" - " color: #3b2f00;" + " background-color: transparent;" + " color: #2f3b52;" " font-weight: bold;" "}" ) + self._status_container.hide() else: - self._status_label.setText("") - self._status_label.setStyleSheet( - "QLabel {" - " padding: 4px 10px;" - " border-radius: 10px;" - " background-color: #d9e2f2;" - " color: #2f3b52;" + self._status_label.setText(style.text) + self._status_container.setStyleSheet( + "QWidget {" + " border-radius: 12px;" + f" background-color: {style.badge_bg};" "}" ) + self._status_dot.setStyleSheet( + "QLabel {" + " min-width: 10px;" + " max-width: 10px;" + " min-height: 10px;" + " max-height: 10px;" + " border-radius: 5px;" + f" background-color: {style.accent_dot};" + "}" + ) + self._status_label.setStyleSheet( + "QLabel {" + " background-color: transparent;" + f" color: {style.badge_fg};" + " font-weight: bold;" + "}" + ) + self._status_container.show() - if hasattr(self.view, "set_busy_overlay"): - self.view.set_busy_overlay(busy, status_text or "BUSY") + for view in self._all_video_views(): + if hasattr(view, "set_busy_overlay_style"): + view.set_busy_overlay_style(style) def set_status_text(self, text: str) -> None: self._status_label.setText(text or "") \ No newline at end of file diff --git a/src/aare/gui/widgets/busy_overlay.py b/src/aare/gui/widgets/busy_overlay.py new file mode 100644 index 00000000..2fdc47f5 --- /dev/null +++ b/src/aare/gui/widgets/busy_overlay.py @@ -0,0 +1,108 @@ +from dataclasses import dataclass + +from PySide6.QtGui import QColor + +from aare.common.tell_models import TellStateModel +from aare.common.models import SessionsStateEnum + + +@dataclass(frozen=True) +class BusyOverlayStyle: + text: str + badge_bg: str + badge_fg: str + overlay_fill: QColor + overlay_border: QColor + overlay_text: QColor + accent_dot: str + + +def build_busy_overlay_style( + *, + is_busy: bool, + tell_state: TellStateModel | None, + session_state: SessionsStateEnum | None = None, +) -> BusyOverlayStyle | None: + if session_state == SessionsStateEnum.Vacant: + return BusyOverlayStyle( + text="SESSION VACANT", + badge_bg="#f1c40f", + badge_fg="#3b2f00", + overlay_fill=QColor(241, 196, 15, 195), + overlay_border=QColor(255, 248, 210, 235), + overlay_text=QColor(59, 47, 0), + accent_dot="#fff6bf", + ) + + if session_state in { + SessionsStateEnum.OwnedByElse, + SessionsStateEnum.PendingYouToElse, + }: + return BusyOverlayStyle( + text="GUEST MODE", + badge_bg="#8e44ad", + badge_fg="#ffffff", + overlay_fill=QColor(142, 68, 173, 190), + overlay_border=QColor(235, 220, 245, 230), + overlay_text=QColor(255, 255, 255), + accent_dot="#f0dfff", + ) + + if not is_busy: + return None + + activity_value = str(getattr(getattr(tell_state, "activity", None), "value", "") or "").lower() + + if activity_value == "mounting": + return BusyOverlayStyle( + text="TELL MOUNTING", + badge_bg="#d64545", + badge_fg="#ffffff", + overlay_fill=QColor(190, 40, 40, 185), + overlay_border=QColor(255, 220, 220, 230), + overlay_text=QColor(255, 255, 255), + accent_dot="#ffdddd", + ) + + if activity_value == "unmounting": + return BusyOverlayStyle( + text="TELL UNMOUNTING", + badge_bg="#e67e22", + badge_fg="#ffffff", + overlay_fill=QColor(230, 126, 34, 190), + overlay_border=QColor(255, 234, 214, 230), + overlay_text=QColor(255, 255, 255), + accent_dot="#fff0db", + ) + + if activity_value == "drying": + return BusyOverlayStyle( + text="TELL DRYING", + badge_bg="#f1c40f", + badge_fg="#3b2f00", + overlay_fill=QColor(241, 196, 15, 195), + overlay_border=QColor(255, 248, 210, 235), + overlay_text=QColor(59, 47, 0), + accent_dot="#fff6bf", + ) + + if activity_value == "cooling": + return BusyOverlayStyle( + text="TELL COOLING", + badge_bg="#3498db", + badge_fg="#ffffff", + overlay_fill=QColor(52, 152, 219, 190), + overlay_border=QColor(220, 240, 255, 235), + overlay_text=QColor(255, 255, 255), + accent_dot="#dff2ff", + ) + + return BusyOverlayStyle( + text="BEAMLINE BUSY", + badge_bg="#e04f39", + badge_fg="#ffffff", + overlay_fill=QColor(224, 79, 57, 195), + overlay_border=QColor(255, 225, 220, 235), + overlay_text=QColor(255, 255, 255), + accent_dot="#ffd8d1", + ) \ No newline at end of file diff --git a/src/aare/gui/widgets/camera_image.py b/src/aare/gui/widgets/camera_image.py index 0300ae64..ded934f4 100644 --- a/src/aare/gui/widgets/camera_image.py +++ b/src/aare/gui/widgets/camera_image.py @@ -23,12 +23,19 @@ from PySide6.QtWidgets import ( QFrame, ) -from aare.common.models import DAQStatusModel, AutofocusSettings, SampleCameraSettings, BeamlineStateEnum, \ - SessionsStateEnum from aare.gui.models.bookmark import SmargonBookmarkList +from aare.gui.widgets.busy_overlay import build_busy_overlay_style, BusyOverlayStyle +from aare.gui.scan_logic.raster_grid_manager import RasterGridManager + +from aare.common.models import ( + DAQStatusModel, + AutofocusSettings, + SampleCameraSettings, + BeamlineStateEnum, + SessionsStateEnum +) from aare.common.coordinate import Coordinate, SmargonCoordinate from aare.common.sample_geometry import SampleGeometryModel -from aare.gui.scan_logic.raster_grid_manager import RasterGridManager from aare.common.logger_config import setup_logger logger = setup_logger("aareGUI") @@ -77,6 +84,7 @@ class SampleCameraImageLabel(QGraphicsView): self.__last_grid_update_ts = 0.0 self.__grid_update_min_interval_s = 1.0 / 25.0 self.__tell_state = None + self.__busy_overlay_style: BusyOverlayStyle | None = None self.__geom = geom self.__bookmarks: SmargonBookmarkList = SmargonBookmarkList() @@ -223,9 +231,11 @@ class SampleCameraImageLabel(QGraphicsView): return f"TELL {activity_name}".upper() def __draw_busy_overlay(self, painter: QPainter): - if not self.__is_daq_busy: + if self.__busy_overlay_style is None: return + style = self.__busy_overlay_style + painter.save() painter.resetTransform() @@ -234,13 +244,8 @@ class SampleCameraImageLabel(QGraphicsView): font.setBold(True) painter.setFont(font) - painter.setPen(QPen(QColor(255, 255, 255, 200), 2, Qt.PenStyle.SolidLine)) - painter.setBrush(QColor(255, 0, 0, 150)) - - text = self.__busy_overlay_text() - font_metrics = QFontMetrics(font) - text_rect = font_metrics.boundingRect(text) + text_rect = font_metrics.boundingRect(style.text) padding_x = 20 padding_y = 14 @@ -260,14 +265,16 @@ class SampleCameraImageLabel(QGraphicsView): bg_height ) + painter.setPen(QPen(style.overlay_border, 2, Qt.PenStyle.SolidLine)) + painter.setBrush(style.overlay_fill) painter.drawRoundedRect(bg_rect, 10, 10) - painter.setPen(QPen(QColor(255, 255, 255), 2, Qt.PenStyle.SolidLine)) + painter.setPen(QPen(style.overlay_text, 2, Qt.PenStyle.SolidLine)) text_pos = QPoint( position_x + padding_x, position_y + padding_y + font_metrics.ascent() ) - painter.drawText(text_pos, text) + painter.drawText(text_pos, style.text) painter.restore() @@ -617,6 +624,15 @@ class SampleCameraImageLabel(QGraphicsView): self.__session_state = new_session_state self.update() + new_busy_style = build_busy_overlay_style( + is_busy=bool(s.busy), + tell_state=s.tell_state, + session_state=self.__session_state, + ) + if new_busy_style != self.__busy_overlay_style: + self.__busy_overlay_style = new_busy_style + self.update() + if s.bl.shutter_open != self.__shutter: self.__shutter = s.bl.shutter_open diff --git a/src/aare/gui/widgets/video_image.py b/src/aare/gui/widgets/video_image.py index 6924266e..266641ab 100644 --- a/src/aare/gui/widgets/video_image.py +++ b/src/aare/gui/widgets/video_image.py @@ -2,6 +2,8 @@ from PySide6.QtCore import Qt, Slot, QRectF from PySide6.QtGui import QPainter, QPixmap, QImage, QFont, QColor, QPen, QFontMetrics from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem +from aare.gui.widgets.busy_overlay import BusyOverlayStyle + class VideoGraphicsView(QGraphicsView): def __init__(self): @@ -30,8 +32,7 @@ class VideoGraphicsView(QGraphicsView): self.min_zoom = 0.1 self.max_zoom = 5.0 - self._busy_overlay_visible = False - self._busy_overlay_text = "BUSY" + self._busy_overlay_style: BusyOverlayStyle | None = None @Slot(QImage) def update_frame(self, qt_image : QImage): @@ -45,9 +46,10 @@ class VideoGraphicsView(QGraphicsView): self.viewport().update() - def set_busy_overlay(self, visible: bool, text: str = "BUSY") -> None: - self._busy_overlay_visible = bool(visible) - self._busy_overlay_text = str(text or "BUSY").upper() + def set_busy_overlay_style(self, style: BusyOverlayStyle | None) -> None: + if self._busy_overlay_style == style: + return + self._busy_overlay_style = style self.viewport().update() def fit_to_view(self): @@ -106,9 +108,11 @@ class VideoGraphicsView(QGraphicsView): def drawForeground(self, painter: QPainter, rect: QRectF): super().drawForeground(painter, rect) - if not self._busy_overlay_visible: + if self._busy_overlay_style is None: return + style = self._busy_overlay_style + painter.save() painter.resetTransform() painter.setRenderHint(QPainter.RenderHint.Antialiasing, True) @@ -118,14 +122,15 @@ class VideoGraphicsView(QGraphicsView): font.setBold(True) painter.setFont(font) - text = self._busy_overlay_text fm = QFontMetrics(font) - text_rect = fm.boundingRect(text) + text_rect = fm.boundingRect(style.text) - padding_x = 20 + dot_diameter = 14 + gap = 12 + padding_x = 22 padding_y = 14 - bg_width = text_rect.width() + padding_x * 2 - bg_height = text_rect.height() + padding_y * 2 + bg_width = text_rect.width() + dot_diameter + gap + padding_x * 2 + bg_height = max(text_rect.height(), dot_diameter) + padding_y * 2 viewport_width = self.viewport().width() viewport_height = self.viewport().height() @@ -135,15 +140,19 @@ class VideoGraphicsView(QGraphicsView): bg_rect = QRectF(pos_x, pos_y, bg_width, bg_height) - painter.setPen(QPen(QColor(255, 255, 255, 220), 2)) - painter.setBrush(QColor(200, 30, 30, 185)) + painter.setPen(QPen(style.overlay_border, 2)) + painter.setBrush(style.overlay_fill) painter.drawRoundedRect(bg_rect, 14, 14) - painter.setPen(QPen(QColor(255, 255, 255), 1)) - painter.drawText( - bg_rect.left() + padding_x, - bg_rect.top() + padding_y + fm.ascent(), - text, - ) + dot_x = bg_rect.left() + padding_x + dot_y = bg_rect.top() + (bg_rect.height() - dot_diameter) / 2 + painter.setPen(Qt.PenStyle.NoPen) + painter.setBrush(QColor(style.accent_dot)) + painter.drawEllipse(QRectF(dot_x, dot_y, dot_diameter, dot_diameter)) + + painter.setPen(QPen(style.overlay_text, 1)) + text_x = dot_x + dot_diameter + gap + text_y = bg_rect.top() + padding_y + fm.ascent() + painter.drawText(text_x, text_y, style.text) painter.restore() \ No newline at end of file