From 266f8337a9a1fcc163d2abf8a70a2b1038c454d2 Mon Sep 17 00:00:00 2001 From: Dawn Date: Fri, 7 Aug 2026 11:54:45 +0200 Subject: [PATCH] fix: window and dock sizing behavior Three main-window chrome fixes: the un-maximized geometry is pre-set to half-screen centered so leaving maximized mode no longer restores an oversized size hint (the beamline state bar also stops imposing its ~2000px minimum width); closing a popped-out dock re-docks it instead of silently hiding the panel, and pop-outs open enlarged and clamped to the screen; state-bar buttons only update font/stylesheet/cursor when values change, since repolishing every DAQ tick dropped the hover cursor under a resting mouse. Co-Authored-By: Claude Fable 5 --- src/aare/gui/gui.py | 9 +++++ src/aare/gui/main_window.py | 38 +++++++++++++++++++ src/aare/gui/panels/beamline_state_panel.py | 41 +++++++++++++++++---- 3 files changed, 80 insertions(+), 8 deletions(-) diff --git a/src/aare/gui/gui.py b/src/aare/gui/gui.py index 93d9646c..ca2876c7 100644 --- a/src/aare/gui/gui.py +++ b/src/aare/gui/gui.py @@ -189,6 +189,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. + 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() diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py index 0dbddc3a..2ca59df5 100644 --- a/src/aare/gui/main_window.py +++ b/src/aare/gui/main_window.py @@ -884,6 +884,13 @@ class MainWindow(QMainWindow): register_tutorials(self, self.tutorial_manager) + # Every dock: closing its popped-out window brings it back into the + # layout instead of hiding it (eventFilter), and popping out opens + # an enlarged window (2x width, 3x height, clamped to the screen). + for dock in self.findChildren(QDockWidget): + dock.installEventFilter(self) + dock.topLevelChanged.connect(self._on_dock_top_level_changed) + def _setup_global_shortcuts(self) -> None: self._shortcut_manual_sample = QAction("Expand Manual Sample", self) self._shortcut_manual_sample.setShortcut(QKeySequence("Ctrl+M")) @@ -2410,7 +2417,38 @@ class MainWindow(QMainWindow): def _mark_user_interaction(self) -> None: self._refresh_idle_activity(report_backend=True) + @Slot(bool) + def _on_dock_top_level_changed(self, floating: bool) -> None: + dock = self.sender() + if floating and isinstance(dock, QDockWidget): + # Pop-outs open enlarged instead of keeping the cramped docked + # size. Deferred: the window is mid-reparent while the signal + # fires. + QTimer.singleShot(0, lambda d=dock: self._enlarge_floating_dock(d)) + + def _enlarge_floating_dock(self, dock: QDockWidget) -> None: + if not dock.isFloating(): + return + screen = dock.screen().availableGeometry() + dock.resize( + min(dock.width() * 2, int(screen.width() * 0.9)), + min(dock.height() * 3, int(screen.height() * 0.9)), + ) + # Keep the enlarged window fully on screen. + geo = dock.frameGeometry() + dx = min(0, screen.right() - geo.right()) + dy = min(0, screen.bottom() - geo.bottom()) + if dx or dy: + dock.move(geo.x() + dx, geo.y() + dy) + def eventFilter(self, obj, event): + # Closing a floated (popped-out) dock re-docks it instead of hiding — + # otherwise the panel silently disappears and has to be restored via + # the View menu. + if event.type() == QEvent.Type.Close and isinstance(obj, QDockWidget) and obj.isFloating(): + obj.setFloating(False) + event.ignore() + return True try: if event.type() in { QEvent.Type.MouseButtonPress, diff --git a/src/aare/gui/panels/beamline_state_panel.py b/src/aare/gui/panels/beamline_state_panel.py index c8da2279..db372118 100644 --- a/src/aare/gui/panels/beamline_state_panel.py +++ b/src/aare/gui/panels/beamline_state_panel.py @@ -5,7 +5,13 @@ from PySide6.QtCore import Qt, QTimer, Signal, Slot from PySide6.QtGui import QCursor, QFont, QFontMetrics from PySide6.QtWidgets import QFrame, QHBoxLayout, QLabel, QMenu, QPushButton, QSizePolicy, QToolTip -from aare.gui.styles import STATE_AVAILABLE, STATE_MSG_ERROR, STATE_MSG_INFO, STATE_UNAVAILABLE +from aare.gui.styles import ( + FONT_VALUE, + STATE_AVAILABLE, + STATE_MSG_ERROR, + STATE_MSG_INFO, + STATE_UNAVAILABLE, +) # Shortcut transitions from the "Available transitions" menu in # widgets/status_bar.py show_state_menu — these come ON TOP of the one-hop @@ -154,7 +160,7 @@ class BeamlineStatePanel(QFrame): if index: separator = QLabel("–", self) separator.setStyleSheet( - f"color: {STATE_UNAVAILABLE}; background: transparent; border: none; font-size: 18px;" + f"color: {STATE_UNAVAILABLE}; background: transparent; border: none; font-size: {FONT_VALUE};" ) layout.addWidget(separator) button = HoverableButton(label, self) @@ -178,6 +184,15 @@ class BeamlineStatePanel(QFrame): layout.addStretch(1) self._apply_highlight() + def minimumSizeHint(self): + # The 13-button strip would otherwise impose a ~2000px minimum on the + # whole main window, so an un-maximized window could never fit the + # screen. Width 0: the strip adapts (two-line labels) and, below that, + # clips — the window stays freely resizable. + hint = super().minimumSizeHint() + hint.setWidth(0) + return hint + def resizeEvent(self, event) -> None: super().resizeEvent(event) self._update_label_mode() @@ -325,21 +340,31 @@ class BeamlineStatePanel(QFrame): font = QFont(self.font()) font.setPixelSize(18) font.setWeight(QFont.Weight.Bold if bold else QFont.Weight.Normal) - button.setFont(font) - button.setStyleSheet( + if button.font() != font: + button.setFont(font) + # 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. + qss = ( f"QPushButton {{ border: none; background: transparent; color: {color};" f" padding: 1px 8px; }}" f" QPushButton:hover {{ color: {color}; }}" ) + 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: - button.setCursor(Qt.CursorShape.PointingHandCursor) - button.setToolTip(self._TOOLTIPS.get(state, state.display_name())) + cursor = Qt.CursorShape.PointingHandCursor + tooltip = self._TOOLTIPS.get(state, state.display_name()) else: - button.setCursor(Qt.CursorShape.ForbiddenCursor) - button.setToolTip("") + cursor = Qt.CursorShape.ForbiddenCursor + tooltip = "" + if button.cursor().shape() != cursor: + button.setCursor(cursor) + if button.toolTip() != tooltip: + button.setToolTip(tooltip) def set_current_state(self, state: BeamlineStateEnum | None) -> None: self._current_state = state