diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py
index cb47f0f9..99a437e6 100644
--- a/src/aare/gui/main_window.py
+++ b/src/aare/gui/main_window.py
@@ -551,6 +551,7 @@ class MainWindow(QMainWindow):
# hidden, as the queue engine; its buttons are reparented here so all
# their existing wiring keeps working.
dewar_tab = QWidget()
+ dewar_tab.setObjectName("dewarTab")
dewar_layout = QVBoxLayout(dewar_tab)
dewar_layout.setContentsMargins(0, 0, 0, 0)
dewar_layout.setSpacing(2)
@@ -844,6 +845,8 @@ class MainWindow(QMainWindow):
)
self.status_bar = StatusBar(self._decoded_token, parent=self)
+ # Created after the first _apply_theme, so hand it the theme directly.
+ self.status_bar.set_theme(self._theme_mode)
self.setStatusBar(self.status_bar)
self.daq = DAQWorker(base_url=self._base_url, token=self._token)
@@ -1290,6 +1293,7 @@ class MainWindow(QMainWindow):
dewar_panel.set_status_chip(self.tell_samples.table_model.status_filter)
dewar_tab = QWidget()
+ dewar_tab.setObjectName("dewarTab")
dewar_layout = QVBoxLayout(dewar_tab)
dewar_layout.setContentsMargins(0, 0, 0, 0)
dewar_layout.setSpacing(2)
@@ -1817,6 +1821,10 @@ class MainWindow(QMainWindow):
self.setStyleSheet(build_app_stylesheet(self._theme_mode))
# State colors are painted in code per DAQ tick — QSS can't reach them.
self.beamline_state_panel.set_theme(self._theme_mode)
+ # Status bar flags likewise; it is created after the first
+ # _apply_theme call in __init__, hence the guard.
+ if hasattr(self, "status_bar"):
+ self.status_bar.set_theme(self._theme_mode)
self.sample_camera.set_theme(self._theme_mode)
if old_look is None:
return
diff --git a/src/aare/gui/models/sample_queue_model.py b/src/aare/gui/models/sample_queue_model.py
index e6d6844e..f6a1c3c7 100644
--- a/src/aare/gui/models/sample_queue_model.py
+++ b/src/aare/gui/models/sample_queue_model.py
@@ -4,7 +4,7 @@ from PySide6.QtCore import QAbstractTableModel, Qt
from PySide6.QtGui import QBrush
from aare.gui.constants import LOGGER_NAME
-from aare.gui.styles import SAMPLE_ROW_ACTIVE_BG, SAMPLE_ROW_QUEUED_BG, WHITE, qcolor
+from aare.gui.styles import SAMPLE_ROW_ACTIVE_BG, SAMPLE_ROW_QUEUED_BG, SAMPLE_STATUS_TEXT, qcolor
logger = setup_logger(LOGGER_NAME)
@@ -59,12 +59,16 @@ class SampleQueueSpreadsheet(QAbstractTableModel):
elif role == Qt.ItemDataRole.TextAlignmentRole:
return Qt.AlignmentFlag.AlignCenter
elif role == Qt.ItemDataRole.BackgroundRole:
+ # Tint only the head-of-queue row; plain rows return None so the
+ # theme QSS paints them (hardcoded WHITE fills broke dark mode).
if index.row() == 0:
if self._running:
return QBrush(qcolor(SAMPLE_ROW_ACTIVE_BG))
else:
return QBrush(qcolor(SAMPLE_ROW_QUEUED_BG))
- return QBrush(qcolor(WHITE))
+ elif role == Qt.ItemDataRole.ForegroundRole and index.row() == 0:
+ # Fixed dark ink on the tint so dark-theme white text stays legible.
+ return QBrush(qcolor(SAMPLE_STATUS_TEXT))
return None
def headerData(self, section, orientation, role=None):
diff --git a/src/aare/gui/panels/developer_help_dialog.py b/src/aare/gui/panels/developer_help_dialog.py
index a8c2b47a..734f46e8 100644
--- a/src/aare/gui/panels/developer_help_dialog.py
+++ b/src/aare/gui/panels/developer_help_dialog.py
@@ -32,6 +32,7 @@ from PySide6.QtWidgets import (
from aare.gui.constants import LOGGER_NAME
from aare.gui.log import QtLogEmitter, QtLogHandler
from aare.gui.styles import (
+ FLAT_CARD_RADIUS,
PANEL_BG_FAINT,
PANEL_BG_SOFT,
PANEL_BORDER,
@@ -69,8 +70,16 @@ class DeveloperHelpDialog(QDialog):
self._banner.setVisible(self._is_staff)
self._banner.setWordWrap(True)
self._banner.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
+ # Square cards throughout this dialog: the rounded corner cut-outs
+ # render black on the container's non-composited X11.
self._banner.setStyleSheet(
- card_style(PANEL_BG_SOFT, PANEL_BORDER, selector="QLabel", extra="padding: 6px 8px;")
+ card_style(
+ PANEL_BG_SOFT,
+ PANEL_BORDER,
+ selector="QLabel",
+ radius=FLAT_CARD_RADIUS,
+ extra="padding: 6px 8px;",
+ )
)
root.addWidget(self._banner)
@@ -89,7 +98,6 @@ class DeveloperHelpDialog(QDialog):
"QLineEdit {"
f" background: {WHITE};"
f" border: 1px solid {PANEL_BORDER_DARK};"
- " border-radius: 6px;"
" padding: 4px 8px;"
"}"
)
@@ -149,7 +157,9 @@ class DeveloperHelpDialog(QDialog):
self._details_frame = QFrame(self)
self._details_frame.setFrameShape(QFrame.Shape.StyledPanel)
- self._details_frame.setStyleSheet(card_style(PANEL_BG_FAINT, PANEL_BORDER))
+ self._details_frame.setStyleSheet(
+ card_style(PANEL_BG_FAINT, PANEL_BORDER, radius=FLAT_CARD_RADIUS)
+ )
details_layout = QVBoxLayout(self._details_frame)
details_layout.setContentsMargins(10, 10, 10, 10)
@@ -179,7 +189,13 @@ class DeveloperHelpDialog(QDialog):
self._detail_help.setWordWrap(True)
self._detail_help.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
self._detail_help.setStyleSheet(
- card_style(WHITE, PANEL_BORDER_LIGHT, selector="QLabel", extra="padding: 8px;")
+ card_style(
+ WHITE,
+ PANEL_BORDER_LIGHT,
+ selector="QLabel",
+ radius=FLAT_CARD_RADIUS,
+ extra="padding: 8px;",
+ )
)
details_layout.addWidget(QLabel("Help:", self))
details_layout.addWidget(self._detail_help, 1)
diff --git a/src/aare/gui/panels/reference_tools_panel.py b/src/aare/gui/panels/reference_tools_panel.py
index 09c409fc..de489191 100644
--- a/src/aare/gui/panels/reference_tools_panel.py
+++ b/src/aare/gui/panels/reference_tools_panel.py
@@ -7,7 +7,7 @@ from PySide6.QtGui import QBrush
from PySide6.QtWidgets import QAbstractItemView, QFrame, QGridLayout, QHeaderView, QMenu, QTableView
from aare.gui.constants import LOGGER_NAME
-from aare.gui.styles import SAMPLE_ROW_QUEUED_BG, WHITE, qcolor
+from aare.gui.styles import SAMPLE_ROW_QUEUED_BG, SAMPLE_STATUS_TEXT, qcolor
from aare.gui.widgets.title_label import TitleLabel
logger = setup_logger(LOGGER_NAME)
@@ -74,9 +74,19 @@ class ReferenceToolsModel(QAbstractTableModel):
elif role == Qt.ItemDataRole.TextAlignmentRole:
return Qt.AlignmentFlag.AlignCenter
elif role == Qt.ItemDataRole.BackgroundRole:
+ # Tint only the current-reference row; plain rows return None so
+ # the theme QSS paints them (a hardcoded WHITE fill here was the
+ # big white table in dark mode and fought the light theme's
+ # alternating stripes).
if self._sorted_samples[index.row()].db_id == self.current_reference:
return QBrush(qcolor(SAMPLE_ROW_QUEUED_BG))
- return QBrush(qcolor(WHITE))
+ elif (
+ role == Qt.ItemDataRole.ForegroundRole
+ and self._sorted_samples[index.row()].db_id == self.current_reference
+ ):
+ # Fixed dark ink on the tint — the tint stays pale in BOTH themes,
+ # so the dark theme's near-white text would vanish on it.
+ return QBrush(qcolor(SAMPLE_STATUS_TEXT))
return None
@@ -152,7 +162,7 @@ class ReferenceToolsModel(QAbstractTableModel):
self.dataChanged.emit(
self.index(0, 0),
self.index(self.rowCount() - 1, self.columnCount() - 1),
- [Qt.ItemDataRole.BackgroundRole],
+ [Qt.ItemDataRole.BackgroundRole, Qt.ItemDataRole.ForegroundRole],
)
@@ -191,8 +201,11 @@ class ReferenceToolsPanel(QFrame):
layout.addWidget(TitleLabel("Reference tools", parent=self), 0, 0, 1, 4)
self.table_view = QTableView(parent=self)
- # Row colors carry the separation — no grid lines.
+ # Row colors carry the separation — no grid lines. Alternating rows
+ # come from the theme QSS (alternate-background-color), same as the
+ # Dewar sample list.
self.table_view.setShowGrid(False)
+ self.table_view.setAlternatingRowColors(True)
layout.addWidget(self.table_view, 1, 0, 1, 4)
# initialize model with provided samples (or adopt the shared one)
diff --git a/src/aare/gui/panels/sample_queue_panel.py b/src/aare/gui/panels/sample_queue_panel.py
index 13eeedd7..affa50f8 100644
--- a/src/aare/gui/panels/sample_queue_panel.py
+++ b/src/aare/gui/panels/sample_queue_panel.py
@@ -68,6 +68,9 @@ class SampleQueuePanel(QFrame):
layout.addWidget(TitleLabel("Sample queue", self))
self.table_view = QTableView(self)
+ # Themed stripes from the QSS, matching the other sample tables — the
+ # model no longer paints plain rows white.
+ self.table_view.setAlternatingRowColors(True)
self.table_model = SampleQueueSpreadsheet(show_user=show_user)
self.table_view.setModel(self.table_model)
diff --git a/src/aare/gui/styles.py b/src/aare/gui/styles.py
index 5dffb881..c6f3021c 100644
--- a/src/aare/gui/styles.py
+++ b/src/aare/gui/styles.py
@@ -352,6 +352,40 @@ STATUS_WARN = "#fe640b" # baton waiting / warming cryo / tell busy (peach)
STATUS_INFO = "#1e66f5" # cold cryo (blue)
STATUS_VACANT = "#df8e1d" # baton vacant (yellow)
STATUS_REQUEST = "#04a5e5" # baton request (sky)
+# Dark variants (Catppuccin Mocha) — the latte values above sink into the
+# sunset sky. Painted in code per DAQ tick, so status_colors(theme) hands
+# them out — same pattern as state_colors below.
+DARK_STATUS_OK = "#a6e3a1" # mocha green
+DARK_STATUS_ALERT = "#f38ba8" # mocha red
+DARK_STATUS_WARN = "#fab387" # mocha peach
+DARK_STATUS_INFO = "#89b4fa" # mocha blue
+DARK_STATUS_VACANT = "#f9e2af" # mocha yellow
+DARK_STATUS_REQUEST = "#89dceb" # mocha sky
+DARK_STATE_TELL_TEXT = "#bac2de" # mocha subtext1 — status-bar TELL idle line
+
+
+def status_colors(theme: str) -> dict[str, str]:
+ """Status-bar flag colors for the given theme (painted in code)."""
+ if theme == THEME_SUNSET:
+ return {
+ "ok": DARK_STATUS_OK,
+ "alert": DARK_STATUS_ALERT,
+ "warn": DARK_STATUS_WARN,
+ "info": DARK_STATUS_INFO,
+ "vacant": DARK_STATUS_VACANT,
+ "request": DARK_STATUS_REQUEST,
+ "tell": DARK_STATE_TELL_TEXT,
+ }
+ return {
+ "ok": STATUS_OK,
+ "alert": STATUS_ALERT,
+ "warn": STATUS_WARN,
+ "info": STATUS_INFO,
+ "vacant": STATUS_VACANT,
+ "request": STATUS_REQUEST,
+ "tell": STATE_TELL_TEXT,
+ }
+
# -- Beamline state panel ---------------------------------------------------
# The panel paints these in code per DAQ tick (data-driven), so it asks
@@ -398,7 +432,7 @@ SAMPLE_ROW_ALT_BG = "#eef1f5" # staggered row grey (alternates with white)
SAMPLE_STATUS_QUEUED_BG = "#ffe4c4" # pale orange — waiting in the automation queue
SAMPLE_STATUS_FLAGGED_BG = "#ffd9d9" # pale red — automation failed on this sample
SAMPLE_STATUS_MEASURED_BG = "#dcf2e0" # pale green — already has collected data
-SAMPLE_STATUS_SELECTED_BG = "#d8e8fd" # pale blue — table selection highlight
+SAMPLE_STATUS_SELECTED_BG = "#84abd9" # pale blue — table selection highlight
# Fixed ink on the pastel tints above: the tints stay light in BOTH themes,
# so theme-following text (white in Sunset) would vanish on them. Models
# return this as ForegroundRole wherever they return a tint.
@@ -1641,7 +1675,9 @@ def _sunset_stylesheet() -> str:
}
QLabel#beamlineStateTellLabel {
- color: $dark_subtext;
+ /* Same color as the Current-state neighbor — subtext was unreadable
+ on the selected-state blue band. */
+ color: $dark_text;
font-size: $font_body_lg;
font-weight: 700;
padding-left: 4px;
@@ -1671,11 +1707,22 @@ def _sunset_stylesheet() -> str:
background: $dark_surface;
}
+ /* Dewar tab page: the automation button row sits on this bare QWidget
+ below the panel's border — left transparent it shows the near-black
+ gradient bottom, reading as an unpainted hole. */
+ QWidget#dewarTab {
+ background-color: $dark_surface;
+ }
+ QWidget#logPanel { background-color: $dark_surface; }
+
/* No L3 border here: the light theme's pale hairline read as a white
frame around dark tables. */
QTableView, QPlainTextEdit {
border: none;
}
+ QPlainTextEdit {
+ background: $dark_table_bg; /* solid — transparent renders black on the container's X11 */
+ }
/* Selection + staggered rows, dark flavor. Solid fills on purpose —
a transparent viewport renders black here (see DARK_TABLE_BG). */
diff --git a/src/aare/gui/widgets/status_bar.py b/src/aare/gui/widgets/status_bar.py
index aca8d4d1..23fb4094 100644
--- a/src/aare/gui/widgets/status_bar.py
+++ b/src/aare/gui/widgets/status_bar.py
@@ -8,15 +8,7 @@ from PySide6.QtGui import QFont
from PySide6.QtWidgets import QDialog, QLabel, QMenu, QMessageBox, QSizePolicy, QStatusBar
from aare.gui.constants import LOGGER_NAME
-from aare.gui.styles import (
- STATE_TELL_TEXT,
- STATUS_ALERT,
- STATUS_INFO,
- STATUS_OK,
- STATUS_REQUEST,
- STATUS_VACANT,
- STATUS_WARN,
-)
+from aare.gui.styles import THEME_SUNRISE, status_colors
from aare.gui.widgets.baton_request_dialog import BatonRequestDialog
from aare.gui.widgets.clickable_label import ClickableLabel
from aare.gui.widgets.pgroup_dialog import PGroupDialog
@@ -54,6 +46,11 @@ class StatusBar(QStatusBar):
self._is_staff = self._decoded_token.staff
self._allowed_pgroups = self._decoded_token.pgroups
+ # Per-theme flag colors (MainWindow._apply_theme calls set_theme) —
+ # painted in code per DAQ tick, QSS cannot reach the rich-text spans.
+ self._colors = status_colors(THEME_SUNRISE)
+ self._message_is_error = False
+
self._message_clear_timer = QTimer(self)
self._message_clear_timer.setSingleShot(True)
self._message_clear_timer.timeout.connect(self.clear_connection_message)
@@ -109,12 +106,24 @@ class StatusBar(QStatusBar):
self.addPermanentWidget(self.busy_label)
self.addPermanentWidget(self.session_label)
+ def set_theme(self, theme: str) -> None:
+ """Adopt the theme's flag colors: recolor the connection message and
+ re-render the DAQ-driven labels from the last status right away."""
+ self._colors = status_colors(theme)
+ self._apply_message_style()
+ if self._status is not None:
+ self.update_daq_status(self._status)
+
+ def _apply_message_style(self) -> None:
+ color = self._colors["alert"] if self._message_is_error else self._colors["ok"]
+ self.message_label.setStyleSheet(f"color: {color}; font-weight: bold;")
+
@Slot(str, bool)
def show_connection_message(self, msg: str, is_error: bool = True):
- color = STATUS_ALERT if is_error else STATUS_OK
+ self._message_is_error = is_error
self._message_clear_timer.stop()
self.message_label.setText(msg)
- self.message_label.setStyleSheet(f"color: {color}; font-weight: bold;")
+ self._apply_message_style()
self.message_label.setVisible(bool(msg))
if not is_error:
@@ -155,9 +164,13 @@ class StatusBar(QStatusBar):
self.transmission.set_value(f"{status.bl.transmission:.5f}")
if status.bl.ring_current_mA < 5.0:
- self.ring_current.set_value(f"{status.bl.ring_current_mA:.2f}", STATUS_ALERT)
+ self.ring_current.set_value(
+ f"{status.bl.ring_current_mA:.2f}", self._colors["alert"]
+ )
elif status.bl.ring_current_mA < 390.0:
- self.ring_current.set_value(f"{status.bl.ring_current_mA:.2f}", STATUS_WARN)
+ self.ring_current.set_value(
+ f"{status.bl.ring_current_mA:.2f}", self._colors["warn"]
+ )
else:
self.ring_current.set_value(f"{status.bl.ring_current_mA:.2f}")
@@ -165,28 +178,28 @@ class StatusBar(QStatusBar):
self.wvl.set_value(f"{status.diffraction.wavelength_angstrom:.2f}")
if status.bl.cryojet_K < 110.0:
- self.cryo_label.set_value(f"{status.bl.cryojet_K:.1f}", STATUS_INFO)
+ self.cryo_label.set_value(f"{status.bl.cryojet_K:.1f}", self._colors["info"])
elif status.bl.cryojet_K < 250.0:
- self.cryo_label.set_value(f"{status.bl.cryojet_K:.1f}", STATUS_WARN)
+ self.cryo_label.set_value(f"{status.bl.cryojet_K:.1f}", self._colors["warn"])
else:
- self.cryo_label.set_value(f"{status.bl.cryojet_K:.1f}", STATUS_ALERT)
+ self.cryo_label.set_value(f"{status.bl.cryojet_K:.1f}", self._colors["alert"])
if status.bl.shutter_open:
self.shutter_label.setText(
- f"""Fast Shutter: Open ☢️ """
+ f"""Fast Shutter: Open ☢️ """
)
else:
self.shutter_label.setText(
- f"""Fast Shutter: Closed 🚪 """
+ f"""Fast Shutter: Closed 🚪 """
)
if status.bl.exp_shutter_open:
self.exp_shutter_label.setText(
- f"""ExpHutch Shutter: Open """
+ f"""ExpHutch Shutter: Open """
)
else:
self.exp_shutter_label.setText(
- f"""ExpHutch Shutter: Closed 🚪 """
+ f"""ExpHutch Shutter: Closed 🚪 """
)
if status.session.current_pgroup is not None:
@@ -197,29 +210,29 @@ class StatusBar(QStatusBar):
self.state_label.setText(f"""State: {status.state.display_name()} """)
tell_text = "—"
- tell_color = STATE_TELL_TEXT
+ tell_color = self._colors["tell"]
if status.tell_state is not None:
tell_text = status.tell_state.activity.display_name()
if status.tell_state.activity.value == "error":
- tell_color = STATUS_ALERT
+ tell_color = self._colors["alert"]
elif status.tell_state.activity.value in {
"mounting",
"unmounting",
"drying",
"cooling",
}:
- tell_color = STATUS_WARN
+ tell_color = self._colors["warn"]
else:
- tell_color = STATUS_OK
+ tell_color = self._colors["ok"]
self.tell_state_label.setText(f"Tell: {tell_text} ")
self.tell_state_label.setStyleSheet(f"color: {tell_color};")
if status.busy:
- busy_flag = f""" Busy 🔒 """
+ busy_flag = f""" Busy 🔒 """
else:
- busy_flag = f""" Idle 🔓 """
+ busy_flag = f""" Idle 🔓 """
html_content = f"""Beamline: {busy_flag} """
@@ -227,15 +240,23 @@ class StatusBar(QStatusBar):
session_flag = ""
if status.session.session == SessionsStateEnum.Vacant:
- session_flag = f""" Vacant 🔓 """
+ session_flag = (
+ f""" Vacant 🔓 """
+ )
elif status.session.session == SessionsStateEnum.OwnedByYou:
- session_flag = f""" Owned ⬤ """
+ session_flag = f""" Owned ⬤ """
elif status.session.session == SessionsStateEnum.OwnedByElse:
- session_flag = f""" Other 🔒 """
+ session_flag = (
+ f""" Other 🔒 """
+ )
elif status.session.session == SessionsStateEnum.PendingYouToElse:
- session_flag = f""" Waiting... ⏳ """
+ session_flag = (
+ f""" Waiting... ⏳ """
+ )
elif status.session.session == SessionsStateEnum.PendingElseToYou:
- session_flag = f""" Request! ⚡ """
+ session_flag = (
+ f""" Request! ⚡ """
+ )
html_content_session = f"""Session: {session_flag}"""
self.session_label.setText(html_content_session)
@@ -603,7 +624,8 @@ class StatusBar(QStatusBar):
def _generate_pgroup_dialogue(self, curr: str | None = None, pgroups: list | None = None):
logger.info(pgroups)
- dialog = PGroupDialog(curr_pgroup=curr, pgroups=pgroups)
+ dialog = PGroupDialog(curr_pgroup=curr, pgroups=pgroups, parent=self.window())
+
if dialog.exec() == QDialog.DialogCode.Accepted:
entered_text = dialog.get_input()
if pgroups and entered_text not in pgroups: