[feat] FramelessResizer: all-edge resize for frameless floating windows#2058
[feat] FramelessResizer: all-edge resize for frameless floating windows#2058chibondking wants to merge 1 commit into
Conversation
Popped-out panadapter and applet windows use Qt::FramelessWindowHint, which removes OS-provided resize borders. The previous workaround was a QSizeGrip in the bottom-right corner, which is unreliable on Wayland and covers only one corner. New FramelessResizer (src/gui/FramelessResizer.h/.cpp) provides all-8-edge resize using QWindow::startSystemResize() — the same compositor-managed path already used for drag-to-move via startSystemMove() in TitleBar and ContainerWidget. The filter is installed on the QWindow (native handle) rather than on the QWidget tree. QWindow receives all platform mouse events before they are dispatched to the widget hierarchy, so resize intent is detected across the whole window without touching any child widget's event stream. This avoids breaking nested controls (knobs, drag tiles, sub-containers) that happen to be near the window edges — the regression that caused PR aethersdr#2042 to be reverted. Original approach (PR aethersdr#2042, reverted): recursively installed an event filter on the window and every descendant widget, with a ChildAdded watcher to pick up new children. For the Aetherial Audio tx_dsp container this put the filter in front of hundreds of controls and had the 6 px top-edge zone intercept clicks at y=0–6, calling startSystemResize() before the title bar's mousePressEvent could call startSystemMove(). Revised approach: one filter on the QWindow; cursor feedback uses QGuiApplication::setOverrideCursor() so the resize cursor wins over any child widget cursor without touching the widget tree. MouseButtonPress is consumed only when the cursor is within 6 px of a window edge. Timing: the native QWindow is created lazily on first show(). If install() is called before show() (e.g. from the constructor), the filter starts on the QWidget itself and migrates to the QWindow on QEvent::WinIdChange. The old QSizeGrip bottom-right rows in PanFloatingWindow and FloatingContainerWindow are removed since the resizer covers all edges. CMakeLists: FramelessResizer.cpp added to the main AetherSDR target and to the three container test executables that compile FloatingContainerWindow.cpp directly (container_widget_test, container_manager_test, container_nesting_test). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Nice work @chibondking — this is a clean, well-thought-out redesign that directly addresses the regression from PR #2042.
What I like:
- Installing on the
QWindowinstead of recursively on widget descendants is the right call. One filter, no child-widget interference. - The WinIdChange migration handles the lazy-creation timing correctly.
- Moving the
install()to the constructor in both consumers fixes a latent bug in the old code:takeContainer()was adding a new QSizeGrip row on every call, leaking layout items if a container was swapped. The resizer is now correctly one-shot. - RAII via
QObject(window)parent — resizer lifetime is tied to the window. No leak.
One thing to be aware of (not blocking):
QGuiApplication::setOverrideCursor() / restoreOverrideCursor() operates on a global stack shared across the entire application. With multiple floating windows open simultaneously, if two FramelessResizer instances both push/restore cursors, there's a theoretical stack imbalance risk. In practice this should be fine because Qt delivers Leave to window A before MouseMove to window B, so each instance's m_cursorOverridden flag stays synchronized with the stack. But if you ever hit a stuck resize cursor in multi-window scenarios, this is the place to look. A per-window QWidget::setCursor() approach wouldn't have this issue, but it also wouldn't override child widget cursors — so the current tradeoff makes sense.
Minor observation:
startSystemResize() can return false on platforms that don't support compositor-managed resize (e.g. older X11 WMs without _NET_WM_MOVERESIZE). The return value is currently ignored, which means on those platforms the press is consumed but no resize happens. This is probably acceptable — the user just gets a no-op click at the edge, which is no worse than the old QSizeGrip on Wayland. If you wanted to be extra defensive, you could fall back to return false (letting the event propagate) when startSystemResize() fails, but I wouldn't block on that.
All files are within scope. Conventions look good — AetherSDR namespace, AppSettings (not QSettings), C++20 member init. No Copilot flags to verify. LGTM overall, thanks for the contribution.
…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>
|
Claude here on Jeremy's behalf — auto-closed via the merge trailer in #2068. Pulled, applied your design as-is on the cherry-pick, ran the full test plan: hover/drag from each of 8 edges/corners, knob/slider/chain-tile interaction in the middle (the v1 failure mode — confirmed fixed here), and the cursor-leak edge case (cursor at edge → move off window to desktop). The cursor-leak test surfaced one tiny gap I patched on the cherry-pick: a destructor that calls The QWindow-level event filter design is the right architectural choice — it sidesteps the v1 regression entirely. Thanks for the careful redesign and the thorough split-out of the four #2008 sub-fixes. That's three of four landed. #2060 is the last one and should sail through next. 73, Jeremy KK7GWY & Claude (AI dev partner) |
Popped-out panadapter and applet windows use Qt::FramelessWindowHint, which removes OS-provided resize borders. The previous workaround was a QSizeGrip in the bottom-right corner, which is unreliable on Wayland and covers only one corner.
New FramelessResizer (src/gui/FramelessResizer.h/.cpp) provides all-8-edge resize using QWindow::startSystemResize() — the same compositor-managed path already used for drag-to-move via startSystemMove() in TitleBar and ContainerWidget.
The filter is installed on the QWindow (native handle) rather than on the QWidget tree. QWindow receives all platform mouse events before they are dispatched to the widget hierarchy, so resize intent is detected across the whole window without touching any child widget's event stream. This avoids breaking nested controls (knobs, drag tiles, sub-containers) that happen to be near the window edges — the regression that caused PR #2042 to be reverted.
Original approach (PR #2042, reverted): recursively installed an event filter on the window and every descendant widget, with a ChildAdded watcher to pick up new children. For the Aetherial Audio tx_dsp container this put the filter in front of hundreds of controls and had the 6 px top-edge zone intercept clicks at y=0–6, calling startSystemResize() before the title bar's mousePressEvent could call startSystemMove().
Revised approach: one filter on the QWindow; cursor feedback uses QGuiApplication::setOverrideCursor() so the resize cursor wins over any child widget cursor without touching the widget tree. MouseButtonPress is consumed only when the cursor is within 6 px of a window edge.
Timing: the native QWindow is created lazily on first show(). If install() is called before show() (e.g. from the constructor), the filter starts on the QWidget itself and migrates to the QWindow on QEvent::WinIdChange.
The old QSizeGrip bottom-right rows in PanFloatingWindow and FloatingContainerWindow are removed since the resizer covers all edges.
CMakeLists: FramelessResizer.cpp added to the main AetherSDR target and to the three container test executables that compile FloatingContainerWindow.cpp directly (container_widget_test, container_manager_test, container_nesting_test).