feat: rename themes to sunrise/sunset, add bluebird theme with Catppuccin-Latte light palette

Sunrise/sunset replace the original/portrait tokens (QSettings values
migrate on restore); bluebird is sunrise with a flat sky. The light
palette is repainted with Catppuccin Latte across alerts, chips, cards,
log, splash, inputs, and status flags, plus button/input height caps
and hover rules in the QSS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 10:35:55 +02:00
co-authored by Claude Fable 5
parent 4ef2ad64d7
commit c13ea11430
5 changed files with 319 additions and 202 deletions
+27 -10
View File
@@ -98,9 +98,10 @@ from aare.gui.styles import (
DARK_TEXT,
DOCK_CONTENT_LEFT_PAD,
SEPARATOR_HINT_DELAY_MS,
THEME_BLUEBIRD,
THEME_FADE_MS,
THEME_ORIGINAL,
THEME_PORTRAIT,
THEME_SUNRISE,
THEME_SUNSET,
build_app_stylesheet,
qcolor,
)
@@ -179,7 +180,7 @@ class MainWindow(QMainWindow):
# default) so the mutable list is per-instance (RUF012).
self._pre_vacancy_open_banners: list[TitleLabel] = []
self._theme_mode = THEME_ORIGINAL
self._theme_mode = THEME_SUNRISE
self._theme_action_group = None
self._use_legacy_theme_action = None
self._use_portrait_theme_action = None
@@ -1754,7 +1755,7 @@ class MainWindow(QMainWindow):
assert isinstance(app, QApplication) # palette() lives on QApplication
if not hasattr(self, "_default_palette"):
self._default_palette = app.palette()
if self._theme_mode == THEME_PORTRAIT:
if self._theme_mode == THEME_SUNSET:
palette = QPalette(self._default_palette)
for role in (
QPalette.ColorRole.ButtonText,
@@ -1788,7 +1789,11 @@ class MainWindow(QMainWindow):
def _restore_theme_settings(self) -> None:
settings = QSettings("PSI", "AareGUI")
# str() wrap: settings.value is typed object even with type=str.
self._theme_mode = str(settings.value("appearance/theme", THEME_ORIGINAL, type=str))
saved = str(settings.value("appearance/theme", THEME_SUNRISE, type=str))
# Migrate the pre-rename tokens so a saved theme survives the value
# change (styles.py: "original"->"sunrise", "portrait"->"sunset").
saved = {"original": THEME_SUNRISE, "portrait": THEME_SUNSET}.get(saved, saved)
self._theme_mode = saved
def _save_theme_settings(self) -> None:
settings = QSettings("PSI", "AareGUI")
@@ -1796,12 +1801,17 @@ class MainWindow(QMainWindow):
@Slot()
def use_legacy_theme(self) -> None:
self._theme_mode = THEME_ORIGINAL
self._theme_mode = THEME_SUNRISE
self._apply_theme()
@Slot()
def use_portrait_theme(self) -> None:
self._theme_mode = THEME_PORTRAIT
self._theme_mode = THEME_SUNSET
self._apply_theme()
@Slot()
def use_bluebird_theme(self) -> None:
self._theme_mode = THEME_BLUEBIRD
self._apply_theme()
def create_menu_bar(self):
@@ -1839,17 +1849,24 @@ class MainWindow(QMainWindow):
self._use_legacy_theme_action = QAction("Sunrise Theme (default)", self)
self._use_legacy_theme_action.setCheckable(True)
self._use_legacy_theme_action.setChecked(self._theme_mode == THEME_ORIGINAL)
self._use_legacy_theme_action.setChecked(self._theme_mode == THEME_SUNRISE)
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("Sunset Theme", self)
self._use_portrait_theme_action = QAction("Sunset Theme (work in progress)", self)
self._use_portrait_theme_action.setCheckable(True)
self._use_portrait_theme_action.setChecked(self._theme_mode == THEME_PORTRAIT)
self._use_portrait_theme_action.setChecked(self._theme_mode == THEME_SUNSET)
self._use_portrait_theme_action.triggered.connect(self.use_portrait_theme)
self._theme_action_group.addAction(self._use_portrait_theme_action)
self._use_bluebird_theme_action = QAction("Bluebird Theme", self)
self._use_bluebird_theme_action.setCheckable(True)
self._use_bluebird_theme_action.setChecked(self._theme_mode == THEME_BLUEBIRD)
self._use_bluebird_theme_action.triggered.connect(self.use_bluebird_theme)
self._theme_action_group.addAction(self._use_bluebird_theme_action)
view_menu.addAction(self._use_legacy_theme_action)
view_menu.addAction(self._use_bluebird_theme_action)
view_menu.addAction(self._use_portrait_theme_action)
view_menu.addSeparator()
view_menu.addAction(self._portrait_mode_action)
+2 -2
View File
@@ -5,7 +5,7 @@ from PySide6.QtCore import Qt, QTimer, Signal, Slot
from PySide6.QtGui import QCursor, QFont, QFontMetrics
from PySide6.QtWidgets import QFrame, QHBoxLayout, QLabel, QMenu, QPushButton, QSizePolicy, QToolTip
from aare.gui.styles import FONT_VALUE, THEME_ORIGINAL, state_colors
from aare.gui.styles import FONT_VALUE, THEME_SUNRISE, state_colors
# Shortcut transitions from the "Available transitions" menu in
# widgets/status_bar.py show_state_menu — these come ON TOP of the one-hop
@@ -141,7 +141,7 @@ class BeamlineStatePanel(QFrame):
self._hover_hint_timer.timeout.connect(self._show_hover_hint)
# Per-theme colors (MainWindow._apply_theme calls set_theme).
self._colors = state_colors(THEME_ORIGINAL)
self._colors = state_colors(THEME_SUNRISE)
self._separators: list[QLabel] = []
layout = QHBoxLayout(self)
+281 -185
View File
@@ -3,8 +3,14 @@ from __future__ import annotations
from pathlib import Path
from string import Template
THEME_ORIGINAL = "original"
THEME_PORTRAIT = "portrait"
# These string values are the tokens persisted in QSettings("appearance/theme").
# They were renamed from "original"/"portrait"; MainWindow._restore_theme_settings
# migrates the old tokens so a user's saved theme survives the rename.
THEME_SUNRISE = "sunrise"
THEME_SUNSET = "sunset"
# Sunrise with the sky gradient flattened to its top color — for consoles
# where the gradient banding distracts, and as a plain-background baseline.
THEME_BLUEBIRD = "bluebird"
# ---------------------------------------------------------------------------
# Color palette. Change values HERE to try a different look — the QSS below
@@ -16,16 +22,16 @@ THEME_PORTRAIT = "portrait"
# -- Light theme ------------------------------------------------------------
BACKGROUND = "#e2e7ee"
# App-wide dusk-sky gradient (sampled from the reference photo): slate blue
# fading through pale grey-lavender into warm cream. Painted once per
# top-level window (QMainWindow/QDialog) while plain child widgets stay
# transparent, so the window reads as ONE continuous sky instead of every
# widget restarting the gradient. Tune the stops here; set all three stops
# to BACKGROUND to get the old flat look back.
# App-wide sunrise-sky gradient (sampled from the reference photo taken at dawn
# near Dawn's house at Windisch: slate blue fading through pale grey-lavender
# into warm cream. Painted once per top-level window (QMainWindow/QDialog)
# while plain child widgets stay transparent, so the window reads as ONE
# continuous sky instead of every widget restarting the gradient.
# Tune the transition point here:
BACKGROUND_GRADIENT_TOP = "#84abd9" # RHEL9 window-frame blue (sampled from screenshot)
BACKGROUND_GRADIENT_MID = "#c6cad6"
BACKGROUND_GRADIENT_MID_POS = "0.55" # 0..1 — where the mid stop sits
BACKGROUND_GRADIENT_BOTTOM = "#f6e9dd"
BACKGROUND_GRADIENT_MID = "#bbd6f6"
BACKGROUND_GRADIENT_MID_POS = "0.65" # 0..1 — where the mid stop sits
BACKGROUND_GRADIENT_BOTTOM = "#dbe9f9"
APP_BACKGROUND = (
"qlineargradient(x1:0, y1:0, x2:0, y2:1,"
f" stop:0 {BACKGROUND_GRADIENT_TOP},"
@@ -52,7 +58,12 @@ SURFACE = "#f2f2f2" # input fields, cards, group boxes
# Buttons: flat + hairline like the dark theme (the explicit border is what
# switches Qt from bulky native chrome to compact QSS box rendering).
BUTTON_BG = "#f7f9fc"
# Half-transparent so the sky gradient shimmers through, same glass idea as
# INPUT_BG — slightly more solid so clickables read as raised faces.
BUTTON_BG = "rgba(255, 255, 255, 50%)"
# Hover: a dark ink tint instead of more white — darkens whatever sky shade
# is behind the button (the dark theme hovers LIGHTER, see DARK_ELEVATED_HOVER).
BUTTON_BG_HOVER = "rgba(76, 79, 105, 18%)"
BUTTON_BORDER = "#c9cfd8" # same tone as FRAME_L3_COLOR, separate knob
# Spin/combo arrow glyphs: tiny PNGs — this Qt draws neither native glyphs
@@ -102,7 +113,7 @@ BANNER_TAB_GAP = 6
# one for SEPARATOR_HINT_DELAY_MS (or a drag starts) — then only the exact
# separator under the cursor fills with SEPARATOR_HINT. The rest/drag gate
# lives in MainWindow.event(); the QSS :hover part picks the one separator.
SEPARATOR_HINT = "rgba(168, 178, 192, 50%)" # scrollbar-track grey @50%
SEPARATOR_HINT = "rgba(168, 178, 192, 20%)" # scrollbar-track grey @50%
SEPARATOR_HINT_DELAY_MS = 888 # int, used in code, not QSS
# Theme-switch screenshot cross-fade duration (int ms, used in code).
@@ -133,15 +144,17 @@ SECONDARY_BG = "#dfe9fb"
SECONDARY_BG_HOVER = "#d3e1f8"
# Alert banners (alertKind: error / success / waiting=warning):
ERROR_BG = "#fbe4e6"
ERROR_BORDER = "#d97a84"
ERROR_TEXT = "#8f1d2c"
SUCCESS_BG = "#e7f6ea"
SUCCESS_BORDER = "#7bbf8e"
SUCCESS_TEXT = "#1f6a3a"
WARNING_BG = "#fff8e1"
WARNING_BORDER = "#ffb300"
WARNING_TEXT = "#e65100"
# Catppuccin Latte: BG = 15% accent over base, border = 50%, text = 65% over
# Latte text — same recipe as the chips/cards/log blocks below.
ERROR_BG = "#ebcfd9" # red wash
ERROR_BORDER = "#e08097"
ERROR_TEXT = "#a3254a"
SUCCESS_BG = "#d5e5d7" # green wash
SUCCESS_BORDER = "#98c890"
SUCCESS_TEXT = "#448441"
WARNING_BG = "#f1dcd2" # peach wash
WARNING_BORDER = "#f6aa80"
WARNING_TEXT = "#c05d2c"
# Axis video status + beamline state bar:
STATUS_IDLE_BG = "#d9e2f2"
@@ -181,6 +194,7 @@ DARK_APP_BACKGROUND = (
DARK_PANEL2 = "#0e1728" # panel2 — deepest opaque: menus, popups, tooltips
DARK_SURFACE = "#1c2b4a" # panel — cards, state panel, scrollbar track
DARK_ELEVATED = "#253148" # glass2 (7% white) flattened — buttons, banners
DARK_ELEVATED_HOVER = "#32405c" # one glass step lighter — button hover
# Solid, not transparent: scroll-area viewports don't composite the window
# gradient on the container's X11 and render BLACK instead. Lighter sky-navy
# so tables don't read near-black against the backdrop.
@@ -238,108 +252,106 @@ WHITE = "#ffffff"
# ONLY for theme-independent light surfaces (tutorial callout). To "reset" a
# themed label, clear its stylesheet ("") so the theme color applies — a
# hardcoded black reset is invisible in the dark theme.
DEFAULT_TEXT = "#000000"
NOTE_TEXT = "#555555" # tutorial hints, TELL sample details
DIM_TEXT = "#666666" # baton dialog timers
HINT_TEXT = "#999999" # baton dialog fine print
HEADING_TEXT = "#1e293b" # card headings (slate-800)
SUBTLE_TEXT = "#334155" # card body text (slate-700)
MUTED_TEXT = "#475569" # neutral chip / idle step text (slate-600)
FAINT_TEXT = "#64748b" # pending/skipped step text (slate-500)
DEFAULT_TEXT = "#4c4f69" # latte text
NOTE_TEXT = "#5c5f77" # tutorial hints, TELL sample details (latte subtext1)
DIM_TEXT = "#6c6f85" # baton dialog timers (latte subtext0)
HINT_TEXT = "#9ca0b0" # baton dialog fine print (latte overlay0)
HEADING_TEXT = "#4c4f69" # card headings (latte text)
SUBTLE_TEXT = "#5c5f77" # card body text (latte subtext1)
MUTED_TEXT = "#6c6f85" # neutral chip / idle step text (latte subtext0)
FAINT_TEXT = "#8c8fa1" # pending/skipped step text (latte overlay1)
SHADOW = "#000000" # drop shadows & tutorial scrim; alpha stays at call site
# -- Semantic action colors -------------------------------------------------
GO_TEXT = "#4e9a06" # green start/run/measure button text
ABORT_TEXT = "#a40000" # abort button text
ALERT_TEXT = "#ff0000" # out-of-range motor labels
PATH_WARN_TEXT = "#c80000" # file-exists warning in path panel
DANGER_ACCENT = "#d9534f" # invalid p-group border + message text
GO_TEXT = "#40a02b" # green start/run/measure button text
ABORT_TEXT = "#d20f39" # abort button text (red)
ALERT_TEXT = "#d20f39" # out-of-range motor labels (red)
PATH_WARN_TEXT = "#e64553" # file-exists warning in path panel (maroon)
DANGER_ACCENT = "#e64553" # invalid p-group border + message text (maroon)
# -- Status chips (local contact status) — "good" reuses SUCCESS_BG/TEXT ----
CHIP_WARN_BG = "#fff3cd"
CHIP_WARN_TEXT = "#7a4b00"
CHIP_BAD_BG = "#fdeaea"
CHIP_BAD_TEXT = "#8b1e1e"
CHIP_NEUTRAL_BG = "#e9eef5" # text uses MUTED_TEXT
CHIP_INFO_BG = "#e8f1ff"
CHIP_INFO_TEXT = "#12406a"
CHIP_WARN_BG = "#ede2d5" # yellow wash
CHIP_WARN_TEXT = "#ac7838"
CHIP_BAD_BG = "#ebcfd9" # red wash
CHIP_BAD_TEXT = "#a3254a"
CHIP_NEUTRAL_BG = "#e6e9ef" # latte mantle; text uses MUTED_TEXT
CHIP_INFO_BG = "#d0dcf5" # blue wash
CHIP_INFO_TEXT = "#2e5ec4"
# -- Status cards (beamline recovery, local contact error frame) ------------
# TODO: recovery-card colors are inherited from the old ad-hoc design and
# stand out against the app palette — retheme them here when ready.
WARN_CARD_BORDER = "#f0c36d"
BAD_CARD_BORDER = "#e6a8a8"
INFO_CARD_BG = "#eef6ff"
INFO_CARD_BORDER = "#a8c7e6"
PENDING_CARD_BG = "#fff7db"
PENDING_CARD_BORDER = "#e7cb73"
WARN_CARD_BORDER = "#e9cea9" # yellow border
BAD_CARD_BORDER = "#e5a2b3" # red border
INFO_CARD_BG = "#d0dcf5" # blue wash
INFO_CARD_BORDER = "#a6c0f5"
PENDING_CARD_BG = "#ede2d5" # yellow wash
PENDING_CARD_BORDER = "#e9cea9"
# -- Log panel --------------------------------------------------------------
# TODO: console-log notification colors are inherited from the old ad-hoc
# design and stand out against the app palette — retheme them here when ready.
LOG_BORDER = "#8a8a8a"
LOG_PANEL_BG = "#fff4f4"
LOG_ERROR_BG = "#fff1f1"
LOG_ERROR_BORDER = "#dd6666"
LOG_WARN_BG = "#fff8e8"
LOG_WARN_BORDER = "#d7aa42"
LOG_SUCCESS_BG = "#eefaf0"
LOG_SUCCESS_BORDER = "#6cb37a"
LOG_INFO_BG = "#eef5ff"
LOG_INFO_BORDER = "#6b9bd6"
LOG_BORDER = "#8c8fa1" # latte overlay1
LOG_PANEL_BG = "#ecdae2" # faint red wash
LOG_ERROR_BG = "#ebcfd9"
LOG_ERROR_BORDER = "#e08097"
LOG_WARN_BG = "#ede2d5"
LOG_WARN_BORDER = "#e7c089"
LOG_SUCCESS_BG = "#d5e5d7"
LOG_SUCCESS_BORDER = "#98c890"
LOG_INFO_BG = "#d0dcf5"
LOG_INFO_BORDER = "#86acf5"
# -- Automation panel + progress steps --------------------------------------
AUTOMATION_TITLE_TEXT = "#1f2937"
AUTOMATION_HINT_TEXT = "#374151"
STEP_RUNNING_TEXT = "#2563eb" # same blue as PRIMARY, separate knob
STEP_SUCCESS_TEXT = "#15803d"
STEP_FAILED_TEXT = "#b91c1c"
STEP_PAUSED_TEXT = "#c2410c"
STEP_DONE_BG = "#ecfdf3"
STEP_DONE_TEXT = "#166534"
STEP_DONE_BORDER = "#a7f3d0"
STEP_ACTIVE_BG = "#eff6ff"
STEP_ACTIVE_TEXT = "#1d4ed8"
STEP_ACTIVE_BORDER = "#bfdbfe"
STEP_FAILED_BG = "#fef2f2"
STEP_FAILED_BORDER = "#fecaca"
STEP_PAUSED_BG = "#fff7ed"
STEP_PAUSED_BORDER = "#fed7aa"
STEP_IDLE_BG = "#f8fafc"
STEP_IDLE_BORDER = "#e2e8f0"
AUTOMATION_TITLE_TEXT = "#4c4f69" # latte text
AUTOMATION_HINT_TEXT = "#5c5f77" # latte subtext1
STEP_RUNNING_TEXT = "#1e66f5" # latte blue — same as PRIMARY, separate knob
STEP_SUCCESS_TEXT = "#40a02b" # green
STEP_FAILED_TEXT = "#d20f39" # red
STEP_PAUSED_TEXT = "#c05d2c" # peach ink
STEP_DONE_BG = "#d5e5d7" # green wash
STEP_DONE_TEXT = "#448441"
STEP_DONE_BORDER = "#b2d5ae"
STEP_ACTIVE_BG = "#d0dcf5" # blue wash
STEP_ACTIVE_TEXT = "#2e5ec4"
STEP_ACTIVE_BORDER = "#a6c0f5"
STEP_FAILED_BG = "#ebcfd9" # red wash
STEP_FAILED_BORDER = "#e5a2b3"
STEP_PAUSED_BG = "#f1dcd2" # peach wash
STEP_PAUSED_BORDER = "#f4c0a3"
STEP_IDLE_BG = "#eff1f5" # latte base
STEP_IDLE_BORDER = "#dce0e8" # latte crust
# -- Baton request dialog ---------------------------------------------------
BATON_OK_BG = "#4caf50"
BATON_OK_HOVER = "#45a049"
BATON_OK_PRESSED = "#3d8b40"
BATON_DANGER_BG = "#f44336"
BATON_DANGER_HOVER = "#da190b"
BATON_DANGER_PRESSED = "#c41000"
BATON_WARN = "#ff9800"
BATON_INFO = "#2196f3"
LIGHT_BORDER = "#cccccc"
PROGRESS_TRACK_BG = "#f0f0f0"
# Hover/pressed are the accent mixed 12%/24% toward Latte text.
BATON_OK_BG = "#40a02b" # green
BATON_OK_HOVER = "#419632"
BATON_OK_PRESSED = "#438d3a"
BATON_DANGER_BG = "#d20f39" # red
BATON_DANGER_HOVER = "#c2173f"
BATON_DANGER_PRESSED = "#b21e45"
BATON_WARN = "#fe640b" # peach
BATON_INFO = "#1e66f5" # blue
LIGHT_BORDER = "#bcc0cc" # latte surface1
PROGRESS_TRACK_BG = "#e6e9ef" # latte mantle
# -- Splash screen ----------------------------------------------------------
SPLASH_BG = "#222222"
SPLASH_BORDER = "#444444"
SPLASH_ACCENT = "#0078d7"
SPLASH_BG = "#dce0e8" # latte crust (progress-bar track)
SPLASH_BORDER = "#bcc0cc" # latte surface1
SPLASH_ACCENT = "#1e66f5" # latte blue
SPLASH_TEXT = "#4c4f69" # latte text — bar % and loading message
# -- Numeric inputs ---------------------------------------------------------
# Translucent, not solid white: the sky gradient shimmers through the field
# while text stays on a light ground. Raise the % for a more solid face.
INPUT_BG = "rgba(255, 255, 255, 33%)"
INPUT_INVALID_BG = "#ffd5d5"
INPUT_DISABLED_BG = "#f0f0f0"
INPUT_DISABLED_INVALID_BG = "#f0e1e1"
INPUT_INVALID_BG = "#e9c4cf" # red 20% over latte base
INPUT_DISABLED_BG = "#e6e9ef" # latte mantle
INPUT_DISABLED_INVALID_BG = "#ecdae2" # faint red wash
# -- Status bar flags (hex equivalents of the old CSS named colors) ---------
STATUS_OK = "#008000" # closed / idle / owned / tell ok (was "green")
STATUS_ALERT = "#ff0000" # open / busy / other-owner / hot cryo (was "red")
STATUS_WARN = "#ffa500" # baton waiting / warming cryo / tell busy (was "orange")
STATUS_INFO = "#0000ff" # cold cryo (was "blue")
STATUS_VACANT = "#ffff00" # baton vacant (was "yellow")
STATUS_REQUEST = "#00ffff" # baton request (was "cyan")
# -- Status bar flags (Catppuccin Latte) ------------------------------------
STATUS_OK = "#40a02b" # closed / idle / owned / tell ok (green)
STATUS_ALERT = "#d20f39" # open / busy / other-owner / hot cryo (red)
STATUS_WARN = "#fe640b" # baton waiting / warming cryo / tell busy (peach)
STATUS_INFO = "#1e66f5" # cold cryo (blue)
STATUS_VACANT = "#df8e1d" # baton vacant (yellow)
STATUS_REQUEST = "#04a5e5" # baton request (sky)
# -- Beamline state panel ---------------------------------------------------
# The panel paints these in code per DAQ tick (data-driven), so it asks
@@ -357,7 +369,7 @@ DARK_STATE_MSG_INFO = "#89b4fa" # mocha blue
def state_colors(theme: str) -> dict[str, str]:
"""Beamline-state text colors for the given theme."""
if theme == THEME_PORTRAIT:
if theme == THEME_SUNSET:
return {
"available": DARK_STATE_AVAILABLE,
"unavailable": DARK_STATE_UNAVAILABLE,
@@ -393,86 +405,89 @@ SAMPLE_STATUS_SELECTED_BG = "#d8e8fd" # pale blue — table selection highlight
SAMPLE_STATUS_TEXT = "#263043"
# -- Camera / video overlay (painter colors, alpha at call site) ------------
BEAM_OPEN = "#00ff00" # beam marker: shutter open
BEAM_IDLE = "#f57900" # beam marker: idle
BEAM_BUSY = "#ff0000" # beam marker: busy
BEAM_MARKING = "#663399" # beam marker: marking mode
MARKER_GREEN = "#32cd32" # loop-centering click marker
PATH_START = "#008000" # raster path gradient start + start circle
PATH_END = "#ff0000" # raster path gradient end + end circle
LEGEND_BG = "#141414"
LEGEND_TEXT = "#f0f0f0"
TOOLTIP_TEXT = "#e6e6e6" # camera coords tooltip pen — NOT the QToolTip popup
MARK_TOOLTIP_GOLD = "#ffd700"
MARK_TOOLTIP_ORANGE = "#ffa500"
MARK_TOOLTIP_RED = "#ff0000"
MARK_BADGE_BG = "#b43c00"
# Palette: Catppuccin Latte (light flavor) — softer than the old pure-RGB set.
BEAM_OPEN = "#40a02b" # beam marker: shutter open (green)
BEAM_IDLE = "#fe640b" # beam marker: idle (peach)
BEAM_BUSY = "#d20f39" # beam marker: busy (red)
BEAM_MARKING = "#8839ef" # beam marker: marking mode (mauve)
MARKER_GREEN = "#40a02b" # loop-centering click marker (green)
PATH_START = "#40a02b" # raster path gradient start + start circle (green)
PATH_END = "#d20f39" # raster path gradient end + end circle (red)
LEGEND_BG = "#eff1f5" # base
LEGEND_TEXT = "#4c4f69" # text
TOOLTIP_TEXT = "#4c4f69" # camera coords tooltip pen — NOT the QToolTip popup
MARK_TOOLTIP_GOLD = "#df8e1d" # yellow
MARK_TOOLTIP_ORANGE = "#fe640b" # peach
MARK_TOOLTIP_RED = "#d20f39" # red
MARK_BADGE_BG = "#fe640b" # peach
# Prediction class overlay colors. The chart variant historically used pure
# green (#00ff00) while the overlay used CSS green (#008000) — both kept.
# Prediction class overlay colors. The old pure-green vs CSS-green split
# between chart and overlay collapses to the single Latte green.
CLASS_COLORS = {
"pin": "#ff0000",
"loop_all": "#008000",
"loop_face": "#ffff00",
"crystal": "#0000ff",
"needle": "#ff00ff",
"ice": "#00ffff",
"pin": "#d20f39", # red
"loop_all": "#40a02b", # green
"loop_face": "#df8e1d", # yellow
"crystal": "#1e66f5", # blue
"needle": "#ea76cb", # pink
"ice": "#04a5e5", # sky
}
CHART_CLASS_COLORS = {
"Pin": "#ff0000",
"Loop_all": "#00ff00",
"Loop_face": "#ffff00",
"Crystal": "#0000ff",
"Needle": "#ff00ff",
"Ice": "#00ffff",
"Pin": "#d20f39",
"Loop_all": "#40a02b",
"Loop_face": "#df8e1d",
"Crystal": "#1e66f5",
"Needle": "#ea76cb",
"Ice": "#04a5e5",
}
TARGET_COLORS = {"Cyan": "#00ffff", "Dark Blue": "#0046a0", "Dark Red": "#8c1919"}
TARGET_COLORS = {"Cyan": "#04a5e5", "Dark Blue": "#1e66f5", "Dark Red": "#e64553"}
BOOKMARK_COLORS = {
"red": "#ff0000",
"green": "#008000",
"blue": "#0000ff",
"indigo": "#4b0082",
"lime": "#00ff00",
"red": "#d20f39",
"green": "#40a02b",
"blue": "#1e66f5",
"indigo": "#8839ef", # mauve
"lime": "#179299", # teal — Latte has one green; teal keeps the pair distinct
}
# -- Busy overlay (per-source color coding) ---------------------------------
BUSY_YELLOW = "#f1c40f"
BUSY_YELLOW_BORDER = "#fff8d2"
BUSY_YELLOW_DOT = "#fff6bf"
BUSY_YELLOW_TEXT_DARK = "#3b2f00"
BUSY_PURPLE = "#8e44ad"
BUSY_PURPLE_BORDER = "#ebdcf5"
BUSY_PURPLE_DOT = "#f0dfff"
BUSY_RED_BADGE = "#d64545"
BUSY_RED_FILL = "#be2828"
BUSY_RED_BORDER = "#ffdcdc"
BUSY_RED_DOT = "#ffdddd"
BUSY_ORANGE = "#e67e22"
BUSY_ORANGE_BORDER = "#ffead6"
BUSY_ORANGE_DOT = "#fff0db"
BUSY_BLUE = "#3498db"
BUSY_BLUE_BORDER = "#dcf0ff"
BUSY_BLUE_DOT = "#dff2ff"
BUSY_PSI_RED = "#e04f39"
BUSY_PSI_RED_BORDER = "#ffe1dc"
BUSY_PSI_RED_DOT = "#ffd8d1"
# Catppuccin Latte accents; BORDER/DOT are 25%/20% mixes toward Latte base.
BUSY_YELLOW = "#df8e1d" # yellow
BUSY_YELLOW_BORDER = "#ebd8bf"
BUSY_YELLOW_DOT = "#ecddca"
BUSY_YELLOW_TEXT_DARK = "#4c4f69" # text
BUSY_PURPLE = "#8839ef" # mauve
BUSY_PURPLE_BORDER = "#d5c3f4"
BUSY_PURPLE_DOT = "#daccf4"
BUSY_RED_BADGE = "#d20f39" # red
BUSY_RED_FILL = "#d20f39" # red
BUSY_RED_BORDER = "#e8b8c6"
BUSY_RED_DOT = "#e9c4cf"
BUSY_ORANGE = "#fe640b" # peach
BUSY_ORANGE_BORDER = "#f3ceba"
BUSY_ORANGE_DOT = "#f2d5c6"
BUSY_BLUE = "#1e66f5" # blue
BUSY_BLUE_BORDER = "#bbcef5"
BUSY_BLUE_DOT = "#c5d5f5"
BUSY_PSI_RED = "#e64553" # maroon — closest Latte to the PSI brand red
BUSY_PSI_RED_BORDER = "#edc6cc"
BUSY_PSI_RED_DOT = "#edcfd5"
# -- Charts (prediction metrics, target stability, fluorescence) ------------
CHART_BLUE = "#1f77b4"
CHART_BLUE_LIGHT = "#6baed6"
CHART_BLUE_PALE = "#9ecae1"
CHART_RED = "#d62728"
CHART_RED_LIGHT = "#ff9896"
CHART_RED_DARK = "#c43c39"
CHART_ORANGE = "#ff7f0e"
CHART_ORANGE_PALE = "#ffbb78"
CHART_GREEN = "#2ca02c"
CHART_GREEN_PALE = "#98df8a"
CHART_CYAN = "#17becf"
CHART_PURPLE = "#9467bd"
CHART_MUTED = "#888888"
# Catppuccin Latte; PALE variants are 35% mixes toward Latte base.
CHART_BLUE = "#1e66f5" # blue
CHART_BLUE_LIGHT = "#04a5e5" # sky
CHART_BLUE_PALE = "#a6c0f5"
CHART_RED = "#d20f39" # red
CHART_RED_LIGHT = "#dd7878" # flamingo
CHART_RED_DARK = "#e64553" # maroon
CHART_ORANGE = "#fe640b" # peach
CHART_ORANGE_PALE = "#f4c0a3"
CHART_GREEN = "#40a02b" # green
CHART_GREEN_PALE = "#b2d5ae"
CHART_CYAN = "#179299" # teal
CHART_PURPLE = "#8839ef" # mauve
CHART_MUTED = "#8c8fa1" # overlay1
CONFIDENCE_BIN_COLORS = [CHART_RED, CHART_ORANGE, CHART_ORANGE_PALE, CHART_GREEN_PALE, CHART_GREEN]
SPECTRUM_LINE = "#cc0000"
SPECTRUM_LINE = "#d20f39" # red
# -- Generic panels (developer help, raster table) --------------------------
PANEL_BG_SOFT = "#f6f6f6"
@@ -508,9 +523,9 @@ SLIDER_FILL = "#8ba3c7"
# -- Scrollbars (rounded, no arrows) ----------------------------------------
# Flipped on request: the track is now the darker grey and the draggable
# handle the light one; hover therefore lightens further instead of darkening.
SCROLLBAR_TRACK = "#a8b2c0"
SCROLLBAR_HANDLE = "#d8dde5"
SCROLLBAR_HANDLE_HOVER = "#eef1f6"
SCROLLBAR_TRACK = "#E4E4E4"
SCROLLBAR_HANDLE = "#D4D4D4"
SCROLLBAR_HANDLE_HOVER = "#C4C4C4"
# Disabled-input fill and the slider colors used to borrow the scrollbar
# knobs; own knobs so the scrollbar flip above doesn't drag them along.
@@ -567,14 +582,20 @@ def card_style(
def build_app_stylesheet(theme: str) -> str:
if theme == THEME_PORTRAIT:
return _portrait_stylesheet()
return _original_stylesheet()
if theme == THEME_SUNSET:
return _sunset_stylesheet()
if theme == THEME_BLUEBIRD:
# Same sheet as Sunrise, sky flattened to the solid top color.
return _sunrise_stylesheet({"app_background": BACKGROUND_GRADIENT_MID})
return _sunrise_stylesheet()
def _original_stylesheet() -> str:
def _sunrise_stylesheet(overrides: dict[str, str] | None = None) -> str:
mapping = _palette()
if overrides:
mapping.update(overrides)
return Template("""
/* Dusk-sky gradient: only top-level windows paint it (rule order
/* Sunrise-sky gradient: only top-level windows paint it (rule order
matters — this must come AFTER the transparent QWidget rule so it
wins the specificity tie). Knobs: BACKGROUND_GRADIENT_* above. */
QWidget {
@@ -603,10 +624,31 @@ def _original_stylesheet() -> str:
}
/* Flat compact buttons, mirroring the dark theme's elevated+hairline
look — the explicit border drops the padded native chrome. */
look — the explicit border drops the padded native chrome.
min/max-height + vertical padding pinned to the SAME values as
the inputs below: without them each widget derives its own height
from sizeHint and buttons end up taller than the entry boxes.
CAUTION: this cap hits EVERY QPushButton/QToolButton/QComboBox.
Icon buttons with a fixed size (dock title-bar popout/close) or
multi-line buttons (beamline state strip) get squashed: the padding
shrinks their content box and Qt scales the icon/text down. Any such
widget must opt out with a more specific rule or its own widget-level
stylesheet setting padding: 0 / min-height: 0 / its real max-height —
see popout_window._titlebar_button and the beamlineStatePanel rule
below for the two existing patterns. */
QPushButton, QToolButton, QComboBox {
background-color: $button_bg;
border: 1px solid $button_border;
min-height: 16px;
max-height: 16px;
padding: 1px 8px;
}
/* Hover darkens in the light theme (ink tint over the sky); the dark
theme lightens instead — direction always moves toward contrast. */
QPushButton:hover, QToolButton:hover, QComboBox:hover {
background-color: $button_bg_hover;
}
/* Inputs — centralized (was per-widget INPUT_BG stylesheets, which
@@ -617,6 +659,18 @@ def _original_stylesheet() -> str:
QLineEdit, QAbstractSpinBox {
background-color: $input_bg;
border: 1px solid $button_border;
min-height: 16px;
max-height: 16px;
padding: 1px 6px;
}
/* Beamline state strip: exempt from the global control-height cap.
Its entries are QPushButtons that wrap to two lines when the window
is narrow (see BeamlineStatePanel._update_label_mode) — the 16px cap
would clip the second line. */
QFrame#beamlineStatePanel QPushButton {
min-height: 0px;
max-height: 64px;
}
QLineEdit:read-only, QAbstractSpinBox:read-only {
@@ -875,6 +929,18 @@ def _original_stylesheet() -> str:
image: url($check_mark);
}
/* Radios stay ROUND — single-choice groups must read as radios, not
checkboxes. Checked = accent dot (a check mark in a circle reads
as a squashed checkbox). */
QRadioButton::indicator {
border-radius: 6px;
}
QRadioButton::indicator:checked {
image: none;
background: $primary;
}
QCheckBox::indicator:disabled, QRadioButton::indicator:disabled {
background: $disabled_input_bg;
}
@@ -1191,10 +1257,10 @@ def _original_stylesheet() -> str:
QWidget#portraitRoot QScrollBar::sub-line:vertical {
height: 0px;
}
""").substitute(_palette())
""").substitute(mapping)
def _portrait_stylesheet() -> str:
def _sunset_stylesheet() -> str:
return Template("""
/* Sunset-sky gradient — same transparent-children scheme as the light
theme: only top-level windows paint the sky (rule order matters, see
@@ -1214,11 +1280,29 @@ def _portrait_stylesheet() -> str:
}
/* Interactive faces sit one step above the backdrop (site: glass2)
with the faint gold hairline. */
with the faint gold hairline. Same pinned height as the light sheet
so buttons and entry boxes match in both themes. */
QPushButton, QToolButton, QComboBox,
QLineEdit, QAbstractSpinBox {
background-color: $dark_elevated;
border: 1px solid $dark_border_faint;
min-height: 16px;
max-height: 16px;
padding-top: 1px;
padding-bottom: 1px;
}
/* Hover lightens in the dark theme (one glass step up); the light
theme darkens instead — direction always moves toward contrast. */
QPushButton:hover, QToolButton:hover, QComboBox:hover {
background-color: $dark_elevated_hover;
}
/* Beamline state strip: exempt from the height cap — two-line labels
(see the light sheet's matching rule). */
QFrame#beamlineStatePanel QPushButton {
min-height: 0px;
max-height: 64px;
}
/* Text views (console log) read fine straight on the sky. */
@@ -1251,6 +1335,16 @@ def _portrait_stylesheet() -> str:
image: url($dark_check_mark);
}
/* Radios stay ROUND (see the light-theme note). Checked = gold dot. */
QRadioButton::indicator {
border-radius: 7px;
}
QRadioButton::indicator:checked {
image: none;
background: $dark_accent;
}
QCheckBox::indicator:disabled, QRadioButton::indicator:disabled {
background: $dark_disabled;
}
@@ -1767,6 +1861,8 @@ def _portrait_stylesheet() -> str:
if __name__ == "__main__":
# ponytail: smallest check that fails if a $name has no matching constant
for _theme in (THEME_ORIGINAL, THEME_PORTRAIT):
for _theme in (THEME_SUNRISE, THEME_SUNSET, THEME_BLUEBIRD):
assert "$" not in build_app_stylesheet(_theme)
assert APP_BACKGROUND not in build_app_stylesheet(THEME_BLUEBIRD)
# This line was added by Claude. But I would do the same. So all gude.
print("gude")
+5 -3
View File
@@ -1,7 +1,7 @@
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication, QProgressBar, QSplashScreen
from aare.gui.styles import SPLASH_ACCENT, SPLASH_BG, SPLASH_BORDER, WHITE, qcolor
from aare.gui.styles import SPLASH_ACCENT, SPLASH_BG, SPLASH_BORDER, SPLASH_TEXT, qcolor
class LoadingSplashScreen(QSplashScreen):
@@ -17,7 +17,7 @@ class LoadingSplashScreen(QSplashScreen):
border-radius: 5px;
text-align: center;
background-color: {SPLASH_BG};
color: {WHITE};
color: {SPLASH_TEXT};
}}
QProgressBar::chunk {{
background-color: {SPLASH_ACCENT};
@@ -28,6 +28,8 @@ class LoadingSplashScreen(QSplashScreen):
self.progress.setValue(value)
if message:
self.showMessage(
message, Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignCenter, qcolor(WHITE)
message,
Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignCenter,
qcolor(SPLASH_TEXT),
)
QApplication.processEvents()
+4 -2
View File
@@ -2,7 +2,7 @@ from PySide6.QtCore import QSettings, Qt, QTimer
from PySide6.QtGui import QPainter, QPalette
from PySide6.QtWidgets import QHBoxLayout, QLabel, QLayout, QPushButton, QStyle, QStyleOption
from aare.gui.styles import BANNER_TEXT, BANNER_TEXT_SHADOW, FONT_BODY, qcolor
from aare.gui.styles import BANNER_TEXT, BANNER_TEXT_SHADOW, FONT_VALUE, qcolor
# Universal vertical rhythm between stacked panels: each panel contributes
# PANEL_VMARGIN top and bottom, the column adds PANEL_VSPACING between them,
@@ -65,9 +65,11 @@ class TitleLabel(QLabel):
self.toggle_button = QPushButton("", self)
# Bare glyph, no pill: the shared beamlineStateToggleButton QSS paints
# a translucent white background, which is unwanted on these banners.
# FONT_VALUE (18px), not FONT_BODY: a bare +/- glyph reads smaller than
# the 16px banner title beside it; the big-glyph size evens them out.
self.toggle_button.setStyleSheet(
f"QPushButton {{ background: transparent; border: none;"
f" color: {BANNER_TEXT}; font-size: {FONT_BODY}; font-weight: 700; }}"
f" color: {BANNER_TEXT}; font-size: {FONT_VALUE}; font-weight: 700; }}"
)
self.toggle_button.setToolTip("Minimise panel")
self.toggle_button.setFixedSize(21, 21)