Skip to content

Fix Minimal Mode reverting on macOS when entered while maximized (#2365)#2367

Merged
ten9876 merged 1 commit into
mainfrom
fix/minimal-mode-macos-revert-2365
May 5, 2026
Merged

Fix Minimal Mode reverting on macOS when entered while maximized (#2365)#2367
ten9876 merged 1 commit into
mainfrom
fix/minimal-mode-macos-revert-2365

Conversation

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Fixes #2365.

Summary

On macOS, activating Minimal Mode from a maximized/fullscreen window made the window briefly collapse to applet width and immediately snap back to its full-size geometry. The View → Minimal Mode menu item, Ctrl+M, and the title-bar minimal-mode button all exhibited the flash-then-revert behavior.

Root cause

MainWindow::toggleMinimalMode(true) calls setFixedWidth(260) on the top-level window. On Windows/Linux this synchronously clears Qt::WindowMaximized/Qt::WindowFullScreen before the function returns. On macOS, AppKit delivers the WindowStateChange notification asynchronously, so the Maximized bit survives into the deferred event.

When MainWindow::changeEvent then fires with m_minimalMode == true and the Maximized bit still set, it schedules a toggleMinimalMode(false) because there is no entering-side re-entry guard symmetric to m_exitingMinimalMode. That handler restores FullModeGeometry (which encoded the original maximized rect), so the window snaps back.

This regressed in #2299 (commit 0270b78), which collapsed the multiple minimal-exit paths into the single changeEvent branch and exposed the missing entering-side guard.

Fix

Two small, localized changes (src/gui/MainWindow.cpp, src/gui/MainWindow.h):

  1. Drop the Maximized/FullScreen bit at the top of the enter path. In toggleMinimalMode(true), after saveGeometry() is captured into FullModeGeometry, call showNormal() if the window is currently maximized or fullscreen. FullModeGeometry preserves the maximized rect, so exit-minimal still restores the user's pre-minimal window.
  2. Add a symmetric m_enteringMinimalMode guard alongside m_exitingMinimalMode. Set at the start of toggleMinimalMode(true), cleared via QTimer::singleShot(0) so any AppKit-deferred WindowStateChange queued during the enter path drains through changeEvent's early-return before the guard drops. changeEvent honors the new flag at its early-return.

Defense-in-depth — either change alone would resolve the symptom; the combination guards against any further AppKit-deferred notifications between setFixedWidth(260) and the function returning.

Risk

Low — additive change (one extra showNormal() + one boolean flag). On Windows/Linux the windowState() & (Maximized|FullScreen) check is false at entry (synchronous state clear), so the new code path is a no-op.

Test plan

  • macOS: Maximize the window → Ctrl+M → window stays in narrow minimal layout (does not revert).
  • macOS: Same via View → Minimal Mode menu item.
  • macOS: Same via title-bar minimal-mode button.
  • macOS: From minimal, exit via Ctrl+M / double-click title bar / maximize button → returns to the user's pre-minimal window.
  • macOS: Enter Minimal Mode from a non-maximized window (regression check) → behaves as before.
  • Windows / Linux: Enter/exit Minimal Mode from maximized and normal states (regression check).

Generated by AetherClaude (automated agent for AetherSDR)

On macOS, AppKit delivers Qt::WindowStateChange asynchronously, so the
Maximized/FullScreen bit could survive setFixedWidth(260) inside
toggleMinimalMode(true). The deferred WindowStateChange then arrived
with m_minimalMode==true and the Maximized bit still set, causing
changeEvent to schedule a spurious toggleMinimalMode(false) — the
window collapsed for a frame and snapped back to the full-size geometry
restored from FullModeGeometry.

- toggleMinimalMode(true): drop maximized/fullscreen state via
  showNormal() before forcing the applet width. FullModeGeometry is
  saved first, so exit can still restore the user's pre-minimal window.
- Add m_enteringMinimalMode guard symmetric to m_exitingMinimalMode and
  honor it in changeEvent. Cleared via singleShot(0) so any AppKit-
  deferred WindowStateChange queued during the enter path drains through
  the early-return before the guard drops.

Both behaviors are no-ops on Windows/Linux where setFixedWidth on a
maximized top-level synchronously clears the state bit.

Regressed in #2299, which collapsed minimal-exit paths into the single
changeEvent branch and exposed the missing entering-side guard.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@aethersdr-agent aethersdr-agent Bot requested a review from ten9876 as a code owner May 5, 2026 02:05
@aethersdr-agent aethersdr-agent Bot enabled auto-merge (squash) May 5, 2026 02:05

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good — thanks for the careful diagnosis and writeup.

The fix is well-targeted and the asymmetry analysis matches what I see in the code:

  • MainWindow.cpp:4201 — adding m_enteringMinimalMode to changeEvent's early-return is the natural symmetric counterpart to the existing m_exitingMinimalMode guard at MainWindow.cpp:4213. The ordering is right too: !m_minimalMode short-circuits ahead of the new flag, so the exit path doesn't have to clear it.
  • MainWindow.cpp:9863showNormal() runs after saveGeometry() is captured into FullModeGeometry, so the exit path still restores the user's pre-minimal (maximized) window. Confirmed by the existing exit-side handling at MainWindow.cpp:9903-9908 which independently checks the maximized bit on exit.
  • The deferred QTimer::singleShot(0, ...) to clear the guard mirrors the existing m_exitingMinimalMode clear pattern and gives any AppKit-deferred WindowStateChange a chance to drain through changeEvent's early-return.

On Windows/Linux the new windowState() & (Qt::WindowMaximized | Qt::WindowFullScreen) check is effectively dead code on entry (synchronous state clearing), so the regression check on those platforms should be straightforward.

Minor non-blocking observation: there's a theoretical race if the user toggles minimal mode rapidly enough that a second enter happens before the first deferred clear fires — the first timer would clear the flag while the second enter is still in flight. This is unlikely in practice (it requires sub-event-loop human input timing) and the showNormal() defense-in-depth covers most of the surface anyway, so I don't think it needs addressing here.

Nice fix.

@ten9876 ten9876 merged commit a253231 into main May 5, 2026
5 checks passed
@ten9876 ten9876 deleted the fix/minimal-mode-macos-revert-2365 branch May 5, 2026 05:52
@ten9876

ten9876 commented May 5, 2026

Copy link
Copy Markdown
Collaborator

Claude here on Jeremy's behalf — merged via admin squash. Stale-code audit clean, root-cause precise (correctly identifies #2299's regression source), defense-in-depth fix is the right shape. Jeremy will smoke-test on macOS and follow up here if anything surfaces.

73, Jeremy KK7GWY & Claude (AI dev partner)

aethersdr-agent Bot added a commit that referenced this pull request May 26, 2026
…. Principle XIV.

Two changes in src/gui/MainWindow.cpp:

1. closeEvent now refreshes MinimalModeGeometry when quitting from
   Minimal Mode (guarded against maximized/fullscreen abnormal state
   to match toggleMinimalMode(false)). Previously the per-mode key was
   only written by toggleMinimalMode(false), so users who launched in
   Minimal Mode and quit without ever toggling out never updated it.

2. The constructor's auto-enter-minimal-mode and AetherialStrip
   restore blocks have moved from L1641-1648 to immediately after the
   geometry restore at L4847. The earlier call site caused
   toggleMinimalMode(true) to snapshot the default startup rect into
   FullModeGeometry (corrupting the previous session's saved rect) and
   to restoreGeometry(MinimalModeGeometry) against a window not yet
   placed on the correct screen.

User report: drift only visible with FramelessWindowHint on Windows
because native chrome lets the WM cache pos() and partially compensate
for the missing per-mode save; Frameless removes that safety net so
the bug surfaces. Fix addresses the root cause (no MinimalModeGeometry
save on close + wrong-order restore on open), making position stable
in both chrome modes.

Risk note: the reorder preserves the m_enteringMinimalMode guard
inside toggleMinimalMode (L13528/L13576) that hardens the macOS
AppKit-deferred WindowStateChange path from #2367.

Persisting MinimalModeGeometry inline alongside MainWindowGeometry
keeps the per-mode key fresh atomically with the rest of the close-
event state (Principle XIV: persist atomically — the AppSettings
save() at end of closeEvent commits the full updated state in one
shot, never partially).

Blast radius: risk_score=0.358, 12 high-risk affected (top:
AppSettings::instance, ThemeManager::instance, ThemeManager::applyStyleSheet).
The downstream surface is the existing AppSettings/RadioModel calls
closeEvent already makes; this commit adds one more setValue and a
reordering, no new structural edges.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Minimal mode not working

2 participants