diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py
index a12c13f6..de2755eb 100644
--- a/src/aare/gui/main_window.py
+++ b/src/aare/gui/main_window.py
@@ -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")
diff --git a/src/aare/gui/models/user_sample_model.py b/src/aare/gui/models/user_sample_model.py
index c9e08e2e..207234a5 100644
--- a/src/aare/gui/models/user_sample_model.py
+++ b/src/aare/gui/models/user_sample_model.py
@@ -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
diff --git a/src/aare/gui/panels/abr_tweak_panel.py b/src/aare/gui/panels/abr_tweak_panel.py
index d2ee5e35..b9f80132 100644
--- a/src/aare/gui/panels/abr_tweak_panel.py
+++ b/src/aare/gui/panels/abr_tweak_panel.py
@@ -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("")
diff --git a/src/aare/gui/panels/file_path_panel.py b/src/aare/gui/panels/file_path_panel.py
index d162ccef..b18521fa 100644
--- a/src/aare/gui/panels/file_path_panel.py
+++ b/src/aare/gui/panels/file_path_panel.py
@@ -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:
"
"{date} - date in format yyyymmdd
"
@@ -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:
"
"{date} - date in format yyyymmdd
"
@@ -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()
diff --git a/src/aare/gui/panels/log_panel.py b/src/aare/gui/panels/log_panel.py
index fcb56443..9d1033ea 100644
--- a/src/aare/gui/panels/log_panel.py
+++ b/src/aare/gui/panels/log_panel.py
@@ -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; }"
)
diff --git a/src/aare/gui/panels/samcam_panel.py b/src/aare/gui/panels/samcam_panel.py
index 55e520a3..e7a591ab 100644
--- a/src/aare/gui/panels/samcam_panel.py
+++ b/src/aare/gui/panels/samcam_panel.py
@@ -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)
diff --git a/src/aare/gui/panels/tell_sample_panel.py b/src/aare/gui/panels/tell_sample_panel.py
index b085a098..637c73d8 100644
--- a/src/aare/gui/panels/tell_sample_panel.py
+++ b/src/aare/gui/panels/tell_sample_panel.py
@@ -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()
diff --git a/src/aare/gui/styles.py b/src/aare/gui/styles.py
index 02241a6d..0e0dd335 100644
--- a/src/aare/gui/styles.py
+++ b/src/aare/gui/styles.py
@@ -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 {
diff --git a/src/aare/gui/widgets/login.py b/src/aare/gui/widgets/login.py
index 0f71f1f8..758e7510 100644
--- a/src/aare/gui/widgets/login.py
+++ b/src/aare/gui/widgets/login.py
@@ -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
diff --git a/src/aare/gui/widgets/number_line_edit.py b/src/aare/gui/widgets/number_line_edit.py
index 802023b5..98f22441 100644
--- a/src/aare/gui/widgets/number_line_edit.py
+++ b/src/aare/gui/widgets/number_line_edit.py
@@ -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)
diff --git a/src/aare/gui/widgets/title_label.py b/src/aare/gui/widgets/title_label.py
index 13fc734b..bf1d15d6 100644
--- a/src/aare/gui/widgets/title_label.py
+++ b/src/aare/gui/widgets/title_label.py
@@ -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