[fix] Frameless floating windows: resize, state persistence, geometry#2008
Conversation
## What changed
### New: FramelessResizer (src/gui/FramelessResizer.h/.cpp)
Added a reusable QObject event-filter that provides all-8-edge resize for
any Qt::FramelessWindowHint top-level window using QWindow::startSystemResize()
— the same compositor-managed path already used for drag-to-move via
startSystemMove().
Previously, frameless pop-out windows (panadapters and applets) only had a
QSizeGrip in the bottom-right corner. On Linux/Wayland this grip often does
nothing. The new resizer installs itself recursively on the window and all
child widgets (picking up new children via ChildAdded events), translates
cursor positions to window-local coordinates, detects which of the 8
edges/corners the mouse is near (within 6 px), updates the cursor shape, and
calls startSystemResize() on press. When the window is not frameless the
resizer is a no-op so it does not interfere with native decorations.
The old QSizeGrip rows in PanFloatingWindow and FloatingContainerWindow were
removed since the resizer supersedes them.
---
### Fix 1: Frameless mode toggle not propagating to floating windows
**Symptom:** Switching View -> Frameless Window updated the main window chrome
but left any already-open floating panadapter or applet windows unchanged.
**Root cause:** setFramelessWindow() in MainWindow only modified its own
Qt::FramelessWindowHint flag. PanFloatingWindow and FloatingContainerWindow
had the flag hardcoded at construction and were never updated.
**Changes:**
- PanFloatingWindow constructor now reads AppSettings["FramelessWindow"]
instead of hardcoding Qt::FramelessWindowHint.
- PanFloatingWindow::setFramelessMode(bool) added — same setWindowFlags +
setGeometry + show() pattern as MainWindow::setFramelessWindow.
- FloatingContainerWindow constructor same fix.
- FloatingContainerWindow::setFramelessMode(bool) added.
- PanadapterStack::setFramelessMode(bool) added — iterates all live floating
windows and calls setFramelessMode on each.
- ContainerManager::setFramelessMode(bool) added — same for applet windows.
- MainWindow::setFramelessWindow() now calls m_panStack->setFramelessMode()
and containerManager()->setFramelessMode() after updating itself.
---
### Fix 2: Floating panadapter state not saved or restored across restarts
**Symptom:** Popped-out panadapters came back docked after every restart.
**Root cause:** PanadapterStack saved window geometry (position/size) but
never recorded which pans were floating. There was no restore path.
**Changes:**
- PanadapterStack::saveFloatingState() — writes the comma-separated list of
currently-floating pan IDs to AppSettings["FloatingPanIds"]. Called
automatically from floatPanadapter(), dockPanadapter(), and prepareShutdown().
- PanadapterStack::restoreFloatingState() — reads "FloatingPanIds" and calls
floatPanadapter() for each ID that exists and is not already floating. Safe
on reconnect (unknown IDs are skipped, already-floating pans are no-ops).
- MainWindow m_layoutRestoreTimer lambda: previously did an early return for
single-pan sessions, blocking any restore. Restructured to always call
restoreFloatingState() regardless of pan count after layout settles (~1 s
after radio connect).
---
### Fix 3: Floating applet state not restored across restarts
**Symptom:** Popped-out applet containers (EQ, S-Meter, etc.) came back
docked after every restart.
**Root cause:** ContainerManager::restoreState() was fully implemented and
ContainerManager::saveState() was already called on every float/dock
transition, but restoreState() was never called anywhere. The ContainerTree
JSON key was written at runtime but never read back on launch.
**Changes:**
- ContainerManager::prepareShutdown() now calls saveState() first, guaranteeing
the ContainerTree JSON reflects the final floating/visibility state before
windows close — previously a crash or force-quit could leave it stale.
- MainWindow: after `m_appletPanel = new AppletPanel(splitter)`, a
QTimer::singleShot(0) schedules containerManager()->restoreState(). The
one-event-loop delay lets AppletPanel's own legacy-migration singleShot(0)
timers fire first (they write ContainerTree for pre-container-system saves),
then restoreState() reads the up-to-date JSON and re-floats containers.
---
### Fix 4: Floating window geometry wrong after frameless mode change
**Symptom:** A window saved while frameless would appear at an offset position
after switching to decorated mode (or vice versa), by roughly the height of
the OS title bar.
**Root cause:** QWidget::saveGeometry() serialises frame-extent data (the
platform's window decoration size) alongside the window position.
restoreGeometry() then applies those frame extents when placing the window. If
the decoration mode has changed between save and restore, the wrong frame size
is applied, shifting the window by the title bar height (~30 px on most
Linux/Windows themes).
**Changes (PanFloatingWindow + FloatingContainerWindow):**
- saveWindowGeometry() / saveGeometryToKey() now store plain "x,y,w,h" from
geometry() (the client-area rect, always frame-agnostic) instead of the
opaque QWidget::saveGeometry() blob.
- restoreWindowGeometry() / restoreAndEnsureVisible() parse the "x,y,w,h"
format and call setGeometry() directly — no frame arithmetic, works
identically frameless or decorated.
- Legacy fallback: if the stored value is not four integers (i.e. it is a
base64 blob from a build before this fix), the code falls through to the old
restoreGeometry() path so existing saves are not lost on upgrade.
- Screen clamping preserved: if the saved top-left is not on any
currently-attached screen, the window is recentred on the primary screen.
---
## Files changed
CMakeLists.txt add FramelessResizer to all targets
src/gui/FramelessResizer.h new: all-edge resize event-filter
src/gui/FramelessResizer.cpp new
src/gui/PanFloatingWindow.h add setFramelessMode()
src/gui/PanFloatingWindow.cpp fix constructor, setFramelessMode,
frame-agnostic geometry save/restore
src/gui/PanadapterStack.h add setFramelessMode, save/restoreFloatingState
src/gui/PanadapterStack.cpp implement above, hook into lifecycle
src/gui/containers/FloatingContainerWindow.h add setFramelessMode()
src/gui/containers/FloatingContainerWindow.cpp fix constructor, setFramelessMode,
frame-agnostic geometry save/restore
src/gui/containers/ContainerManager.h add setFramelessMode()
src/gui/containers/ContainerManager.cpp implement setFramelessMode,
saveState in prepareShutdown
src/gui/MainWindow.cpp propagate frameless toggle,
call restoreState on startup,
restore floating pans in layout timer
|
@AetherClaude what are your thoughts on this PR? |
|
This is a really solid PR — four distinct bugs fixed with clean, well-scoped changes and a nice new reusable primitive in FramelessResizer design — Using Frame-agnostic geometry ("x,y,w,h") — This is the correct fix for the title-bar offset bug. restoreFloatingState() restructuring — The old ContainerManager::prepareShutdown() calling saveState() — Good catch. Previously a force-quit could leave the ContainerTree JSON stale, so containers would come back in the wrong state on next launch. A couple of things I'd flag:
Overall this is well-reasoned work. The fixes are targeted at real root causes rather than papering over symptoms, and the FramelessResizer is a clean abstraction that earns its keep by replacing platform-inconsistent QSizeGrip behavior with compositor-managed resize across the board. |
There was a problem hiding this comment.
Nice contribution, @chibondking. The PR description is excellent — thorough root-cause analysis for each fix, and the changes are well-scoped. Some notes after reading the full diff:
Looks good
- FramelessResizer is clean and well-designed. Using
startSystemResize()(the compositor-managed path) is the right call, especially for Wayland. Recursive event filter installation withChildAddedpickup covers late-added widgets correctly, and the no-op whenFramelessWindowHintis absent is a nice safety rail. - Frame-agnostic geometry (
"x,y,w,h"viageometry()instead ofsaveGeometry()blobs) is a solid fix for the title-bar-height offset problem. The legacy base64 fallback means existing user settings survive the upgrade — thoughtful. setFramelessModepattern in bothPanFloatingWindowandFloatingContainerWindowcorrectly snapshots geometry beforesetWindowFlags(which hides the window internally) and restores it after. Identical pattern to howMainWindow::setFramelessWindowalready works.saveFloatingState/restoreFloatingState— the guardm_pans.contains(id) && !m_floatingWindows.contains(id)in restore is safe for reconnect and stale-ID scenarios.prepareShutdowncallingsaveState()in ContainerManager closes the race where force-quit could leave stale JSON.- Layout timer restructure — wrapping the multi-pan logic in
if (panCount > 1)and unconditionally callingrestoreFloatingState()after is the right fix for single-pan-float restore.
Minor items to consider
-
FramelessResizer::attachToWidgetsetssetMouseTracking(true)on every child widget. This is needed for the hover cursor updates, but it's a broad side-effect. If a child widget explicitly disables mouse tracking for performance reasons (unlikely in this codebase, but possible), this would override that. Since the resizer is a no-op whenFramelessWindowHintis off, the mouse tracking overhead is always present even in decorated mode. Not a blocker — just worth being aware of. -
FloatingContainerWindow::restoreAndEnsureVisiblescreen clamping diverges slightly fromPanFloatingWindow::restoreWindowGeometry. InPanFloatingWindow, you clamp to primary screen center when off-screen. InFloatingContainerWindow, you clamp relative to the anchor widget's screen. Both are reasonable but the inconsistency is worth noting — if a user has all floating windows on a now-disconnected monitor, pan windows jump to primary while container windows jump to the anchor's screen. Probably fine in practice. -
ContainerManager::setFramelessModechecksif (win)butPanadapterStack::setFramelessModedoes not. TheQPointer-style null check in ContainerManager is defensive against a window being destroyed mid-iteration (e.g., from a close event). PanadapterStack uses raw pointers inm_floatingWindows, so if a window were somehow destroyed before being removed from the map, this would crash. Low risk since shutdown ordering looks correct, but a null guard would be consistent.
Overall this is solid work — fixes real UX bugs with the right platform APIs. Thanks for the detailed write-up on each fix. 73!
|
Claude here on Jeremy's behalf — heads up that we're reverting this PR (#2042) because of a regression in the Aetherial Audio container after the merge. Multiple applets in the popped-out tx_dsp window stopped responding correctly to mouse events — knob drags, chain-tile drag-to-reorder, and sub-container interactions all broke. Most likely cause: the new The other three fixes in the PR are genuinely valuable on their own and don't share this risk:
If you'd like to split those four into separate PRs (any subset is welcome) we'd land them quickly. The Sorry for the round-trip — your diagnosis of all four root causes was solid; this is purely a deployment regression. 73, Jeremy KK7GWY & Claude (AI dev partner) |
…windows (#2068) Originally by @chibondking — popped-out panadapter and applet windows use Qt::FramelessWindowHint which removes OS-provided resize borders. Previously a single QSizeGrip in the bottom-right corner; unreliable on Wayland and only one corner. FramelessResizer is a small QObject event filter installed on the QWindow (native handle) — sees mouse events at the platform level before they're dispatched to the widget hierarchy. Edge proximity within 6 px → cursor change + startSystemResize on left-click. Crucially, never touches the widget event stream, so nested controls (knobs, drag tiles, chain widget) respond normally even when near the window edge. Resolves the regression that caused the v1 implementation (#2008) to be reverted. Two cursor-leak guards added on top of the cherry-pick: - Destructor calls leaveEdgeZone() so windows destroyed while at an edge don't leak the override cursor onto the desktop. - The existing leaveEdgeZone() inside the eventFilter Phase 2 early return (when the window flips to decorated mode at runtime via #2059's setFramelessMode) ensures the override is restored when the resizer becomes a no-op. Closes #2058. Co-authored-by: CJ Johnson <cj@cedrick.io> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
What changed
New: FramelessResizer (src/gui/FramelessResizer.h/.cpp)
Added a reusable QObject event-filter that provides all-8-edge resize for any Qt::FramelessWindowHint top-level window using QWindow::startSystemResize() — the same compositor-managed path already used for drag-to-move via startSystemMove().
Previously, frameless pop-out windows (panadapters and applets) only had a QSizeGrip in the bottom-right corner. On Linux/Wayland this grip often does nothing. The new resizer installs itself recursively on the window and all child widgets (picking up new children via ChildAdded events), translates cursor positions to window-local coordinates, detects which of the 8 edges/corners the mouse is near (within 6 px), updates the cursor shape, and calls startSystemResize() on press. When the window is not frameless the resizer is a no-op so it does not interfere with native decorations.
The old QSizeGrip rows in PanFloatingWindow and FloatingContainerWindow were removed since the resizer supersedes them.
Fix 1: Frameless mode toggle not propagating to floating windows
Symptom: Switching View -> Frameless Window updated the main window chrome but left any already-open floating panadapter or applet windows unchanged.
Root cause: setFramelessWindow() in MainWindow only modified its own Qt::FramelessWindowHint flag. PanFloatingWindow and FloatingContainerWindow had the flag hardcoded at construction and were never updated.
Changes:
Fix 2: Floating panadapter state not saved or restored across restarts
Symptom: Popped-out panadapters came back docked after every restart.
Root cause: PanadapterStack saved window geometry (position/size) but never recorded which pans were floating. There was no restore path.
Changes:
Fix 3: Floating applet state not restored across restarts
Symptom: Popped-out applet containers (EQ, S-Meter, etc.) came back docked after every restart.
Root cause: ContainerManager::restoreState() was fully implemented and ContainerManager::saveState() was already called on every float/dock transition, but restoreState() was never called anywhere. The ContainerTree JSON key was written at runtime but never read back on launch.
Changes:
m_appletPanel = new AppletPanel(splitter), a QTimer::singleShot(0) schedules containerManager()->restoreState(). The one-event-loop delay lets AppletPanel's own legacy-migration singleShot(0) timers fire first (they write ContainerTree for pre-container-system saves), then restoreState() reads the up-to-date JSON and re-floats containers.Fix 4: Floating window geometry wrong after frameless mode change
Symptom: A window saved while frameless would appear at an offset position after switching to decorated mode (or vice versa), by roughly the height of the OS title bar.
Root cause: QWidget::saveGeometry() serialises frame-extent data (the platform's window decoration size) alongside the window position. restoreGeometry() then applies those frame extents when placing the window. If the decoration mode has changed between save and restore, the wrong frame size is applied, shifting the window by the title bar height (~30 px on most Linux/Windows themes).
Changes (PanFloatingWindow + FloatingContainerWindow):
Files changed
CMakeLists.txt add FramelessResizer to all targets
src/gui/FramelessResizer.h new: all-edge resize event-filter
src/gui/FramelessResizer.cpp new
src/gui/PanFloatingWindow.h add setFramelessMode()
src/gui/PanFloatingWindow.cpp fix constructor, setFramelessMode,
frame-agnostic geometry save/restore
src/gui/PanadapterStack.h add setFramelessMode, save/restoreFloatingState
src/gui/PanadapterStack.cpp implement above, hook into lifecycle
src/gui/containers/FloatingContainerWindow.h add setFramelessMode()
src/gui/containers/FloatingContainerWindow.cpp fix constructor, setFramelessMode,
frame-agnostic geometry save/restore
src/gui/containers/ContainerManager.h add setFramelessMode()
src/gui/containers/ContainerManager.cpp implement setFramelessMode,
saveState in prepareShutdown
src/gui/MainWindow.cpp propagate frameless toggle,
call restoreState on startup,
restore floating pans in layout timer