From 913efb275f62893933041f9cd822865c33642c54 Mon Sep 17 00:00:00 2001 From: jensenpat Date: Thu, 16 Jul 2026 08:26:19 -0700 Subject: [PATCH 1/2] Fix Aetherial Audio edge resizing, Principle VIII. --- src/gui/AetherialAudioStrip.cpp | 78 ++------------------------------- src/gui/AetherialAudioStrip.h | 8 ---- 2 files changed, 4 insertions(+), 82 deletions(-) diff --git a/src/gui/AetherialAudioStrip.cpp b/src/gui/AetherialAudioStrip.cpp index e9558071c..4b6d4f48f 100644 --- a/src/gui/AetherialAudioStrip.cpp +++ b/src/gui/AetherialAudioStrip.cpp @@ -5,6 +5,7 @@ #include "AetherDspWidget.h" #include "FramelessMoveHelper.h" +#include "FramelessResizer.h" #include "StripChainWidget.h" #include "StripRxChainWidget.h" #include "ClientEqApplet.h" @@ -54,15 +55,10 @@ #include #include #include -#include #include "core/ThemeManager.h" namespace AetherSDR { -namespace { -constexpr int kResizeMargin = 6; -} - AetherialAudioStrip::AetherialAudioStrip(AudioEngine* engine, QWidget* parent) : QWidget(parent, Qt::Window) , m_audio(engine) @@ -94,9 +90,9 @@ AetherialAudioStrip::AetherialAudioStrip(AudioEngine* engine, QWidget* parent) } resize(1140, initialHeight); - // Track mouse without buttons pressed so the resize cursor updates - // while hovering the bare margin around the embedded grid. - setMouseTracking(true); + // Listen at the native-window boundary so edge presses still reach the + // resize handler when the child-heavy strip content covers every margin. + FramelessResizer::install(this); // Outer layout has zero margins so the title bar can run edge-to-edge // across the whole window (matching the applet ContainerTitleBar). @@ -845,72 +841,6 @@ void AetherialAudioStrip::hideEvent(QHideEvent* ev) QWidget::hideEvent(ev); } -Qt::Edges AetherialAudioStrip::edgesAt(const QPoint& pos) const -{ - if (!(windowFlags() & Qt::FramelessWindowHint)) - return {}; - if (isMaximized() || isFullScreen()) - return {}; - - Qt::Edges edges; - if (pos.x() <= kResizeMargin) - edges |= Qt::LeftEdge; - else if (pos.x() >= width() - kResizeMargin) - edges |= Qt::RightEdge; - if (pos.y() <= kResizeMargin) - edges |= Qt::TopEdge; - else if (pos.y() >= height() - kResizeMargin) - edges |= Qt::BottomEdge; - return edges; -} - -void AetherialAudioStrip::updateResizeCursor(const QPoint& pos) -{ - const Qt::Edges edges = edgesAt(pos); - Qt::CursorShape shape = Qt::ArrowCursor; - // Two diagonals → SizeFDiag (↘↖); the other two → SizeBDiag (↙↗). - if ((edges & (Qt::LeftEdge | Qt::TopEdge)) == (Qt::LeftEdge | Qt::TopEdge) - || (edges & (Qt::RightEdge | Qt::BottomEdge)) == (Qt::RightEdge | Qt::BottomEdge)) { - shape = Qt::SizeFDiagCursor; - } else if ((edges & (Qt::RightEdge | Qt::TopEdge)) == (Qt::RightEdge | Qt::TopEdge) - || (edges & (Qt::LeftEdge | Qt::BottomEdge)) == (Qt::LeftEdge | Qt::BottomEdge)) { - shape = Qt::SizeBDiagCursor; - } else if (edges & (Qt::LeftEdge | Qt::RightEdge)) { - shape = Qt::SizeHorCursor; - } else if (edges & (Qt::TopEdge | Qt::BottomEdge)) { - shape = Qt::SizeVerCursor; - } - setCursor(shape); -} - -void AetherialAudioStrip::mouseMoveEvent(QMouseEvent* ev) -{ - if (!(ev->buttons() & Qt::LeftButton)) - updateResizeCursor(ev->pos()); - QWidget::mouseMoveEvent(ev); -} - -void AetherialAudioStrip::mousePressEvent(QMouseEvent* ev) -{ - if (ev->button() == Qt::LeftButton) { - const Qt::Edges edges = edgesAt(ev->pos()); - if (edges) { - if (auto* h = windowHandle()) { - h->startSystemResize(edges); - ev->accept(); - return; - } - } - } - QWidget::mousePressEvent(ev); -} - -void AetherialAudioStrip::leaveEvent(QEvent* ev) -{ - setCursor(Qt::ArrowCursor); - QWidget::leaveEvent(ev); -} - bool AetherialAudioStrip::eventFilter(QObject* obj, QEvent* ev) { if (obj == m_titleBar && ev->type() == QEvent::MouseMove) { diff --git a/src/gui/AetherialAudioStrip.h b/src/gui/AetherialAudioStrip.h index 0eb0f42e4..2f5369cc1 100644 --- a/src/gui/AetherialAudioStrip.h +++ b/src/gui/AetherialAudioStrip.h @@ -137,19 +137,11 @@ class AetherialAudioStrip : public QWidget { void resizeEvent(QResizeEvent* ev) override; void showEvent(QShowEvent* ev) override; void hideEvent(QHideEvent* ev) override; - // Frameless 8-axis resize: 4 edges + 4 corners. Mouse hover on - // the bare margin around the embedded grid updates the cursor; - // press starts a compositor-managed resize via startSystemResize. - void mouseMoveEvent(QMouseEvent* ev) override; - void mousePressEvent(QMouseEvent* ev) override; - void leaveEvent(QEvent* ev) override; bool eventFilter(QObject* obj, QEvent* ev) override; private: void saveGeometryToSettings(); void restoreGeometryFromSettings(); - Qt::Edges edgesAt(const QPoint& pos) const; - void updateResizeCursor(const QPoint& pos); // Master bypass — snapshot all enabled TX stages, then disable // them. Restores the snapshot on uncheck. Mirrors the docked From d7a50f3c85cab80527ed5afef609f9124922813e Mon Sep 17 00:00:00 2001 From: "Jeremy [KK7GWY]" Date: Sat, 18 Jul 2026 16:47:30 -0700 Subject: [PATCH 2/2] Address review: guard maximize + reserve title-bar move region in FramelessResizer (#4266). Principle VIII. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes @NF0T's two regressions from migrating Aetherial Audio onto the shared FramelessResizer: - Maximize/fullscreen guard: the old AetherialAudioStrip::edgesAt() returned no edges when isMaximized()/isFullScreen(); FramelessResizer::edgesAt() had no such check, so a maximized/fullscreen strip could startSystemResize() from a screen-edge hover. Centralize the guard in FramelessResizer — closes the same latent gap for all nine adopters, not just this one. - Title-bar grab misfiring as a resize: AetherialAudioStrip's title bar is an 18px edge-to-edge strip at y=0, so the default 6px top resize margin overlapped its top third; the QWindow-level filter consumed the press before the title bar's own move-drag ran. Add an optional topMoveReserve to FramelessResizer: the top strip is reserved for the window's move handling (edgesAt returns none there) while resize still works everywhere below it. Aetherial Audio installs with a reserve of its title-bar height, preserving the edge-to-edge look (unlike ConnectionPanel, which insets its identical 18px title bar to clear the edges). Other adopters default to 0 — no behavior change. Full app builds clean; default install() args keep every existing adopter call-site unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/gui/AetherialAudioStrip.cpp | 12 ++++++++++-- src/gui/FramelessResizer.cpp | 23 +++++++++++++++++++---- src/gui/FramelessResizer.h | 10 ++++++++-- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/gui/AetherialAudioStrip.cpp b/src/gui/AetherialAudioStrip.cpp index 4b6d4f48f..19975a926 100644 --- a/src/gui/AetherialAudioStrip.cpp +++ b/src/gui/AetherialAudioStrip.cpp @@ -59,6 +59,12 @@ namespace AetherSDR { +namespace { +// Edge-to-edge title-bar height. Reserved from the frameless resize edge so a +// title-bar grab starts a move, not a top-edge resize (#4266). +constexpr int kTitleBarHeight = 18; +} + AetherialAudioStrip::AetherialAudioStrip(AudioEngine* engine, QWidget* parent) : QWidget(parent, Qt::Window) , m_audio(engine) @@ -92,7 +98,9 @@ AetherialAudioStrip::AetherialAudioStrip(AudioEngine* engine, QWidget* parent) // Listen at the native-window boundary so edge presses still reach the // resize handler when the child-heavy strip content covers every margin. - FramelessResizer::install(this); + // Reserve the edge-to-edge title-bar strip for moves so a title-bar grab + // isn't stolen by the top-edge resize zone (#4266). + FramelessResizer::install(this, 6, kTitleBarHeight); // Outer layout has zero margins so the title bar can run edge-to-edge // across the whole window (matching the applet ContainerTitleBar). @@ -110,7 +118,7 @@ AetherialAudioStrip::AetherialAudioStrip(AudioEngine* engine, QWidget* parent) // it shares with the docked applet panels. { m_titleBar = new QWidget(this); - m_titleBar->setFixedHeight(18); + m_titleBar->setFixedHeight(kTitleBarHeight); m_titleBar->setAttribute(Qt::WA_StyledBackground, true); AetherSDR::ThemeManager::instance().applyStyleSheet(m_titleBar, "QWidget { background: qlineargradient(x1:0,y1:0,x2:0,y2:1," "stop:0 #5a7494, stop:0.5 #384e68, stop:1 {{color.background.1}}); " diff --git a/src/gui/FramelessResizer.cpp b/src/gui/FramelessResizer.cpp index f9cb07fec..a63947cb3 100644 --- a/src/gui/FramelessResizer.cpp +++ b/src/gui/FramelessResizer.cpp @@ -8,13 +8,14 @@ namespace AetherSDR { -void FramelessResizer::install(QWidget* window, int margin) +void FramelessResizer::install(QWidget* window, int margin, int topMoveReserve) { - new FramelessResizer(window, margin); + new FramelessResizer(window, margin, topMoveReserve); } -FramelessResizer::FramelessResizer(QWidget* window, int margin) - : QObject(window), m_window(window), m_margin(margin) +FramelessResizer::FramelessResizer(QWidget* window, int margin, int topMoveReserve) + : QObject(window), m_window(window), m_margin(margin), + m_topMoveReserve(topMoveReserve) { if (window->windowHandle()) { // Native window already exists — install directly on it. @@ -39,6 +40,20 @@ FramelessResizer::~FramelessResizer() Qt::Edges FramelessResizer::edgesAt(const QPoint& p) const { + // A maximized or fullscreen window must not resize from an edge drag — the + // compositor owns its geometry. Previously each adopter guarded this in its + // own edgesAt(); centralizing it here closes the gap for every adopter + // (#4266). + if (m_window->isMaximized() || m_window->isFullScreen()) { + return {}; + } + // Reserve an edge-to-edge top move handle (e.g. a full-width title bar) for + // the window's own move handling rather than the top-edge resize zone, so a + // title-bar grab isn't consumed as a resize (#4266). Resize still works + // everywhere below the reserved strip. + if (m_topMoveReserve > 0 && p.y() < m_topMoveReserve) { + return {}; + } const QRect r = m_window->rect(); Qt::Edges edges; if (p.x() <= m_margin) edges |= Qt::LeftEdge; diff --git a/src/gui/FramelessResizer.h b/src/gui/FramelessResizer.h index b2d543fc0..097560482 100644 --- a/src/gui/FramelessResizer.h +++ b/src/gui/FramelessResizer.h @@ -28,20 +28,26 @@ namespace AetherSDR { class FramelessResizer : public QObject { Q_OBJECT public: - static void install(QWidget* window, int margin = 6); + // `topMoveReserve`: height (px) of an edge-to-edge move handle (e.g. a title + // bar) at the top of the window. The top strip is reserved for the window's + // own move handling instead of the top-edge resize zone, so a title-bar grab + // isn't stolen by the resizer (#4266). 0 (default) keeps the full top edge + // resizable — the behavior for adopters that inset their title bar. + static void install(QWidget* window, int margin = 6, int topMoveReserve = 0); ~FramelessResizer() override; protected: bool eventFilter(QObject* obj, QEvent* ev) override; private: - explicit FramelessResizer(QWidget* window, int margin); + explicit FramelessResizer(QWidget* window, int margin, int topMoveReserve); Qt::Edges edgesAt(const QPoint& windowPos) const; void enterEdgeZone(Qt::Edges edges); void leaveEdgeZone(); QWidget* m_window{nullptr}; int m_margin{6}; + int m_topMoveReserve{0}; bool m_cursorOverridden{false}; Qt::Edges m_lastEdges{}; };