feat: text zoom shortcuts and high-contrast resize lines
CI / lint (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped
CI / lint (pull_request) Successful in 1m0s
CI / test (3.12) (pull_request) Successful in 1m27s
CI / test (3.14) (pull_request) Successful in 1m24s
CI / test (3.13) (pull_request) Successful in 1m36s
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Successful in 1m34s
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Successful in 1m35s
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Successful in 1m39s
CI / test-with-coverage (pull_request) Successful in 1m53s
CI / coverage-analysis (pull_request) Successful in 4s
CI / lint (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped
CI / lint (pull_request) Successful in 1m0s
CI / test (3.12) (pull_request) Successful in 1m27s
CI / test (3.14) (pull_request) Successful in 1m24s
CI / test (3.13) (pull_request) Successful in 1m36s
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Successful in 1m34s
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Successful in 1m35s
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Successful in 1m39s
CI / test-with-coverage (pull_request) Successful in 1m53s
CI / coverage-analysis (pull_request) Successful in 4s
- Ctrl+plus / Ctrl+minus / Ctrl+0 (and View menu entries) scale the QSS FONT_* ladder and the application default font together; the scale is clamped to 0.8-1.6 and persisted in QSettings appearance/font_scale. - Resize gutters keep a 5px hover/drag region but paint only a 2px line with a 1px shadow (hard-stop gradients per orientation). - WCAG contrast fixes: separators were 1.0-1.2:1 against every theme background, now >=3.25:1 (dark theme gold @70%); scrollbar handle 1.17:1 -> 3.38:1; unselected tab text on the sky gradient 2.07:1 -> 4.71:1 via TAB_IDLE_TEXT. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@ from PySide6.QtGui import (
|
||||
QActionGroup,
|
||||
QColor,
|
||||
QCursor,
|
||||
QFont,
|
||||
QGuiApplication,
|
||||
QImage,
|
||||
QKeySequence,
|
||||
@@ -100,6 +101,7 @@ from aare.gui.styles import (
|
||||
APP_BACKGROUND,
|
||||
DARK_TEXT,
|
||||
DOCK_CONTENT_LEFT_PAD,
|
||||
FONT_SCALE_STEP,
|
||||
SEPARATOR_HINT_DELAY_MS,
|
||||
THEME_BLUEBIRD,
|
||||
THEME_FADE_MS,
|
||||
@@ -107,7 +109,9 @@ from aare.gui.styles import (
|
||||
THEME_SUNSET,
|
||||
admin_tip_qss,
|
||||
build_app_stylesheet,
|
||||
font_scale,
|
||||
qcolor,
|
||||
set_font_scale,
|
||||
)
|
||||
|
||||
# Threads
|
||||
@@ -2002,6 +2006,15 @@ class MainWindow(QMainWindow):
|
||||
app.setPalette(palette)
|
||||
else:
|
||||
app.setPalette(self._default_palette)
|
||||
# Text zoom, part 2: the QSS FONT_* ladder only reaches widgets the app
|
||||
# sheet names — everything else renders in the app default font, so
|
||||
# scale that too or body text ignores Ctrl+plus/minus.
|
||||
if not hasattr(self, "_default_app_font"):
|
||||
self._default_app_font = app.font()
|
||||
font = QFont(self._default_app_font)
|
||||
if font_scale() != 1.0:
|
||||
font.setPointSizeF(self._default_app_font.pointSizeF() * font_scale())
|
||||
app.setFont(font)
|
||||
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)
|
||||
@@ -2038,10 +2051,23 @@ class MainWindow(QMainWindow):
|
||||
# change (styles.py: "original"->"sunrise", "portrait"->"sunset").
|
||||
saved = {"original": THEME_SUNRISE, "portrait": THEME_SUNSET}.get(saved, saved)
|
||||
self._theme_mode = saved
|
||||
set_font_scale(float(settings.value("appearance/font_scale", 1.0, type=float)))
|
||||
|
||||
def _save_theme_settings(self) -> None:
|
||||
settings = QSettings("PSI", "AareGUI")
|
||||
settings.setValue("appearance/theme", self._theme_mode)
|
||||
settings.setValue("appearance/font_scale", font_scale())
|
||||
|
||||
def _change_font_zoom(self, direction: int) -> None:
|
||||
"""Ctrl+plus / Ctrl+minus / Ctrl+0 accessibility zoom: step the FONT_*
|
||||
ladder scale and rebuild the app stylesheet (same repolish path as a
|
||||
theme switch, so the whole UI rescales in one pass)."""
|
||||
# round: repeated 0.1 float steps otherwise drift (1.2000000000000002)
|
||||
set_font_scale(
|
||||
1.0 if direction == 0 else round(font_scale() + direction * FONT_SCALE_STEP, 2)
|
||||
)
|
||||
self._save_theme_settings()
|
||||
self._apply_theme()
|
||||
|
||||
@Slot()
|
||||
def use_legacy_theme(self) -> None:
|
||||
@@ -2113,6 +2139,24 @@ class MainWindow(QMainWindow):
|
||||
view_menu.addAction(self._use_bluebird_theme_action)
|
||||
view_menu.addAction(self._use_portrait_theme_action)
|
||||
view_menu.addSeparator()
|
||||
|
||||
# Accessibility text zoom. Ctrl+= alias: StandardKey.ZoomIn is
|
||||
# Ctrl+Shift+= on some platforms and users expect plain Ctrl+plus.
|
||||
bigger_text_action = QAction("Bigger Text", self)
|
||||
bigger_text_action.setShortcuts(
|
||||
[*QKeySequence.keyBindings(QKeySequence.StandardKey.ZoomIn), QKeySequence("Ctrl+=")]
|
||||
)
|
||||
bigger_text_action.triggered.connect(lambda: self._change_font_zoom(1))
|
||||
view_menu.addAction(bigger_text_action)
|
||||
smaller_text_action = QAction("Smaller Text", self)
|
||||
smaller_text_action.setShortcuts(QKeySequence.StandardKey.ZoomOut)
|
||||
smaller_text_action.triggered.connect(lambda: self._change_font_zoom(-1))
|
||||
view_menu.addAction(smaller_text_action)
|
||||
reset_text_action = QAction("Reset Text Size", self)
|
||||
reset_text_action.setShortcut(QKeySequence("Ctrl+0"))
|
||||
reset_text_action.triggered.connect(lambda: self._change_font_zoom(0))
|
||||
view_menu.addAction(reset_text_action)
|
||||
view_menu.addSeparator()
|
||||
view_menu.addAction(self._portrait_mode_action)
|
||||
view_menu.addAction(self._enter_automation_view_action)
|
||||
view_menu.addSeparator()
|
||||
|
||||
+162
-32
@@ -113,16 +113,44 @@ DOCK_CONTENT_LEFT_PAD = 10
|
||||
# banner's bottom edge line.
|
||||
BANNER_TAB_GAP = 6
|
||||
|
||||
# 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, 30%)" # scrollbar-track grey @50%
|
||||
# Idle fill so the resize line is findable before the hover rest — 10% of the
|
||||
# hover alpha. Any base QSS fill replaces the native dotted grip; accepted.
|
||||
SEPARATOR_IDLE = "rgba(168, 178, 192, 10%)"
|
||||
# Resize lines: the hover rest (SEPARATOR_HINT_DELAY_MS, gate in
|
||||
# MainWindow.event(); the QSS :hover part picks the one separator) darkens the
|
||||
# exact separator under the cursor to SEPARATOR_HINT.
|
||||
# Accessibility (WCAG 1.4.11, >=3:1 non-text contrast): the old
|
||||
# rgba(168,178,192) fills measured 1.0-1.2:1 against every theme background —
|
||||
# invisible. Solid slate: idle #47536a >=3.25:1, hover #3f4a5f >=3.74:1
|
||||
# against gradient top/mid/bottom and panel white.
|
||||
SEPARATOR_HINT = "#3f4a5f"
|
||||
SEPARATOR_IDLE = "#47536a"
|
||||
SEPARATOR_HINT_DELAY_MS = 66 # int, used in code, not QSS
|
||||
|
||||
# The gutter keeps this full width for the mouse; only a 2px line + 1px
|
||||
# shadow is painted inside it (a solid 5px bar read too heavy). 5px is a
|
||||
# first guess — adjust here if the grab target feels off.
|
||||
SEPARATOR_REGION = "5px"
|
||||
SEPARATOR_SHADOW = "rgba(31, 41, 59, 25%)"
|
||||
|
||||
|
||||
def _separator_gradient(line: str, shadow: str, axis: str) -> str:
|
||||
"""Paint of a resize gutter: transparent 1px, shadow 1px, line 2px,
|
||||
shadow 1px — hard gradient stops at 1/5 steps of SEPARATOR_REGION.
|
||||
axis "x" runs the gradient left->right (an upright line), "y" top->down
|
||||
(a lying line)."""
|
||||
x2, y2 = ("1", "0") if axis == "x" else ("0", "1")
|
||||
return (
|
||||
f"qlineargradient(x1:0, y1:0, x2:{x2}, y2:{y2},"
|
||||
f" stop:0 transparent, stop:0.19 transparent,"
|
||||
f" stop:0.2 {shadow}, stop:0.39 {shadow},"
|
||||
f" stop:0.4 {line}, stop:0.79 {line},"
|
||||
f" stop:0.8 {shadow}, stop:1 {shadow})"
|
||||
)
|
||||
|
||||
|
||||
SEP_IDLE_X = _separator_gradient(SEPARATOR_IDLE, SEPARATOR_SHADOW, "x")
|
||||
SEP_IDLE_Y = _separator_gradient(SEPARATOR_IDLE, SEPARATOR_SHADOW, "y")
|
||||
SEP_HINT_X = _separator_gradient(SEPARATOR_HINT, SEPARATOR_SHADOW, "x")
|
||||
SEP_HINT_Y = _separator_gradient(SEPARATOR_HINT, SEPARATOR_SHADOW, "y")
|
||||
|
||||
# Theme-switch screenshot cross-fade duration (int ms, used in code).
|
||||
THEME_FADE_MS = 250
|
||||
|
||||
@@ -226,7 +254,14 @@ 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_SEPARATOR_IDLE = "rgba(224, 145, 63, 10%)" # DARK_ACCENT @10%, idle resize line
|
||||
# DARK_ACCENT @70%: 10% measured 1.16:1 on DARK_BG (invisible); 70% blends to
|
||||
# >=3.74:1 — same WCAG 1.4.11 floor as the light-theme separators.
|
||||
DARK_SEPARATOR_IDLE = "rgba(224, 145, 63, 70%)"
|
||||
DARK_SEPARATOR_SHADOW = "rgba(0, 0, 0, 45%)"
|
||||
DARK_SEP_IDLE_X = _separator_gradient(DARK_SEPARATOR_IDLE, DARK_SEPARATOR_SHADOW, "x")
|
||||
DARK_SEP_IDLE_Y = _separator_gradient(DARK_SEPARATOR_IDLE, DARK_SEPARATOR_SHADOW, "y")
|
||||
DARK_SEP_HINT_X = _separator_gradient(DARK_ACCENT, DARK_SEPARATOR_SHADOW, "x")
|
||||
DARK_SEP_HINT_Y = _separator_gradient(DARK_ACCENT, DARK_SEPARATOR_SHADOW, "y")
|
||||
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)
|
||||
@@ -271,6 +306,10 @@ HINT_TEXT = "#9ca0b0" # baton dialog fine print (latte overlay0)
|
||||
HEADING_TEXT = "#4c4f69" # card headings (latte text)
|
||||
SUBTLE_TEXT = "#5c5f77" # card body text (latte subtext1)
|
||||
MUTED_TEXT = "#6c6f85" # neutral chip / idle step text (latte subtext0)
|
||||
# Unselected tab labels: tabs also sit on the sky gradient, where MUTED_TEXT
|
||||
# is 2.07:1 — under the 4.5:1 WCAG text floor. This slate reads 4.71:1 on the
|
||||
# gradient top and stays muted next to TEXT.
|
||||
TAB_IDLE_TEXT = "#2f3b52"
|
||||
FAINT_TEXT = "#8c8fa1" # pending/skipped step text (latte overlay1)
|
||||
SHADOW = "#000000" # drop shadows & tutorial scrim; alpha stays at call site
|
||||
|
||||
@@ -471,6 +510,9 @@ LEGEND_BG = "#eff1f5" # base
|
||||
LEGEND_TEXT = "#4c4f69" # text
|
||||
TOOLTIP_TEXT = "#4c4f69" # camera coords tooltip pen — NOT the QToolTip popup
|
||||
SCALE_BAR_GREY = "#8c8fa1" # hover HUD scale bar (Latte overlay1 grey)
|
||||
MARK_TOOLTIP_GOLD = "#df8e1d" # yellow
|
||||
MARK_TOOLTIP_ORANGE = "#fe640b" # peach
|
||||
MARK_TOOLTIP_RED = "#d20f39" # red
|
||||
MARK_BADGE_BG = "#fe640b" # peach
|
||||
|
||||
# Prediction class overlay colors. The old pure-green vs CSS-green split
|
||||
@@ -500,11 +542,22 @@ BOOKMARK_COLORS = {
|
||||
"lime": "#179299", # teal — Latte has one green; teal keeps the pair distinct
|
||||
}
|
||||
|
||||
# -- Busy overlay: PSI red for every busy state, blue for robot cooling ------
|
||||
# -- Busy overlay (per-source color coding) ---------------------------------
|
||||
# Catppuccin Latte accents; BORDER/DOT are 25%/20% mixes toward Latte base.
|
||||
BUSY_YELLOW = "#df8e1d" # yellow
|
||||
BUSY_YELLOW_BORDER = "#ebd8bf"
|
||||
BUSY_YELLOW_DOT = "#ecddca"
|
||||
BUSY_YELLOW_TEXT_DARK = "#4c4f69" # text
|
||||
BUSY_PURPLE = "#8839ef" # mauve
|
||||
BUSY_PURPLE_BORDER = "#d5c3f4"
|
||||
BUSY_PURPLE_DOT = "#daccf4"
|
||||
BUSY_RED_BADGE = "#d20f39" # red
|
||||
BUSY_RED_FILL = "#d20f39" # red
|
||||
BUSY_RED_BORDER = "#e8b8c6"
|
||||
BUSY_RED_DOT = "#e9c4cf"
|
||||
BUSY_ORANGE = "#fe640b" # peach
|
||||
BUSY_ORANGE_BORDER = "#f3ceba"
|
||||
BUSY_ORANGE_DOT = "#f2d5c6"
|
||||
BUSY_BLUE = "#1e66f5" # blue
|
||||
BUSY_BLUE_BORDER = "#bbcef5"
|
||||
BUSY_BLUE_DOT = "#c5d5f5"
|
||||
@@ -550,6 +603,27 @@ FONT_LABEL = "14px" # form labels, secondary text — always bold
|
||||
FONT_HINT = "12px" # hints
|
||||
FONT_FINE = "11px" # fine print, queue titles
|
||||
|
||||
# -- Font zoom (Ctrl+plus / Ctrl+minus, accessibility) ----------------------
|
||||
# Whole-app text scale applied to the FONT_* ladder when the QSS is built, so
|
||||
# one re-apply of the stylesheet rescales every rule. Floats, so _palette()'s
|
||||
# str filter never picks them up as colors.
|
||||
FONT_SCALE_MIN = 0.8
|
||||
FONT_SCALE_MAX = 1.6
|
||||
FONT_SCALE_STEP = 0.1
|
||||
_font_scale = 1.0
|
||||
|
||||
|
||||
def font_scale() -> float:
|
||||
return _font_scale
|
||||
|
||||
|
||||
def set_font_scale(scale: float) -> None:
|
||||
# Clamped: below 0.8 the fine-print sizes fall under 9px (unreadable),
|
||||
# above 1.6 the fixed-height banner rows start clipping their text.
|
||||
global _font_scale
|
||||
_font_scale = max(FONT_SCALE_MIN, min(FONT_SCALE_MAX, scale))
|
||||
|
||||
|
||||
# -- Hover tooltips (the QToolTip popup; styled borderless) -----------------
|
||||
TOOLTIP_BG = "#f7f9fc"
|
||||
TOOLTIP_FG = "#263043"
|
||||
@@ -577,11 +651,12 @@ def admin_tip_qss(theme: str) -> str:
|
||||
SLIDER_FILL = "#8ba3c7"
|
||||
|
||||
# -- 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.
|
||||
# Accessibility re-flip (WCAG 1.4.11): the light-on-light pairing measured
|
||||
# 1.17:1 handle-vs-track — the handle was not findable. Dark handle on the
|
||||
# light track gives 3.38:1 (5.02:1 on hover); hover darkens again.
|
||||
SCROLLBAR_TRACK = "#E4E4E4"
|
||||
SCROLLBAR_HANDLE = "#D4D4D4"
|
||||
SCROLLBAR_HANDLE_HOVER = "#C4C4C4"
|
||||
SCROLLBAR_HANDLE = "#7a7a7a"
|
||||
SCROLLBAR_HANDLE_HOVER = "#5f5f5f"
|
||||
|
||||
# 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.
|
||||
@@ -598,7 +673,15 @@ FLAT_CARD_RADIUS = "0px" # recovery + console-log cards stay square
|
||||
def _palette() -> dict[str, str]:
|
||||
# Why: one substitution mapping instead of ~40 kwargs per stylesheet, and
|
||||
# a renamed/missing constant fails loudly (KeyError) instead of silently.
|
||||
return {k.lower(): v for k, v in globals().items() if k.isupper() and isinstance(v, str)}
|
||||
mapping = {k.lower(): v for k, v in globals().items() if k.isupper() and isinstance(v, str)}
|
||||
if _font_scale != 1.0:
|
||||
# ponytail: only the app-sheet FONT_* ladder scales; widgets that
|
||||
# import FONT_* into local f-string QSS keep 1.0 — port them to the
|
||||
# app sheet if zoom must reach them.
|
||||
for k, v in mapping.items():
|
||||
if k.startswith("font_") and v.endswith("px"):
|
||||
mapping[k] = f"{round(int(v[:-2]) * _font_scale)}px"
|
||||
return mapping
|
||||
|
||||
|
||||
def qcolor(color: str, alpha: int | None = None):
|
||||
@@ -1026,7 +1109,10 @@ def _sunrise_stylesheet(overrides: dict[str, str] | None = None) -> str:
|
||||
border-top-right-radius: 4px;
|
||||
padding: 4px 14px;
|
||||
margin-top: 3px;
|
||||
color: $muted_text;
|
||||
/* tab_idle_text, not muted_text: camera tabs sit on the sky
|
||||
gradient where muted grey measured 2.07:1 (WCAG text floor is
|
||||
4.5:1); the darker slate is 4.71:1 there, 11.2:1 on panels. */
|
||||
color: $tab_idle_text;
|
||||
}
|
||||
|
||||
QTabBar::tab:selected {
|
||||
@@ -1082,24 +1168,42 @@ def _sunrise_stylesheet(overrides: dict[str, str] | None = None) -> str:
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Idle resize lines: faint fill (10% of the hover alpha) so the drag
|
||||
target is findable without hunting. This base fill replaces the
|
||||
native dotted grip — traded away on purpose for discoverability. */
|
||||
QMainWindow::separator, QSplitter::handle {
|
||||
background: $separator_idle;
|
||||
/* Resize gutters: the mouse keeps the full $separator_region, but only
|
||||
a 2px line + 1px shadow paints (gradients in _separator_gradient).
|
||||
An upright line needs the x-gradient: that is a :vertical main-window
|
||||
separator but a :horizontal splitter handle (handle orientation
|
||||
follows the splitter, not the bar). */
|
||||
QMainWindow::separator {
|
||||
width: $separator_region;
|
||||
height: $separator_region;
|
||||
background: transparent;
|
||||
}
|
||||
QMainWindow::separator:vertical, QSplitter::handle:horizontal {
|
||||
background: $sep_idle_x;
|
||||
}
|
||||
QMainWindow::separator:horizontal, QSplitter::handle:vertical {
|
||||
background: $sep_idle_y;
|
||||
}
|
||||
QSplitter::handle:horizontal { width: $separator_region; }
|
||||
QSplitter::handle:vertical { height: $separator_region; }
|
||||
|
||||
/* 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;
|
||||
QMainWindow[separatorHint="true"]::separator:vertical:hover {
|
||||
background: $sep_hint_x;
|
||||
}
|
||||
QMainWindow[separatorHint="true"]::separator:horizontal:hover {
|
||||
background: $sep_hint_y;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
QSplitter::handle:horizontal:hover, QSplitter::handle:horizontal:pressed {
|
||||
background: $sep_hint_x;
|
||||
}
|
||||
QSplitter::handle:vertical:hover, QSplitter::handle:vertical:pressed {
|
||||
background: $sep_hint_y;
|
||||
}
|
||||
|
||||
QFrame#beamlineControls,
|
||||
@@ -1743,16 +1847,32 @@ def _sunset_stylesheet() -> str:
|
||||
}
|
||||
|
||||
/* Idle + hover resize lines, dark flavor — see the light-theme note. */
|
||||
QMainWindow::separator, QSplitter::handle {
|
||||
background: $dark_separator_idle;
|
||||
QMainWindow::separator {
|
||||
width: $separator_region;
|
||||
height: $separator_region;
|
||||
background: transparent;
|
||||
}
|
||||
QMainWindow::separator:vertical, QSplitter::handle:horizontal {
|
||||
background: $dark_sep_idle_x;
|
||||
}
|
||||
QMainWindow::separator:horizontal, QSplitter::handle:vertical {
|
||||
background: $dark_sep_idle_y;
|
||||
}
|
||||
QSplitter::handle:horizontal { width: $separator_region; }
|
||||
QSplitter::handle:vertical { height: $separator_region; }
|
||||
|
||||
QMainWindow[separatorHint="true"]::separator:vertical:hover {
|
||||
background: $dark_sep_hint_x;
|
||||
}
|
||||
QMainWindow[separatorHint="true"]::separator:horizontal:hover {
|
||||
background: $dark_sep_hint_y;
|
||||
}
|
||||
|
||||
QMainWindow[separatorHint="true"]::separator:hover {
|
||||
background: $dark_accent;
|
||||
QSplitter::handle:horizontal:hover, QSplitter::handle:horizontal:pressed {
|
||||
background: $dark_sep_hint_x;
|
||||
}
|
||||
|
||||
QSplitter::handle:hover, QSplitter::handle:pressed {
|
||||
background: $dark_accent;
|
||||
QSplitter::handle:vertical:hover, QSplitter::handle:vertical:pressed {
|
||||
background: $dark_sep_hint_y;
|
||||
}
|
||||
|
||||
/* Plain scroll containers stay frameless. */
|
||||
@@ -1925,5 +2045,15 @@ if __name__ == "__main__":
|
||||
for _theme in (THEME_SUNRISE, THEME_SUNSET, THEME_BLUEBIRD):
|
||||
assert "$" not in build_app_stylesheet(_theme)
|
||||
assert APP_BACKGROUND not in build_app_stylesheet(THEME_BLUEBIRD)
|
||||
# Font zoom: the FONT_* ladder must follow the scale, and the scale must
|
||||
# clamp to its documented bounds.
|
||||
set_font_scale(1.3)
|
||||
assert "font-size: 21px" in build_app_stylesheet(THEME_SUNRISE) # title 16->21
|
||||
set_font_scale(99.0)
|
||||
assert font_scale() == FONT_SCALE_MAX
|
||||
set_font_scale(0.0)
|
||||
assert font_scale() == FONT_SCALE_MIN
|
||||
set_font_scale(1.0)
|
||||
assert "font-size: 16px" in build_app_stylesheet(THEME_SUNRISE)
|
||||
# This line was added by Claude. But I would do the same. So all gude.
|
||||
print("gude")
|
||||
|
||||
@@ -2,8 +2,9 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from PySide6.QtCore import QSettings, Qt
|
||||
from PySide6.QtWidgets import QDockWidget
|
||||
from PySide6.QtWidgets import QApplication, QDockWidget
|
||||
|
||||
from aare.gui import styles
|
||||
from aare.gui.main_window import MainWindow
|
||||
from aare.gui.styles import THEME_BLUEBIRD, THEME_SUNRISE, THEME_SUNSET
|
||||
|
||||
@@ -502,6 +503,50 @@ def test_theme_settings_migrate_and_slots_switch(qtbot, mock_ui_state):
|
||||
assert win._theme_mode == THEME_SUNRISE
|
||||
|
||||
|
||||
def test_font_zoom_steps_clamps_and_resets(qtbot, mock_ui_state):
|
||||
with (
|
||||
patch("requests.get"),
|
||||
patch("aare.gui.main_window.DAQWorker"),
|
||||
patch("aare.gui.main_window.PredictionSubscriber"),
|
||||
patch("aare.gui.main_window.VideoThread"),
|
||||
patch("aare.gui.main_window.JFJochDBusClient"),
|
||||
patch("aare.gui.main_window.jwt.decode") as mock_jwt,
|
||||
):
|
||||
mock_jwt.return_value = {
|
||||
"sub": "testuser",
|
||||
"staff": True,
|
||||
"pgroups": ["p123"],
|
||||
"session": 15,
|
||||
}
|
||||
win = _make_window(qtbot)
|
||||
|
||||
settings = QSettings("PSI", "AareGUI")
|
||||
saved = settings.value("appearance/font_scale")
|
||||
try:
|
||||
base_pt = win._default_app_font.pointSizeF()
|
||||
win._change_font_zoom(1)
|
||||
assert styles.font_scale() == pytest.approx(1.1)
|
||||
# Part 2 of the zoom: the app default font scales with the ladder.
|
||||
app = QApplication.instance()
|
||||
assert isinstance(app, QApplication) # narrow from QCoreApplication|None
|
||||
assert app.font().pointSizeF() == pytest.approx(base_pt * 1.1)
|
||||
for _ in range(20):
|
||||
win._change_font_zoom(1)
|
||||
assert styles.font_scale() == styles.FONT_SCALE_MAX
|
||||
win._change_font_zoom(0)
|
||||
assert styles.font_scale() == 1.0
|
||||
assert app.font().pointSizeF() == pytest.approx(base_pt)
|
||||
for _ in range(20):
|
||||
win._change_font_zoom(-1)
|
||||
assert styles.font_scale() == styles.FONT_SCALE_MIN
|
||||
finally:
|
||||
styles.set_font_scale(1.0)
|
||||
if saved is None:
|
||||
settings.remove("appearance/font_scale")
|
||||
else:
|
||||
settings.setValue("appearance/font_scale", saved)
|
||||
|
||||
|
||||
def test_restore_window_state_heals_all_hidden_docks(qtbot, mock_ui_state):
|
||||
with (
|
||||
patch("requests.get"),
|
||||
|
||||
Reference in New Issue
Block a user