Skip to content

fix(flexcontrol): clamp AetherControl window to the screen height (#3662)#3795

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
jensenpat:fix/3662-aethercontrol-clip
Jun 26, 2026
Merged

fix(flexcontrol): clamp AetherControl window to the screen height (#3662)#3795
ten9876 merged 1 commit into
aethersdr:mainfrom
jensenpat:fix/3662-aethercontrol-clip

Conversation

@jensenpat

Copy link
Copy Markdown
Collaborator

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:

const QSize required = minimumSizeHint().expandedTo(QSize(430, 0));
setMinimumSize(required);   // required.height() ≈ 1114, never bounded to the screen

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

  • When the full (non-compact) 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 (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 Settings menu, 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 constructs FlexControlDialog on 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):

Build enforced min height screen avail compact auto-engaged result
Unfixed 1114 px 871 px no 243 px taller than the screen → clipped
Fixed 871 px 871 px yes fits, fully on-screen ✅
[ OK ] primary screen reports an available height           availH=871
[ OK ] compact toggle is present
[ OK ] enforced minimum height fits the screen              minH=871 availH=871
[ OK ] compact auto-engaged on a short screen               availH=871 < floor 1000
All tests passed.

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

  • Opening a menu-launched dialog — the AetherControl window is reachable only via Settings → AetherControl…. The bridge's invoke only 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: an openMenu <name> verb, or let invoke <action> resolve and trigger() a named QAction anywhere 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

…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>
@jensenpat jensenpat marked this pull request as ready for review June 25, 2026 20:17
@jensenpat jensenpat requested review from a team as code owners June 25, 2026 20:17

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 (not QSettings), QSignalBlocker RAII guard, and screen() is null-checked via the availableScreenHeight() helper returning 0 when undeterminable.
  • Recursion is bounded — the auto-engage path sets m_compactButton->setChecked(true) then re-enters updateCompactMode(), which now takes the if (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:

  1. Auto-engage vs. the persisted setting. The auto-engage uses QSignalBlocker, so it correctly does not persist FlexControlCompactMode — good, it stays re-evaluated each launch. But the consequence is: on a short screen, if a user manually unchecks Compact, the toggled handler (FlexControlDialog.cpp:1045) writes "False" + saves, then calls updateCompactMode(), 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.

  2. Test is environment-dependent. flex_control_dialog_size_test reads the real primary screen. On a CI runner with a tall virtual display (or the offscreen platform reporting a large default), availH >= kFullLayoutFloor, so the two meaningful assertions (compact auto-engaged) are skipped and only the minH <= availH invariant runs — which a tall non-compact window also satisfies. So the regression-specific path isn't guaranteed to be exercised in CI. The headline minH <= availH invariant 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

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_compactButton null-check before setChecked, screen() null-check in availableScreenHeight(), and QSignalBlocker to 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 (not QSettings) 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 neighboring help_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):

  1. There's no QScrollArea wrapping the body, so the body can't actually scroll — the clamp squeezes/clips the widgets rather than offering a scrollbar.
  2. 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 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.

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.

@ten9876 ten9876 merged commit d7e7a77 into aethersdr:main Jun 26, 2026
6 checks passed
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.

AetherControl (cut window)

2 participants