style: visual and interaction polish
CI / test (3.11) (pull_request) Successful in 1m23s
CI / test (3.12) (pull_request) Successful in 1m22s
CI / test (3.13) (pull_request) Successful in 1m21s
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Successful in 1m26s
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Successful in 1m42s
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Successful in 1m40s
CI / test-with-coverage (pull_request) Successful in 2m2s
CI / coverage-analysis (pull_request) Successful in 4s
CI / lint (pull_request) Successful in 2m37s
CI / lint (push) Successful in 29s
Docs build and publish / docker (push) Successful in 12s
CI / test (3.11) (push) Canceled after 34s
CI / test (3.12) (push) Canceled after 30s
CI / test (3.13) (push) Canceled after 29s
CI / test-with-beamline-plugins (pxi_bec) (push) Canceled after 25s
CI / test-with-coverage (push) Canceled after 19s
CI / test-with-beamline-plugins (pxii_bec) (push) Canceled after 24s
CI / test-with-beamline-plugins (pxiii_bec) (push) Canceled after 20s
CI / coverage-analysis (push) Canceled after 0s
Build and Publish / release (push) Failing after 5s

Windowed startup at 50%x70% instead of maximized; overlay legend now
defaults off (it covers the camera image); square slider handles with
grip-line PNG assets (the round handle clipped flat); warmer sunrise
gradient bottom; abr/beam-mark layout tweaks; hover cues on the
beamline-state entries; DPI/font-aware pop-out titlebar icons; the
out-of-date Tutorial menu entries are hidden until content is redone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit was merged in pull request #129.
This commit is contained in:
2026-08-11 10:35:55 +02:00
co-authored by Claude Fable 5
parent b08817024d
commit 833a6d136f
11 changed files with 76 additions and 44 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 B

