Added style sheet options - protrait style needs to be added to more places.

This commit is contained in:
appleb_m
2026-06-18 14:26:12 +02:00
parent 08d6cbc699
commit 157619de64
8 changed files with 692 additions and 320 deletions
+84 -27
View File
@@ -2,7 +2,7 @@ import time
import jwt
from PySide6.QtCore import Qt, Slot, Signal, QTimer, QSettings, QEvent
from PySide6.QtGui import QAction, QPixmap, QKeySequence, QGuiApplication
from PySide6.QtGui import QAction, QPixmap, QKeySequence, QGuiApplication, QActionGroup
from PySide6.QtWidgets import (
QMainWindow,
QWidget,
@@ -11,12 +11,11 @@ from PySide6.QtWidgets import (
QMessageBox,
QDockWidget,
QTabWidget,
QStackedWidget
QStackedWidget,
)
#Common imports
from aare.common.auth_models import BatonStatus
from aare.common.beamline import cfg_get
from aare.common.coordinate import Coordinate, SmargonCoordinate
from aare.common.diffraction_geometry import DiffractionGeometry
from aare.common.logger_config import setup_logger
@@ -25,6 +24,7 @@ from aare.common.sample_geometry import SampleGeometryModel
#Gui Models
from aare.gui.models.gui_state_manager import UIStateManager
from aare.gui.styles import build_app_stylesheet, THEME_ORIGINAL, THEME_PORTRAIT
#panels
from aare.gui.panels.LogPanel import LogDock
@@ -95,11 +95,17 @@ class MainWindow(QMainWindow):
):
super().__init__()
self._theme_mode = THEME_ORIGINAL
self._theme_action_group = None
self._use_legacy_theme_action = None
self._use_portrait_theme_action = None
self.__base_url = base_url
self.__token = token
self.__mounting = False
self.__samcam_feed_banner_active = False
self.__samcam_feed_banner_message = "Sample camera feed unavailable"
self._automation_critical_banner_active = False
self._dev_help_dialog = None
self._beamline_recovery_dialog = None
@@ -166,6 +172,7 @@ class MainWindow(QMainWindow):
self.setStyleSheet("background-color: rgb(216, 228, 253);")
root_widget = QWidget(parent=self)
root_widget.setObjectName("mainContentRoot")
root_layout = QVBoxLayout(root_widget)
root_layout.setContentsMargins(0, 0, 0, 0)
root_layout.setSpacing(0)
@@ -180,6 +187,7 @@ class MainWindow(QMainWindow):
root_layout.addWidget(self.content_stack, 1)
top_widget = QWidget(parent=root_widget)
top_widget.setObjectName("standardMainPage")
top_widget_layout = QHBoxLayout(top_widget)
top_widget.setLayout(top_widget_layout)
@@ -300,21 +308,12 @@ class MainWindow(QMainWindow):
self.compact_automation_page = QWidget(parent=root_widget)
self.compact_automation_page.setObjectName("compactAutomationPage")
self.compact_automation_page.setStyleSheet(
"""
QWidget#compactAutomationPage {
background-color: rgb(216, 228, 253);
}
"""
)
self.compact_automation_page_layout = QVBoxLayout(self.compact_automation_page)
self.compact_automation_page_layout.setContentsMargins(18, 18, 18, 18)
self.compact_automation_page_layout.setSpacing(0)
self.compact_automation_page_layout.addWidget(self.compact_automation_panel)
# ── Portrait mode page ──────────────────────────────────────────
# Uses a dedicated SampleCameraImageLabel so the compact_sample_camera
# remains available for the existing compact automation view.
self.portrait_sample_camera = SampleCameraImageLabel(
geom=geom,
raster=self.raster,
@@ -325,21 +324,9 @@ class MainWindow(QMainWindow):
sample_camera_widget=self.portrait_sample_camera,
parent=root_widget,
)
self.portrait_mode_panel.setObjectName("portraitModePage")
self.portrait_mode_panel.setStyleSheet(
self.portrait_mode_panel.styleSheet() +
"QWidget#portraitModePage { background-color: #071018; }"
)
self.portrait_mode_page = QWidget(parent=root_widget)
self.portrait_mode_page.setObjectName("portraitModePage")
self.portrait_mode_page.setStyleSheet(
"""
QWidget#portraitModePage {
background-color: #071018;
}
"""
)
portrait_page_layout = QHBoxLayout(self.portrait_mode_page)
portrait_page_layout.setContentsMargins(0, 0, 0, 0)
portrait_page_layout.setSpacing(0)
@@ -534,6 +521,8 @@ class MainWindow(QMainWindow):
self.setCentralWidget(root_widget)
self.setWindowTitle("AareGUI")
self._restore_theme_settings()
self._apply_theme()
self.create_menu_bar()
self._update_view_mode_actions()
self._setup_global_shortcuts()
@@ -874,6 +863,23 @@ class MainWindow(QMainWindow):
)
self.addAction(self._shortcut_console_log)
def _return_to_main_view_for_shutdown(self) -> None:
try:
if getattr(self, "content_stack", None) is None:
return
current_widget = self.content_stack.currentWidget()
if hasattr(self, "portrait_mode_page") and current_widget is self.portrait_mode_page:
self._return_from_portrait_mode()
elif bool(getattr(self, "_in_compact_automation_view", False)):
self._return_from_compact_automation_view()
if hasattr(self, "content_stack") and hasattr(self, "_standard_main_page"):
self.content_stack.setCurrentWidget(self._standard_main_page)
except Exception as e:
logger.warning(f"Failed to return to main view during shutdown: {e}")
def _restore_samcam_overlay_settings(self) -> None:
settings = QSettings("PSI", "AareGUI")
show_detections = settings.value("samcam/show_detections", True, type=bool)
@@ -1253,9 +1259,29 @@ class MainWindow(QMainWindow):
def start_interactive_tutorial(self) -> None:
self.tutorial_manager.start("manual_workflow_demo")
def _apply_theme(self) -> None:
self.setStyleSheet(build_app_stylesheet(self._theme_mode))
def _restore_theme_settings(self) -> None:
settings = QSettings("PSI", "AareGUI")
self._theme_mode = settings.value("appearance/theme", THEME_ORIGINAL, type=str)
def _save_theme_settings(self) -> None:
settings = QSettings("PSI", "AareGUI")
settings.setValue("appearance/theme", self._theme_mode)
@Slot()
def use_legacy_theme(self) -> None:
self._theme_mode = THEME_ORIGINAL
self._apply_theme()
@Slot()
def use_portrait_theme(self) -> None:
self._theme_mode = THEME_PORTRAIT
self._apply_theme()
def create_menu_bar(self):
"""Create a menu bar with File->Quit and Help->About."""
# Main menu bar
menu_bar = self.menuBar()
file_menu = menu_bar.addMenu("File")
@@ -1281,6 +1307,25 @@ class MainWindow(QMainWindow):
view_menu = menu_bar.addMenu("View")
self._theme_action_group = QActionGroup(self)
self._theme_action_group.setExclusive(True)
self._use_legacy_theme_action = QAction("Legacy Theme", self)
self._use_legacy_theme_action.setCheckable(True)
self._use_legacy_theme_action.setChecked(self._theme_mode == THEME_ORIGINAL)
self._use_legacy_theme_action.triggered.connect(self.use_legacy_theme)
self._theme_action_group.addAction(self._use_legacy_theme_action)
self._use_portrait_theme_action = QAction("Portrait Theme", self)
self._use_portrait_theme_action.setCheckable(True)
self._use_portrait_theme_action.setChecked(self._theme_mode == THEME_PORTRAIT)
self._use_portrait_theme_action.triggered.connect(self.use_portrait_theme)
self._theme_action_group.addAction(self._use_portrait_theme_action)
view_menu.addAction(self._use_legacy_theme_action)
view_menu.addAction(self._use_portrait_theme_action)
view_menu.addSeparator()
if self._beamline_state_panel_enabled:
self._show_beamline_state_action = QAction("Show Beamline State Panel", self)
self._show_beamline_state_action.setCheckable(True)
@@ -2006,16 +2051,22 @@ class MainWindow(QMainWindow):
self._restore_panel_visibility_settings()
def closeEvent(self, event) -> None:
try:
self._return_to_main_view_for_shutdown()
except Exception as e:
logger.warning(f"Failed to restore main view before close: {e}")
try:
# TODO put all setting related handling into state_manager
self.state_manager.save_window(self)
self._save_samcam_overlay_settings()
self._save_panel_visibility_settings()
self._save_theme_settings()
except Exception as e:
logger.warning(f"Failed to save main window state: {e}")
# End session before closing so the backend removes this GUI from Redis immediately
# End session before closing so the backend removes this GUI from Redis immediately
try:
self.daq.end_session_on_close()
except Exception as e:
@@ -2031,6 +2082,12 @@ class MainWindow(QMainWindow):
def cleanup(self):
if getattr(self, "_cleanup_done", False):
return
try:
self._return_to_main_view_for_shutdown()
except Exception as e:
logger.warning(f"Failed to restore main view during cleanup: {e}")
self._cleanup_done = True
try:
@@ -2066,7 +2123,7 @@ class MainWindow(QMainWindow):
self._stop_axis_camera_threads()
for attr_name in (
"prediction_thread",
"prediction_thread",
):
thread = getattr(self, attr_name, None)
if thread is None:
+18 -66
View File
@@ -12,38 +12,18 @@ class AxisVideoPanel(QWidget):
super().__init__(parent)
self._title_label = QLabel(title, self)
self._title_label.setStyleSheet("font-weight: bold;")
self._status_container = QWidget(self)
self._status_container.setStyleSheet(
"QWidget {"
" border-radius: 12px;"
" background-color: #d9e2f2;"
"}"
)
self._status_container.setObjectName("axisVideoStatusContainer")
self._status_container.setProperty("busyState", "idle")
self._status_dot = QLabel(self._status_container)
self._status_dot.setObjectName("axisVideoStatusDot")
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.setObjectName("axisVideoStatusLabel")
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)
@@ -72,6 +52,12 @@ class AxisVideoPanel(QWidget):
root_layout.addLayout(controls_layout)
root_layout.addWidget(self.view)
def _refresh_status_style(self) -> None:
for widget in (self._status_container, self._status_dot, self._status_label):
widget.style().unpolish(widget)
widget.style().polish(widget)
widget.update()
def _all_video_views(self) -> list[VideoGraphicsView]:
views: list[VideoGraphicsView] = []
if isinstance(self.view, VideoGraphicsView):
@@ -86,55 +72,21 @@ class AxisVideoPanel(QWidget):
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 {"
" background-color: transparent;"
" color: #2f3b52;"
" font-weight: bold;"
"}"
)
self._status_container.setProperty("busyState", "idle")
self._status_dot.setStyleSheet("background-color: transparent;")
self._status_label.setStyleSheet("")
self._refresh_status_style()
self._status_container.hide()
else:
self._status_label.setText(style.text)
self._status_container.setStyleSheet(
"QWidget {"
" border-radius: 12px;"
f" background-color: {style.badge_bg};"
"}"
)
self._status_container.setProperty("busyState", "active")
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};"
"}"
f"background-color: {style.accent_dot};"
)
self._status_label.setStyleSheet(
"QLabel {"
" background-color: transparent;"
f" color: {style.badge_fg};"
" font-weight: bold;"
"}"
f"color: {style.badge_fg};"
)
self._refresh_status_style()
self._status_container.show()
for view in self._all_video_views():
+16 -51
View File
@@ -64,6 +64,7 @@ class BeamlineStatePanel(QFrame):
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("beamlineStatePanel")
self.setFrameShape(QFrame.Shape.StyledPanel)
self.setFrameShadow(QFrame.Shadow.Raised)
self.setFixedWidth(self.set_width)
@@ -87,16 +88,16 @@ class BeamlineStatePanel(QFrame):
self._label_hover_bg = "rgba(244, 196, 48, 0.22)"
self._group_colors: dict[BeamlineStateEnum, QColor] = {
BeamlineStateEnum.DewarTransfer: QColor(128, 90, 213), # Purple
BeamlineStateEnum.SampleExchange: QColor(237, 137, 54), # Orange
BeamlineStateEnum.RobotSampleExchange: QColor(237, 137, 54), # Orange
BeamlineStateEnum.SampleAlignment: QColor(72, 187, 120), # Green
BeamlineStateEnum.BeamLocation: QColor(72, 187, 120), # Green
BeamlineStateEnum.BeamstopAlignment: QColor(72, 187, 120), # Green
BeamlineStateEnum.FluxMeasurement: QColor(72, 187, 120), # Green
BeamlineStateEnum.DataCollection: QColor(236, 72, 153), # Pink
BeamlineStateEnum.XtalSnapshot: QColor(236, 72, 153), # Pink
BeamlineStateEnum.XrayFluorescence: QColor(236, 72, 153), # Pink
BeamlineStateEnum.DewarTransfer: QColor(128, 90, 213),
BeamlineStateEnum.SampleExchange: QColor(237, 137, 54),
BeamlineStateEnum.RobotSampleExchange: QColor(237, 137, 54),
BeamlineStateEnum.SampleAlignment: QColor(72, 187, 120),
BeamlineStateEnum.BeamLocation: QColor(72, 187, 120),
BeamlineStateEnum.BeamstopAlignment: QColor(72, 187, 120),
BeamlineStateEnum.FluxMeasurement: QColor(72, 187, 120),
BeamlineStateEnum.DataCollection: QColor(236, 72, 153),
BeamlineStateEnum.XtalSnapshot: QColor(236, 72, 153),
BeamlineStateEnum.XrayFluorescence: QColor(236, 72, 153),
}
self._group_label_colors: dict[BeamlineStateEnum, str] = {
@@ -153,54 +154,26 @@ class BeamlineStatePanel(QFrame):
self._station_widgets: dict[BeamlineStateEnum, QLabel | QPushButton] = {}
self.title = QLabel(self)
self.title.setObjectName("beamlineStateTitle")
self.title.setText("<H3>Beamline state</H3>")
self.title.setStyleSheet("background-color: #4B0082; color: #ffffff;")
self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.title.setFixedHeight(self.title_height)
self.title.setGeometry(0, 0, self.set_width, self.title_height)
self.toggle_button = QPushButton("", self)
self.toggle_button.setObjectName("beamlineStateToggleButton")
self.toggle_button.setToolTip("Minimise beamline state panel")
self.toggle_button.setFixedSize(28, 28)
self.toggle_button.move(self.set_width - 36, 11)
self.toggle_button.clicked.connect(self.toggle_collapsed)
self.toggle_button.setStyleSheet("""
QPushButton {
border: none;
border-radius: 14px;
background-color: rgba(255, 255, 255, 0.20);
color: white;
font-size: 16px;
font-weight: 700;
}
QPushButton:hover {
background-color: rgba(255, 255, 255, 0.32);
}
""")
self.current_label = QLabel("Current: —", self)
self.current_label.setStyleSheet("""
QLabel {
color: rgb(30, 41, 59);
font-size: 18px;
font-weight: 700;
padding-left: 4px;
background: transparent;
}
""")
self.current_label.setObjectName("beamlineStateCurrentLabel")
self.current_label.move(14, 58)
self.current_label.adjustSize()
self.tell_label = QLabel("Tell: —", self)
self.tell_label.setStyleSheet("""
QLabel {
color: rgb(55, 67, 87);
font-size: 15px;
font-weight: 600;
padding-left: 4px;
background: transparent;
}
""")
self.tell_label.setObjectName("beamlineStateTellLabel")
self.tell_label.move(14, 86)
self.tell_label.adjustSize()
@@ -580,15 +553,7 @@ class BeamlineStatePanel(QFrame):
tell_color = "green"
self.tell_label.setText(tell_text)
self.tell_label.setStyleSheet(f"""
QLabel {{
color: {tell_color};
font-size: 15px;
font-weight: 600;
padding-left: 4px;
background: transparent;
}}
""")
self.tell_label.setStyleSheet(f"color: {tell_color};")
self.tell_label.adjustSize()
def set_current_state(self, state: BeamlineStateEnum | None) -> None:
@@ -34,110 +34,6 @@ class CompactAutomationPanel(QFrame):
self.setFrameShape(QFrame.Shape.NoFrame)
self.setObjectName("compactAutomationPanel")
self.setStyleSheet(
"""
QFrame#compactAutomationPanel {
background: #d8e4fd;
border: none;
border-radius: 18px;
}
QFrame#compactCameraCard,
QFrame#compactControlsCard,
QFrame#compactProgressCard,
QFrame#compactQueueCard,
QFrame#compactQueueItem {
background: #e6eefc;
border: 1px solid #b9ccee;
border-radius: 16px;
}
QLabel#compactSectionTitle {
background: transparent;
color: #17324d;
font-size: 14px;
font-weight: 700;
}
QLabel#compactSectionHint {
background: transparent;
color: #51657d;
font-size: 12px;
}
QLabel#compactQueueTitle {
background: transparent;
color: #51657d;
font-size: 11px;
font-weight: 700;
}
QLabel#compactQueueValue {
background: transparent;
color: #10263a;
font-size: 14px;
font-weight: 700;
}
QToolButton#compactMenuButton {
background: #cbdcf8;
color: #17324d;
border: 1px solid #9fb9e5;
border-radius: 14px;
padding: 10px 14px;
font-size: 18px;
font-weight: 700;
}
QToolButton#compactMenuButton:hover {
background: #bfd4f6;
}
QLabel#compactProgressSummary {
background: transparent;
color: #17324d;
font-size: 13px;
padding: 2px 2px 6px 2px;
}
QLabel#compactProgressStep {
background: #dfe9fb;
border: 1px solid #bfd1ef;
border-radius: 10px;
padding: 8px 6px;
}
QPushButton#compactPrimaryButton {
background: #2563eb;
color: white;
border: none;
border-radius: 14px;
padding: 14px 18px;
font-size: 15px;
font-weight: 700;
}
QPushButton#compactPrimaryButton:hover {
background: #1d4ed8;
}
QPushButton#compactSecondaryButton,
QToolButton#compactSecondaryButton {
background: #dfe9fb;
color: #17324d;
border: 1px solid #b2c7eb;
border-radius: 14px;
padding: 14px 18px;
font-size: 14px;
font-weight: 700;
}
QPushButton#compactSecondaryButton:hover,
QToolButton#compactSecondaryButton:hover {
background: #d3e1f8;
}
"""
)
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(18, 18, 18, 18)
+1 -28
View File
@@ -26,33 +26,6 @@ BUTTON_BG = "#132131"
LED_OFF = "#1C2E3E"
ACTIVE_STEP = "#FFFFFF"
PORTRAIT_STYLE = f"""
QWidget {{
background: {BG};
color: {TEXT};
font-family: 'Inter', 'SF Pro Display', Arial, sans-serif;
font-size: 14px;
}}
QScrollArea {{
border: none;
background: transparent;
}}
QScrollBar:vertical {{
background: {CARD_BG};
width: 4px;
border-radius: 2px;
}}
QScrollBar::handle:vertical {{
background: {ACCENT_DIM};
border-radius: 2px;
min-height: 20px;
}}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
height: 0px;
}}
"""
# ---------------------------------------------------------------------------
# LED step indicator
# ---------------------------------------------------------------------------
@@ -269,7 +242,7 @@ class PortraitModePanel(QWidget):
def __init__(self, sample_camera_widget: QWidget, parent=None):
super().__init__(parent)
self.setStyleSheet(PORTRAIT_STYLE)
self.setObjectName("portraitRoot")
self.setMaximumWidth(self.PORTRAIT_WIDTH)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
+493
View File
@@ -0,0 +1,493 @@
from __future__ import annotations
THEME_ORIGINAL = "original"
THEME_PORTRAIT = "portrait"
def build_app_stylesheet(theme: str) -> str:
if theme == THEME_PORTRAIT:
return _portrait_stylesheet()
return _original_stylesheet()
def _original_stylesheet() -> str:
return """
QMainWindow, QWidget {
background-color: rgb(216, 228, 253);
color: rgb(30, 41, 59);
}
QWidget#mainContentRoot,
QWidget#standardMainPage,
QWidget#compactAutomationPage {
background-color: rgb(216, 228, 253);
}
QWidget#portraitModePage {
background-color: #071018;
}
QFrame#compactAutomationPanel {
background: #d8e4fd;
border: none;
border-radius: 18px;
}
QFrame#compactCameraCard,
QFrame#compactControlsCard,
QFrame#compactProgressCard,
QFrame#compactQueueCard,
QFrame#compactQueueItem {
background: #e6eefc;
border: 1px solid #b9ccee;
border-radius: 16px;
}
QLabel#compactSectionTitle {
background: transparent;
color: #17324d;
font-size: 14px;
font-weight: 700;
}
QLabel#compactSectionHint {
background: transparent;
color: #51657d;
font-size: 12px;
}
QLabel#compactQueueTitle {
background: transparent;
color: #51657d;
font-size: 11px;
font-weight: 700;
}
QLabel#compactQueueValue {
background: transparent;
color: #10263a;
font-size: 14px;
font-weight: 700;
}
QToolButton#compactMenuButton {
background: #cbdcf8;
color: #17324d;
border: 1px solid #9fb9e5;
border-radius: 14px;
padding: 10px 14px;
font-size: 18px;
font-weight: 700;
}
QToolButton#compactMenuButton:hover {
background: #bfd4f6;
}
QPushButton#compactPrimaryButton {
background: #2563eb;
color: white;
border: none;
border-radius: 14px;
padding: 14px 18px;
font-size: 15px;
font-weight: 700;
}
QPushButton#compactPrimaryButton:hover {
background: #1d4ed8;
}
QPushButton#compactSecondaryButton,
QToolButton#compactSecondaryButton {
background: #dfe9fb;
color: #17324d;
border: 1px solid #b2c7eb;
border-radius: 14px;
padding: 14px 18px;
font-size: 14px;
font-weight: 700;
}
QPushButton#compactSecondaryButton:hover,
QToolButton#compactSecondaryButton:hover {
background: #d3e1f8;
}
QFrame#alertBanner[alertKind="error"] {
background-color: #fbe4e6;
border: 2px solid #d97a84;
border-radius: 12px;
margin: 8px 12px 8px 12px;
}
QFrame#alertBanner[alertKind="success"] {
background-color: #e7f6ea;
border: 2px solid #7bbf8e;
border-radius: 12px;
margin: 8px 12px 8px 12px;
}
QFrame#alertBanner[alertKind="waiting"] {
background-color: #fff8e1;
border: 2px solid #ffb300;
border-radius: 12px;
margin: 8px 12px 8px 12px;
}
QFrame#alertBanner QLabel {
font-weight: 700;
font-size: 20px;
padding: 2px 6px 2px 6px;
}
QFrame#alertBanner[alertKind="error"] QLabel {
color: #8f1d2c;
}
QFrame#alertBanner[alertKind="success"] QLabel {
color: #1f6a3a;
}
QFrame#alertBanner[alertKind="waiting"] QLabel {
color: #e65100;
}
QWidget#axisVideoStatusContainer[busyState="idle"] {
border-radius: 12px;
background-color: #d9e2f2;
}
QWidget#axisVideoStatusContainer[busyState="active"] {
border-radius: 12px;
}
QLabel#axisVideoStatusDot {
min-width: 10px;
max-width: 10px;
min-height: 10px;
max-height: 10px;
border-radius: 5px;
background-color: transparent;
}
QLabel#axisVideoStatusLabel {
background-color: transparent;
color: #2f3b52;
font-weight: bold;
}
QFrame#beamlineStatePanel {
background: rgb(216, 228, 253);
border: 1px solid rgb(185, 204, 238);
border-radius: 12px;
}
QLabel#beamlineStateTitle {
background-color: #4B0082;
color: #ffffff;
}
QPushButton#beamlineStateToggleButton {
border: none;
border-radius: 14px;
background-color: rgba(255, 255, 255, 0.20);
color: white;
font-size: 16px;
font-weight: 700;
}
QPushButton#beamlineStateToggleButton:hover {
background-color: rgba(255, 255, 255, 0.32);
}
QLabel#beamlineStateCurrentLabel {
color: rgb(30, 41, 59);
font-size: 18px;
font-weight: 700;
padding-left: 4px;
background: transparent;
}
QLabel#beamlineStateTellLabel {
color: rgb(55, 67, 87);
font-size: 15px;
font-weight: 600;
padding-left: 4px;
background: transparent;
}
QWidget#portraitRoot,
QWidget#portraitRoot QWidget {
background: #071018;
color: #F5F7FA;
font-family: 'Inter', 'SF Pro Display', Arial, sans-serif;
font-size: 14px;
}
QWidget#portraitRoot QScrollArea {
border: none;
background: transparent;
}
QWidget#portraitRoot QScrollBar:vertical {
background: #0E1A26;
width: 4px;
border-radius: 2px;
}
QWidget#portraitRoot QScrollBar::handle:vertical {
background: #1A3A36;
border-radius: 2px;
min-height: 20px;
}
QWidget#portraitRoot QScrollBar::add-line:vertical,
QWidget#portraitRoot QScrollBar::sub-line:vertical {
height: 0px;
}
"""
def _portrait_stylesheet() -> str:
return """
QMainWindow, QWidget {
background: #071018;
color: #F5F7FA;
}
QWidget#mainContentRoot,
QWidget#standardMainPage,
QWidget#compactAutomationPage,
QWidget#portraitModePage {
background: #071018;
}
QTabWidget::pane,
QScrollArea,
QDockWidget,
QDockWidget > QWidget {
background: #071018;
color: #F5F7FA;
}
QFrame#compactAutomationPanel {
background: #071018;
border: none;
border-radius: 18px;
}
QFrame#compactCameraCard,
QFrame#compactControlsCard,
QFrame#compactProgressCard,
QFrame#compactQueueCard,
QFrame#compactQueueItem {
background: #0E1A26;
border: 1px solid #1A3A36;
border-radius: 16px;
}
QLabel#compactSectionTitle {
background: transparent;
color: #62D8C8;
font-size: 14px;
font-weight: 700;
}
QLabel#compactSectionHint {
background: transparent;
color: #8A9BB0;
font-size: 12px;
}
QLabel#compactQueueTitle {
background: transparent;
color: #8A9BB0;
font-size: 11px;
font-weight: 700;
}
QLabel#compactQueueValue {
background: transparent;
color: #F5F7FA;
font-size: 14px;
font-weight: 700;
}
QToolButton#compactMenuButton {
background: #132131;
color: #62D8C8;
border: 1px solid #1A3A36;
border-radius: 14px;
padding: 10px 14px;
font-size: 18px;
font-weight: 700;
}
QToolButton#compactMenuButton:hover {
background: #1A3A36;
}
QPushButton#compactPrimaryButton {
background: #62D8C8;
color: #071018;
border: none;
border-radius: 14px;
padding: 14px 18px;
font-size: 15px;
font-weight: 700;
}
QPushButton#compactPrimaryButton:hover {
background: #7ce6d8;
}
QPushButton#compactSecondaryButton,
QToolButton#compactSecondaryButton {
background: #132131;
color: #F5F7FA;
border: 1px solid #1A3A36;
border-radius: 14px;
padding: 14px 18px;
font-size: 14px;
font-weight: 700;
}
QPushButton#compactSecondaryButton:hover,
QToolButton#compactSecondaryButton:hover {
background: #1A3A36;
}
QFrame#alertBanner[alertKind="error"] {
background: #1A0E0E;
border: 2px solid #8f1d2c;
border-radius: 12px;
margin: 8px 12px 8px 12px;
}
QFrame#alertBanner[alertKind="success"] {
background: #0E1A12;
border: 2px solid #2a7a44;
border-radius: 12px;
margin: 8px 12px 8px 12px;
}
QFrame#alertBanner[alertKind="waiting"] {
background: #2B2208;
border: 2px solid #ffb300;
border-radius: 12px;
margin: 8px 12px 8px 12px;
}
QFrame#alertBanner QLabel {
font-weight: 700;
font-size: 20px;
padding: 2px 6px 2px 6px;
}
QFrame#alertBanner[alertKind="error"] QLabel {
color: #ffb3bc;
}
QFrame#alertBanner[alertKind="success"] QLabel {
color: #a8f0c0;
}
QFrame#alertBanner[alertKind="waiting"] QLabel {
color: #ffd166;
}
QWidget#axisVideoStatusContainer[busyState="idle"] {
border-radius: 12px;
background-color: #132131;
}
QWidget#axisVideoStatusContainer[busyState="active"] {
border-radius: 12px;
}
QLabel#axisVideoStatusDot {
min-width: 10px;
max-width: 10px;
min-height: 10px;
max-height: 10px;
border-radius: 5px;
background-color: transparent;
}
QLabel#axisVideoStatusLabel {
background-color: transparent;
color: #8A9BB0;
font-weight: bold;
}
QFrame#beamlineStatePanel {
background: #0E1A26;
border: 1px solid #1A3A36;
border-radius: 12px;
}
QLabel#beamlineStateTitle {
background-color: #132131;
color: #F5F7FA;
}
QPushButton#beamlineStateToggleButton {
border: none;
border-radius: 14px;
background-color: rgba(255, 255, 255, 0.10);
color: #F5F7FA;
font-size: 16px;
font-weight: 700;
}
QPushButton#beamlineStateToggleButton:hover {
background-color: rgba(255, 255, 255, 0.18);
}
QLabel#beamlineStateCurrentLabel {
color: #F5F7FA;
font-size: 18px;
font-weight: 700;
padding-left: 4px;
background: transparent;
}
QLabel#beamlineStateTellLabel {
color: #8A9BB0;
font-size: 15px;
font-weight: 600;
padding-left: 4px;
background: transparent;
}
QWidget#portraitRoot,
QWidget#portraitRoot QWidget {
background: #071018;
color: #F5F7FA;
font-family: 'Inter', 'SF Pro Display', Arial, sans-serif;
font-size: 14px;
}
QWidget#portraitRoot QScrollArea {
border: none;
background: transparent;
}
QWidget#portraitRoot QScrollBar:vertical {
background: #0E1A26;
width: 4px;
border-radius: 2px;
}
QWidget#portraitRoot QScrollBar::handle:vertical {
background: #1A3A36;
border-radius: 2px;
min-height: 20px;
}
QWidget#portraitRoot QScrollBar::add-line:vertical,
QWidget#portraitRoot QScrollBar::sub-line:vertical {
height: 0px;
}
"""
+16 -43
View File
@@ -11,6 +11,9 @@ class AlertBanner(QFrame):
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("alertBanner")
self.setProperty("alertKind", "error")
self._current_message = None
self._current_is_error = None
self._clear_timer = QTimer(self)
@@ -41,6 +44,14 @@ class AlertBanner(QFrame):
self.setVisible(False)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
def _set_alert_kind(self, kind: str) -> None:
self.setProperty("alertKind", kind)
self.style().unpolish(self)
self.style().polish(self)
self.style().unpolish(self._label)
self.style().polish(self._label)
self.update()
@Slot(str, bool)
def show_message(self, msg: str, is_error: bool = True, auto_clear_ms: int | None = None):
"""Show error (red) or success (green) message."""
@@ -53,36 +64,10 @@ class AlertBanner(QFrame):
if is_error:
decorated = f"🛑 {msg} 🛑"
self.setStyleSheet(
"QFrame {"
" background-color: #fbe4e6;"
" border: 2px solid #d97a84;"
" border-radius: 12px;"
" margin: 8px 12px 8px 12px;"
"}"
"QLabel {"
" color: #8f1d2c;"
" font-weight: 700;"
" font-size: 20px;"
" padding: 2px 6px 2px 6px;"
"}"
)
self._set_alert_kind("error")
else:
decorated = f"{msg}"
self.setStyleSheet(
"QFrame {"
" background-color: #e7f6ea;"
" border: 2px solid #7bbf8e;"
" border-radius: 12px;"
" margin: 8px 12px 8px 12px;"
"}"
"QLabel {"
" color: #1f6a3a;"
" font-weight: 700;"
" font-size: 20px;"
" padding: 2px 6px 2px 6px;"
"}"
)
self._set_alert_kind("success")
if auto_clear_ms is None:
timeout = 5000
else:
@@ -123,20 +108,7 @@ class AlertBanner(QFrame):
def _apply_waiting_style(self):
"""Apply yellow/waiting style."""
self.setStyleSheet(
"QFrame {"
" background-color: #fff8e1;"
" border: 2px solid #ffb300;"
" border-radius: 12px;"
" margin: 8px 12px 8px 12px;"
"}"
"QLabel {"
" color: #e65100;"
" font-weight: 700;"
" font-size: 20px;"
" padding: 2px 6px 2px 6px;"
"}"
)
self._set_alert_kind("waiting")
def _update_waiting_text(self):
"""Update the waiting message text, including countdown if active."""
@@ -167,5 +139,6 @@ class AlertBanner(QFrame):
self._current_message = None
self._current_is_error = None
self._clear_timer.stop()
self._set_alert_kind("error")
self._label.clear()
self.setVisible(False)
self.setVisible(False)
+64 -1
View File
@@ -3,11 +3,13 @@ from unittest.mock import MagicMock, patch
from PySide6.QtCore import Qt
from aare.gui.main_window import MainWindow
@pytest.fixture
def mock_ui_state():
with patch("aare.gui.main_window.UIStateManager") as mock:
yield mock
def test_main_window_init(qtbot, mock_ui_state):
with patch("requests.get") as mock_get, \
patch("aare.gui.main_window.DAQWorker"), \
@@ -241,4 +243,65 @@ def test_idle_timeout_does_not_close_while_automation_active(qtbot, mock_ui_stat
with patch("aare.gui.main_window.time.time", return_value=111.0):
win._check_idle_timeout()
win.close.assert_not_called()
win.close.assert_not_called()
def test_cleanup_returns_from_portrait_mode(qtbot, mock_ui_state):
with patch("requests.get"), \
patch("aare.gui.main_window.DAQWorker"), \
patch("aare.gui.main_window.PredictionSubscriber"), \
patch("aare.gui.main_window.VideoThread"), \
patch("aare.gui.main_window.JFJochDBusClient"), \
patch("aare.gui.main_window.jwt.decode") as mock_jwt:
mock_jwt.return_value = {"sub": "testuser", "staff": True, "pgroups": ["p123"], "session": 15}
fake_token = "header.payload.signature"
win = MainWindow(
base_url=None,
token=fake_token,
default_image=None,
zmq_addr=None,
pred_zmq_addr=None,
beamline_cam_addr=None,
gonio_cam_addr=None,
gonio_cam_id=None
)
qtbot.addWidget(win)
win.enter_portrait_mode()
assert win.content_stack.currentWidget() is win.portrait_mode_page
win.cleanup()
assert win.content_stack.currentWidget() is win._standard_main_page
def test_cleanup_returns_from_compact_automation_view(qtbot, mock_ui_state):
with patch("requests.get"), \
patch("aare.gui.main_window.DAQWorker"), \
patch("aare.gui.main_window.PredictionSubscriber"), \
patch("aare.gui.main_window.VideoThread"), \
patch("aare.gui.main_window.JFJochDBusClient"), \
patch("aare.gui.main_window.jwt.decode") as mock_jwt:
mock_jwt.return_value = {"sub": "testuser", "staff": True, "pgroups": ["p123"], "session": 15}
fake_token = "header.payload.signature"
win = MainWindow(
base_url=None,
token=fake_token,
default_image=None,
zmq_addr=None,
pred_zmq_addr=None,
beamline_cam_addr=None,
gonio_cam_addr=None,
gonio_cam_id=None
)
qtbot.addWidget(win)
win.enter_compact_automation_view()
assert win.content_stack.currentWidget() is win.compact_automation_page
win.cleanup()
assert win.content_stack.currentWidget() is win._standard_main_page