fix(gui): fill the floating window for width-capped applets (#3451)#3827
Conversation
…r#3451) Width-capped applets clamp their own maximumWidth so they sit tidily in the narrow docked strip: Antenna Genius and ShackSwitch cap at 260 px, the docked RX-DSP applet at 250 px. That cap makes sense docked, but when the container is popped out into its own FloatingContainerWindow (default 300 px wide, and freely resizable) the 260-px content stays pinned to the left edge with a growing dead zone on the right — the left-justified look reported in aethersdr#3451. Fix: ContainerWidget now tracks dock state for its body content. While floating it lifts each child's width cap so the content fills the window; on re-dock it restores the original cap. Uncapped applets (e.g. the Amplifier / Power Genius, which has no cap) are unaffected — they already filled the width. Also gives each ContainerWidget an objectName (its stable id: "AG", "AMP", "SS", …) and accessible names to the title-bar float/pin/close buttons, so the agent automation bridge can scope a control to a specific container (e.g. invoke "AG/containerFloatToggle"). This is what made the fix provable headlessly and closes a real bridge reachability gap (aethersdr#3646). Adds container_widget_test coverage: cap is lifted on float, restored on dock, and uncapped content is left untouched in both states. 💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat
There was a problem hiding this comment.
Thanks for the well-documented fix, @jensenpat — the root-cause writeup and before/after screenshots make this easy to reason about, and the approach (lift the width cap while floating, restore it on re-dock rather than centering) is the right call.
I traced the dock transition end-to-end and the logic holds up:
FloatingContainerWindow::takeContainerreparents the container and then callssetDockMode(Floating)(FloatingContainerWindow.cpp:62-65), so the body children are already inm_bodyLayoutwhen the policy loop runs — the cap actually gets lifted.releaseContainermirrors it. BothsetContent/insertChildWidgetalso re-apply per the current mode, so a swap-while-floating is covered.- Filling (rather than centering) is consistent with the layout:
m_bodyLayout->addWidget(content, 1)is aQVBoxLayoutwith no horizontal alignment, so liftingmaximumWidthlets the content stretch to the window edge. - The save-once guard (
if (!m_savedMaxWidths.contains(child))) correctly preserves the original docked cap across repeated floats, andrestoreWidthPolicyusestake()so the entry is cleared on restore. - The test exercises both the capped and uncapped paths and the full float→dock→float cycle. CI is green across all six checks (build, macOS, Windows, CodeQL, a11y).
Two minor, non-blocking notes:
-
Stale entry in
m_savedMaxWidthsif a body child is destroyed without going throughremoveChildWidget/setContent. In the normal lifecycle this is fine (the hash dies with the container, orrestoreWidthPolicyclears the entry). The only theoretical hazard is a child deleted while the container lives on, followed by a new widget reusing that address —contains()would then short-circuit the save andrestorewould write back a stale cap. The pointer is never dereferenced so there's no UAF, but if you want to be defensive you could clear the entry on aQObject::destroyedconnection. Genuinely low severity; mentioning for completeness, not asking you to change it. -
Scope: the
objectName/accessible-name additions (ContainerWidget.cpp, ContainerTitleBar.cpp, #3646) are bridge-reachability changes bundled alongside the #3451 fix. You called this out explicitly in the PR body and they're tiny and low-risk, so no objection — just flagging that they're a separate concern from the title.
Looks good to me. Nice work.
🤖 aethersdr-agent · cost: $4.4711 · model: claude-opus-4-8
ten9876
left a comment
There was a problem hiding this comment.
Clean, correct, tested layout fix. Verified the cap save/restore: the double-save guard (if !m_savedMaxWidths.contains(child)) correctly remembers the original docked cap rather than the lifted QWIDGETSIZE_MAX; restoreWidthPolicy uses take() so re-float re-saves fresh; content-swap paths (setContent/insert/removeChildWidget) apply/restore so children leave with their original cap; uncapped content is a genuine no-op (matching the Amplifier byte-identical claim); setDockMode re-applies to all body children. Good call filling the window rather than centering. Test covers docked-keeps → float-lifts → re-dock-restores → uncapped-untouched. No TX/protocol surface.
Non-blocking nits: m_savedMaxWidths keyed by raw QWidget* could dangle if a child is destroyed while floating without removeChildWidget (harmless — key-only, never deref); and the ContainerTitleBar.cpp:83 comment is truncated mid-sentence. Thanks @jensenpat.
What & why
Fixes #3451. When a width-capped applet is popped out of the docked strip into its own floating window, its content stayed pinned to the left edge with a growing dead zone on the right — the "left-justified" look the reporter showed for Antenna Genius.
Root cause. Several applets clamp their own
maximumWidthso they sit tidily in the narrow right-hand strip:AntennaGeniusApplet.cpp:55—setMaximumWidth(260)ShackSwitchApplet.cpp:82—setMaximumWidth(260)ClientRxDspApplet.cpp:17—setMaximumWidth(250)That cap is right when docked, but
FloatingContainerWindowopens at 300 px (and is freely resizable) with a plainQVBoxLayoutand no alignment, so the 260-px content hugs the left edge. The wider the user drags the window, the bigger the dead zone.Fix.
ContainerWidgetnow tracks dock state for its body content: while floating it lifts each child's width cap so the content fills the window; on re-dock it restores the original cap (remembered per child). Rather than centering the capped content — which only makes the wasted space symmetric — the content now actually uses the real estate, which is what was asked for here.The Amplifier / Power Genius applet has no width cap, so it already filled the window; the change is a verified no-op for it (see below). The same fix also covers ShackSwitch and the RX-DSP applet.
Screenshots — affected applets (floated, widened to 440 px)
Antenna Genius — the reported applet (fixed)
Before, the controls stop at 260 px; after, the device combo, Connect, Manual IP, the AUTO bars and the Port A/B rows all stretch to the window edge. (Shown disconnected, so the per-antenna button grid is empty — when connected that 4-column grid fills the extra width too.)
Power Genius / Amplifier — checked, already effective
You flagged this one too. I checked it: it has no width cap, so the PWR/SWR/Id gauges already span the full width. Before and after pixels are byte-identical — no change needed. So among the two named applets, only Antenna Genius had the dead-space bug.
How the agent automation bridge proved it
The fix was verified headlessly with the agent automation bridge on an isolated, non-connecting instance (no radio contention):
get radioconnected: false(offline, no radio touched)invoke AG/containerFloatToggle clickok, newValue: ↙resize AG 440 560actual 440×560grab AGThis relied on two small bridge-reachability additions in this PR: every
ContainerWidgetnow carries anobjectNameequal to its stable id (AG,AMP,SS, …), and the title-bar float/pin/close buttons carry accessible names — so a driver can scope a control to one container (AG/containerFloatToggle). Closes a real gap (#3646): before this, no floating container's title-bar controls were addressable.Tests
tests/container_widget_test.cppgains coverage: docked content keeps its cap → floating lifts it toQWIDGETSIZE_MAX→ re-docking restores it; uncapped content is untouched in both states. All container tests pass.💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat