feat: dusk-gradient theme overhaul with transparent-widget scheme

Both stylesheets move to transparent children over a gradient painted
only by top-level windows; inputs, buttons, headers, and menus get
opaque faces back. Sunset drops Catppuccin Macchiato for the Daemmerung
dusk palette. Spin/combo arrows and check indicators become theme-tinted
PNGs. Widgets stop carrying inline input/text stylesheets so the theme
QSS can win (empty stylesheet = theme reset).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
duan_jandClaude Fable 5 committed 2026-08-08 15:05:34 +02:00
1 parent 2f9fb10a8a
commit d198297881
11 files changed
+641 -164

No files matched your search

+2 -2
View File
@@ -71,7 +71,7 @@ from aare.gui.scan_logic.raster_grid_manager import RasterGridManager
from aare.gui.scan_logic.rotation_scan_manager import RotationScanManager
from aare.gui.scan_logic.sample_mount_logic import SampleMountLogic
from aare.gui.styles import (
BACKGROUND,
APP_BACKGROUND,
DOCK_CONTENT_LEFT_PAD,
THEME_ORIGINAL,
THEME_PORTRAIT,
@@ -213,7 +213,7 @@ class MainWindow(QMainWindow):
)
raise
self.setStyleSheet(f"background-color: {BACKGROUND};")
self.setStyleSheet(f"background-color: {APP_BACKGROUND};")
root_widget = QWidget(parent=self)
root_widget.setObjectName("mainContentRoot")
+9
View File
@@ -11,6 +11,7 @@ from aare.gui.styles import (
SAMPLE_STATUS_FLAGGED_BG,
SAMPLE_STATUS_MEASURED_BG,
SAMPLE_STATUS_QUEUED_BG,
SAMPLE_STATUS_TEXT,
qcolor,
)
@@ -121,6 +122,14 @@ class UserSampleSpreadsheet(QAbstractTableModel):
color = self._status_color(self._sorted_samples[index.row()])
if color is not None:
return QBrush(qcolor(color))
elif role == Qt.ItemDataRole.ForegroundRole:
# Tinted cells get fixed dark ink: the tints stay light pastel in
# BOTH themes, so Sunset's white theme text would vanish on them.
if (
index.column() == COL_STATUS
and self._status_color(self._sorted_samples[index.row()]) is not None
):
return QBrush(qcolor(SAMPLE_STATUS_TEXT))
elif role == Qt.ItemDataRole.TextAlignmentRole: # Align text to center
return Qt.AlignmentFlag.AlignCenter
return None # For other roles, return None
+4 -4
View File
@@ -4,7 +4,7 @@ from PySide6.QtCore import Signal, Slot
from PySide6.QtGui import Qt
from PySide6.QtWidgets import QGridLayout, QLabel, QPushButton, QWidget
from aare.gui.styles import ALERT_TEXT, DEFAULT_TEXT
from aare.gui.styles import ALERT_TEXT
from aare.gui.widgets.button_with_payload import ButtonWithPayload
from aare.gui.widgets.number_line_edit import NumberLineEdit
from aare.gui.widgets.title_label import TitleLabel
@@ -148,15 +148,15 @@ class AbrTweakWidget(QWidget):
if abs(s.geom.aerotech.x) >= 0.001:
self._abr_buttons.gmx_label.setStyleSheet(f"color: {ALERT_TEXT};")
else:
self._abr_buttons.gmx_label.setStyleSheet(f"color: {DEFAULT_TEXT};")
self._abr_buttons.gmx_label.setStyleSheet("")
self._abr_buttons.gmy_label.setText(f"{s.geom.aerotech_meas.y:.3f}")
if abs(s.geom.aerotech.y) >= 0.001:
self._abr_buttons.gmy_label.setStyleSheet(f"color: {ALERT_TEXT};")
else:
self._abr_buttons.gmy_label.setStyleSheet(f"color: {DEFAULT_TEXT};")
self._abr_buttons.gmy_label.setStyleSheet("")
self._abr_buttons.gmz_label.setText(f"{s.geom.aerotech_meas.z:.3f}")
if abs(s.geom.aerotech.z) >= 0.001:
self._abr_buttons.gmz_label.setStyleSheet(f"color: {ALERT_TEXT};")
else:
self._abr_buttons.gmz_label.setStyleSheet(f"color: {DEFAULT_TEXT};")
self._abr_buttons.gmz_label.setStyleSheet("")
+4 -7
View File
@@ -7,7 +7,7 @@ from aarecommon.models.models import DAQStatusModel, SampleShortInfo
from PySide6.QtCore import Qt, Signal, Slot
from PySide6.QtWidgets import QGridLayout, QLabel, QLineEdit, QMessageBox, QSpinBox, QWidget
from aare.gui.styles import DEFAULT_TEXT, PATH_WARN_TEXT, SURFACE
from aare.gui.styles import PATH_WARN_TEXT
from aare.gui.widgets.title_label import TitleLabel
## Logic for filenames:
@@ -47,7 +47,6 @@ class FilePathPanel(QWidget):
grid_layout.addWidget(QLabel("Directory", parent=self), 1, 0)
self.directory_edit = QLineEdit("{date}/{puck}/{pos}", parent=self)
self.directory_edit.setStyleSheet(f"background-color: {SURFACE};")
self.directory_edit.setToolTip(
"Provide subdirectory for your files. The following macros are allowed: <br/>"
"<b>{date}</b> - date in format yyyymmdd <br/>"
@@ -61,7 +60,6 @@ class FilePathPanel(QWidget):
grid_layout.addWidget(QLabel("File prefix", parent=self), 2, 0)
self.file_prefix_edit = QLineEdit("{sample}", parent=self)
self.file_prefix_edit.setStyleSheet(f"background-color: {SURFACE};")
self.file_prefix_edit.setToolTip(
"Provide file prefix for your files. The following macros are allowed: <br/>"
"<b>{date}</b> - date in format yyyymmdd <br/>"
@@ -75,7 +73,6 @@ class FilePathPanel(QWidget):
grid_layout.addWidget(QLabel("Run number", parent=self), 3, 0)
self.run_number_edit = QSpinBox(parent=self)
self.run_number_edit.setStyleSheet(f"background-color: {SURFACE};")
self.run_number_edit.setValue(1)
self.run_number_edit.setRange(1, 999)
self.run_number_edit.setAlignment(Qt.AlignmentFlag.AlignRight)
@@ -163,9 +160,9 @@ class FilePathPanel(QWidget):
effective = self._effective_dataset_base(self._filename)
exists = os.path.exists(f"{effective}_master.h5") or os.path.exists(effective)
self.file_name_label.setText(effective + "_master.h5")
self.file_name_label.setStyleSheet(
f"color: {PATH_WARN_TEXT};" if exists else f"color: {DEFAULT_TEXT};"
)
# Empty stylesheet = reset to the THEME text color (a hardcoded
# "default" black would be invisible on the dark theme).
self.file_name_label.setStyleSheet(f"color: {PATH_WARN_TEXT};" if exists else "")
self.path_updated.emit(self._filename)
@Slot()
+7 -3
View File
@@ -25,6 +25,7 @@ from aare.gui.styles import (
LOG_SUCCESS_BORDER,
LOG_WARN_BG,
LOG_WARN_BORDER,
TEXT,
card_style,
)
from aare.gui.widgets.popout_window import DockTitleBar, PopoutWindow
@@ -125,9 +126,12 @@ class RuntimeNotificationWidget(QFrame):
radius=FLAT_CARD_RADIUS,
)
# Transparent children: the app-wide QWidget background rule would
# otherwise paint opaque strips over the card tint.
+ " QLabel#runtimeNotificationTitle { font-weight: bold; background: transparent; }"
+ " QLabel#runtimeNotificationMessage { background: transparent; }"
# otherwise paint opaque strips over the card tint. Text color is
# pinned dark: the card fills above stay light pastel in BOTH
# themes, so theme-following text goes white-on-cream in Sunset.
+ f" QLabel#runtimeNotificationTitle {{ color: {TEXT};"
+ " font-weight: bold; background: transparent; }"
+ f" QLabel#runtimeNotificationMessage {{ color: {TEXT}; background: transparent; }}"
+ " QWidget#runtimeNotificationBody { background: transparent; }"
)
-7
View File
@@ -13,7 +13,6 @@ from PySide6.QtWidgets import (
QWidget,
)
from aare.gui.styles import INPUT_BG
from aare.gui.widgets.title_label import TitleLabel
@@ -46,7 +45,6 @@ class SamcamPanel(QWidget):
self.exposure_spinbox = QDoubleSpinBox()
self.exposure_spinbox.setRange(0, 1.0) # Adjust range as needed
self.exposure_spinbox.setSingleStep(0.001)
self.exposure_spinbox.setStyleSheet(f"QDoubleSpinBox {{ background-color: {INPUT_BG}; }}")
self.exposure_spinbox.setDecimals(3)
self.exposure_spinbox.valueChanged.connect(self._changed)
@@ -54,7 +52,6 @@ class SamcamPanel(QWidget):
self.gain_spinbox.setRange(0, 1000) # Adjust range as needed
self.gain_spinbox.setSingleStep(1)
self.gain_spinbox.setDecimals(1)
self.gain_spinbox.setStyleSheet(f"QDoubleSpinBox {{ background-color: {INPUT_BG}; }}")
self.gain_spinbox.valueChanged.connect(self._changed)
exposure_gain_layout.addWidget(QLabel("Exposure (s):"))
@@ -71,9 +68,6 @@ class SamcamPanel(QWidget):
screenshot_filename_label = QLabel("Filename:")
self.screenshot_filename_edit = QLineEdit()
self.screenshot_filename_edit.setPlaceholderText("optional")
self.screenshot_filename_edit.setStyleSheet(
f"QLineEdit {{ background-color: {INPUT_BG}; }}"
)
screenshot_filename_layout.addWidget(screenshot_filename_label)
screenshot_filename_layout.addWidget(self.screenshot_filename_edit)
@@ -81,7 +75,6 @@ class SamcamPanel(QWidget):
screenshot_message_label = QLabel("Message:")
self.screenshot_message_edit = QLineEdit()
self.screenshot_message_edit.setPlaceholderText("optional")
self.screenshot_message_edit.setStyleSheet(f"QLineEdit {{ background-color: {INPUT_BG}; }}")
screenshot_message_layout.addWidget(screenshot_message_label)
screenshot_message_layout.addWidget(self.screenshot_message_edit)
+9 -27
View File
@@ -20,15 +20,6 @@ from PySide6.QtWidgets import (
from aare.gui.constants import LOGGER_NAME
from aare.gui.models.user_sample_model import COL_STATUS, UserSampleSpreadsheet
from aare.gui.styles import (
CHIP_NEUTRAL_BG,
MUTED_TEXT,
SAMPLE_STATUS_FLAGGED_BG,
SAMPLE_STATUS_MEASURED_BG,
SAMPLE_STATUS_QUEUED_BG,
TAB_FACE_BG,
TEXT,
)
from aare.gui.widgets.title_label import TitleLabel
logger = setup_logger(LOGGER_NAME)
@@ -162,11 +153,11 @@ class TellSamplePanel(QFrame):
chip_row.setSpacing(0)
self.status_chips = QButtonGroup(self)
self.status_chips.setExclusive(True)
for label, key, checked_bg in (
("All", None, CHIP_NEUTRAL_BG),
("Queued", "queued", SAMPLE_STATUS_QUEUED_BG),
("Flagged", "flagged", SAMPLE_STATUS_FLAGGED_BG),
("Measured", "measured", SAMPLE_STATUS_MEASURED_BG),
for label, key in (
("All", None),
("Queued", "queued"),
("Flagged", "flagged"),
("Measured", "measured"),
):
if key == "queued":
chip = QueueDropChip(label, self)
@@ -188,19 +179,10 @@ class TellSamplePanel(QFrame):
chip.setChecked(key is None)
chip.setProperty("status_key", key)
chip.setCursor(Qt.CursorShape.PointingHandCursor)
chip.setStyleSheet(
# Same font, padding and hover hint as the QTabBar tabs above
# (no bold, default size): unchecked tabs sit 3px lower
# (raised-selection effect), the checked one wears its
# row-tint fill, hover underlines just the text.
f"QPushButton {{ background: {TAB_FACE_BG}; color: {MUTED_TEXT};"
f" border: none;"
f" border-top-left-radius: 4px; border-top-right-radius: 4px;"
f" margin-top: 3px; padding: 4px 14px; }}"
f"QPushButton:hover:!checked {{ color: {TEXT}; text-decoration: underline; }}"
f"QPushButton:checked {{ background: {checked_bg}; color: {TEXT};"
f" margin-top: 0px; padding: 6px 14px 5px 14px; }}"
)
# Look lives in the per-theme QPushButton#filterChip rules in
# styles.py (status_key picks the checked row-tint fill there) —
# an inline stylesheet here would pin one theme's colors.
chip.setObjectName("filterChip")
self.status_chips.addButton(chip)
chip_row.addWidget(chip)
chip_row.addStretch()
+577 -65
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
from pathlib import Path
from string import Template
THEME_ORIGINAL = "original"
@@ -15,12 +16,58 @@ 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.
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"
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).
BUTTON_BG = "#f7f9fc"
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()
# 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)
@@ -46,6 +93,12 @@ FRAME_L3_COLOR = "#c9cfd8"
# left edge lines up with the left-column panels above (Loop centering).
DOCK_CONTENT_LEFT_PAD = 10
# 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, 50%)" # scrollbar-track grey @50%
# Tab face fill: the selected Dewar/Auxiliary tab AND the unchecked
# All/Queued/Flagged/Measured filter buttons share this background.
TAB_FACE_BG = "#ffffff"
@@ -88,35 +141,95 @@ 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
# -- Portrait (dark) theme: Catppuccin Macchiato ----------------------------
# https://catppuccin.com/palette — token names in comments. DARK_BG was set to
# Macchiato mantle by hand, so the rest follows that flavor.
DARK_BG = "#1e2030" # mantle
DARK_TEXT = "#cad3f5" # text
DARK_SURFACE = "#24273a" # base — cards, state panel, scrollbar track
DARK_ELEVATED = "#363a4f" # surface0 — buttons, title strip, idle status pill
DARK_BORDER = "#494d64" # surface1 — card borders, scrollbar handle, button hover fill
DARK_ACCENT = "#8bd5ca" # teal
DARK_ACCENT_HOVER = "#a2ddd5" # teal +20% white — palette has no lighter teal step
DARK_MUTED = "#a5adcb" # subtext0 — secondary text
# Alert banners: full-strength color for border/text, 25%-over-DARK_BG tint
# for bg (Catppuccin defines no alert backgrounds, so these are blends).
DARK_ERROR_BG = "#523a4a" # red 25% over mantle
DARK_ERROR_BORDER = "#ed8796" # red
DARK_ERROR_TEXT = "#ed8796" # red
DARK_SUCCESS_BG = "#404e49" # green 25% over mantle
DARK_SUCCESS_BORDER = "#a6da95" # green
DARK_SUCCESS_TEXT = "#a6da95" # green
DARK_WARNING_BG = "#524d4c" # yellow 25% over mantle
DARK_WARNING_BORDER = "#eed49f" # yellow
DARK_WARNING_TEXT = "#eed49f" # yellow
# -- 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 = "#d9b98c"
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
# 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"
DEFAULT_TEXT = "#000000" # labels that reset to plain black
# 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
@@ -204,7 +317,9 @@ SPLASH_BORDER = "#444444"
SPLASH_ACCENT = "#0078d7"
# -- Numeric inputs ---------------------------------------------------------
INPUT_BG = "#ffffff"
# 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"
@@ -218,6 +333,7 @@ STATUS_VACANT = "#ffff00" # baton vacant (was "yellow")
STATUS_REQUEST = "#00ffff" # baton request (was "cyan")
# -- Beamline state panel ---------------------------------------------------
# The panel paints these in code per DAQ tick (data-driven), not via QSS.
STATE_AVAILABLE = "#ed8936"
STATE_UNAVAILABLE = "#8c96a5"
STATE_MSG_ERROR = "#c81e1e"
@@ -238,6 +354,10 @@ SAMPLE_STATUS_QUEUED_BG = "#ffe4c4" # pale orange — waiting in the automation
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 = "#d8e8fd" # 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) ------------
BEAM_OPEN = "#00ff00" # beam marker: shutter open
@@ -344,18 +464,26 @@ FONT_FINE = "11px" # fine print, queue titles
# -- Hover tooltips (the QToolTip popup; styled borderless) -----------------
TOOLTIP_BG = "#f7f9fc"
TOOLTIP_FG = "#263043"
DARK_TOOLTIP_BG = "#363a4f" # surface0
DARK_TOOLTIP_FG = "#cad3f5" # text
DARK_TOOLTIP_BG = "#0e1728" # dusk panel2 — deepest opaque (menus/tooltips)
DARK_TOOLTIP_FG = "#e9edf4" # dusk text
# -- 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: grey track, darker draggable handle) ---
SCROLLBAR_TRACK = "#d8dde5"
SCROLLBAR_HANDLE = "#a8b2c0"
SCROLLBAR_HANDLE_HOVER = "#8794a6"
# -- 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"
# 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
@@ -413,15 +541,127 @@ def build_app_stylesheet(theme: str) -> str:
def _original_stylesheet() -> str:
return Template("""
QMainWindow, QWidget {
background-color: $background;
/* Dusk-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. */
QPushButton, QToolButton, QComboBox {
background-color: $button_bg;
border: 1px solid $button_border;
}
/* 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;
}
QLineEdit:read-only, QAbstractSpinBox:read-only {
background-color: $input_disabled_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: $background;
background-color: transparent;
}
QWidget#portraitModePage {
@@ -438,7 +678,7 @@ def _original_stylesheet() -> str:
}
QFrame#compactAutomationPanel {
background: $background;
background: transparent;
border: none;
border-radius: 18px;
}
@@ -587,6 +827,25 @@ def _original_stylesheet() -> str:
font-weight: bold;
}
/* 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);
}
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,
@@ -596,11 +855,11 @@ def _original_stylesheet() -> str:
}
QLineEdit:disabled, QAbstractSpinBox:disabled, QComboBox:disabled {
background: $scrollbar_track;
background: $disabled_input_bg;
}
QSlider::sub-page:horizontal:disabled {
background: $scrollbar_handle;
background: $slider_muted;
}
QTabWidget::pane {
@@ -640,10 +899,60 @@ def _original_stylesheet() -> str:
text-decoration: underline;
}
QMainWindow::separator {
background: $border;
width: 4px;
height: 4px;
/* 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,
@@ -652,7 +961,7 @@ def _original_stylesheet() -> str:
}
QFrame#beamlineStatePanel {
background: $background;
background: transparent;
border-top: 1px solid $border;
}
@@ -661,6 +970,20 @@ def _original_stylesheet() -> str:
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. */
@@ -699,8 +1022,9 @@ def _original_stylesheet() -> str:
border: $frame_l2_width solid $frame_l2_color;
}
/* No L3 border — matches the dark theme (borderless data views). */
QTableView, QPlainTextEdit {
border: $frame_l3_width solid $frame_l3_color;
border: none;
}
/* Pale-blue selection with readable dark text in every sample table;
@@ -736,7 +1060,7 @@ def _original_stylesheet() -> str:
WheelValueGuard. */
QSlider::groove:horizontal {
height: 6px;
background: $scrollbar_track;
background: $slider_track;
border-radius: 3px;
}
@@ -747,7 +1071,7 @@ def _original_stylesheet() -> str:
QSlider::handle:horizontal {
background: $white;
border: 1px solid $scrollbar_handle;
border: 1px solid $slider_muted;
width: 14px;
margin: -5px 0;
border-radius: 7px;
@@ -839,23 +1163,187 @@ def _original_stylesheet() -> str:
def _portrait_stylesheet() -> str:
return Template("""
QMainWindow, QWidget {
background: $dark_bg;
/* 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;
}
QWidget#mainContentRoot,
QWidget#standardMainPage,
QWidget#compactAutomationPage,
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. */
QPushButton, QToolButton, QComboBox,
QLineEdit, QAbstractSpinBox {
background-color: $dark_elevated;
border: 1px solid $dark_border_faint;
}
/* 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);
}
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[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: $dark_bg;
background: transparent;
color: $dark_text;
}
@@ -893,7 +1381,7 @@ def _portrait_stylesheet() -> str:
}
QFrame#compactAutomationPanel {
background: $dark_bg;
background: transparent;
border: none;
border-radius: 18px;
}
@@ -917,13 +1405,13 @@ def _portrait_stylesheet() -> str:
QLabel#compactSectionHint {
background: transparent;
color: $dark_muted;
color: $dark_subtext;
font-size: $font_hint;
}
QLabel#compactQueueTitle {
background: transparent;
color: $dark_muted;
color: $dark_subtext;
font-size: $font_fine;
font-weight: 700;
}
@@ -950,8 +1438,8 @@ def _portrait_stylesheet() -> str:
}
QPushButton#compactPrimaryButton {
background: $dark_accent;
color: $dark_bg;
background: $dark_accent_fill;
color: $dark_on_accent;
border: none;
border-radius: 14px;
padding: 14px 18px;
@@ -960,7 +1448,7 @@ def _portrait_stylesheet() -> str:
}
QPushButton#compactPrimaryButton:hover {
background: $dark_accent_hover;
background: $dark_accent_fill_hover;
}
QPushButton#compactSecondaryButton,
@@ -1038,20 +1526,22 @@ def _portrait_stylesheet() -> str:
QLabel#axisVideoStatusLabel {
background-color: transparent;
color: $dark_muted;
color: $dark_subtext;
font-weight: bold;
}
QFrame#beamlineStatePanel {
background: $dark_surface;
border-top: 1px solid $dark_border;
background: transparent;
border-top: 1px solid transparent;
}
QLabel#beamlineStateTitle {
background-color: $dark_elevated;
color: $dark_text;
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. */
@@ -1072,35 +1562,57 @@ def _portrait_stylesheet() -> str:
}
QLabel#beamlineStateTellLabel {
color: $dark_muted;
color: $dark_subtext;
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. */
/* 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;
}
/* No L3 border here: the light theme's pale hairline read as a white
frame around dark tables. */
QTableView, QPlainTextEdit {
border: $frame_l3_width solid $frame_l3_color;
border: none;
}
/* Selection + staggered rows, dark flavor. */
/* Selection + staggered rows, dark flavor. Solid fills on purpose —
a transparent viewport renders black here (see DARK_TABLE_BG). */
QTableView {
background: $dark_bg;
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 {
+2 -2
View File
@@ -7,7 +7,7 @@ from PySide6.QtCore import QByteArray, QUrl, QUrlQuery, Slot
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QLineEdit, QPushButton, QVBoxLayout
from aare.gui.styles import BACKGROUND
from aare.gui.styles import APP_BACKGROUND
class LoginDialog(QDialog):
@@ -16,7 +16,7 @@ class LoginDialog(QDialog):
self.token = ""
self.setWindowTitle("User Authentication")
self.setMinimumWidth(400)
self.setStyleSheet(f"background-color: {BACKGROUND};")
self.setStyleSheet(f"background-color: {APP_BACKGROUND};")
self._base_url = base_url
self._reply = None
self._network_manager = None
+16 -24
View File
@@ -2,10 +2,12 @@ from PySide6.QtCore import Qt, Signal, Slot
from PySide6.QtGui import QDoubleValidator
from PySide6.QtWidgets import QCheckBox, QHBoxLayout, QLineEdit, QWidget
from aare.gui.styles import INPUT_BG, INPUT_DISABLED_BG, INPUT_DISABLED_INVALID_BG, INPUT_INVALID_BG
class NumberLineEdit(QLineEdit):
"""Colors are centralized: the per-theme INPUT rules in styles.py key on
the :read-only pseudo-class and the "invalid" dynamic property set here —
no inline stylesheets, so both themes restyle these fields."""
newValue = Signal(float)
def __init__(
@@ -15,8 +17,6 @@ class NumberLineEdit(QLineEdit):
self._read_only: bool = False
self._is_valid: bool = True
self.setStyleSheet(f"background-color: {INPUT_BG};")
# Use a QDoubleValidator to only allow valid floating point numbers
self.validator = QDoubleValidator()
self.validator.setNotation(QDoubleValidator.Notation.StandardNotation)
@@ -39,14 +39,19 @@ class NumberLineEdit(QLineEdit):
format_string = f"{{:.{self.decimal_count}f}}"
return format_string.format(i)
def _set_invalid(self, invalid: bool) -> None:
if self.property("invalid") == invalid:
return
self.setProperty("invalid", invalid)
# Property selectors are only re-evaluated on repolish.
self.style().unpolish(self)
self.style().polish(self)
@Slot(str)
def on_text_changed(self, text: str):
# when text changes check validation and change the colour of the line edit
self._is_valid = self.validate(text)
if self._is_valid:
self.setStyleSheet(f"background-color: {INPUT_BG};")
else:
self.setStyleSheet(f"background-color: {INPUT_INVALID_BG};")
self._set_invalid(not self._is_valid)
@Slot()
def on_editing_finished(self):
@@ -84,22 +89,10 @@ class NumberLineEdit(QLineEdit):
return self.validator.validate(str(text), 0)[0] == QDoubleValidator.State.Acceptable
def setReadOnly(self, ro: bool):
# change state of read only and change colour of line edit based on read only state and validator
# Colors follow via the QSS :read-only pseudo-class (updates without
# a repolish); validity is already tracked by the invalid property.
super().setReadOnly(ro)
self._read_only = ro
if self._read_only and self._is_valid:
self.setStyleSheet(f"background-color: {INPUT_DISABLED_BG};")
elif not self._read_only and self._is_valid:
self.setStyleSheet(f"background-color: {INPUT_BG};")
elif self._read_only and not self._is_valid:
self.setStyleSheet(f"background-color: {INPUT_DISABLED_INVALID_BG};")
elif not self._read_only and not self._is_valid:
self.setStyleSheet(f"background-color: {INPUT_INVALID_BG};")
else:
print(
f"unknown ro state: {self._read_only} or validity {self._is_valid} default to writeable"
)
self.setStyleSheet(f"background-color: {INPUT_BG};")
def get_default(self) -> float:
return float(self.initial_value)
@@ -167,15 +160,14 @@ class CheckedLineEdit(QWidget):
self.setReadOnly()
self.check_box.blockSignals(True)
# No inline checkbox fills: the theme's :disabled rules grey it.
if self._busy:
self.check_box.setEnabled(False)
self.check_box.setStyleSheet(f"background-color: {INPUT_DISABLED_BG};")
if self._checked:
self.editor.force_update_value(self._internal_value)
else:
self.check_box.setEnabled(True)
self.check_box.setStyleSheet(f"background-color: {INPUT_BG};")
self.check_box.blockSignals(False)
self.blockSignals(False)
+11 -23
View File
@@ -1,17 +1,8 @@
from PySide6.QtCore import QSettings, Qt, QTimer
from PySide6.QtGui import QPainter
from PySide6.QtGui import QPainter, QPalette
from PySide6.QtWidgets import QHBoxLayout, QLabel, QLayout, QPushButton, QStyle, QStyleOption
from aare.gui.styles import (
BANNER,
BANNER_TEXT,
BANNER_TEXT_SHADOW,
FONT_BODY,
FONT_HINT,
FONT_TITLE,
MUTED_TEXT,
qcolor,
)
from aare.gui.styles import BANNER_TEXT, BANNER_TEXT_SHADOW, FONT_BODY, qcolor
# Universal vertical rhythm between stacked panels: each panel contributes
# PANEL_VMARGIN top and bottom, the column adds PANEL_VSPACING between them,
@@ -38,12 +29,11 @@ def tighten_column(layout: QLayout) -> None:
def section_title(text: str, parent=None) -> QLabel:
"""Small in-panel section heading — for controls grouped under one
shared TitleLabel banner (e.g. the Beam Config. panel)."""
shared TitleLabel banner (e.g. the Beam Config. panel). Look lives in
the per-theme QLabel#sectionTitle rules in styles.py."""
label = QLabel(text, parent)
label.setObjectName("sectionTitle")
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
label.setStyleSheet(
f"color: {MUTED_TEXT}; font-size: {FONT_HINT}; font-weight: 700; background: transparent;"
)
return label
@@ -55,13 +45,9 @@ class TitleLabel(QLabel):
# Plain text + QSS font instead of <H3>: rich-text heading margins
# would clip vertically in the halved banner height.
self.setText(text)
# Scoped selector: an unscoped widget stylesheet propagates to child
# widgets and would paint the toggle button solid purple, overriding
# the app QSS.
self.setStyleSheet(
f"TitleLabel {{ background-color: {BANNER}; color: {BANNER_TEXT};"
f" font-size: {FONT_TITLE}; font-weight: 700; }}"
)
# No widget stylesheet: the banner look lives in the per-theme
# TitleLabel rules in styles.py — a stylesheet set here would win
# over the theme and pin the light banner into the dark theme.
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
# Half the original 50px: the full-height banner wasted vertical space.
@@ -125,7 +111,9 @@ class TitleLabel(QLabel):
)
painter.setPen(qcolor(BANNER_TEXT_SHADOW, 110))
painter.drawText(rect.translated(0, 1), flags, text)
painter.setPen(qcolor(BANNER_TEXT))
# QSS-resolved 'color' (per-theme TitleLabel rule), not a constant —
# light paints banner white, dark paints dusk gold.
painter.setPen(self.palette().color(QPalette.ColorRole.WindowText))
painter.drawText(rect, flags, text)
def mousePressEvent(self, event):