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:
1 parent
2f9fb10a8a
commit
d198297881
11 files changed
+641
-164
No files matched your search
@@ -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")
|
||||
|
||||
@@ -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,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("")
|
||||
@@ -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()
|
||||
|
||||
@@ -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; }"
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user