Skip to content

fix(gui): reopen the Aetherial strip after minimize — Principle XI. - #5366

Open
crypticpy wants to merge 2 commits into
aethersdr:mainfrom
crypticpy:pr/5365-aetherial-strip-minimize-lockout
Open

fix(gui): reopen the Aetherial strip after minimize — Principle XI.#5366
crypticpy wants to merge 2 commits into
aethersdr:mainfrom
crypticpy:pr/5365-aetherial-strip-minimize-lockout

Conversation

@crypticpy

Copy link
Copy Markdown

Summary

Fixes #5365.

Minimizing the Aetherial Audio Channel Strip made it unrecoverable from its own
button: no number of presses on the AetherVoice button (or the chain applet's
nub) would bring the window back, leaving the Dock/taskbar as the only way to
reach it.

Two Qt behaviours combine to cause it, and toggleAetherialStrip() was written
as if neither existed:

  1. QWidget::isVisible() stays true while a window is minimized, so
    if (m_aetherialStrip->isVisible()) sent a minimized strip down the hide()
    branch.
  2. QWidget::show() on a minimized window restores its saved state — still
    minimized — so the follow-up press did not recover it either.

The decision is extracted into gui/WindowShowState.{h,cpp}:

  • windowIsShowing(w) — visible and not minimized.
  • showAndRaiseWindow(w)showNormal() when minimized, show() otherwise,
    then raise + activate.

showNormal() stays guarded on isMinimized() because calling it
unconditionally would clear a Maximized or FullScreen window — the same reason
the net-reminder and tray raise paths in MainWindow_Nets.cpp guard it (#3918).
That helper is where those two sites should eventually converge; they are
deliberately not touched here, to keep this change scoped to the reported
bug.

The docked CWX and DVK panels use the same bare isVisible() shape but are
child widgets that cannot be minimized independently, so they are unaffected and
are left alone.

Constitution principle honored

Principle XI — Fixes Are Demonstrated. New regression test
window_show_state_test drives a real QWidget through
hidden → shown → minimized → restored and runs the toggle exactly as
MainWindow does. Mutation-checked three ways, each failing a different
assertion:

Principle VIII — Evidence Over Assertion. Both Qt behaviours above are
pinned against a real QWidget on the offscreen platform, not asserted from
memory.

Test plan

  • Local build passes (cmake --build build) — clean, exit 0
  • Behavior verified on a real radio if applicable — N/A: pure window
    management in MainWindow, independent of the connected backend
  • Existing tests pass (CI) — window_show_state_test passes; full local
    suite run on this build tree shows only the two failures that reproduce
    unchanged on clean main (bridge_docs_check, hl2_state_restore_test),
    neither of which touches GUI window state
  • Reproduction steps documented if user-reported bug — in Aetherial Audio Channel Strip cannot be reopened after minimizing — its own button no longer restores it #5365

Checklist

  • Commits are signed (docs/COMMIT-SIGNING.md) — GPG, GitHub reports
    verified: true
  • No new flat-key AppSettings calls — this change adds no settings
  • Code is clean-room — AetherSDR's own widget code against public Qt API
    (Principle IV)
  • All meter UI uses MeterSmootherN/A, no meter UI is touched
  • Documentation updated if user-visible behavior changed — no doc change
    needed; no document describes the old toggle behaviour. CHANGELOG.md
    deliberately untouched
  • Security-sensitive changes reference a GHSA if applicable — N/A

Note on the claim protocol (AGENTS.md §Issue / PR Claim Protocol): assignee
changes are rejected for an account without write access to this repo, so the
Fixes #5365 link is the visible claim on the issue timeline instead.

Minimizing the Aetherial Audio Channel Strip made it unrecoverable from
its own button: no number of presses on the AetherVoice button (or the
chain applet's nub) would bring the window back, leaving the taskbar/Dock
as the only way to reach it.

Two Qt behaviours combine to cause it, and toggleAetherialStrip() was
written as if neither existed:

  1. QWidget::isVisible() stays TRUE while a window is minimized, so
     `if (m_aetherialStrip->isVisible())` sent a minimized strip down the
     hide() branch.
  2. QWidget::show() on a minimized window restores its SAVED state —
     still minimized — so the follow-up press did not recover it either.

Both are now pinned against a real QWidget rather than asserted from
memory, on the offscreen platform where they reproduce.

Extract the decision into gui/WindowShowState.{h,cpp}:

  * windowIsShowing(w)     — visible AND not minimized.
  * showAndRaiseWindow(w)  — showNormal() when minimized, show() otherwise,
                             then raise + activate.

showNormal() stays guarded on isMinimized() because calling it
unconditionally would clear a Maximized or FullScreen window — the same
reason the net-reminder and tray raise paths in MainWindow_Nets.cpp guard
it (aethersdr#3918). The helper is where those two sites should eventually
converge; not touched here to keep this change to the reported bug.

The docked CWX and DVK panels use the same bare isVisible() shape but are
child widgets that cannot be minimized independently, so they are
unaffected and are left alone.

New regression test window_show_state_test drives a real QWidget through
hidden → shown → minimized → restored and runs the toggle exactly as
MainWindow does. Mutation-checked three ways, each failing a different
assertion:

  * windowIsShowing() without the !isMinimized() term      → 3 failures
  * showAndRaiseWindow() with an unconditional show()      → 2 failures
  * showAndRaiseWindow() with an unconditional showNormal()→ 1 failure
    (the aethersdr#3918 maximized guard)

Found in local field use on macOS 26.5.2 (25F84) / Qt 6.11.1; filed as aethersdr#5365.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HjpgedubkqsxhbYrsTzMR
@crypticpy
crypticpy requested review from a team as code owners September 1, 2026 04:04

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Issue fit

#5365: the Aetherial strip button treated a minimized strip window as "already showing", so pressing it did nothing — the operator had to un-minimize by hand. The fix makes windowIsShowing() require !isMinimized() and adds showAndRaiseWindow() that un-minimizes before raising. Correct and directly on-target, extracted into a testable free function with a mutation-checked regression test (window_show_state_test) that pins both Qt behaviors and the #3918 maximize guard. Principle XI honored.

Scope

Clean — the helper, its one call site, the test. CHANGELOG.md correctly untouched. Preflight: no sockets, pure widget-state test.

Blockers

None.

Nits (non-blocking)

  1. Minimize-from-maximized reopens un-maximized (inline). showNormal() clears both the Minimized and Maximized bits, so maximize the strip → minimize → reopen brings it back at normal size, not maximized — a corner of the exact reopen path this PR fixes, and one the test misses (case 7 only checks maximize is preserved when the window was never minimized). w->setWindowState(w->windowState() & ~Qt::WindowMinimized) clears only the minimized bit and preserves maximize.
  2. The isMinimized()/showNormal()/raise pattern now lives in three places (MainWindow_Nets.cpp:226-247 has two copies for #3918). The body acknowledges convergence "eventually"; this helper takes a QWidget* and could absorb those net-reminder/tray paths now, so a future fix (like nit 1) lands once.

What was verified vs read

  • Verified by me: showNormal() clears the maximized bit (nit 1's mechanism, uncovered by the test); the helper resolves unqualified in MainWindow.cpp's namespace.
  • From the automated pass, verified: first-press-creates-then-shows still works; the mutation test genuinely pins the two Qt behaviors.
  • Not run: no bridge session (the widget-state helper is unit-covered), tests read not executed — no CI yet, which is a merge gate.

Comment thread src/gui/WindowShowState.cpp Outdated
return;
// showNormal() only for a minimized window — see the header note on #3918.
if (w->isMinimized())
w->showNormal();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit (non-blocking) — showNormal() drops a prior maximize. It clears both the Minimized and Maximized bits, so minimize a maximized strip and reopen → it returns un-maximized. Clear only the minimized bit to preserve the pre-minimize size:

Suggested change
w->showNormal();
if (w->isMinimized())
w->setWindowState(w->windowState() & ~Qt::WindowMinimized);
else
w->show();

Add a test row (maximize → minimize → reopen → still maximized); case 7 doesn't cover the minimized-from-maximized path.

… — Principle XI.

Review of aethersdr#5366 pointed out that showNormal() clears the Maximized and
FullScreen bits along with Minimized, so a strip that was maximized, then
minimized, then reopened from its button came back at normal size. That
is a corner of the exact reopen path the PR fixes. showAndRaiseWindow()
now clears only Qt::WindowMinimized via setWindowState(), which is a
pending state on a hidden widget and immediate on a visible one, so the
isMinimized() branch goes away too.

The same isMinimized()/showNormal()/raise sequence lived twice more in
MainWindow_Nets.cpp (the net-reminder tune and the tray-message click,
both aethersdr#3918). They carried the same latent un-maximize and now call the
helper, so the fix lands once. Braces restored on the strip toggle to
match the surrounding style.

Test: window_show_state_test gains case 8 — maximize, minimize, toggle —
and asserts the window is showing AND still maximized. Mutation-checked:
the old showNormal() body fails only that final assertion.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhDXan9Qe86EbypHmxzHUy
@crypticpy

crypticpy commented Sep 2, 2026

Copy link
Copy Markdown
Author

@ten9876 Both nits taken, pushed as 5a48797.

Nit 1 — minimize-from-maximized. showAndRaiseWindow() now clears only Qt::WindowMinimized through setWindowState(), exactly as you suggested. That also removes the isMinimized() branch: on a hidden widget the cleared state is pending and show() applies it, on a visible one it takes effect at once and show() is a no-op. Case 8 in window_show_state_test pins it (maximize → minimize → toggle → still maximized). Mutation-checked against the old showNormal() body: only the final "restored window is still maximized" assertion fails, so the row is measuring what it claims.

Nit 2 — the two copies in MainWindow_Nets.cpp. Both (net-reminder tune, tray-message click) now call showAndRaiseWindow(this). They had the identical latent un-maximize, so the nit-1 fix reaches them in the same commit. Disclosing here since it widens the diff by one file beyond #5365's strip: no behaviour change other than preserving maximize on those two raise paths.

One style-only change of my own: the strip toggle in MainWindow.cpp gets its braces back to match the surrounding code.

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.

Aetherial Audio Channel Strip cannot be reopened after minimizing — its own button no longer restores it

2 participants