Skip to content

[feat] FramelessResizer: all-edge resize for frameless floating windows#2058

Closed
chibondking wants to merge 1 commit into
aethersdr:mainfrom
chibondking:fix/frameless-resizer
Closed

[feat] FramelessResizer: all-edge resize for frameless floating windows#2058
chibondking wants to merge 1 commit into
aethersdr:mainfrom
chibondking:fix/frameless-resizer

Conversation

@chibondking

Copy link
Copy Markdown
Collaborator

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).

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>

@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 work @chibondking — this is a clean, well-thought-out redesign that directly addresses the regression from PR #2042.

What I like:

  • Installing on the QWindow instead 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.

ten9876 added a commit that referenced this pull request Apr 27, 2026
…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>
@ten9876

ten9876 commented Apr 27, 2026

Copy link
Copy Markdown
Collaborator

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 leaveEdgeZone() so windows destroyed while the override cursor is active don't leak the resize cursor onto the desktop. Five lines on top of your work; otherwise applied as-is.

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)

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.

2 participants