new_gui: Catppuccin theming (Latte/Mocha) + readability + beam-path pipeline

- retarget theme.py to Catppuccin Latte (light) + Mocha (dark)
- route on-fill text through accent_text (Mocha's light pastels need dark text);
  fixes unreadable buttons
- transport buttons: labelled + sized (Play/Pause/Skip/End were cramped glyphs)
- top-bar ☾/☀ theme toggle: live rebuild of views, backend kept, persisted to
  QSettings (wiring split into persistent vs view-facing for safe re-wire)
- beam-path pipeline signature: continuous rail, green completed segment, mauve
  focal bloom on the active station
- palette-ize stray colours (slider grooves, alert tints, state LED hues, legend)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
appleb_m
2026-06-25 13:54:11 +02:00
co-authored by Claude Opus 4.8
parent 963af28cf4
commit ebd60d4c5d
15 changed files with 294 additions and 177 deletions
+7
View File
@@ -16,6 +16,13 @@ uv run python -m aare.gui.new_gui.app -u https://host -c /path/to.crt -p tcp://h
`BEAMLINE` (X06DA / X10SA / X06SA / unset→SIMULATED) drives all config via
`cfg_get`, exactly like the existing GUI.
**Theme:** Catppuccin — **Latte** (light) and **Mocha** (dark), toggled with the
☾/☀ button in the top bar (persisted to QSettings; rebuilds the views live while
keeping the backend connection). `theme.py` is the single source of truth; text
on coloured fills routes through `accent_text` so Mocha's light pastels stay
legible. The Mount→Center→Raster→XRF→Collect tracker is a **beam-path rail**
the completed length glows green and the active station is a mauve focal bloom.
**Offline dev mode:** with no server (`base_url is None`, e.g. `BEAMLINE=SIMULATED`
and no `-u`), `dev_seed.py` injects a few fake samples + a synthetic live status +
a fake camera frame so the full Manual flow (mount → pipeline, motors, bookmarks)
+2 -2
View File
@@ -95,7 +95,7 @@ class LibraryRow(QWidget):
if added:
css = f"background:{palette.success_tint}; color:{palette.success}; font-size:13px;"
else:
css = f"background:{palette.accent}; color:#fff; font-size:17px;"
css = f"background:{palette.accent}; color:{palette.accent_text}; font-size:17px;"
self._add.setStyleSheet(
f"QPushButton {{ {css} border:none; border-radius:8px; font-weight:600; }}"
)
@@ -455,7 +455,7 @@ class AutomationView(QWidget):
self._start = QPushButton("▶ Start automation")
self._start.setCursor(Qt.PointingHandCursor)
self._start.setStyleSheet(
f"QPushButton {{ background:{p.accent}; color:#fff; border:none;"
f"QPushButton {{ background:{p.accent}; color:{p.accent_text}; border:none;"
f" border-radius:10px; padding:12px 26px; font-size:14px; font-weight:700; }}"
)
self._start.clicked.connect(self.start_requested)
+137 -81
View File
@@ -23,7 +23,7 @@ from aare.gui.new_gui.automation_view import AutomationView
from aare.gui.new_gui.manual_view import ManualView
from aare.gui.new_gui.state import AppState
from aare.gui.new_gui.status_bar import StatusBar
from aare.gui.new_gui.theme import LIGHT, build_qss
from aare.gui.new_gui.theme import DARK, LIGHT, build_qss
from aare.gui.new_gui.top_bar import TopBar
from aare.gui.new_gui.widgets.alert_banner import AlertBanner
from aare.gui.new_gui.widgets import preconditions
@@ -33,6 +33,12 @@ logger = setup_logger("aareGUI.new")
_BEAMLINE_SUBTITLE = {"X06DA": "PXIII", "X10SA": "PXII", "X06SA": "PXI"}
def _saved_palette():
"""Initial theme from QSettings (defaults to Catppuccin Latte / light)."""
from PySide6.QtCore import QSettings
return DARK if QSettings("PSI", "AareGUI-new").value("theme") == "dark" else LIGHT
def _collect_defaults() -> dict:
"""Scan defaults from the beamline YAML (with safe fall-backs)."""
rot = "data_collection_settings.default_rotation_settings"
@@ -61,7 +67,7 @@ class MainWindow(QWidget):
self._gonio_cam_addr = gonio_cam_addr
self._beamline_cam_addr = beamline_cam_addr
self._gonio_cam_id = gonio_cam_id
self._palette = LIGHT
self._palette = _saved_palette()
self._defaults = _collect_defaults()
# automation run bookkeeping
@@ -78,6 +84,11 @@ class MainWindow(QWidget):
self._baton_poll.setInterval(1000)
self._baton_poll.timeout.connect(self._poll_baton_timeout)
# last payloads cached so a live theme rebuild can re-feed the new views
self._last_status = None
self._last_spreadsheet = None
self._last_reference_tools = None
self.setObjectName("AppRoot")
self.setStyleSheet(build_qss(self._palette))
self.setWindowTitle("AareGUI")
@@ -86,34 +97,19 @@ class MainWindow(QWidget):
beamline = str(mx_beamline().name if hasattr(mx_beamline(), "name")
else mx_beamline())
subtitle = _BEAMLINE_SUBTITLE.get(beamline, "")
bl_label = f"{beamline} · {subtitle}" if subtitle else beamline
self._bl_label = f"{beamline} · {subtitle}" if subtitle else beamline
self.state = AppState(self)
# chrome + body
self.top_bar = TopBar(self._palette, bl_label)
self.alert = AlertBanner(self._palette)
self.status_bar = StatusBar(self._palette)
self.body = QStackedWidget()
self.manual = ManualView(self.state, self._palette, self._defaults)
self.automation = AutomationView(self.state, self._palette)
from aare.gui.new_gui.staff_view import StaffView
self.staff = StaffView(self._palette)
self.body.addWidget(self.manual)
self.body.addWidget(self.automation)
self.body.addWidget(self.staff)
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(self.top_bar)
layout.addWidget(self.alert)
layout.addWidget(self.body, 1)
layout.addWidget(self.status_bar)
self._root_layout = QVBoxLayout(self)
self._root_layout.setContentsMargins(0, 0, 0, 0)
self._root_layout.setSpacing(0)
self._build_chrome_and_body()
# backend
self._build_backend(pred_zmq_addr)
self._wire()
self._wire_persistent()
self._wire_views()
self._build_shortcuts()
self._restore_state()
@@ -127,6 +123,57 @@ class MainWindow(QWidget):
except Exception as exc:
logger.warning("Dev seed failed: %s", exc)
def _build_chrome_and_body(self) -> None:
"""Create the top bar / alert / body / status bar. Repeatable so a theme
switch can rebuild every view with the new palette (backend untouched)."""
from aare.gui.new_gui.staff_view import StaffView
self.top_bar = TopBar(self._palette, self._bl_label)
self.alert = AlertBanner(self._palette)
self.status_bar = StatusBar(self._palette)
self.body = QStackedWidget()
self.manual = ManualView(self.state, self._palette, self._defaults)
self.automation = AutomationView(self.state, self._palette)
self.staff = StaffView(self._palette)
self.body.addWidget(self.manual)
self.body.addWidget(self.automation)
self.body.addWidget(self.staff)
self._root_layout.addWidget(self.top_bar)
self._root_layout.addWidget(self.alert)
self._root_layout.addWidget(self.body, 1)
self._root_layout.addWidget(self.status_bar)
# ----------------------------------------------------------- theme switch
def _on_theme_toggle(self) -> None:
from PySide6.QtCore import QSettings
new = DARK if self._palette is LIGHT else LIGHT
QSettings("PSI", "AareGUI-new").setValue(
"theme", "dark" if new is DARK else "light")
self.apply_theme(new)
def apply_theme(self, palette) -> None:
self._palette = palette
mode = self.state.mode
for w in (self.top_bar, self.alert, self.body, self.status_bar):
self._root_layout.removeWidget(w)
w.setParent(None)
w.deleteLater()
self._build_chrome_and_body()
self.setStyleSheet(build_qss(palette))
self._wire_views()
# restore staff gating, mode, and last-seen data into the fresh views
self.top_bar.set_staff(self._staff)
self._on_mode_changed(mode)
if self._baton is not None:
self.top_bar.update_baton_status(self._baton)
if self._last_spreadsheet is not None:
self._on_spreadsheet(self._last_spreadsheet)
if self._last_reference_tools is not None:
self._on_reference_tools(self._last_reference_tools)
if self._last_status is not None:
self.status_bar.update_daq_status(self._last_status)
self.manual.update_daq_status(self._last_status)
self.staff.update_daq_status(self._last_status)
# ------------------------------------------------------------- backend
def _build_backend(self, pred_zmq_addr) -> None:
from aare.gui.threads.daq_worker import DAQWorker
@@ -169,43 +216,86 @@ class MainWindow(QWidget):
return None
# -------------------------------------------------------------- wiring
def _wire(self) -> None:
# Wiring is split so a theme switch can rebuild the views (see apply_theme):
# _wire_persistent — backend<->self/state edges, connected ONCE.
# _wire_views — every edge touching a rebuildable view widget; safe to
# re-run because the old widgets are deleted (their
# connections drop) and each new connection is unique.
def _wire_persistent(self) -> None:
s = self.state
# mode toggle <-> state <-> body
self.top_bar.mode_changed.connect(s.set_mode)
s.mode_changed.connect(self._on_mode_changed)
s.mount_requested.connect(self._on_mount_requested)
s.unmount_requested.connect(self.daq.unmount)
# live status fan-out
self.daq.update.connect(self.status_bar.update_daq_status)
self.daq.update.connect(self.manual.update_daq_status)
self.daq.update.connect(self._on_status)
self.daq.baton_status_changed.connect(self.top_bar.update_baton_status)
self.daq.baton_status_changed.connect(self._on_baton_status)
# sample list -> changer + library
self.daq.spreadsheet.connect(self._on_spreadsheet)
self.daq.reference_tools.connect(self._on_reference_tools)
# baton controls
self.top_bar.request_baton.connect(self.daq.request_baton)
self.top_bar.release_baton.connect(self.daq.release_baton)
self.top_bar.cancel_baton_request.connect(self._on_cancel_baton)
self.top_bar.tools_clicked.connect(self._open_staff_tools)
self.daq.baton_request_result.connect(self._on_baton_request_result)
self.daq.baton_response_result.connect(self._on_baton_response_result)
if hasattr(self.daq, "baton_incoming_request"):
self.daq.baton_incoming_request.connect(self._on_baton_incoming)
if hasattr(self.daq, "baton_timeout_checked"):
self.daq.baton_timeout_checked.connect(self._on_baton_timeout)
if hasattr(self.daq, "fluorimeter_spectrum_update"):
self.daq.fluorimeter_spectrum_update.connect(self._on_spectrum)
self.daq.automated_scan_done.connect(self._on_auto_done)
# errors / alerts -> banner (handlers read self.alert dynamically)
if hasattr(self.daq, "http_error"):
self.daq.http_error.connect(lambda msg: self._note(msg, error=True))
if hasattr(self.daq, "auth_error"):
self.daq.auth_error.connect(self._on_auth_error)
if hasattr(self.daq, "sample_missing"):
self.daq.sample_missing.connect(self._on_sample_missing)
if hasattr(self.daq, "polled_devices_status"):
self.daq.polled_devices_status.connect(self._on_device_status)
if hasattr(self.daq, "detector_error"):
self.daq.detector_error.connect(self._on_device_status)
if hasattr(self.daq, "automation_critical_failure"):
self.daq.automation_critical_failure.connect(
lambda msg: self._note(msg, error=True))
if hasattr(self.daq, "manual_collection_critical_failure"):
self.daq.manual_collection_critical_failure.connect(
lambda msg: self._note(msg, error=True))
if hasattr(self.daq, "pss_alarm_changed"):
self.daq.pss_alarm_changed.connect(
lambda *a: self.alert.show_message(
"PSS alarm — check hutch door / interlocks.", "warning"))
if hasattr(self.daq, "standard_scan_completed"):
self.daq.standard_scan_completed.connect(self._on_rotation_completed)
if hasattr(self.daq, "raster_scan_completed"):
self.daq.raster_scan_completed.connect(self._on_raster_banner)
if self.camera_thread is not None and hasattr(
self.camera_thread, "update_daq_status"):
self.daq.update.connect(self.camera_thread.update_daq_status)
def _wire_views(self) -> None:
s = self.state
# mode toggle + theme switch
self.top_bar.mode_changed.connect(s.set_mode)
self.top_bar.theme_toggle_requested.connect(self._on_theme_toggle)
# live status fan-out -> view widgets
self.daq.update.connect(self.status_bar.update_daq_status)
self.daq.update.connect(self.manual.update_daq_status)
self.daq.update.connect(self.staff.update_daq_status)
self.daq.baton_status_changed.connect(self.top_bar.update_baton_status)
# baton controls
self.top_bar.request_baton.connect(self.daq.request_baton)
self.top_bar.release_baton.connect(self.daq.release_baton)
self.top_bar.cancel_baton_request.connect(self._on_cancel_baton)
self.top_bar.tools_clicked.connect(self._open_staff_tools)
# beamline state switching (clickable LED)
self.status_bar.state_change_requested.connect(self._on_state_change)
# manual intents -> DAQ
# manual intents -> DAQ (scans precondition-gated at the boundary)
self.manual.center_loop_requested.connect(self.daq.center_loop)
self.manual.center_at_point_requested.connect(self._on_center_at_point)
# Scans are precondition-gated (ring/shutter/hutch) at the boundary.
self.manual.raster_scan_requested.connect(self._do_raster)
self.manual.rotation_scan_requested.connect(self._do_rotation)
self.manual.smart_params_requested.connect(self._on_smart_params)
@@ -237,20 +327,13 @@ class MainWindow(QWidget):
spec.start_clicked.connect(self.daq.fluorimeter_start)
spec.stop_clicked.connect(self.daq.fluorimeter_stop)
spec.snapshot_clicked.connect(self.daq.fluorimeter_request_snapshot)
if hasattr(self.daq, "fluorimeter_spectrum_update"):
self.daq.fluorimeter_spectrum_update.connect(self._on_spectrum)
if hasattr(self.daq, "fluorimeter_update"):
self.daq.fluorimeter_update.connect(spec.update_live)
# state mount intents -> DAQ
s.mount_requested.connect(self._on_mount_requested)
s.unmount_requested.connect(self.daq.unmount)
# motors -> DAQ
# motors
self._wire_motors(self.manual.motors)
# staff alignment view
self.daq.update.connect(self.staff.update_daq_status)
self.staff.state_requested.connect(self._on_state_change)
if hasattr(self.daq, "beam_center"):
self.staff.beam_center_changed.connect(self.daq.beam_center)
@@ -273,40 +356,12 @@ class MainWindow(QWidget):
self.automation.save_template_requested.connect(
lambda: self._note("Save as template is not wired yet.")
)
self.daq.automated_scan_done.connect(self._on_auto_done)
if hasattr(self.daq, "automation_progress"):
self.daq.automation_progress.connect(
self.automation.progress.update_progress
)
# errors / alerts -> banner
if hasattr(self.daq, "http_error"):
self.daq.http_error.connect(lambda msg: self._note(msg, error=True))
if hasattr(self.daq, "auth_error"):
self.daq.auth_error.connect(self._on_auth_error)
if hasattr(self.daq, "sample_missing"):
self.daq.sample_missing.connect(self._on_sample_missing)
if hasattr(self.daq, "polled_devices_status"):
self.daq.polled_devices_status.connect(self._on_device_status)
if hasattr(self.daq, "detector_error"):
self.daq.detector_error.connect(self._on_device_status)
if hasattr(self.daq, "automation_critical_failure"):
self.daq.automation_critical_failure.connect(
lambda msg: self._note(msg, error=True))
if hasattr(self.daq, "manual_collection_critical_failure"):
self.daq.manual_collection_critical_failure.connect(
lambda msg: self._note(msg, error=True))
if hasattr(self.daq, "pss_alarm_changed"):
self.daq.pss_alarm_changed.connect(
lambda *a: self.alert.show_message(
"PSS alarm — check hutch door / interlocks.", "warning"))
# completion notices
if hasattr(self.daq, "standard_scan_completed"):
self.daq.standard_scan_completed.connect(self._on_rotation_completed)
if hasattr(self.daq, "raster_scan_completed"):
self.daq.raster_scan_completed.connect(self._on_raster_banner)
# camera stream
# camera stream -> camera widgets
if self.camera_thread is not None:
cam = self.manual.camera
self.camera_thread.image.connect(cam.update_pixmap)
@@ -316,8 +371,6 @@ class MainWindow(QWidget):
self.camera_thread.target_point.connect(cam.update_target_point)
if hasattr(self.camera_thread, "prediction"):
self.camera_thread.prediction.connect(cam.update_predictions)
if hasattr(self.camera_thread, "update_daq_status"):
self.daq.update.connect(self.camera_thread.update_daq_status)
# staff-view camera mirrors the same stream
scam = self.staff.camera
self.camera_thread.image.connect(scam.update_pixmap)
@@ -350,6 +403,7 @@ class MainWindow(QWidget):
@Slot(object)
def _on_spreadsheet(self, payload) -> None:
self._last_spreadsheet = payload
samples = getattr(payload, "s", payload)
try:
samples = list(samples)
@@ -449,6 +503,7 @@ class MainWindow(QWidget):
@Slot(object)
def _on_reference_tools(self, payload) -> None:
self._last_reference_tools = payload
tools = getattr(payload, "s", payload)
try:
tools = list(tools)
@@ -459,6 +514,7 @@ class MainWindow(QWidget):
# ---- staff + control (guest) state ----
@Slot(object)
def _on_status(self, s) -> None:
self._last_status = s
session = getattr(s, "session", None)
staff = bool(getattr(session, "staff", False)) if session else False
if staff != self._staff:
+9 -8
View File
@@ -23,14 +23,15 @@ STATE_OPTIONS: tuple[tuple[str, str], ...] = (
("X-ray fluorescence", "xray_fluorescence"),
)
# LED colour by BeamlineStateEnum name (group colours from the old GUI).
# LED colour by BeamlineStateEnum name (Catppuccin group hues, legible on
# both Latte and Mocha).
_STATE_COLORS: dict[str, str] = {
"DewarTransfer": "#805ad5",
"SampleExchange": "#ed8936", "RobotSampleExchange": "#ed8936",
"SampleAlignment": "#48bb78", "BeamLocation": "#48bb78",
"BeamstopAlignment": "#48bb78", "FluxMeasurement": "#48bb78",
"DataCollection": "#ec4899", "XtalSnapshot": "#ec4899",
"XrayFluorescence": "#ec4899",
"DewarTransfer": "#7287fd", # lavender
"SampleExchange": "#fe640b", "RobotSampleExchange": "#fe640b", # peach
"SampleAlignment": "#40a02b", "BeamLocation": "#40a02b", # green
"BeamstopAlignment": "#40a02b", "FluxMeasurement": "#40a02b",
"DataCollection": "#ea76cb", "XtalSnapshot": "#ea76cb", # pink
"XrayFluorescence": "#ea76cb",
}
@@ -65,7 +66,7 @@ class StatusBar(QWidget):
# clickable State LED
self._state_dot = QLabel()
self._state_dot.setFixedSize(8, 8)
self._set_state_dot("#a49d8f")
self._set_state_dot(self._p.text_faint)
self._state = ClickableLabel()
self._state.setObjectName("StatusItem")
self._state.setTextFormat(Qt.RichText)
+53 -49
View File
@@ -48,60 +48,64 @@ class Palette:
cam_stop2: str
# --- Catppuccin Latte (light) ---------------------------------------------
# https://catppuccin.com — accents are dark enough that white reads on them.
LIGHT = Palette(
app_bg="#f3f1ea",
panel_bg="#fbfaf6",
header_bg="#f7f5ef",
surface="#ffffff",
border_panel="#e5e1d7",
border_control="#ddd8cc",
pending_track="#e1dccf",
pending_node="#c9c2b2",
chip_off_bg="#efece3",
text_primary="#221f19",
text_secondary="#4d473d",
text_muted="#7a756a",
text_faint="#a49d8f",
accent="#6d4bd1",
app_bg="#e6e9ef", # mantle
panel_bg="#eff1f5", # base
header_bg="#e6e9ef", # mantle
surface="#ffffff", # cards/inputs (lift off base)
border_panel="#ccd0da", # surface0
border_control="#bcc0cc", # surface1
pending_track="#ccd0da",
pending_node="#acb0be", # surface2
chip_off_bg="#dce0e8", # crust
text_primary="#4c4f69", # text
text_secondary="#5c5f77", # subtext1
text_muted="#6c6f85", # subtext0
text_faint="#8c8fa1", # overlay1
accent="#8839ef", # mauve
accent_text="#ffffff",
accent_tint_bg="#f0e9fb",
accent_tint_border="#cabaf2",
accent_ring="rgba(109,75,209,.28)",
success="#1a7f37",
success_tint="#e3f2e8",
breakpoint="#c2641f",
danger="#cf222e",
cam_stop0="#2b2622",
cam_stop1="#141312",
cam_stop2="#0a0a0b",
accent_tint_bg="#f3e9fd",
accent_tint_border="#d2b3f7",
accent_ring="rgba(136,57,239,.28)",
success="#40a02b", # green
success_tint="#e4f1de",
breakpoint="#fe640b", # peach
danger="#d20f39", # red
cam_stop0="#313244", # camera is always dark (mocha surfaces)
cam_stop1="#1e1e2e",
cam_stop2="#11111b",
)
# --- Catppuccin Mocha (dark) ----------------------------------------------
# Accents are LIGHT pastels -> foreground on a fill must be the dark base.
DARK = Palette(
app_bg="#15121c",
panel_bg="#1e1a28",
header_bg="#1e1a28",
surface="#241f30",
border_panel="#332c44",
border_control="#332c44",
pending_track="#332c44",
pending_node="#4a4159",
chip_off_bg="#2a2438",
text_primary="#ece8f4",
text_secondary="#cfc8db",
text_muted="#9c95ad",
text_faint="#807890",
accent="#9b7bff",
accent_text="#190f2e",
accent_tint_bg="#2c2440",
accent_tint_border="#473a63",
accent_ring="rgba(155,123,255,.32)",
success="#3fb950",
success_tint="#16331f",
breakpoint="#e0975a",
danger="#f0837d",
cam_stop0="#2b2622",
cam_stop1="#141312",
cam_stop2="#0a0a0b",
app_bg="#1e1e2e", # base
panel_bg="#181825", # mantle
header_bg="#181825", # mantle
surface="#313244", # surface0
border_panel="#313244", # surface0
border_control="#45475a", # surface1
pending_track="#45475a",
pending_node="#6c7086", # overlay0
chip_off_bg="#313244", # surface0
text_primary="#cdd6f4", # text
text_secondary="#bac2de", # subtext1
text_muted="#a6adc8", # subtext0
text_faint="#7f849c", # overlay1
accent="#cba6f7", # mauve
accent_text="#1e1e2e", # base — dark text on light mauve
accent_tint_bg="#302d41",
accent_tint_border="#45437a",
accent_ring="rgba(203,166,247,.40)",
success="#a6e3a1", # green
success_tint="#293a2c",
breakpoint="#fab387", # peach
danger="#f38ba8", # red
cam_stop0="#313244",
cam_stop1="#1e1e2e",
cam_stop2="#11111b",
)
+15 -2
View File
@@ -11,7 +11,7 @@ from PySide6.QtWidgets import (
QWidget,
)
from aare.gui.new_gui.theme import TOPBAR_H, Palette
from aare.gui.new_gui.theme import LIGHT, TOPBAR_H, Palette
class ModeToggle(QWidget):
@@ -57,6 +57,7 @@ class TopBar(QWidget):
release_baton = Signal()
cancel_baton_request = Signal()
tools_clicked = Signal()
theme_toggle_requested = Signal()
def __init__(self, palette: Palette, beamline_label: str, parent=None):
super().__init__(parent)
@@ -128,6 +129,18 @@ class TopBar(QWidget):
self._tools_btn.setVisible(False)
lay.addWidget(self._tools_btn)
# theme toggle (Latte <-> Mocha)
self._theme_btn = QPushButton("" if p is LIGHT else "")
self._theme_btn.setCursor(Qt.PointingHandCursor)
self._theme_btn.setToolTip("Switch theme (light / dark)")
self._theme_btn.setFixedSize(34, 34)
self._theme_btn.setStyleSheet(
f"QPushButton {{ background:{p.surface}; border:1px solid {p.border_control};"
f" border-radius:8px; font-size:15px; color:{p.text_secondary}; }}"
)
self._theme_btn.clicked.connect(self.theme_toggle_requested)
lay.addWidget(self._theme_btn)
# avatar
self._avatar = QLabel("--")
self._avatar.setObjectName("Avatar")
@@ -168,7 +181,7 @@ class TopBar(QWidget):
return
primary = self._baton_action in ("grab", "request")
if primary:
css = (f"background:{p.accent}; color:#fff; border:none;")
css = (f"background:{p.accent}; color:{p.accent_text}; border:none;")
else:
css = (f"background:{p.surface}; border:1px solid {p.border_control};"
f" color:{p.text_secondary};")
+1 -1
View File
@@ -47,7 +47,7 @@ class AlertBanner(QWidget):
"info": (p.accent_tint_bg, p.accent_tint_border, p.text_primary, ""),
"success": (p.success_tint, p.success, p.text_primary, ""),
"warning": (p.chip_off_bg, p.breakpoint, p.text_primary, ""),
"error": ("#fbe9e7", p.danger, p.text_primary, ""),
"error": (p.chip_off_bg, p.danger, p.text_primary, ""),
}
bg, border, fg, icon = styles.get(level, styles["info"])
self.setStyleSheet(
@@ -60,7 +60,7 @@ class BatonIncomingDialog(QDialog):
)
accept = QPushButton("✓ Accept")
accept.setStyleSheet(
f"QPushButton {{ background:{palette.success}; color:#fff; border:none;"
f"QPushButton {{ background:{palette.success}; color:{palette.accent_text}; border:none;"
f" border-radius:8px; padding:8px 16px; font-weight:600; }}"
)
refuse.clicked.connect(self._on_refuse)
+1 -1
View File
@@ -95,7 +95,7 @@ class BookmarksBar(QWidget):
b = QPushButton(text)
b.setCursor(Qt.PointingHandCursor)
if accent:
css = f"background:{p.accent}; color:#fff; border:none;"
css = f"background:{p.accent}; color:{p.accent_text}; border:none;"
elif danger:
css = f"background:transparent; color:{p.danger}; border:none;"
else:
+3 -3
View File
@@ -105,7 +105,7 @@ class CameraViewport(QWidget):
tile.setAlignment(Qt.AlignCenter)
tile.setFixedSize(56, 56)
tile.setStyleSheet(
f"background:{p.accent}; color:#fff; border-radius:14px; font-size:27px;"
f"background:{p.accent}; color:{p.accent_text}; border-radius:14px; font-size:27px;"
)
title = QLabel("No sample mounted")
title.setAlignment(Qt.AlignCenter)
@@ -121,7 +121,7 @@ class CameraViewport(QWidget):
btn = QPushButton("⊕ Mount sample")
btn.setCursor(Qt.PointingHandCursor)
btn.setStyleSheet(
f"QPushButton {{ background:{p.accent}; color:#fff; border:none;"
f"QPushButton {{ background:{p.accent}; color:{p.accent_text}; border:none;"
f" border-radius:10px; padding:11px 22px; font-size:14px; font-weight:600; }}"
)
btn.clicked.connect(self.mount_clicked)
@@ -513,7 +513,7 @@ class CameraViewport(QWidget):
self._paint_legend(painter, r)
def _paint_legend(self, painter: QPainter, r: QRect) -> None:
items = [("#9b7bff", "Target + coords"),
items = [("#cba6f7", "Target + coords"),
("#d29922", "Loop / face"),
("#3fb950", "Crystal")]
pad, line_h, sw = 11, 18, 9
+2 -2
View File
@@ -264,7 +264,7 @@ class MotorsPanel(QWidget):
@staticmethod
def _slider_qss(p: Palette) -> str:
return (
f"QSlider::groove:horizontal {{ height:4px; background:#e9e5db;"
f"QSlider::groove:horizontal {{ height:4px; background:{p.pending_track};"
f" border-radius:2px; }}"
f" QSlider::sub-page:horizontal {{ background:{p.accent};"
f" border-radius:2px; }}"
@@ -276,7 +276,7 @@ class MotorsPanel(QWidget):
p = self._p
if b.isChecked():
b.setStyleSheet(
f"QPushButton {{ background:{p.accent}; color:#fff; border:none;"
f"QPushButton {{ background:{p.accent}; color:{p.accent_text}; border:none;"
f" border-radius:6px; font-size:11px; font-weight:600; padding:5px 10px; }}")
else:
b.setStyleSheet(
+47 -12
View File
@@ -21,10 +21,10 @@ from PySide6.QtWidgets import QHBoxLayout, QPushButton, QWidget
from aare.gui.new_gui.state import STAGES, Stage
from aare.gui.new_gui.theme import Palette
NODE_W = 70
NODE_H = 62
NODE_W = 72
NODE_H = 66
CIRCLE_R = 12
CIRCLE_CY = 18
CIRCLE_CY = 20
class _Connector(QWidget):
@@ -44,9 +44,15 @@ class _Connector(QWidget):
def paintEvent(self, event): # noqa: N802
painter = QPainter(self)
col = self._p.success if self._done else self._p.pending_track
y = CIRCLE_CY
painter.fillRect(QRect(0, y - 1, self.width(), 2), QColor(col))
if self._done:
glow = QColor(self._p.success)
glow.setAlpha(64)
painter.fillRect(QRect(0, y - 3, self.width(), 6), glow)
painter.fillRect(QRect(0, y - 1, self.width(), 2), QColor(self._p.success))
else:
painter.fillRect(QRect(0, y - 1, self.width(), 2),
QColor(self._p.pending_track))
class StageNode(QWidget):
@@ -128,7 +134,7 @@ class StageNode(QWidget):
return
p = self._p
if self._has_break:
css = (f"background:{p.breakpoint}; color:#fff;"
css = (f"background:{p.breakpoint}; color:{p.accent_text};"
f" border:1px solid {p.breakpoint};")
else:
css = (f"background:{p.surface}; color:{p.text_faint};"
@@ -152,9 +158,10 @@ class StageNode(QWidget):
painter.drawRoundedRect(self.rect().adjusted(0, 0, -1, -1), 10, 10)
cx, cy, r = NODE_W // 2, CIRCLE_CY, CIRCLE_R
self._paint_beam(painter, cx, cy)
circle = QRect(cx - r, cy - r, 2 * r, 2 * r)
glyph = ""
glyph_color = "#fff"
glyph_color = p.accent_text
if self._start_here:
painter.setBrush(Qt.NoBrush)
@@ -223,12 +230,40 @@ class StageNode(QWidget):
painter.drawText(QRect(0, cy + r + 16, NODE_W, 12),
Qt.AlignHCenter | Qt.AlignTop, caption)
def _paint_beam(self, painter: QPainter, cx: int, cy: int) -> None:
"""The beam-path rail running through the station, left→right.
Completed length glows green (the beam has passed); at the active
station the green front meets the upcoming dim track.
"""
p = self._p
w = NODE_W
def seg(x0: int, x1: int, color: str, glow: bool = False) -> None:
if x1 <= x0:
return
if glow:
g = QColor(color)
g.setAlpha(64)
painter.fillRect(QRect(x0, cy - 3, x1 - x0, 6), g)
painter.fillRect(QRect(x0, cy - 1, x1 - x0, 2), QColor(color))
if self._done:
seg(0, w, p.success, glow=True)
elif self._selected and not self._start_here:
seg(0, cx, p.success, glow=True) # beam front reaches here
seg(cx, w, p.pending_track)
else:
seg(0, w, p.pending_track)
def _draw_ring(self, painter: QPainter, circle: QRect) -> None:
ring = QColor(self._p.accent)
ring.setAlpha(72)
painter.setBrush(Qt.NoBrush)
painter.setPen(QPen(ring, 3))
painter.drawEllipse(circle.adjusted(-3, -3, 3, 3))
# Soft focal bloom — the focused beam spot at the active station.
for grow, alpha in ((3, 90), (7, 40), (11, 18)):
ring = QColor(self._p.accent)
ring.setAlpha(alpha)
painter.setBrush(Qt.NoBrush)
painter.setPen(QPen(ring, 2))
painter.drawEllipse(circle.adjusted(-grow, -grow, grow, grow))
class PipelineTracker(QWidget):
+13 -12
View File
@@ -41,7 +41,7 @@ def _primary_button(text: str, palette: Palette) -> QPushButton:
b.setObjectName("Primary")
b.setCursor(Qt.PointingHandCursor)
b.setStyleSheet(
f"QPushButton {{ background:{palette.accent}; color:#fff; border:none;"
f"QPushButton {{ background:{palette.accent}; color:{palette.accent_text}; border:none;"
f" border-radius:8px; padding:8px 16px; font-size:12.5px; font-weight:600; }}"
)
return b
@@ -441,7 +441,7 @@ class ContextualSettings(QStackedWidget):
def _style_automate_chip(self, on: bool) -> None:
p = self._p
if on:
css = f"background:{p.accent}; color:#fff; border:none;"
css = f"background:{p.accent}; color:{p.accent_text}; border:none;"
else:
css = f"background:{p.chip_off_bg}; color:{p.text_faint}; border:none;"
self._automate_chip.setStyleSheet(
@@ -467,7 +467,7 @@ class ContextualSettings(QStackedWidget):
p = self._p
if on:
self._draw_btn.setText("✏ Drawing… (drag on camera)")
css = f"background:{p.accent}; color:#fff; border:none;"
css = f"background:{p.accent}; color:{p.accent_text}; border:none;"
else:
self._draw_btn.setText("✏ Draw grid")
css = f"background:{p.surface}; border:1px solid {p.border_control}; color:{p.text_secondary};"
@@ -525,10 +525,10 @@ class TransportRow(QWidget):
btns = QHBoxLayout()
btns.setSpacing(6)
self._play = self._tbtn("")
self._pause = self._tbtn("❚❚")
self._skip = self._tbtn("")
self._end = self._tbtn("", danger=True)
self._play = self._tbtn("", "Play")
self._pause = self._tbtn("", "Pause")
self._skip = self._tbtn("", "Skip")
self._end = self._tbtn("", "End", danger=True)
self._play.clicked.connect(self.play)
self._pause.clicked.connect(self.pause)
self._skip.clicked.connect(self.skip)
@@ -562,9 +562,9 @@ class TransportRow(QWidget):
self._unmount.clicked.connect(self.unmount)
lay.addWidget(self._unmount)
def _tbtn(self, glyph: str, danger: bool = False) -> QPushButton:
b = QPushButton(glyph)
b.setFixedSize(34, 34)
def _tbtn(self, glyph: str, label: str, danger: bool = False) -> QPushButton:
b = QPushButton(f"{glyph} {label}")
b.setMinimumHeight(32)
b.setCursor(Qt.PointingHandCursor)
b._danger = danger # type: ignore[attr-defined]
self._style_tbtn(b, active=False)
@@ -573,12 +573,13 @@ class TransportRow(QWidget):
def _style_tbtn(self, b: QPushButton, active: bool) -> None:
p = self._p
if active:
css = f"background:{p.accent}; color:#fff; border:none;"
css = f"background:{p.accent}; color:{p.accent_text}; border:none;"
else:
color = p.danger if getattr(b, "_danger", False) else p.text_secondary
css = f"background:{p.surface}; border:1px solid {p.border_control}; color:{color};"
b.setStyleSheet(
f"QPushButton {{ {css} border-radius:8px; font-size:11px; }}"
f"QPushButton {{ {css} border-radius:8px; font-size:12px;"
f" font-weight:600; padding:6px 14px; }}"
)
def update_from_state(self, state) -> None:
@@ -86,7 +86,7 @@ class SampleRow(QWidget):
if mounted:
badge = QLabel("MOUNTED")
badge.setStyleSheet(
f"font-size:10px; background:{palette.accent}; color:#fff;"
f"font-size:10px; background:{palette.accent}; color:{palette.accent_text};"
" padding:2px 7px; border-radius:9px; font-weight:600;"
)
lay.addWidget(badge)
@@ -232,7 +232,7 @@ class SampleChangerPanel(QWidget):
def _style_mount_btn(self) -> None:
p = self._p
if self._mount_btn.isEnabled():
css = f"background:{p.accent}; color:#fff; border:none;"
css = f"background:{p.accent}; color:{p.accent_text}; border:none;"
else:
css = (f"background:{p.chip_off_bg}; color:{p.text_faint};"
f" border:none;")
@@ -89,7 +89,7 @@ class SpectrumView(QWidget):
b.setCursor(Qt.PointingHandCursor)
p = self._p
if primary:
css = f"background:{p.accent}; color:#fff; border:none;"
css = f"background:{p.accent}; color:{p.accent_text}; border:none;"
else:
css = (f"background:{p.surface}; border:1px solid {p.border_control};"
f" color:{p.text_secondary};")