fix(flexcontrol): clamp AetherControl window to the screen height (#3662)#3795
Conversation
…thersdr#3662) The AetherControl (FlexControlDialog) non-compact layout has a ~1114 px intrinsic minimum height. updateCompactMode() pinned that as a hard minimum via setMinimumSize(required) with no screen-height clamp, so on a screen whose available (taskbar/menu-bar-excluded) height is smaller than that the window opened taller than the display with its bottom clipped off-screen and no way to shrink it back. This bites a 1920x1080 panel after the Windows taskbar (~1040 px usable) and any DPI-scaled laptop, matching the reporter's 1080p flex6400 screenshot. Fix: when the full layout would exceed the available screen height, auto-engage the existing compact layout (which fits) instead of pinning an oversized minimum. Add a screen-height backstop to both the non-compact and compact paths so the enforced minimum never exceeds the display in either mode. Proven with a new standalone regression test (flex_control_dialog_size_test) that constructs the dialog on the real screen and asserts the enforced minimum height never exceeds the available screen height. On this 871 px-tall display: unfixed -> minH 1114 > 871 (clipped); fixed -> minH 871, compact auto-engaged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Nice fix — the root-cause analysis is spot on (a hard-pinned minimumSize from minimumSizeHint() with no screen clamp), and the remedy is minimal and local. The mechanics check out:
- Conventions ✓ —
AppSettings(notQSettings),QSignalBlockerRAII guard, andscreen()is null-checked via theavailableScreenHeight()helper returning 0 when undeterminable. - Recursion is bounded — the auto-engage path sets
m_compactButton->setChecked(true)then re-entersupdateCompactMode(), which now takes theif (compact)branch, so it can only recurse one level. Good. - Backstop coverage is coherent — the
required.setHeight(availH)fallback only reaches the no-compact-button case, since when a compact button exists and is unchecked the auto-engage branch handles it first.
All six checks (build, macOS, Windows, CodeQL) pass.
A couple of small, non-blocking notes:
-
Auto-engage vs. the persisted setting. The auto-engage uses
QSignalBlocker, so it correctly does not persistFlexControlCompactMode— good, it stays re-evaluated each launch. But the consequence is: on a short screen, if a user manually unchecks Compact, thetoggledhandler (FlexControlDialog.cpp:1045) writes"False"+ saves, then callsupdateCompactMode(), which immediately re-engages compact under the blocker. The button visibly bounces back to checked while settings now read"False". It's harmless (re-derived on next launch), but the bounce is slightly confusing UX — worth a comment noting the toggle is intentionally pinned on a screen too short for the full layout. -
Test is environment-dependent.
flex_control_dialog_size_testreads the real primary screen. On a CI runner with a tall virtual display (or theoffscreenplatform reporting a large default),availH >= kFullLayoutFloor, so the two meaningful assertions (compact auto-engaged) are skipped and only theminH <= availHinvariant runs — which a tall non-compact window also satisfies. So the regression-specific path isn't guaranteed to be exercised in CI. The headlineminH <= availHinvariant is still a real guard, so this is fine as-is, just flagging that the "did compact actually auto-engage" coverage is opportunistic rather than deterministic.
Neither is a blocker. Thanks for the thorough writeup, the proof table, and flagging the automation-bridge gap (openMenu/menu-launched-dialog) separately — that's genuinely useful context. 👍
🤖 aethersdr-agent · cost: $2.3932 · model: claude-opus-4-8
There was a problem hiding this comment.
Thanks for the thorough writeup and the standalone regression test, @jensenpat — the root-cause analysis (hard-pinned ~1114 px minimum with no screen clamp) is clear and the fix is appropriately minimal and local.
What looks good:
- Reuses the existing, already-fitting compact layout rather than inventing a new path — clean.
- Proper guards throughout:
m_compactButtonnull-check beforesetChecked,screen()null-check inavailableScreenHeight(), andQSignalBlockerto avoid re-entrant toggle signals. - The
updateCompactMode()recursion is bounded — the re-entry sets the button checked first, so it takes the compact branch and returns; no infinite loop. - Correctly does not persist the auto-engaged state to
FlexControlCompactMode— it's an adaptive runtime decision, so the user's stored full-mode preference is preserved if they later move to a taller display. Nice. - Uses
AppSettings(notQSettings) in both the fix and the test, per convention. - All 6 CI checks pass; the new GUI test follows the same build-only (no
add_test()) pattern as the neighboringhelp_dialog_test, which is consistent for display-dependent tests.
One minor note — the compact-path backstop comment in applyCompactWindowSize():
// ... the body may need to scroll, but the window stays fully on-screen and
// resizable rather than clipping off the bottom edge.Two small inaccuracies for the extreme case (a screen shorter than the compact layout's own ~887 px minimum):
- There's no
QScrollAreawrapping the body, so the body can't actually scroll — the clamp squeezes/clips the widgets rather than offering a scrollbar. - The very next line calls
setFixedHeight(required.height()), which pins the height, so the window isn't resizable taller either.
The fix is still strictly better than the old behavior (the window stays on-screen instead of overflowing), and this path is a rare edge (sub-~887 px usable height), so it's not a blocker — just worth tightening the comment so it doesn't overstate "scroll" / "resizable" for that corner. No change needed to the logic itself.
Solid fix overall.
🤖 aethersdr-agent · cost: $1.3754 · model: claude-opus-4-8
ten9876
left a comment
There was a problem hiding this comment.
Verified the fix on every path:
- Recursion terminates: updateCompactMode() branches on m_compactButton->isChecked() at the top, so the non-compact branch setting setChecked(true) then recursing lands in the compact branch. The QSignalBlocker correctly suppresses the redundant signal-driven re-entry.
- Backstop on both paths (non-compact and applyCompactWindowSize) clamps required.height to availH, so even when compact is unavailable or the screen is shorter than the compact layout, the window stays on-screen and resizable rather than clipping.
- availableScreenHeight() is null-safe (screen() returns primary even pre-show; 0 skips the clamp), uses availableGeometry() (taskbar-excluded), and is cross-platform (QScreen) — good given the cross-platform principle even though the bug was Windows-reported.
- The auto-engaged compact is screen-driven, not persisted — correct, so a later move to a tall screen restores the full layout.
The regression test (flex_control_dialog_size_test) asserts the enforced minimum never exceeds the available screen height + compact auto-engages on a short screen — solid guard, builds green.
And the menu-launched-dialog bridge gap recorded here was just closed by #3819 (closed-menu invoke + menu open), so this dialog is now bridge-openable for future automated checks. Thanks @jensenpat.
Issue
#3662 — the AetherControl window opens taller than the screen and its bottom (control strip / status readout) is clipped off, with no way to shrink it back. Reported on a Flex 6400, Windows 10, 1920×1080 @ 100% UI scale.
Root cause
The AetherControl dialog (
FlexControlDialog) non-compact layout is a tall stack — header (70) + knob panel (420) + control strip (~110) + status frame (148) + aux grid (~200) + frameless title bar (18) + margins — with a ~1114 px intrinsic minimum height (measured).At the end of
updateCompactMode()the non-compact branch pinned that intrinsic minimum as a hard floor with no screen clamp:On any display whose available (taskbar/menu-bar-excluded) height is below ~1114 px the window is forced taller than the screen and clips off the bottom — and because the minimum is hard-pinned, it can't be resized smaller to recover. A 1920×1080 panel after the Windows taskbar leaves ~1040 px usable; DPI scaling (125 %/150 %) shrinks that further. Compact mode produces a window that fits but was never auto-engaged, so a first-time user just saw a cut-off window.
Fix
availableScreenHeight()helper) so the enforced minimum never exceeds the display in either mode — on a screen even shorter than the compact layout, the window stays fully on-screen and resizable rather than clipping.Minimal and local to
FlexControlDialog.cpp(+ one private helper). No protocol/FlexLib involvement.Proof
The agent automation bridge could not open the dialog itself — AetherControl is launched only from a closed
Settingsmenu, and the bridge has no verb to open a top-level menu or trigger a non-visible menu action (gap recorded below). Instead the fix is proven by a new standalone runtime regression test,flex_control_dialog_size_test, which constructsFlexControlDialogon the real screen and asserts the enforced minimum height never exceeds the available screen height.Run on this 871 px-tall display (shorter than the reporter's, so it reproduces the bug harder):
On the reporter's 1920×1080 (~1040 px usable), the 1114 px non-compact minimum overflows exactly as reported; the fix engages compact (887 px intrinsic), which fits comfortably.
Agent automation bridge — gap found
Settings → AetherControl…. The bridge'sinvokeonly matches visible menu actions, and there is no verb to open a top-level menu or trigger a menu action by name while its menu is closed. So no menu-only dialog (AetherControl, Network, MQTT, …) can be opened through the bridge alone. Proposed: anopenMenu <name>verb, or letinvoke <action>resolve andtrigger()a namedQActionanywhere in the menu bar even when its menu isn't shown. (This run fell back to a standalone GUI regression test for the proof.)Fixes #3662
💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat