Beam location, Beamstop alignment and Flux measurement grey out for non-staff in the state strip and the status-bar transitions menu. All "not admin" denials now show as an auto-dismissing QToolTip on a Catppuccin red wash instead of a click-to-close QMessageBox. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1921 lines
66 KiB
Python
1921 lines
66 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from string import Template
|
|
|
|
# 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
|
|
# and the widgets that import these use them everywhere. Every UPPERCASE
|
|
# constant is exposed to the QSS templates as $lowercase_name (see _palette),
|
|
# so no color literals live inside the stylesheets themselves.
|
|
# Any value can be "transparent" (e.g. to hide a border or a fill).
|
|
# string.Template ($name) instead of f-strings so the QSS braces stay literal.
|
|
|
|
# -- Light theme ------------------------------------------------------------
|
|
BACKGROUND = "#e2e7ee"
|
|
# 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 = "#adccf1"
|
|
BACKGROUND_GRADIENT_MID_POS = "0.55" # 0..1 — where the mid stop sits
|
|
BACKGROUND_GRADIENT_BOTTOM = "#f8e9c5"
|
|
APP_BACKGROUND = (
|
|
"qlineargradient(x1:0, y1:0, x2:0, y2:1,"
|
|
f" stop:0 {BACKGROUND_GRADIENT_TOP},"
|
|
f" stop:{BACKGROUND_GRADIENT_MID_POS} {BACKGROUND_GRADIENT_MID},"
|
|
f" stop:1 {BACKGROUND_GRADIENT_BOTTOM})"
|
|
)
|
|
BANNER = "#a2b7e4"
|
|
BANNER_TEXT = "#F8F8FC" # must contrast with BANNER
|
|
BANNER_TEXT_SHADOW = "#000000" # soft shadow under banner titles; alpha in TitleLabel
|
|
# Banner edge lines (1px left + bottom): each line fades in from transparent,
|
|
# peaks at BANNER_EDGE mid-line and fades out at the far end — a soft sheen,
|
|
# not a hard rule. BANNER_EDGE is the peak color; "transparent" hides both.
|
|
BANNER_EDGE = "#ffffff"
|
|
BANNER_EDGE_H = ( # bottom line — fades along the banner width
|
|
"qlineargradient(x1:0, y1:0, x2:1, y2:0,"
|
|
f" stop:0 transparent, stop:0.5 {BANNER_EDGE}, stop:1 transparent)"
|
|
)
|
|
BANNER_EDGE_V = ( # left line — fades along the banner height
|
|
"qlineargradient(x1:0, y1:0, x2:0, y2:1,"
|
|
f" stop:0 transparent, stop:0.5 {BANNER_EDGE}, stop:1 transparent)"
|
|
)
|
|
TEXT = "#263043"
|
|
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).
|
|
# 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
|
|
# nor QSS border-triangles inside styled spin buttons, so image files are
|
|
# the only reliable path. Colors live in the PNGs (TEXT / DARK_TEXT at
|
|
# generation time); regenerate them if those knobs change.
|
|
_GRAPHICS_DIR = Path(__file__).resolve().parent / "graphics"
|
|
SPIN_ARROW_UP = (_GRAPHICS_DIR / "spin_arrow_up_light.png").as_posix()
|
|
SPIN_ARROW_DOWN = (_GRAPHICS_DIR / "spin_arrow_down_light.png").as_posix()
|
|
DARK_SPIN_ARROW_UP = (_GRAPHICS_DIR / "spin_arrow_up_dark.png").as_posix()
|
|
DARK_SPIN_ARROW_DOWN = (_GRAPHICS_DIR / "spin_arrow_down_dark.png").as_posix()
|
|
# Slider-handle grip lines (3 vertical ticks) — QSS can't draw interior
|
|
# lines, so they are tiny PNG assets like the spin arrows above.
|
|
SLIDER_GRIP = (_GRAPHICS_DIR / "slider_grip_light.png").as_posix()
|
|
DARK_SLIDER_GRIP = (_GRAPHICS_DIR / "slider_grip_dark.png").as_posix()
|
|
# Check marks (PRIMARY blue / dusk gold at generation time):
|
|
CHECK_MARK = (_GRAPHICS_DIR / "check_mark_light.png").as_posix()
|
|
DARK_CHECK_MARK = (_GRAPHICS_DIR / "check_mark_dark.png").as_posix()
|
|
|
|
# Borders (all can be "transparent" to hide the line):
|
|
BORDER = "transparent" # main dividers, e.g. the beamline state bar top line
|
|
CARD_BORDER = "transparent" # cards / group boxes (Local Contact, automation)
|
|
COMPACT_BORDER = "transparent" # compact-automation cards and buttons
|
|
|
|
# -- Box-frame borders by nesting level -------------------------------------
|
|
# One knob per level so the amount of "boxing" is tunable in one place:
|
|
# L1: top-level pop-out window edge (painted by PopoutWindow)
|
|
# L2: panel frames inside a dock/tab (TELL sample changer, reference
|
|
# tools, sample queue) — transparent by default: banner + table
|
|
# already delimit the panel, extra boxes read as clutter
|
|
# L3: data views (tables, log text) — a light line so scrollable content
|
|
# keeps a visible edge
|
|
# (The tab-widget pane is a fourth box, already governed by BORDER above.)
|
|
FRAME_L1_WIDTH = "0px" # default off — the pop-out reads as one clean surface
|
|
FRAME_L1_COLOR = "#bdbdbd"
|
|
FRAME_L2_WIDTH = "0px"
|
|
FRAME_L2_COLOR = "transparent"
|
|
FRAME_L3_WIDTH = "1px"
|
|
FRAME_L3_COLOR = "#c9cfd8"
|
|
|
|
# Left inset (px, int — used in code, not QSS) for bottom-dock content so its
|
|
# left edge lines up with the left-column panels above (Loop centering).
|
|
DOCK_CONTENT_LEFT_PAD = 10
|
|
|
|
# Gap (px, int — used in code, not QSS) between a TitleLabel banner and a tab
|
|
# bar sitting directly under it (Exp. Config.), so the tabs don't touch the
|
|
# banner's bottom edge line.
|
|
BANNER_TAB_GAP = 6
|
|
|
|
# Resize-line hint: dock separators stay invisible until the mouse rests on
|
|
# 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, 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).
|
|
THEME_FADE_MS = 250
|
|
|
|
# Tab face fill: the selected Dewar/Auxiliary tab AND the unchecked
|
|
# All/Queued/Flagged/Measured filter buttons share this background.
|
|
TAB_FACE_BG = "#ffffff"
|
|
|
|
# Selection highlight (combo popups, menus, list rows, text selections).
|
|
# Qt's style palette used to supply its own blue here; now it is the banner
|
|
# blue, and THIS is the knob for it. (Table-row selection has its own paler
|
|
# knob: SAMPLE_STATUS_SELECTED_BG.)
|
|
SELECTION_BG = BANNER
|
|
SELECTION_TEXT = "#263043" # same as TEXT — readable on the banner blue
|
|
|
|
# Compact-automation page:
|
|
COMPACT_CARD_BG = "#e6eefc"
|
|
COMPACT_TITLE = "#17324d" # section titles + menu/secondary button text
|
|
COMPACT_HINT = "#51657d" # secondary text: hints, queue titles
|
|
COMPACT_VALUE = "#10263a"
|
|
COMPACT_MENU_BG = "#cbdcf8"
|
|
COMPACT_MENU_BG_HOVER = "#bfd4f6"
|
|
PRIMARY = "#2563eb" # main action button
|
|
PRIMARY_HOVER = "#1d4ed8"
|
|
PRIMARY_TEXT = "white"
|
|
SECONDARY_BG = "#dfe9fb"
|
|
SECONDARY_BG_HOVER = "#d3e1f8"
|
|
|
|
# Alert banners (alertKind: error / success / waiting=warning):
|
|
# 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"
|
|
STATUS_LABEL_TEXT = "#2f3b52"
|
|
STATE_TOGGLE_TEXT = "white" # collapse glyph sitting on the BANNER strip
|
|
STATE_CURRENT_TEXT = "#1e293b" # same slate as HEADING_TEXT, separate knob
|
|
STATE_TELL_TEXT = "#374357" # also the status-bar TELL line
|
|
|
|
# -- Sunset (dark) theme: Daemmerung DUSK palette ---------------------------
|
|
# Adopted from ~/repos/aaregui2 (focus/theme.py, jdawnduan.com Daemmerung).
|
|
# Grouped by ROLE, mirroring the source Palette dataclass, so a later change
|
|
# touches one block. The site's translucent glass is flattened to opaque hex
|
|
# (this QSS paints widgets opaque); the radial sunset backdrop is NOT
|
|
# adopted — it needs the transparent-children scheme the Sunrise theme uses.
|
|
|
|
# Surfaces:
|
|
DARK_BG = "#15213a" # bg — window backdrop
|
|
# Sunset-sky backdrop (reference photo): near-black navy zenith; the fade
|
|
# begins at MID_POS (0.55, same as the day theme), runs through the blue
|
|
# band low in the window, and the warm glow is squeezed into the last
|
|
# stretch below BLUE_POS. Warm tone deliberately dimmer than the photo's
|
|
# cream — dusk text is light. Same transparent-children scheme as
|
|
# APP_BACKGROUND; set all stops to DARK_BG for a flat backdrop.
|
|
DARK_BACKGROUND_GRADIENT_TOP = "#0c1a33"
|
|
DARK_BACKGROUND_GRADIENT_MID = "#16294d" # fade onset tone
|
|
DARK_BACKGROUND_GRADIENT_MID_POS = "0.55" # 0..1 — where the fade begins
|
|
DARK_BACKGROUND_GRADIENT_BLUE = "#2c5f9e" # the blue band
|
|
DARK_BACKGROUND_GRADIENT_BLUE_POS = "0.90" # 0..1 — warm glow only below this
|
|
DARK_BACKGROUND_GRADIENT_BOTTOM = "#2c5f9e" # the warm glow doesn't seem to fit
|
|
DARK_APP_BACKGROUND = (
|
|
"qlineargradient(x1:0, y1:0, x2:0, y2:1,"
|
|
f" stop:0 {DARK_BACKGROUND_GRADIENT_TOP},"
|
|
f" stop:{DARK_BACKGROUND_GRADIENT_MID_POS} {DARK_BACKGROUND_GRADIENT_MID},"
|
|
f" stop:{DARK_BACKGROUND_GRADIENT_BLUE_POS} {DARK_BACKGROUND_GRADIENT_BLUE},"
|
|
f" stop:1 {DARK_BACKGROUND_GRADIENT_BOTTOM})"
|
|
)
|
|
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.
|
|
DARK_TABLE_BG = "#263a61"
|
|
|
|
# Hairlines — the site's gold line flattened over bg at its three alphas:
|
|
DARK_BORDER_FAINT = "#31313b" # border (14%) — input/button edges
|
|
DARK_BORDER = "#423a3c" # border2 (22%) — cards, scrollbar handle
|
|
DARK_BORDER_STRONG = "#624c3d" # border3 (38%) — emphasized edges
|
|
|
|
# Text ramp (bright -> dim):
|
|
DARK_TEXT = "#e9edf4" # text — primary
|
|
DARK_SUBTEXT = "#aebccd" # subtext — secondary labels, hints
|
|
DARK_MUTED = "#7e8ea4" # muted — tertiary, unselected tabs
|
|
DARK_OVERLAY = "#66778f" # overlay — disabled text
|
|
DARK_DISABLED = "#3c4a66" # disabled — disabled fills
|
|
|
|
# Accents — gold is IDENTITY (titles, highlights), blue is ACTION (primary
|
|
# buttons); the site keeps the two apart on purpose:
|
|
DARK_ACCENT = "#e0913f" # gold
|
|
DARK_ACCENT_HOVER = "#eaa253" # accent2 — brighter gold
|
|
DARK_ACCENT_FILL = "#89b4fa" # action blue
|
|
DARK_ACCENT_FILL_HOVER = "#9ec2fb" # +10% white, derived (site has no step)
|
|
DARK_ON_ACCENT = "#1a1320" # text on either accent fill
|
|
|
|
# Banner strips (TitleLabel + beamline state title): gold edge sheen that
|
|
# fades in/out like the light theme's BANNER_EDGE_H/V.
|
|
DARK_BANNER_EDGE = "#e0913f"
|
|
DARK_BANNER_EDGE_H = (
|
|
"qlineargradient(x1:0, y1:0, x2:1, y2:0,"
|
|
f" stop:0 transparent, stop:0.5 {DARK_BANNER_EDGE}, stop:1 transparent)"
|
|
)
|
|
DARK_BANNER_EDGE_V = (
|
|
"qlineargradient(x1:0, y1:0, x2:0, y2:1,"
|
|
f" stop:0 transparent, stop:0.5 {DARK_BANNER_EDGE}, stop:1 transparent)"
|
|
)
|
|
|
|
# Status — border/text full strength, bg = 25% blend over DARK_BG (the dusk
|
|
# palette defines no alert backgrounds, so these are computed blends):
|
|
DARK_ERROR_BG = "#48374d" # alarm 25% over bg
|
|
DARK_ERROR_BORDER = "#e07a85" # alarm
|
|
DARK_ERROR_TEXT = "#e07a85" # alarm
|
|
DARK_SUCCESS_BG = "#2f4e5d" # green 25% over bg
|
|
DARK_SUCCESS_BORDER = "#7dd6c6" # green
|
|
DARK_SUCCESS_TEXT = "#7dd6c6" # green
|
|
DARK_WARNING_BG = "#473741" # warn (copper) 25% over bg
|
|
DARK_WARNING_BORDER = "#dd7a56" # warn
|
|
DARK_WARNING_TEXT = "#dd7a56" # warn
|
|
|
|
# -- Shared chrome (light-theme widgets) ------------------------------------
|
|
# Extracted from per-widget literals so the whole app is themeable from this
|
|
# file. The same hex may appear under two names when the roles differ —
|
|
# separate knobs on purpose.
|
|
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 = "#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 = "#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 = "#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) ------------
|
|
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 --------------------------------------------------------------
|
|
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 = "#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 ---------------------------------------------------
|
|
# 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 = "#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 = "#e9c4cf" # red 20% over latte base
|
|
INPUT_DISABLED_BG = "#e6e9ef" # latte mantle
|
|
INPUT_DISABLED_INVALID_BG = "#ecdae2" # faint red wash
|
|
# Motor batch-move states (MotorMoveGroup "movestate" dynamic property):
|
|
# staged-but-not-sent target vs motor in motion — see motor_move_group.py.
|
|
INPUT_PENDING_BG = "#f2cdba" # peach 25% over latte base
|
|
INPUT_MOVING_BG = "#c3ddc3" # green 25% over latte base
|
|
DARK_INPUT_PENDING_BG = "#473741" # warn (copper) 25% over bg
|
|
DARK_INPUT_MOVING_BG = "#2f4e5d" # green 25% over bg
|
|
|
|
# -- 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)
|
|
# Dark variants (Catppuccin Mocha) — the latte values above sink into the
|
|
# sunset sky. Painted in code per DAQ tick, so status_colors(theme) hands
|
|
# them out — same pattern as state_colors below.
|
|
DARK_STATUS_OK = "#a6e3a1" # mocha green
|
|
DARK_STATUS_ALERT = "#f38ba8" # mocha red
|
|
DARK_STATUS_WARN = "#fab387" # mocha peach
|
|
DARK_STATUS_INFO = "#89b4fa" # mocha blue
|
|
DARK_STATUS_VACANT = "#f9e2af" # mocha yellow
|
|
DARK_STATUS_REQUEST = "#89dceb" # mocha sky
|
|
DARK_STATE_TELL_TEXT = "#bac2de" # mocha subtext1 — status-bar TELL idle line
|
|
|
|
|
|
def status_colors(theme: str) -> dict[str, str]:
|
|
"""Status-bar flag colors for the given theme (painted in code)."""
|
|
if theme == THEME_SUNSET:
|
|
return {
|
|
"ok": DARK_STATUS_OK,
|
|
"alert": DARK_STATUS_ALERT,
|
|
"warn": DARK_STATUS_WARN,
|
|
"info": DARK_STATUS_INFO,
|
|
"vacant": DARK_STATUS_VACANT,
|
|
"request": DARK_STATUS_REQUEST,
|
|
"tell": DARK_STATE_TELL_TEXT,
|
|
}
|
|
return {
|
|
"ok": STATUS_OK,
|
|
"alert": STATUS_ALERT,
|
|
"warn": STATUS_WARN,
|
|
"info": STATUS_INFO,
|
|
"vacant": STATUS_VACANT,
|
|
"request": STATUS_REQUEST,
|
|
"tell": STATE_TELL_TEXT,
|
|
}
|
|
|
|
|
|
# -- Beamline state panel ---------------------------------------------------
|
|
# The panel paints these in code per DAQ tick (data-driven), so it asks
|
|
# state_colors(theme) below instead of QSS. Light values unchanged; dark
|
|
# values are Catppuccin Mocha so they stay readable on the sunset sky.
|
|
STATE_AVAILABLE = "#ed8936"
|
|
STATE_UNAVAILABLE = "#8c96a5"
|
|
STATE_MSG_ERROR = "#c81e1e"
|
|
STATE_MSG_INFO = "#005caa"
|
|
DARK_STATE_AVAILABLE = "#fab387" # mocha peach
|
|
DARK_STATE_UNAVAILABLE = "#7f849c" # mocha overlay1
|
|
DARK_STATE_MSG_ERROR = "#f38ba8" # mocha red
|
|
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_SUNSET:
|
|
return {
|
|
"available": DARK_STATE_AVAILABLE,
|
|
"unavailable": DARK_STATE_UNAVAILABLE,
|
|
"error": DARK_STATE_MSG_ERROR,
|
|
"info": DARK_STATE_MSG_INFO,
|
|
}
|
|
return {
|
|
"available": STATE_AVAILABLE,
|
|
"unavailable": STATE_UNAVAILABLE,
|
|
"error": STATE_MSG_ERROR,
|
|
"info": STATE_MSG_INFO,
|
|
}
|
|
|
|
|
|
# -- Sample tables + raster grid --------------------------------------------
|
|
SAMPLE_ROW_ACTIVE_BG = "#ff6600"
|
|
SAMPLE_ROW_QUEUED_BG = "#729fcf" # mounted/current-sample row (legacy name)
|
|
SAMPLE_ROW_HIGHLIGHT_BG = "#d8e4fd"
|
|
RASTER_GRID_LINE = "#729fcf"
|
|
TABLE_SHADE_BG = "#e0e0e0"
|
|
|
|
# -- Sample status row tints (dewar/queue view) -----------------------------
|
|
# Status colors now fill the rounded dot in the table's status column; rows
|
|
# themselves alternate WHITE / SAMPLE_ROW_ALT_BG. Tune the palette here.
|
|
SAMPLE_ROW_ALT_BG = "#eef1f5" # staggered row grey (alternates with white)
|
|
SAMPLE_STATUS_QUEUED_BG = "#ffe4c4" # pale orange — waiting in the automation queue
|
|
SAMPLE_STATUS_FLAGGED_BG = "#ffd9d9" # pale red — automation failed on this sample
|
|
SAMPLE_STATUS_MEASURED_BG = "#dcf2e0" # pale green — already has collected data
|
|
SAMPLE_STATUS_SELECTED_BG = "#84abd9" # pale blue — table selection highlight
|
|
# Fixed ink on the pastel tints above: the tints stay light in BOTH themes,
|
|
# so theme-following text (white in Sunset) would vanish on them. Models
|
|
# return this as ForegroundRole wherever they return a tint.
|
|
SAMPLE_STATUS_TEXT = "#263043"
|
|
|
|
# -- Camera / video overlay (painter colors, alpha at call site) ------------
|
|
# 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 old pure-green vs CSS-green split
|
|
# between chart and overlay collapses to the single Latte green.
|
|
CLASS_COLORS = {
|
|
"pin": "#d20f39", # red
|
|
"loop_all": "#40a02b", # green
|
|
"loop_face": "#df8e1d", # yellow
|
|
"crystal": "#1e66f5", # blue
|
|
"needle": "#ea76cb", # pink
|
|
"ice": "#04a5e5", # sky
|
|
}
|
|
CHART_CLASS_COLORS = {
|
|
"Pin": "#d20f39",
|
|
"Loop_all": "#40a02b",
|
|
"Loop_face": "#df8e1d",
|
|
"Crystal": "#1e66f5",
|
|
"Needle": "#ea76cb",
|
|
"Ice": "#04a5e5",
|
|
}
|
|
TARGET_COLORS = {"Cyan": "#04a5e5", "Dark Blue": "#1e66f5", "Dark Red": "#e64553"}
|
|
BOOKMARK_COLORS = {
|
|
"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) ---------------------------------
|
|
# 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) ------------
|
|
# 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 = "#d20f39" # red
|
|
|
|
# -- Generic panels (developer help, raster table) --------------------------
|
|
PANEL_BG_SOFT = "#f6f6f6"
|
|
PANEL_BG_FAINT = "#fafafa"
|
|
PANEL_BORDER = "#d0d0d0"
|
|
PANEL_BORDER_DARK = "#bdbdbd"
|
|
PANEL_BORDER_LIGHT = "#e0e0e0"
|
|
TUTORIAL_BORDER = "#555555"
|
|
TUTORIAL_HIGHLIGHT = "#ffff00" # widget spotlight pen (was Qt.yellow)
|
|
|
|
# -- Typography ladder (role-named; tune sizes here, not in widgets) --------
|
|
FONT_HERO = "28px" # portrait main action button
|
|
FONT_ALERT = "18px" # alert banner text
|
|
FONT_VALUE = "18px" # prominent values, big glyph buttons
|
|
FONT_TITLE = "16px" # section / panel titles
|
|
FONT_BODY_LG = "14px" # primary buttons, emphasized body text — always bold
|
|
FONT_BODY = "14px" # default UI text
|
|
FONT_LABEL = "14px" # form labels, secondary text — always bold
|
|
FONT_HINT = "12px" # hints
|
|
FONT_FINE = "11px" # fine print, queue titles
|
|
|
|
# -- Hover tooltips (the QToolTip popup; styled borderless) -----------------
|
|
TOOLTIP_BG = "#f7f9fc"
|
|
TOOLTIP_FG = "#263043"
|
|
DARK_TOOLTIP_BG = "#0e1728" # dusk panel2 — deepest opaque (menus/tooltips)
|
|
DARK_TOOLTIP_FG = "#e9edf4" # dusk text
|
|
|
|
# "Needs admin/staff" warning tips: same passive QToolTip, on a Catppuccin
|
|
# red wash — a denial reads as a warning without a click-to-close dialog.
|
|
ADMIN_TIP_BG = "#d20f39" # latte red
|
|
ADMIN_TIP_FG = "#ffffff"
|
|
DARK_ADMIN_TIP_BG = "#f38ba8" # mocha red
|
|
DARK_ADMIN_TIP_FG = "#11111b" # mocha crust
|
|
|
|
|
|
def admin_tip_qss(theme: str) -> str:
|
|
"""QToolTip rule for admin-only warning tips. Set it on the widget the
|
|
tip is shown for, never app-wide — QToolTip inherits QSS from that
|
|
widget, and an app-wide rule would turn every tooltip red."""
|
|
bg, fg = (
|
|
(DARK_ADMIN_TIP_BG, DARK_ADMIN_TIP_FG)
|
|
if theme == THEME_SUNSET
|
|
else (ADMIN_TIP_BG, ADMIN_TIP_FG)
|
|
)
|
|
# Transparent border required, same as the app-wide QToolTip rule.
|
|
return (
|
|
f"QToolTip {{ background-color: {bg}; color: {fg};"
|
|
f" border: 1px solid transparent; padding: 4px 6px; }}"
|
|
)
|
|
|
|
|
|
# -- Sliders ----------------------------------------------------------------
|
|
# Own knob instead of PRIMARY: full-saturation button blue was too loud for a
|
|
# passive fill (illumination panel). Muted slate-blue, tweak freely.
|
|
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 = "#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.
|
|
DISABLED_INPUT_BG = "#d8dde5"
|
|
SLIDER_TRACK = "#d8dde5" # groove
|
|
SLIDER_MUTED = "#a8b2c0" # handle border + disabled fill
|
|
|
|
# -- Cards ------------------------------------------------------------------
|
|
CARD_RADIUS = "12px" # one radius for every card-shaped frame
|
|
FLAT_CARD_RADIUS = "0px" # recovery + console-log cards stay square
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _palette() -> dict[str, str]:
|
|
# Why: one substitution mapping instead of ~40 kwargs per stylesheet, and
|
|
# a renamed/missing constant fails loudly (KeyError) instead of silently.
|
|
return {k.lower(): v for k, v in globals().items() if k.isupper() and isinstance(v, str)}
|
|
|
|
|
|
def qcolor(color: str, alpha: int | None = None):
|
|
"""QColor from a palette hex string, optional 0-255 alpha.
|
|
|
|
Hue lives here (theme), alpha stays at the call site (behavior). Lazy Qt
|
|
import so this module — and its self-check — stay importable without
|
|
PySide6.
|
|
"""
|
|
from PySide6.QtGui import QColor
|
|
|
|
c = QColor(color)
|
|
if alpha is not None:
|
|
c.setAlpha(alpha)
|
|
return c
|
|
|
|
|
|
def card_style(
|
|
bg: str,
|
|
border: str,
|
|
text: str | None = None,
|
|
*,
|
|
selector: str = "QFrame",
|
|
border_px: int = 1,
|
|
radius: str = CARD_RADIUS,
|
|
extra: str = "",
|
|
) -> str:
|
|
"""One QSS rule for every card-shaped frame: geometry stays uniform
|
|
(CARD_RADIUS unless overridden), only the role colors vary. `extra`
|
|
appends declarations (padding, font-weight, …) inside the same rule."""
|
|
color = f" color: {text};" if text else ""
|
|
tail = f" {extra}" if extra else ""
|
|
return (
|
|
f"{selector} {{ background: {bg};{color}"
|
|
f" border: {border_px}px solid {border}; border-radius: {radius};{tail} }}"
|
|
)
|
|
|
|
|
|
def build_app_stylesheet(theme: str) -> str:
|
|
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 _sunrise_stylesheet(overrides: dict[str, str] | None = None) -> str:
|
|
mapping = _palette()
|
|
if overrides:
|
|
mapping.update(overrides)
|
|
return Template("""
|
|
/* 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 {
|
|
background-color: transparent;
|
|
color: $text;
|
|
}
|
|
|
|
QMainWindow, QDialog, PopoutWindow,
|
|
QDockWidget[floating="true"] {
|
|
background: $app_background;
|
|
}
|
|
|
|
/* These used to take the flat fill from the global QWidget rule; now
|
|
that it is transparent they need an opaque face of their own
|
|
(buttons, inputs, headers) or an opaque popup canvas (menus,
|
|
combo dropdown lists). */
|
|
QHeaderView::section, QMenu,
|
|
QComboBox QAbstractItemView {
|
|
background-color: $background;
|
|
}
|
|
|
|
/* Text views (console log): transparent, SAME as the dark theme — keep
|
|
the two sheets' transparency decisions in lockstep. */
|
|
QTextEdit, QPlainTextEdit {
|
|
background-color: transparent;
|
|
}
|
|
|
|
/* Flat compact buttons, mirroring the dark theme's elevated+hairline
|
|
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
|
|
pinned light fills into the dark theme). Read-only is the QSS
|
|
pseudo-class; "invalid" is a dynamic property set by NumberLineEdit.
|
|
The explicit border drops the tall native input chrome — same
|
|
compact QSS box rendering the dark theme gets. */
|
|
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 {
|
|
background-color: $input_disabled_bg;
|
|
}
|
|
|
|
/* Motor batch-move states (before the invalid rules on purpose: equal
|
|
specificity, so out-of-range red must come last to win). */
|
|
QLineEdit[movestate="pending"] {
|
|
background-color: $input_pending_bg;
|
|
}
|
|
|
|
QLineEdit[movestate="moving"] {
|
|
background-color: $input_moving_bg;
|
|
}
|
|
|
|
QLineEdit[invalid="true"] {
|
|
background-color: $input_invalid_bg;
|
|
}
|
|
|
|
QLineEdit[invalid="true"]:read-only {
|
|
background-color: $input_disabled_invalid_bg;
|
|
}
|
|
|
|
/* Spinboxes: both bare arrows adjacent on the right — up inboard, down
|
|
at the outer edge (wide fields put opposite-side arrows miles apart).
|
|
No button face or frame, the box's own glass is the whole control;
|
|
the value region spans everything left of the pair. Arrows are PNG
|
|
assets (SPIN_ARROW_*) — this Qt draws neither native glyphs nor
|
|
border-triangles inside styled buttons. */
|
|
QAbstractSpinBox {
|
|
padding-left: 6px;
|
|
padding-right: 34px;
|
|
}
|
|
|
|
QAbstractSpinBox::down-button {
|
|
subcontrol-origin: border;
|
|
subcontrol-position: center right;
|
|
width: 16px;
|
|
background: transparent;
|
|
border: none;
|
|
}
|
|
|
|
QAbstractSpinBox::up-button {
|
|
subcontrol-origin: border;
|
|
subcontrol-position: center right;
|
|
left: -16px;
|
|
width: 16px;
|
|
background: transparent;
|
|
border: none;
|
|
}
|
|
|
|
QAbstractSpinBox::down-arrow {
|
|
image: url($spin_arrow_down);
|
|
}
|
|
|
|
QAbstractSpinBox::up-arrow {
|
|
image: url($spin_arrow_up);
|
|
}
|
|
|
|
/* Bare dropdown: no framed native button around the combo arrow. */
|
|
QComboBox::drop-down {
|
|
border: none;
|
|
background: transparent;
|
|
}
|
|
|
|
QComboBox::down-arrow {
|
|
image: url($spin_arrow_down);
|
|
}
|
|
|
|
/* The value area is a QLineEdit INSIDE the spinbox — left alone it
|
|
stacks its own INPUT_BG glass on the spinbox's, reading near-solid
|
|
white. The box already signals "editable"; one glass layer is enough. */
|
|
QAbstractSpinBox QLineEdit {
|
|
background: transparent;
|
|
}
|
|
|
|
/* Table select-all corner: QHeaderView::section above doesn't match it,
|
|
and unpainted it renders black on the container's X11. */
|
|
QTableCornerButton::section {
|
|
background-color: $background;
|
|
border: none;
|
|
}
|
|
|
|
QWidget#mainContentRoot,
|
|
QWidget#standardMainPage,
|
|
QWidget#compactAutomationPage {
|
|
background-color: transparent;
|
|
}
|
|
|
|
QWidget#portraitModePage {
|
|
background-color: $dark_bg;
|
|
}
|
|
|
|
/* Borderless hover hints. The transparent border is required — QToolTip
|
|
only honours the stylesheet background once a border is set. */
|
|
QToolTip {
|
|
background-color: $tooltip_bg;
|
|
color: $tooltip_fg;
|
|
border: 1px solid transparent;
|
|
padding: 4px 6px;
|
|
}
|
|
|
|
QFrame#compactAutomationPanel {
|
|
background: transparent;
|
|
border: none;
|
|
border-radius: 18px;
|
|
}
|
|
|
|
QFrame#compactCameraCard,
|
|
QFrame#compactControlsCard,
|
|
QFrame#compactProgressCard,
|
|
QFrame#compactQueueCard,
|
|
QFrame#compactQueueItem {
|
|
background: $compact_card_bg;
|
|
border: 1px solid $compact_border;
|
|
border-radius: 16px;
|
|
}
|
|
|
|
QLabel#compactSectionTitle {
|
|
background: transparent;
|
|
color: $compact_title;
|
|
font-size: $font_body;
|
|
font-weight: 700;
|
|
}
|
|
|
|
QLabel#compactSectionHint {
|
|
background: transparent;
|
|
color: $compact_hint;
|
|
font-size: $font_hint;
|
|
}
|
|
|
|
QLabel#compactQueueTitle {
|
|
background: transparent;
|
|
color: $compact_hint;
|
|
font-size: $font_fine;
|
|
font-weight: 700;
|
|
}
|
|
|
|
QLabel#compactQueueValue {
|
|
background: transparent;
|
|
color: $compact_value;
|
|
font-size: $font_body;
|
|
font-weight: 700;
|
|
}
|
|
|
|
QToolButton#compactMenuButton {
|
|
background: $compact_menu_bg;
|
|
color: $compact_title;
|
|
border: 1px solid $compact_border;
|
|
border-radius: 14px;
|
|
padding: 10px 14px;
|
|
font-size: $font_value;
|
|
font-weight: 700;
|
|
}
|
|
|
|
QToolButton#compactMenuButton:hover {
|
|
background: $compact_menu_bg_hover;
|
|
}
|
|
|
|
QPushButton#compactPrimaryButton {
|
|
background: $primary;
|
|
color: $primary_text;
|
|
border: none;
|
|
border-radius: 14px;
|
|
padding: 14px 18px;
|
|
font-size: $font_body_lg;
|
|
font-weight: 700;
|
|
}
|
|
|
|
QPushButton#compactPrimaryButton:hover {
|
|
background: $primary_hover;
|
|
}
|
|
|
|
QPushButton#compactSecondaryButton,
|
|
QToolButton#compactSecondaryButton {
|
|
background: $secondary_bg;
|
|
color: $compact_title;
|
|
border: 1px solid $compact_border;
|
|
border-radius: 14px;
|
|
padding: 14px 18px;
|
|
font-size: $font_body;
|
|
font-weight: 700;
|
|
}
|
|
|
|
QPushButton#compactSecondaryButton:hover,
|
|
QToolButton#compactSecondaryButton:hover {
|
|
background: $secondary_bg_hover;
|
|
}
|
|
|
|
QFrame#alertBanner[alertKind="error"] {
|
|
background-color: $error_bg;
|
|
border: 2px solid $error_border;
|
|
border-radius: $card_radius;
|
|
margin: 8px 12px 8px 12px;
|
|
}
|
|
|
|
QFrame#alertBanner[alertKind="success"] {
|
|
background-color: $success_bg;
|
|
border: 2px solid $success_border;
|
|
border-radius: $card_radius;
|
|
margin: 8px 12px 8px 12px;
|
|
}
|
|
|
|
QFrame#alertBanner[alertKind="waiting"] {
|
|
background-color: $warning_bg;
|
|
border: 2px solid $warning_border;
|
|
border-radius: $card_radius;
|
|
margin: 8px 12px 8px 12px;
|
|
}
|
|
|
|
QFrame#alertBanner QLabel {
|
|
font-weight: 700;
|
|
font-size: $font_alert;
|
|
padding: 2px 6px 2px 6px;
|
|
}
|
|
|
|
QFrame#alertBanner[alertKind="error"] QLabel {
|
|
color: $error_text;
|
|
}
|
|
|
|
QFrame#alertBanner[alertKind="success"] QLabel {
|
|
color: $success_text;
|
|
}
|
|
|
|
QFrame#alertBanner[alertKind="waiting"] QLabel {
|
|
color: $warning_text;
|
|
}
|
|
|
|
/* Check/radio indicators: explicit QSS boxes, both SQUARE for
|
|
consistency — hollow = unchecked, accent-filled = checked. Native
|
|
indicator glyphs are unreliable on this Qt once anything nearby is
|
|
styled (same story as the spin arrows). */
|
|
QCheckBox::indicator, QRadioButton::indicator {
|
|
width: 12px;
|
|
height: 12px;
|
|
background: $input_bg;
|
|
border: 1px solid $scrollbar_track;
|
|
}
|
|
|
|
QCheckBox::indicator:checked, QRadioButton::indicator:checked {
|
|
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;
|
|
}
|
|
|
|
/* Disabled = baton-gated ("watch only"): the explicit colors above mask
|
|
Qt's native grey, so spell the greyed state out. */
|
|
QPushButton:disabled, QCheckBox:disabled, QRadioButton:disabled,
|
|
QLabel:disabled, QComboBox:disabled, QLineEdit:disabled,
|
|
QAbstractSpinBox:disabled, QTabBar::tab:disabled {
|
|
color: $faint_text;
|
|
}
|
|
|
|
QLineEdit:disabled, QAbstractSpinBox:disabled, QComboBox:disabled {
|
|
background: $disabled_input_bg;
|
|
}
|
|
|
|
QSlider::sub-page:horizontal:disabled {
|
|
background: $slider_muted;
|
|
}
|
|
|
|
QTabWidget::pane {
|
|
border: 1px solid $border;
|
|
}
|
|
|
|
/* Exp. Config. pane: hand-rolled QTabBar+QStackedWidget (button row sits
|
|
between bar and pages), so it needs the pane border by hand too. */
|
|
#expConfigPane {
|
|
border: 1px solid $border;
|
|
}
|
|
|
|
/* Borderless tabs: the pane isn't outlined, so the native 3-side tab
|
|
boxes hung in the air. Selection shows via fill + the raised effect
|
|
(unselected tabs sit 3px lower). */
|
|
QTabBar::tab {
|
|
background: transparent;
|
|
border: none;
|
|
border-top-left-radius: 4px;
|
|
border-top-right-radius: 4px;
|
|
padding: 4px 14px;
|
|
margin-top: 3px;
|
|
color: $muted_text;
|
|
}
|
|
|
|
QTabBar::tab:selected {
|
|
background: $tab_face_bg;
|
|
color: $text;
|
|
margin-top: 0px;
|
|
padding: 6px 14px 5px 14px;
|
|
}
|
|
|
|
/* Menu-bar-style hover hint: text-decoration underlines only the text
|
|
itself (short, close to the baseline) instead of the full tab box. */
|
|
QTabBar::tab:hover:!selected {
|
|
color: $text;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
/* Sample-list status filter chips — tab-shaped buttons under the Dewar/
|
|
Auxiliary tabs (objectName filterChip, set in tell_sample_panel).
|
|
Shell mirrors QTabBar::tab above; the checked chip wears its row-tint
|
|
color, doubling as the legend. Per-theme HERE, not inline, so the
|
|
dark theme can restyle them. */
|
|
QPushButton#filterChip {
|
|
background: $tab_face_bg;
|
|
color: $muted_text;
|
|
border: none;
|
|
border-top-left-radius: 4px;
|
|
border-top-right-radius: 4px;
|
|
margin-top: 3px;
|
|
padding: 4px 14px;
|
|
}
|
|
|
|
QPushButton#filterChip:hover:!checked {
|
|
color: $text;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
QPushButton#filterChip:checked {
|
|
background: $chip_neutral_bg;
|
|
color: $text;
|
|
margin-top: 0px;
|
|
padding: 6px 14px 5px 14px;
|
|
}
|
|
|
|
QPushButton#filterChip[status_key="queued"]:checked { background: $sample_status_queued_bg; }
|
|
QPushButton#filterChip[status_key="flagged"]:checked { background: $sample_status_flagged_bg; }
|
|
QPushButton#filterChip[status_key="measured"]:checked { background: $sample_status_measured_bg; }
|
|
|
|
/* In-panel section headings (section_title in title_label.py). */
|
|
QLabel#sectionTitle {
|
|
background: transparent;
|
|
color: $muted_text;
|
|
font-size: $font_hint;
|
|
font-weight: 700;
|
|
}
|
|
|
|
/* No base ::separator rule ON PURPOSE: any QSS fill would replace the
|
|
native dotted-grip drawing, and the dots (visible in the dark theme,
|
|
which never styled separators) are wanted in both themes. */
|
|
|
|
/* Resize-line hint — the separatorHint property is flipped by
|
|
MainWindow.event() after a 1s hover rest or on press; :hover limits
|
|
the fill to the exact separator being dragged. */
|
|
QMainWindow[separatorHint="true"]::separator:hover {
|
|
background: $separator_hint;
|
|
}
|
|
|
|
/* Splitter handles (prediction metrics) are plain child widgets the
|
|
property gate above doesn't reach — immediate hover/press hint. */
|
|
QSplitter::handle:hover, QSplitter::handle:pressed {
|
|
background: $separator_hint;
|
|
}
|
|
|
|
QFrame#beamlineControls,
|
|
QFrame#dataCollectionSettings {
|
|
border: 1px solid $border;
|
|
}
|
|
|
|
QFrame#beamlineStatePanel {
|
|
background: transparent;
|
|
border-top: 1px solid $border;
|
|
}
|
|
|
|
QLabel#beamlineStateTitle {
|
|
background-color: $banner;
|
|
color: $banner_text;
|
|
font-size: $font_title;
|
|
font-weight: 700;
|
|
border-left: 1px solid $banner_edge_v;
|
|
border-bottom: 1px solid $banner_edge_h;
|
|
}
|
|
|
|
/* Panel banners — styled per-theme HERE, not on the widget (a widget
|
|
stylesheet would win and pin this light banner into the dark theme).
|
|
TitleLabel hand-paints its text from the QSS-resolved palette color. */
|
|
TitleLabel {
|
|
background-color: $banner;
|
|
color: $banner_text;
|
|
font-size: $font_title;
|
|
font-weight: 700;
|
|
border-left: 1px solid $banner_edge_v;
|
|
border-bottom: 1px solid $banner_edge_h;
|
|
}
|
|
|
|
/* Bare glyph to match the TitleLabel toggles: no pill background. */
|
|
QPushButton#beamlineStateToggleButton {
|
|
border: none;
|
|
background: transparent;
|
|
color: $state_toggle_text;
|
|
font-size: $font_body;
|
|
font-weight: 700;
|
|
}
|
|
|
|
QLabel#beamlineStateCurrentLabel {
|
|
color: $state_current_text;
|
|
font-size: $font_value;
|
|
font-weight: 700;
|
|
padding-left: 4px;
|
|
background: transparent;
|
|
}
|
|
|
|
QLabel#beamlineStateTellLabel {
|
|
color: $state_tell_text;
|
|
font-size: $font_body_lg;
|
|
font-weight: 700;
|
|
padding-left: 4px;
|
|
background: transparent;
|
|
}
|
|
|
|
/* Plain scroll containers stay frameless. */
|
|
QScrollArea {
|
|
border: none;
|
|
}
|
|
|
|
/* Box-frame levels — weights/colors are knobs in styles.py. L2: panel
|
|
frames (replaces their native StyledPanel etching); L3: data views. */
|
|
TellSamplePanel, ReferenceToolsPanel, SampleQueuePanel {
|
|
border: $frame_l2_width solid $frame_l2_color;
|
|
}
|
|
|
|
/* No L3 border — matches the dark theme (borderless data views). */
|
|
QTableView, QPlainTextEdit {
|
|
border: none;
|
|
}
|
|
|
|
/* Pale-blue selection with readable dark text in every sample table;
|
|
staggered grey/white rows where alternation is enabled. */
|
|
QTableView {
|
|
background: $white;
|
|
alternate-background-color: $sample_row_alt_bg;
|
|
selection-background-color: $sample_status_selected_bg;
|
|
selection-color: $text;
|
|
}
|
|
|
|
/* Selection highlight: the banner blue (SELECTION_BG in styles.py)
|
|
instead of the style palette's own blue. Tables keep their paler
|
|
selection rule below. */
|
|
QListView, QTreeView,
|
|
QComboBox QAbstractItemView {
|
|
selection-background-color: $selection_bg;
|
|
selection-color: $selection_text;
|
|
}
|
|
|
|
QMenu::item:selected {
|
|
background: $selection_bg;
|
|
color: $selection_text;
|
|
}
|
|
|
|
QLineEdit, QAbstractSpinBox, QTextEdit, QPlainTextEdit {
|
|
selection-background-color: $selection_bg;
|
|
selection-color: $selection_text;
|
|
}
|
|
|
|
/* Sliders: rounded groove ends (like the scrollbar handles) + square
|
|
handle with grip lines (SLIDER_GRIP asset) — the old 16px round handle
|
|
was clipped flat by the slider's widget height. Wheel adjustment needs
|
|
the right mouse button held — see WheelValueGuard. */
|
|
QSlider::groove:horizontal {
|
|
height: 6px;
|
|
background: $slider_track;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
QSlider::sub-page:horizontal {
|
|
background: $slider_fill;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
QSlider::handle:horizontal {
|
|
background: $white;
|
|
border: 1px solid $slider_muted;
|
|
width: 14px;
|
|
margin: -3px 0;
|
|
image: url($slider_grip);
|
|
}
|
|
|
|
/* Soft scrollbars: square track band (runs flush to the widget edges —
|
|
rounded ends left corner notches in the tables) + rounded draggable
|
|
handle, no end arrows. */
|
|
QScrollBar:vertical {
|
|
background: $scrollbar_track;
|
|
width: 10px;
|
|
border: none;
|
|
margin: 0px;
|
|
}
|
|
|
|
QScrollBar:horizontal {
|
|
background: $scrollbar_track;
|
|
height: 10px;
|
|
border: none;
|
|
margin: 0px;
|
|
}
|
|
|
|
QScrollBar::handle:vertical {
|
|
background: $scrollbar_handle;
|
|
border-radius: 5px;
|
|
min-height: 24px;
|
|
}
|
|
|
|
QScrollBar::handle:horizontal {
|
|
background: $scrollbar_handle;
|
|
border-radius: 5px;
|
|
min-width: 24px;
|
|
}
|
|
|
|
QScrollBar::handle:vertical:hover,
|
|
QScrollBar::handle:vertical:pressed,
|
|
QScrollBar::handle:horizontal:hover,
|
|
QScrollBar::handle:horizontal:pressed {
|
|
background: $scrollbar_handle_hover;
|
|
}
|
|
|
|
QScrollBar::add-line, QScrollBar::sub-line {
|
|
width: 0px;
|
|
height: 0px;
|
|
}
|
|
|
|
QScrollBar::add-page, QScrollBar::sub-page {
|
|
background: transparent;
|
|
}
|
|
|
|
/* The two scrollbar bands meet in a track-colored corner — no white
|
|
square, the bands read as one continuous edge. */
|
|
QAbstractScrollArea::corner {
|
|
background: $scrollbar_track;
|
|
border: none;
|
|
}
|
|
|
|
QWidget#portraitRoot,
|
|
QWidget#portraitRoot QWidget {
|
|
background: $dark_bg;
|
|
color: $dark_text;
|
|
font-family: 'Inter', 'SF Pro Display', Arial, sans-serif;
|
|
font-size: $font_body;
|
|
}
|
|
|
|
QWidget#portraitRoot QScrollArea {
|
|
border: none;
|
|
background: transparent;
|
|
}
|
|
|
|
QWidget#portraitRoot QScrollBar:vertical {
|
|
background: $dark_surface;
|
|
width: 4px;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
QWidget#portraitRoot QScrollBar::handle:vertical {
|
|
background: $dark_border;
|
|
border-radius: 2px;
|
|
min-height: 20px;
|
|
}
|
|
|
|
QWidget#portraitRoot QScrollBar::add-line:vertical,
|
|
QWidget#portraitRoot QScrollBar::sub-line:vertical {
|
|
height: 0px;
|
|
}
|
|
""").substitute(mapping)
|
|
|
|
|
|
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
|
|
the light-theme note). Knobs: DARK_BACKGROUND_GRADIENT_* above. */
|
|
QWidget {
|
|
background-color: transparent;
|
|
color: $dark_text;
|
|
}
|
|
|
|
QMainWindow, QDialog, PopoutWindow,
|
|
QDockWidget[floating="true"] {
|
|
background: $dark_app_background;
|
|
}
|
|
|
|
QWidget#portraitModePage {
|
|
background: $dark_bg;
|
|
}
|
|
|
|
/* Interactive faces sit one step above the backdrop (site: glass2)
|
|
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. */
|
|
QTextEdit, QPlainTextEdit {
|
|
background-color: transparent;
|
|
}
|
|
|
|
/* Menus/popups use the deepest opaque surface (site: panel2). */
|
|
QMenu,
|
|
QComboBox QAbstractItemView {
|
|
background-color: $dark_panel2;
|
|
}
|
|
|
|
QHeaderView::section {
|
|
background-color: $dark_surface;
|
|
color: $dark_text;
|
|
border: none;
|
|
}
|
|
|
|
/* Check/radio indicators, dark flavor — square boxes, gold = checked
|
|
(see the light-theme note). */
|
|
QCheckBox::indicator, QRadioButton::indicator {
|
|
width: 12px;
|
|
height: 12px;
|
|
background: transparent;
|
|
border: 1px solid $dark_muted;
|
|
}
|
|
|
|
QCheckBox::indicator:checked, QRadioButton::indicator:checked {
|
|
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;
|
|
}
|
|
|
|
/* Disabled = baton-gated: overlay text on the disabled fill. */
|
|
QPushButton:disabled, QCheckBox:disabled, QRadioButton:disabled,
|
|
QLabel:disabled, QComboBox:disabled, QLineEdit:disabled,
|
|
QAbstractSpinBox:disabled, QTabBar::tab:disabled {
|
|
color: $dark_overlay;
|
|
}
|
|
|
|
QLineEdit:disabled, QAbstractSpinBox:disabled, QComboBox:disabled {
|
|
background: $dark_disabled;
|
|
}
|
|
|
|
/* Input states, dark flavor — see the light-theme note. */
|
|
QLineEdit:read-only, QAbstractSpinBox:read-only {
|
|
background-color: $dark_disabled;
|
|
}
|
|
|
|
QLineEdit[movestate="pending"] {
|
|
background-color: $dark_input_pending_bg;
|
|
}
|
|
|
|
QLineEdit[movestate="moving"] {
|
|
background-color: $dark_input_moving_bg;
|
|
}
|
|
|
|
QLineEdit[invalid="true"] {
|
|
background-color: $dark_error_bg;
|
|
}
|
|
|
|
/* Spinboxes: both bare arrows adjacent on the right (up inboard, down
|
|
outermost) — see the light-theme note. Arrows are the
|
|
DARK_SPIN_ARROW_* PNG assets. */
|
|
QAbstractSpinBox {
|
|
padding-left: 6px;
|
|
padding-right: 34px;
|
|
}
|
|
|
|
QAbstractSpinBox::down-button {
|
|
subcontrol-origin: border;
|
|
subcontrol-position: center right;
|
|
width: 16px;
|
|
background: transparent;
|
|
border: none;
|
|
}
|
|
|
|
QAbstractSpinBox::up-button {
|
|
subcontrol-origin: border;
|
|
subcontrol-position: center right;
|
|
left: -16px;
|
|
width: 16px;
|
|
background: transparent;
|
|
border: none;
|
|
}
|
|
|
|
QAbstractSpinBox::down-arrow {
|
|
image: url($dark_spin_arrow_down);
|
|
}
|
|
|
|
QAbstractSpinBox::up-arrow {
|
|
image: url($dark_spin_arrow_up);
|
|
}
|
|
|
|
/* Bare dropdown — see the light-theme note. */
|
|
QComboBox::drop-down {
|
|
border: none;
|
|
background: transparent;
|
|
}
|
|
|
|
QComboBox::down-arrow {
|
|
image: url($dark_spin_arrow_down);
|
|
}
|
|
|
|
/* Single glass layer for the embedded value edit — see light-theme note. */
|
|
QAbstractSpinBox QLineEdit {
|
|
background: transparent;
|
|
}
|
|
|
|
/* Filter chips, dark flavor: same shell as the Dewar/Auxiliary tabs,
|
|
but the checked chip keeps its light row-tint fill (the legend role)
|
|
with the fixed dark ink the tints require. */
|
|
QPushButton#filterChip {
|
|
background: transparent;
|
|
color: $dark_muted;
|
|
border: none;
|
|
border-top-left-radius: 4px;
|
|
border-top-right-radius: 4px;
|
|
margin-top: 3px;
|
|
padding: 4px 14px;
|
|
}
|
|
|
|
QPushButton#filterChip:hover:!checked {
|
|
color: $dark_text;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
QPushButton#filterChip:checked {
|
|
background: $chip_neutral_bg;
|
|
color: $sample_status_text;
|
|
margin-top: 0px;
|
|
padding: 6px 14px 5px 14px;
|
|
}
|
|
|
|
QPushButton#filterChip[status_key="queued"]:checked { background: $sample_status_queued_bg; }
|
|
QPushButton#filterChip[status_key="flagged"]:checked { background: $sample_status_flagged_bg; }
|
|
QPushButton#filterChip[status_key="measured"]:checked { background: $sample_status_measured_bg; }
|
|
|
|
/* In-panel section headings — gold, matching the compact page titles. */
|
|
QLabel#sectionTitle {
|
|
background: transparent;
|
|
color: $dark_accent;
|
|
font-size: $font_hint;
|
|
font-weight: 700;
|
|
}
|
|
|
|
/* Panel banners — styled per-theme HERE, not on the widget (a widget
|
|
stylesheet would win and pin the light banner into this theme). Gold
|
|
identity text, hand-painted by TitleLabel from the QSS palette. */
|
|
TitleLabel {
|
|
background-color: $dark_elevated;
|
|
color: $dark_accent;
|
|
font-size: $font_title;
|
|
font-weight: 700;
|
|
border-left: 1px solid $dark_banner_edge_v;
|
|
border-bottom: 1px solid $dark_banner_edge_h;
|
|
}
|
|
|
|
QTabWidget::pane,
|
|
QScrollArea,
|
|
QDockWidget,
|
|
QDockWidget > QWidget {
|
|
background: transparent;
|
|
color: $dark_text;
|
|
}
|
|
|
|
/* Borderless tabs, dark flavor — see the light-theme note. */
|
|
QTabBar::tab {
|
|
background: transparent;
|
|
border: none;
|
|
border-top-left-radius: 4px;
|
|
border-top-right-radius: 4px;
|
|
padding: 4px 14px;
|
|
margin-top: 3px;
|
|
color: $dark_muted;
|
|
}
|
|
|
|
QTabBar::tab:selected {
|
|
background: $dark_elevated;
|
|
color: $dark_text;
|
|
margin-top: 0px;
|
|
padding: 6px 14px 5px 14px;
|
|
}
|
|
|
|
/* Menu-bar-style hover hint — see the light-theme note. */
|
|
QTabBar::tab:hover:!selected {
|
|
color: $dark_text;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
/* Borderless hover hints. The transparent border is required — QToolTip
|
|
only honours the stylesheet background once a border is set. */
|
|
QToolTip {
|
|
background-color: $dark_tooltip_bg;
|
|
color: $dark_tooltip_fg;
|
|
border: 1px solid transparent;
|
|
padding: 4px 6px;
|
|
}
|
|
|
|
QFrame#compactAutomationPanel {
|
|
background: transparent;
|
|
border: none;
|
|
border-radius: 18px;
|
|
}
|
|
|
|
QFrame#compactCameraCard,
|
|
QFrame#compactControlsCard,
|
|
QFrame#compactProgressCard,
|
|
QFrame#compactQueueCard,
|
|
QFrame#compactQueueItem {
|
|
background: $dark_surface;
|
|
border: 1px solid $dark_border;
|
|
border-radius: 16px;
|
|
}
|
|
|
|
QLabel#compactSectionTitle {
|
|
background: transparent;
|
|
color: $dark_accent;
|
|
font-size: $font_body;
|
|
font-weight: 700;
|
|
}
|
|
|
|
QLabel#compactSectionHint {
|
|
background: transparent;
|
|
color: $dark_subtext;
|
|
font-size: $font_hint;
|
|
}
|
|
|
|
QLabel#compactQueueTitle {
|
|
background: transparent;
|
|
color: $dark_subtext;
|
|
font-size: $font_fine;
|
|
font-weight: 700;
|
|
}
|
|
|
|
QLabel#compactQueueValue {
|
|
background: transparent;
|
|
color: $dark_text;
|
|
font-size: $font_body;
|
|
font-weight: 700;
|
|
}
|
|
|
|
QToolButton#compactMenuButton {
|
|
background: $dark_elevated;
|
|
color: $dark_accent;
|
|
border: 1px solid $dark_border;
|
|
border-radius: 14px;
|
|
padding: 10px 14px;
|
|
font-size: $font_value;
|
|
font-weight: 700;
|
|
}
|
|
|
|
QToolButton#compactMenuButton:hover {
|
|
background: $dark_border;
|
|
}
|
|
|
|
QPushButton#compactPrimaryButton {
|
|
background: $dark_accent_fill;
|
|
color: $dark_on_accent;
|
|
border: none;
|
|
border-radius: 14px;
|
|
padding: 14px 18px;
|
|
font-size: $font_body_lg;
|
|
font-weight: 700;
|
|
}
|
|
|
|
QPushButton#compactPrimaryButton:hover {
|
|
background: $dark_accent_fill_hover;
|
|
}
|
|
|
|
QPushButton#compactSecondaryButton,
|
|
QToolButton#compactSecondaryButton {
|
|
background: $dark_elevated;
|
|
color: $dark_text;
|
|
border: 1px solid $dark_border;
|
|
border-radius: 14px;
|
|
padding: 14px 18px;
|
|
font-size: $font_body;
|
|
font-weight: 700;
|
|
}
|
|
|
|
QPushButton#compactSecondaryButton:hover,
|
|
QToolButton#compactSecondaryButton:hover {
|
|
background: $dark_border;
|
|
}
|
|
|
|
QFrame#alertBanner[alertKind="error"] {
|
|
background: $dark_error_bg;
|
|
border: 2px solid $dark_error_border;
|
|
border-radius: $card_radius;
|
|
margin: 8px 12px 8px 12px;
|
|
}
|
|
|
|
QFrame#alertBanner[alertKind="success"] {
|
|
background: $dark_success_bg;
|
|
border: 2px solid $dark_success_border;
|
|
border-radius: $card_radius;
|
|
margin: 8px 12px 8px 12px;
|
|
}
|
|
|
|
QFrame#alertBanner[alertKind="waiting"] {
|
|
background: $dark_warning_bg;
|
|
border: 2px solid $dark_warning_border;
|
|
border-radius: $card_radius;
|
|
margin: 8px 12px 8px 12px;
|
|
}
|
|
|
|
QFrame#alertBanner QLabel {
|
|
font-weight: 700;
|
|
font-size: $font_alert;
|
|
padding: 2px 6px 2px 6px;
|
|
}
|
|
|
|
QFrame#alertBanner[alertKind="error"] QLabel {
|
|
color: $dark_error_text;
|
|
}
|
|
|
|
QFrame#alertBanner[alertKind="success"] QLabel {
|
|
color: $dark_success_text;
|
|
}
|
|
|
|
QFrame#alertBanner[alertKind="waiting"] QLabel {
|
|
color: $dark_warning_text;
|
|
}
|
|
|
|
QFrame#beamlineStatePanel {
|
|
background: transparent;
|
|
border-top: 1px solid transparent;
|
|
}
|
|
|
|
QLabel#beamlineStateTitle {
|
|
background-color: $dark_elevated;
|
|
color: $dark_accent;
|
|
font-size: $font_title;
|
|
font-weight: 700;
|
|
border-left: 1px solid $dark_banner_edge_v;
|
|
border-bottom: 1px solid $dark_banner_edge_h;
|
|
}
|
|
|
|
/* Bare glyph to match the TitleLabel toggles: no pill background. */
|
|
QPushButton#beamlineStateToggleButton {
|
|
border: none;
|
|
background: transparent;
|
|
color: $dark_text;
|
|
font-size: $font_body;
|
|
font-weight: 700;
|
|
}
|
|
|
|
QLabel#beamlineStateCurrentLabel {
|
|
color: $dark_text;
|
|
font-size: $font_value;
|
|
font-weight: 700;
|
|
padding-left: 4px;
|
|
background: transparent;
|
|
}
|
|
|
|
QLabel#beamlineStateTellLabel {
|
|
/* Same color as the Current-state neighbor — subtext was unreadable
|
|
on the selected-state blue band. */
|
|
color: $dark_text;
|
|
font-size: $font_body_lg;
|
|
font-weight: 700;
|
|
padding-left: 4px;
|
|
background: transparent;
|
|
}
|
|
|
|
/* Resize-line hint, dark flavor — see the light-theme note. */
|
|
QMainWindow[separatorHint="true"]::separator:hover {
|
|
background: $dark_accent;
|
|
}
|
|
|
|
QSplitter::handle:hover, QSplitter::handle:pressed {
|
|
background: $dark_accent;
|
|
}
|
|
|
|
/* Plain scroll containers stay frameless. */
|
|
QScrollArea {
|
|
border: none;
|
|
}
|
|
|
|
/* Box-frame levels — weights/colors are knobs in styles.py. Opaque
|
|
fill on purpose: left transparent, the panel band (behind the filter
|
|
chips) renders BLACK on the container's non-composited X11 — same
|
|
trap as DARK_TABLE_BG. */
|
|
TellSamplePanel, ReferenceToolsPanel, SampleQueuePanel {
|
|
border: $frame_l2_width solid $frame_l2_color;
|
|
background: $dark_surface;
|
|
}
|
|
|
|
/* Dewar tab page: the automation button row sits on this bare QWidget
|
|
below the panel's border — left transparent it shows the near-black
|
|
gradient bottom, reading as an unpainted hole. */
|
|
QWidget#dewarTab {
|
|
background-color: $dark_surface;
|
|
}
|
|
QWidget#logPanel { background-color: $dark_surface; }
|
|
|
|
/* No L3 border here: the light theme's pale hairline read as a white
|
|
frame around dark tables. */
|
|
QTableView, QPlainTextEdit {
|
|
border: none;
|
|
}
|
|
QPlainTextEdit {
|
|
background: $dark_table_bg; /* solid — transparent renders black on the container's X11 */
|
|
}
|
|
|
|
/* Selection + staggered rows, dark flavor. Solid fills on purpose —
|
|
a transparent viewport renders black here (see DARK_TABLE_BG). */
|
|
QTableView {
|
|
background: $dark_table_bg;
|
|
alternate-background-color: $dark_surface;
|
|
selection-background-color: $sample_status_selected_bg;
|
|
selection-color: $text;
|
|
}
|
|
|
|
/* Table select-all corner — see the light-theme note. */
|
|
QTableCornerButton::section {
|
|
background-color: $dark_surface;
|
|
border: none;
|
|
}
|
|
|
|
/* Selection highlight — same banner-blue knob as the light theme. */
|
|
QListView, QTreeView,
|
|
QComboBox QAbstractItemView {
|
|
selection-background-color: $selection_bg;
|
|
selection-color: $selection_text;
|
|
}
|
|
|
|
QMenu::item:selected {
|
|
background: $selection_bg;
|
|
color: $selection_text;
|
|
}
|
|
|
|
QLineEdit, QAbstractSpinBox, QTextEdit, QPlainTextEdit {
|
|
selection-background-color: $selection_bg;
|
|
selection-color: $selection_text;
|
|
}
|
|
|
|
/* Sliders, dark flavor — see the light-theme note. */
|
|
QSlider::groove:horizontal {
|
|
height: 6px;
|
|
background: $dark_border;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
QSlider::sub-page:horizontal {
|
|
background: $dark_accent;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
QSlider::handle:horizontal {
|
|
background: $dark_elevated;
|
|
border: 1px solid $dark_muted;
|
|
width: 14px;
|
|
margin: -3px 0;
|
|
image: url($dark_slider_grip);
|
|
}
|
|
|
|
/* Soft scrollbars: square track band + rounded handle, no end arrows
|
|
(see light-theme note on the corner notches). */
|
|
QScrollBar:vertical {
|
|
background: $dark_surface;
|
|
width: 10px;
|
|
border: none;
|
|
margin: 0px;
|
|
}
|
|
|
|
QScrollBar:horizontal {
|
|
background: $dark_surface;
|
|
height: 10px;
|
|
border: none;
|
|
margin: 0px;
|
|
}
|
|
|
|
QScrollBar::handle:vertical {
|
|
background: $dark_border;
|
|
border-radius: 5px;
|
|
min-height: 24px;
|
|
}
|
|
|
|
QScrollBar::handle:horizontal {
|
|
background: $dark_border;
|
|
border-radius: 5px;
|
|
min-width: 24px;
|
|
}
|
|
|
|
QScrollBar::handle:vertical:hover,
|
|
QScrollBar::handle:vertical:pressed,
|
|
QScrollBar::handle:horizontal:hover,
|
|
QScrollBar::handle:horizontal:pressed {
|
|
background: $dark_muted;
|
|
}
|
|
|
|
QScrollBar::add-line, QScrollBar::sub-line {
|
|
width: 0px;
|
|
height: 0px;
|
|
}
|
|
|
|
QScrollBar::add-page, QScrollBar::sub-page {
|
|
background: transparent;
|
|
}
|
|
|
|
/* The two scrollbar bands meet in a track-colored corner. */
|
|
QAbstractScrollArea::corner {
|
|
background: $dark_surface;
|
|
border: none;
|
|
}
|
|
|
|
QWidget#portraitRoot,
|
|
QWidget#portraitRoot QWidget {
|
|
background: $dark_bg;
|
|
color: $dark_text;
|
|
font-family: 'Inter', 'SF Pro Display', Arial, sans-serif;
|
|
font-size: $font_body;
|
|
}
|
|
|
|
QWidget#portraitRoot QScrollArea {
|
|
border: none;
|
|
background: transparent;
|
|
}
|
|
|
|
QWidget#portraitRoot QScrollBar:vertical {
|
|
background: $dark_surface;
|
|
width: 4px;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
QWidget#portraitRoot QScrollBar::handle:vertical {
|
|
background: $dark_border;
|
|
border-radius: 2px;
|
|
min-height: 20px;
|
|
}
|
|
|
|
QWidget#portraitRoot QScrollBar::add-line:vertical,
|
|
QWidget#portraitRoot QScrollBar::sub-line:vertical {
|
|
height: 0px;
|
|
}
|
|
""").substitute(_palette())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# ponytail: smallest check that fails if a $name has no matching constant
|
|
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")
|