+5 -8
View File
@@ -191,18 +191,15 @@ def main():
splash.set_progress(100, "Ready")
splash.finish(win)
# Pre-set the "normal" (un-maximized) geometry as a fraction of the
# primary screen, centered — otherwise leaving maximized mode restores
# the size hint, which is wider than the monitor. Other panels may
# still enforce a somewhat larger minimum; the window then lands on
# that minimum instead.
# Start windowed (not maximized) at a fraction of the primary screen,
# centered — sized explicitly because the size hint is wider than the
# monitor. Other panels may still enforce a somewhat larger minimum;
# the window then lands on that minimum instead.
unmax_w, unmax_h = 0.5, 0.7
available = app.primaryScreen().availableGeometry()
win.resize(int(available.width() * unmax_w), int(available.height() * unmax_h))
win.move(available.center() - win.rect().center())
# Maximized so the window adapts to the monitor instead of its size hint,
# which is taller than a 1920x1200 console.
win.showMaximized()
win.show()
sys.exit(app.exec())
except Exception as e:
+9 -12
View File
@@ -1420,7 +1420,9 @@ class MainWindow(QMainWindow):
show_target_coordinates = bool(
settings.value("samcam/show_target_coordinates", True, type=bool)
)
show_overlay_legend = bool(settings.value("samcam/show_overlay_legend", True, type=bool))
# Legend defaults off — it covers the camera image; users opt in and
# the choice persists via QSettings.
show_overlay_legend = bool(settings.value("samcam/show_overlay_legend", False, type=bool))
compact_overlay_legend = bool(
settings.value("samcam/compact_overlay_legend", False, type=bool)
)
@@ -1693,7 +1695,7 @@ class MainWindow(QMainWindow):
try:
settings = self.portrait_sample_camera.target_overlay_settings()
self.portrait_sample_camera.set_show_overlay_legend(
settings.get("show_overlay_legend", True)
settings.get("show_overlay_legend", False)
)
except Exception:
logger.debug("Could not restore the camera overlay legend", exc_info=True)
@@ -2058,15 +2060,9 @@ class MainWindow(QMainWindow):
local_contact_action.triggered.connect(self.show_local_contact)
help_menu.addAction(local_contact_action)
help_menu.addSeparator()
start_text_tutorial_action = QAction("Start Tutorial (Text)", self)
start_text_tutorial_action.triggered.connect(self.start_text_tutorial)
help_menu.addAction(start_text_tutorial_action)
start_interactive_tutorial_action = QAction("Start Tutorial (Interactive)", self)
start_interactive_tutorial_action.triggered.connect(self.start_interactive_tutorial)
help_menu.addAction(start_interactive_tutorial_action)
# Tutorial entries hidden 2026-08-10: content is out of date. The
# tutorial machinery stays wired — restore the two QActions here
# (Start Tutorial (Text)/(Interactive)) once the content is refreshed.
def _capture_default_window_state(self) -> None:
self._default_window_state = self.saveState()
@@ -2874,7 +2870,8 @@ class MainWindow(QMainWindow):
# the merged Information dock defaults visible, so old values would
# wrongly hide it.
if settings.contains("information"):
self.information_dock.setVisible(settings.value("information", True, type=bool))
# bool() wrap: settings.value is typed object even with type=bool.
self.information_dock.setVisible(bool(settings.value("information", True, type=bool)))
settings.endGroup()
+7 -2
View File
@@ -20,10 +20,15 @@ class AbrTweakButtons(QWidget):
self._step_mm = step_mm
grid_layout = QGridLayout(self)
grid_layout.setColumnStretch(0, 1)
# Caption, arrows, and value pack tight to the left; the empty
# trailing column soaks up all dead width. The values keep AlignRight
# in their content-width column so the decimal points line up without
# drifting over next to the Step column.
grid_layout.setColumnStretch(0, 0)
grid_layout.setColumnStretch(1, 0)
grid_layout.setColumnStretch(2, 0)
grid_layout.setColumnStretch(3, 3)
grid_layout.setColumnStretch(3, 0)
grid_layout.setColumnStretch(4, 1)
grid_layout.addWidget(QLabel("GMX"), 0, 0)
button_gmx_minus = ButtonWithPayload("", payload={"x": -1, "y": 0, "z": 0})
+1 -1
View File
@@ -27,7 +27,7 @@ class BeamMarkWidget(QWidget):
# Shares the readings row instead of a full-width row below.
clear_button = QPushButton("Clear marks")
clear_button.setFixedWidth(90)
clear_button.setFixedWidth(100) # 100 is needed to see everything
grid_layout.addWidget(clear_button, 1, 5)
clear_button.pressed.connect(self.clear_button_pressed)
+13 -2
View File
@@ -356,17 +356,28 @@ class BeamlineStatePanel(QFrame):
# Guarded updates: this runs on every DAQ tick, and re-applying an
# unchanged stylesheet repolishes the button, which drops the hover
# cursor under a resting mouse until it moves again.
# Hover underline (the app-wide tab/chip affordance) only on
# entries that mean something: the current state and reachable
# targets — not the grey dead ends.
hover_underline = (
" text-decoration: underline;" if (is_current or is_pending or is_available) else ""
)
qss = (
f"QPushButton {{ border: none; background: transparent; color: {color};"
f" padding: 1px 8px; }}"
f" QPushButton:hover {{ color: {color}; }}"
f" QPushButton:hover {{ color: {color};{hover_underline} }}"
)
if button.styleSheet() != qss:
button.setStyleSheet(qss)
# Clickability follows availability; unavailable states get the
# forbidden cursor and only the deferred 3 s explanation tooltip.
if is_available:
# The current state is not a click target, but it is not
# forbidden either — plain cursor + a "you are here" tip.
if is_current or is_pending:
cursor = Qt.CursorShape.ArrowCursor
tooltip = f"{state.display_name()}: this is the current state"
elif is_available:
cursor = Qt.CursorShape.PointingHandCursor
tooltip = self._TOOLTIPS.get(state, state.display_name())
else:
+1 -1
View File
@@ -106,7 +106,7 @@ class SamcamPanel(QWidget):
)
self.show_overlay_legend_checkbox = QCheckBox("Show overlay legend")
self.show_overlay_legend_checkbox.setChecked(True)
self.show_overlay_legend_checkbox.setChecked(False)
self.show_overlay_legend_checkbox.toggled.connect(self.show_overlay_legend_changed.emit)
self.compact_overlay_legend_checkbox = QCheckBox("Compact legend")
+15 -10
View File
@@ -29,9 +29,9 @@ BACKGROUND = "#e2e7ee"
# continuous sky instead of every widget restarting the gradient.
# Tune the transition point here:
BACKGROUND_GRADIENT_TOP = "#84abd9" # RHEL9 window-frame blue (sampled from screenshot)
BACKGROUND_GRADIENT_MID = "#bbd6f6"
BACKGROUND_GRADIENT_MID_POS = "0.65" # 0..1 — where the mid stop sits
BACKGROUND_GRADIENT_BOTTOM = "#dbe9f9"
BACKGROUND_GRADIENT_MID = "#adccf1"
BACKGROUND_GRADIENT_MID_POS = "0.55" # 0..1 — where the mid stop sits
BACKGROUND_GRADIENT_BOTTOM = "#f8e9c5"
APP_BACKGROUND = (
"qlineargradient(x1:0, y1:0, x2:0, y2:1,"
f" stop:0 {BACKGROUND_GRADIENT_TOP},"
@@ -75,6 +75,10 @@ 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()
# Slider-handle grip lines (3 vertical ticks) — QSS can't draw interior
# lines, so they are tiny PNG assets like the spin arrows above.
SLIDER_GRIP = (_GRAPHICS_DIR / "slider_grip_light.png").as_posix()
DARK_SLIDER_GRIP = (_GRAPHICS_DIR / "slider_grip_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()
@@ -1164,9 +1168,10 @@ def _sunrise_stylesheet(overrides: dict[str, str] | None = None) -> str:
selection-color: $selection_text;
}
/* Sliders: rounded groove ends (like the scrollbar handles) + round
handle. Wheel adjustment needs the right mouse button held — see
WheelValueGuard. */
/* Sliders: rounded groove ends (like the scrollbar handles) + square
handle with grip lines (SLIDER_GRIP asset) — the old 16px round handle
was clipped flat by the slider's widget height. Wheel adjustment needs
the right mouse button held — see WheelValueGuard. */
QSlider::groove:horizontal {
height: 6px;
background: $slider_track;
@@ -1182,8 +1187,8 @@ def _sunrise_stylesheet(overrides: dict[str, str] | None = None) -> str:
background: $white;
border: 1px solid $slider_muted;
width: 14px;
margin: -5px 0;
border-radius: 7px;
margin: -3px 0;
image: url($slider_grip);
}
/* Soft scrollbars: square track band (runs flush to the widget edges —
@@ -1772,8 +1777,8 @@ def _sunset_stylesheet() -> str:
background: $dark_elevated;
border: 1px solid $dark_muted;
width: 14px;
margin: -5px 0;
border-radius: 7px;
margin: -3px 0;
image: url($dark_slider_grip);
}
/* Soft scrollbars: square track band + rounded handle, no end arrows
+1 -1
View File
@@ -141,7 +141,7 @@ class SampleCameraImageLabel(QGraphicsView):
self._show_target_point = True
self._show_target_coordinates = True
self._show_overlay_legend = True
self._show_overlay_legend = False
self._compact_overlay_legend = False
# "?" badge is a mouse-controls cheatsheet, decoupled from the legend —
# legend visibility is already handled by the panel checkboxes.
+24 -7
View File
@@ -4,7 +4,10 @@ from PySide6.QtWidgets import QDockWidget, QHBoxLayout, QLabel, QToolButton, QVB
from aare.gui.styles import FRAME_L1_COLOR, FRAME_L1_WIDTH, qcolor
# Title-bar buttons: icon fills the button, both the same size.
# Title-bar buttons: icon fills the button, both the same size. These are
# minimums — the real size follows the font metrics (see _titlebar_button),
# so the glyphs keep up with the DPI/font settings of the machine (fixed px
# rendered tiny on the RHEL9 consoles).
TITLEBAR_BUTTON_PX = 22
TITLEBAR_ICON_PX = 18
@@ -14,7 +17,11 @@ def _titlebar_icon(kind: str, color: QColor, size: int = TITLEBAR_ICON_PX) -> QI
pixmaps draw boxed icons, and text glyphs are missing from the
container's fonts. Color comes from the caller's palette so the glyphs
follow the theme (they are pixmaps, QSS color cannot reach them)."""
pixmap = QPixmap(size, size)
# Paint at the physical resolution so the glyph stays crisp on HiDPI.
screen = QGuiApplication.primaryScreen()
dpr = screen.devicePixelRatio() if screen is not None else 1.0
pixmap = QPixmap(round(size * dpr), round(size * dpr))
pixmap.setDevicePixelRatio(dpr)
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
@@ -37,11 +44,21 @@ def _titlebar_icon(kind: str, color: QColor, size: int = TITLEBAR_ICON_PX) -> QI
def _titlebar_button(parent: QWidget, tooltip: str) -> QToolButton:
# Icon is set by DockTitleBar._tint_icons (initially and on theme change).
button = QToolButton(parent)
button.setIconSize(QSize(TITLEBAR_ICON_PX, TITLEBAR_ICON_PX))
button.setFixedSize(TITLEBAR_BUTTON_PX, TITLEBAR_BUTTON_PX)
# Size from the font, not fixed px: a setup with larger fonts/DPI gets
# proportionally larger glyphs. The constants act as the floor.
icon_px = max(TITLEBAR_ICON_PX, parent.fontMetrics().height())
button_px = icon_px + (TITLEBAR_BUTTON_PX - TITLEBAR_ICON_PX)
button.setIconSize(QSize(icon_px, icon_px))
button.setFixedSize(button_px, button_px)
button.setAutoRaise(True)
# No button chrome — the glyph IS the button.
button.setStyleSheet("QToolButton { border: none; background: transparent; }")
# No button chrome — the glyph IS the button. The padding/height zeroing
# is the opt-out from the app sheets' global QToolButton cap (16px +
# padding): without it the light theme's horizontal padding shrinks the
# content box and Qt scales the glyph down to a speck.
button.setStyleSheet(
"QToolButton { border: none; background: transparent;"
f" padding: 0px; min-height: 0px; max-height: {button_px}px; }}"
)
button.setToolTip(tooltip)
button.setCursor(Qt.CursorShape.PointingHandCursor)
return button
@@ -82,7 +99,7 @@ class DockTitleBar(QWidget):
def _tint_icons(self) -> None:
color = self.palette().color(QPalette.ColorRole.WindowText)
for kind, button in self._icon_buttons.items():
button.setIcon(_titlebar_icon(kind, color))
button.setIcon(_titlebar_icon(kind, color, button.iconSize().width()))
def changeEvent(self, event):
# A theme switch lands here as a palette/style change; the glyphs are