diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py index 502034b6..a1475202 100644 --- a/src/aare/gui/main_window.py +++ b/src/aare/gui/main_window.py @@ -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) diff --git a/src/aare/gui/panels/beamline_state_panel.py b/src/aare/gui/panels/beamline_state_panel.py index c3aa89aa..77eebb97 100644 --- a/src/aare/gui/panels/beamline_state_panel.py +++ b/src/aare/gui/panels/beamline_state_panel.py @@ -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) diff --git a/src/aare/gui/styles.py b/src/aare/gui/styles.py index 23d8b63a..ed56d3a7 100644 --- a/src/aare/gui/styles.py +++ b/src/aare/gui/styles.py @@ -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") diff --git a/src/aare/gui/widgets/splash_screen.py b/src/aare/gui/widgets/splash_screen.py index 84a4bd37..6b776aab 100644 --- a/src/aare/gui/widgets/splash_screen.py +++ b/src/aare/gui/widgets/splash_screen.py @@ -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() diff --git a/src/aare/gui/widgets/title_label.py b/src/aare/gui/widgets/title_label.py index bf1d15d6..6c5d5c28 100644 --- a/src/aare/gui/widgets/title_label.py +++ b/src/aare/gui/widgets/title_label.py @@ -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)