diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 41394bc923..4feb1dfb16 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -3876,6 +3876,7 @@ static LRESULT frameHitCode(WindowChrome::FrameHit hit) case WindowChrome::FrameHit::TopRight: return HTTOPRIGHT; case WindowChrome::FrameHit::BottomLeft: return HTBOTTOMLEFT; case WindowChrome::FrameHit::BottomRight: return HTBOTTOMRIGHT; + case WindowChrome::FrameHit::Caption: return HTCAPTION; case WindowChrome::FrameHit::Client: break; } return HTCLIENT; @@ -3887,7 +3888,8 @@ bool MainWindow::nativeEvent(const QByteArray& eventType, void* message, qintptr // Under the expanded client area Qt 6.12 answers WM_NCHITTEST by turning // the live mouse-button state into synthetic presses, which doubles real // clicks and closes menus as they open (#6272). Answer it here instead: - // the resize band at the edges, client area everywhere else. + // the resize band at the edges, the title bar's own background as the + // caption so Windows offers Snap (#6825), client area everywhere else. if (msg && result && msg->message == WM_NCHITTEST && windowFlags().testFlag(Qt::ExpandedClientAreaHint)) { RECT window{}; @@ -3903,7 +3905,18 @@ bool MainWindow::nativeEvent(const QByteArray& eventType, void* message, qintptr const QPoint point(GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam)); const QRect rect(window.left, window.top, window.right - window.left, window.bottom - window.top); - *result = frameHitCode(WindowChrome::expandedFrameHit(point, rect, borderX, borderY)); + // The bar's height is in logical pixels and WM_NCHITTEST is physical. + // Only the bar's empty background may answer HTCAPTION: over its tabs, + // menus and buttons that is what closed menus as they opened (#6272), + // and isSystemMoveAreaAt() is the same test the bar's own drag uses, so + // a point either starts a system move or stays client — never both. + int captionHeight = 0; + if (m_titleBar && m_titleBar->isVisible() + && m_titleBar->isSystemMoveAreaAt(QCursor::pos())) { + captionHeight = int(m_titleBar->height() * devicePixelRatioF()); + } + *result = frameHitCode( + WindowChrome::expandedFrameHit(point, rect, borderX, borderY, captionHeight)); return true; } // The whole window is client area, inset only while maximized so nothing diff --git a/src/gui/WindowChrome.h b/src/gui/WindowChrome.h index dd5331edd6..90274beac1 100644 --- a/src/gui/WindowChrome.h +++ b/src/gui/WindowChrome.h @@ -90,27 +90,47 @@ inline bool claimsWholeWindowAsClient(Qt::WindowFlags flags, const QOperatingSys // live mouse-button state into synthetic presses, doubling real clicks (#6272). // Physical pixels; `borderX` / `borderY` are the resize bands on the left/right // and top/bottom edges (0 while maximized). -enum class FrameHit { Client, Left, Right, Top, Bottom, TopLeft, TopRight, BottomLeft, BottomRight }; - -inline FrameHit expandedFrameHit(const QPoint& point, const QRect& window, int borderX, int borderY) +// +// `Caption` is the title bar's own draggable background: the answer Windows +// needs before it will offer drag-to-edge Snap and list the window in the Snap +// Assist picker (#6825). Only the caller knows which points are the bar's empty +// background rather than its tabs, menus and buttons, so this reports Caption +// for the whole `captionHeight` strip and MainWindow::nativeEvent narrows it +// with TitleBar::isSystemMoveAreaAt() — answering HTCAPTION over an interactive +// widget is what made menus close as they opened (#6272). A `captionHeight` of +// 0 disables it, which is how every non-main window keeps the old behaviour. +enum class FrameHit { Client, Caption, Left, Right, Top, Bottom, TopLeft, TopRight, BottomLeft, BottomRight }; + +inline FrameHit expandedFrameHit(const QPoint& point, const QRect& window, int borderX, int borderY, + int captionHeight = 0) { - if (borderX <= 0 || borderY <= 0 || !window.contains(point)) { + if (!window.contains(point)) { return FrameHit::Client; } - const bool left = point.x() < window.left() + borderX; - const bool right = point.x() > window.right() - borderX; - const bool top = point.y() < window.top() + borderY; - const bool bottom = point.y() > window.bottom() - borderY; - if (left) { - return top ? FrameHit::TopLeft : bottom ? FrameHit::BottomLeft : FrameHit::Left; - } - if (right) { - return top ? FrameHit::TopRight : bottom ? FrameHit::BottomRight : FrameHit::Right; + // Maximized and fullscreen have no resize band, but the caption still has + // to answer or the window cannot be dragged back off the top of the screen. + if (borderX > 0 && borderY > 0) { + const bool left = point.x() < window.left() + borderX; + const bool right = point.x() > window.right() - borderX; + const bool top = point.y() < window.top() + borderY; + const bool bottom = point.y() > window.bottom() - borderY; + if (left) { + return top ? FrameHit::TopLeft : bottom ? FrameHit::BottomLeft : FrameHit::Left; + } + if (right) { + return top ? FrameHit::TopRight : bottom ? FrameHit::BottomRight : FrameHit::Right; + } + if (top) { + return FrameHit::Top; + } + if (bottom) { + return FrameHit::Bottom; + } } - if (top) { - return FrameHit::Top; + if (captionHeight > 0 && point.y() < window.top() + captionHeight) { + return FrameHit::Caption; } - return bottom ? FrameHit::Bottom : FrameHit::Client; + return FrameHit::Client; } // Windows: the native window rect that gives `client`, keeping the frame the diff --git a/tests/unified_title_bar_test.cpp b/tests/unified_title_bar_test.cpp index abe4ba527b..e4e7bb5cfa 100644 --- a/tests/unified_title_bar_test.cpp +++ b/tests/unified_title_bar_test.cpp @@ -204,6 +204,56 @@ int main(int argc, char** argv) "the left/right band and the top/bottom band follow their own widths"); } + // ── The title bar answers as the caption so Windows offers Snap (#6825) ── + { + using Hit = WindowChrome::FrameHit; + const QRect win(100, 50, 1000, 600); + const int band = 8; + const int caption = 52; + + // Without a caption height nothing changes: every existing caller + // (and every window that is not the main window) keeps the old answer. + check(WindowChrome::expandedFrameHit(QPoint(600, 70), win, band, band) == Hit::Client, + "the title bar is client area when no caption height is given"); + + // The bar's background drags the window, which is what Windows needs + // before it offers drag-to-edge Snap and the Snap Assist picker. + check(WindowChrome::expandedFrameHit(QPoint(600, 70), win, band, band, caption) + == Hit::Caption, + "the title bar's background is the caption"); + check(WindowChrome::expandedFrameHit(QPoint(600, 101), win, band, band, caption) + == Hit::Caption, + "the last pixel of the caption strip still drags"); + check(WindowChrome::expandedFrameHit(QPoint(600, 102), win, band, band, caption) + == Hit::Client, + "the first pixel below the caption is client area"); + + // The resize band wins over the caption, or the top edge and the top + // corners would stop resizing the moment the caption was introduced. + check(WindowChrome::expandedFrameHit(QPoint(600, 50), win, band, band, caption) == Hit::Top, + "the top resize band still wins over the caption"); + check(WindowChrome::expandedFrameHit(QPoint(101, 51), win, band, band, caption) + == Hit::TopLeft + && WindowChrome::expandedFrameHit(QPoint(1098, 51), win, band, band, caption) + == Hit::TopRight, + "the top corners still resize under the caption"); + check(WindowChrome::expandedFrameHit(QPoint(101, 70), win, band, band, caption) + == Hit::Left, + "the side band still resizes alongside the caption"); + + // Maximized: no resize band, but the caption must still answer or the + // window cannot be dragged back down off the top of the screen. + check(WindowChrome::expandedFrameHit(QPoint(600, 70), win, 0, 0, caption) == Hit::Caption, + "the caption answers while maximized, so the window can be dragged free"); + check(WindowChrome::expandedFrameHit(QPoint(600, 350), win, 0, 0, caption) == Hit::Client, + "the body is still client area while maximized"); + + // A point outside the window is never the caption. + check(WindowChrome::expandedFrameHit(QPoint(20, 20), win, band, band, caption) + == Hit::Client, + "a point outside the window is not the caption"); + } + // ── Native client-rect save/restore for the Windows expanded frame (#6303) ── { // Rects measured on Windows 10: the expanded frame keeps an 8 px resize