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

: 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):