diff --git a/src/aare/gui/graphics/slider_grip_dark.png b/src/aare/gui/graphics/slider_grip_dark.png new file mode 100644 index 00000000..ef6d413f Binary files /dev/null and b/src/aare/gui/graphics/slider_grip_dark.png differ diff --git a/src/aare/gui/graphics/slider_grip_light.png b/src/aare/gui/graphics/slider_grip_light.png new file mode 100644 index 00000000..81008622 Binary files /dev/null and b/src/aare/gui/graphics/slider_grip_light.png differ diff --git a/src/aare/gui/gui.py b/src/aare/gui/gui.py index 9ccbed73..02303885 100644 --- a/src/aare/gui/gui.py +++ b/src/aare/gui/gui.py @@ -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: diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py index cc254268..c48a5627 100644 --- a/src/aare/gui/main_window.py +++ b/src/aare/gui/main_window.py @@ -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() diff --git a/src/aare/gui/panels/abr_tweak_panel.py b/src/aare/gui/panels/abr_tweak_panel.py index b9f80132..db949e33 100644 --- a/src/aare/gui/panels/abr_tweak_panel.py +++ b/src/aare/gui/panels/abr_tweak_panel.py @@ -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}) diff --git a/src/aare/gui/panels/beam_mark_panel.py b/src/aare/gui/panels/beam_mark_panel.py index 5122ab78..3a892b3b 100644 --- a/src/aare/gui/panels/beam_mark_panel.py +++ b/src/aare/gui/panels/beam_mark_panel.py @@ -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) diff --git a/src/aare/gui/panels/beamline_state_panel.py b/src/aare/gui/panels/beamline_state_panel.py index 77eebb97..50536ad8 100644 --- a/src/aare/gui/panels/beamline_state_panel.py +++ b/src/aare/gui/panels/beamline_state_panel.py @@ -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: diff --git a/src/aare/gui/panels/samcam_panel.py b/src/aare/gui/panels/samcam_panel.py index 7632da88..f4498d85 100644 --- a/src/aare/gui/panels/samcam_panel.py +++ b/src/aare/gui/panels/samcam_panel.py @@ -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") diff --git a/src/aare/gui/styles.py b/src/aare/gui/styles.py index c6f3021c..9394a5c7 100644 --- a/src/aare/gui/styles.py +++ b/src/aare/gui/styles.py @@ -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 diff --git a/src/aare/gui/widgets/camera_image.py b/src/aare/gui/widgets/camera_image.py index 835aac34..27ee8a54 100644 --- a/src/aare/gui/widgets/camera_image.py +++ b/src/aare/gui/widgets/camera_image.py @@ -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. diff --git a/src/aare/gui/widgets/popout_window.py b/src/aare/gui/widgets/popout_window.py index e615bbfc..980c228e 100644 --- a/src/aare/gui/widgets/popout_window.py +++ b/src/aare/gui/widgets/popout_window.py @@ -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