busy_ovoerlay: added a busy_overlay widget so that both axis cameras and the sam cam source the same busy overlay settings

This commit is contained in:
appleb_m
2026-06-08 14:40:26 +02:00
parent 447767f7de
commit dcbd945053
5 changed files with 288 additions and 72 deletions
+28 -15
View File
@@ -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,
+96 -26
View File
@@ -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 "")
+108
View File
@@ -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",
)
+28 -12
View File
@@ -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
+28 -19
View File
@@ -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()