From 9286c8b9c23a613f96636b8e2d1391d89cedd199 Mon Sep 17 00:00:00 2001 From: Zhao Yuhang <2546789017@qq.com> Date: 周六, 07 12月 2024 21:11:05 +0800 Subject: [PATCH] Win32 code cleanup & improve --- src/core/contexts/win32windowcontext.cpp | 674 +++++++++++++++++++++++++++++++++++++++++++------------ 1 files changed, 523 insertions(+), 151 deletions(-) diff --git a/src/core/contexts/win32windowcontext.cpp b/src/core/contexts/win32windowcontext.cpp index e9a6731..3c7ba80 100644 --- a/src/core/contexts/win32windowcontext.cpp +++ b/src/core/contexts/win32windowcontext.cpp @@ -6,13 +6,16 @@ #include <optional> +#include <QtCore/QAbstractEventDispatcher> +#include <QtCore/QDateTime> #include <QtCore/QHash> #include <QtCore/QScopeGuard> #include <QtCore/QTimer> -#include <QtCore/QDateTime> #include <QtGui/QGuiApplication> #include <QtGui/QPainter> #include <QtGui/QPalette> + +#include <QtGui/qpa/qwindowsysteminterface.h> #include <QtGui/private/qhighdpiscaling_p.h> #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) @@ -31,6 +34,12 @@ #include "qwkwindowsextra_p.h" namespace QWK { + + enum IconButtonClickLevelFlag { + IconButtonClicked = 1, + IconButtonDoubleClicked = 2, + IconButtonTriggersClose = 4, + }; // The thickness of an auto-hide taskbar in pixels. static constexpr const quint8 kAutoHideTaskBarThickness = 2; @@ -72,6 +81,8 @@ static void setInternalWindowFrameMargins(QWindow *window, const QMargins &margins) { const QVariant marginsVar = QVariant::fromValue(margins); + + // TODO: Add comments window->setProperty("_q_windowsCustomMargins", marginsVar); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) if (QPlatformWindow *platformWindow = window->handle()) { @@ -115,6 +126,36 @@ SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER); } + static inline bool isFullScreen(HWND hwnd) { + RECT windowRect{}; + ::GetWindowRect(hwnd, &windowRect); + // Compare to the full area of the screen, not the work area. + return (windowRect == getMonitorForWindow(hwnd).rcMonitor); + } + + static inline bool isMaximized(HWND hwnd) { + return ::IsZoomed(hwnd); + } + + static inline bool isMinimized(HWND hwnd) { + return ::IsIconic(hwnd); + } + + static inline bool isWindowNoState(HWND hwnd) { +#if 0 + WINDOWPLACEMENT wp{}; + wp.length = sizeof(wp); + ::GetWindowPlacement(hwnd, &wp); + return ((wp.showCmd == SW_NORMAL) || (wp.showCmd == SW_RESTORE)); +#else + if (isFullScreen(hwnd)) { + return false; + } + const auto style = static_cast<DWORD>(::GetWindowLongPtrW(hwnd, GWL_STYLE)); + return (!(style & (WS_MINIMIZE | WS_MAXIMIZE))); +#endif + } + static inline void bringWindowToFront(HWND hwnd) { HWND oldForegroundWindow = ::GetForegroundWindow(); if (!oldForegroundWindow) { @@ -126,7 +167,7 @@ if (!::IsWindowVisible(hwnd)) { ::ShowWindow(hwnd, SW_SHOW); } - if (IsMinimized(hwnd)) { + if (isMinimized(hwnd)) { // Restore the window if it is minimized. ::ShowWindow(hwnd, SW_RESTORE); // Once we've been restored, throw us on the active monitor. @@ -153,7 +194,7 @@ [[maybe_unused]] const auto &cleaner = qScopeGuard([windowThreadProcessId, currentThreadId]() { ::AttachThreadInput(windowThreadProcessId, currentThreadId, FALSE); // - }); + }); // TODO: Remove it ::BringWindowToTop(hwnd); // Activate the window too. This will force us to the virtual desktop this @@ -161,28 +202,6 @@ ::SetActiveWindow(hwnd); // Throw us on the active monitor. moveWindowToMonitor(hwnd, activeMonitor); - } - - static inline bool isFullScreen(HWND hwnd) { - RECT windowRect{}; - ::GetWindowRect(hwnd, &windowRect); - // Compare to the full area of the screen, not the work area. - return (windowRect == getMonitorForWindow(hwnd).rcMonitor); - } - - static inline bool isWindowNoState(HWND hwnd) { -#if 0 - WINDOWPLACEMENT wp{}; - wp.length = sizeof(wp); - ::GetWindowPlacement(hwnd, &wp); - return ((wp.showCmd == SW_NORMAL) || (wp.showCmd == SW_RESTORE)); -#else - if (isFullScreen(hwnd)) { - return false; - } - const auto style = static_cast<DWORD>(::GetWindowLongPtrW(hwnd, GWL_STYLE)); - return (!(style & (WS_MINIMIZE | WS_MAXIMIZE))); -#endif } static void syncPaintEventWithDwm() { @@ -230,17 +249,18 @@ apis.ptimeEndPeriod(ms_granularity); } - static void showSystemMenu2(HWND hWnd, const POINT &pos, const bool selectFirstEntry, - const bool fixedSize) { + // Returns false if the menu is canceled + static bool showSystemMenu_sys(HWND hWnd, const POINT &pos, const bool selectFirstEntry, + const bool fixedSize) { HMENU hMenu = ::GetSystemMenu(hWnd, FALSE); if (!hMenu) { // The corresponding window doesn't have a system menu, most likely due to the // lack of the "WS_SYSMENU" window style. This situation should not be treated // as an error so just ignore it and return early. - return; + return true; } - const bool maxOrFull = IsMaximized(hWnd) || isFullScreen(hWnd); + const bool maxOrFull = isMaximized(hWnd) || isFullScreen(hWnd); ::EnableMenuItem(hMenu, SC_CLOSE, (MF_BYCOMMAND | MFS_ENABLED)); ::EnableMenuItem(hMenu, SC_MAXIMIZE, (MF_BYCOMMAND | ((maxOrFull || fixedSize) ? MFS_DISABLED : MFS_ENABLED))); @@ -281,7 +301,8 @@ // Popup the system menu at the required position. const auto result = ::TrackPopupMenu( hMenu, - (TPM_RETURNCMD | (QGuiApplication::isRightToLeft() ? TPM_RIGHTALIGN : TPM_LEFTALIGN)), + (TPM_RETURNCMD | (QGuiApplication::isRightToLeft() ? TPM_RIGHTALIGN : TPM_LEFTALIGN) | + TPM_RIGHTBUTTON), pos.x, pos.y, 0, hWnd, nullptr); // Unhighlight the first menu item after the popup menu is closed, otherwise it will keep @@ -290,11 +311,12 @@ if (!result) { // The user canceled the menu, no need to continue. - return; + return false; } // Send the command that the user chooses to the corresponding window. ::PostMessageW(hWnd, WM_SYSCOMMAND, result, 0); + return true; } static inline Win32WindowContext::WindowPart getHitWindowPart(int hitTestResult) { @@ -321,7 +343,6 @@ case HTBORDER: return Win32WindowContext::FixedBorder; default: - Q_UNREACHABLE(); break; } return Win32WindowContext::Outside; @@ -359,6 +380,107 @@ return true; } + static inline constexpr bool isNonClientMessage(const UINT message) { + if (((message >= WM_NCCREATE) && (message <= WM_NCACTIVATE)) || + ((message >= WM_NCMOUSEMOVE) && (message <= WM_NCMBUTTONDBLCLK)) || + ((message >= WM_NCXBUTTONDOWN) && (message <= WM_NCXBUTTONDBLCLK)) +#if (WINVER >= _WIN32_WINNT_WIN8) + || ((message >= WM_NCPOINTERUPDATE) && (message <= WM_NCPOINTERUP)) +#endif + || ((message == WM_NCMOUSEHOVER) || (message == WM_NCMOUSELEAVE))) { + return true; + } else { + return false; + } + } + + static MSG createMessageBlock(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { + MSG msg; + msg.hwnd = hWnd; // re-create MSG structure + msg.message = message; // time and pt fields ignored + msg.wParam = wParam; + msg.lParam = lParam; + + const DWORD dwScreenPos = ::GetMessagePos(); + msg.pt.x = GET_X_LPARAM(dwScreenPos); + msg.pt.y = GET_Y_LPARAM(dwScreenPos); + if (!isNonClientMessage(message)) { + ::ScreenToClient(hWnd, &msg.pt); + } + return msg; + } + + static inline constexpr bool isInputMessage(UINT m) { + switch (m) { + case WM_IME_STARTCOMPOSITION: + case WM_IME_ENDCOMPOSITION: + case WM_IME_COMPOSITION: + case WM_INPUT: + case WM_TOUCH: + case WM_MOUSEHOVER: + case WM_MOUSELEAVE: + case WM_NCMOUSEHOVER: + case WM_NCMOUSELEAVE: + case WM_SIZING: + case WM_MOVING: + case WM_SYSCOMMAND: + case WM_COMMAND: + case WM_DWMNCRENDERINGCHANGED: + case WM_PAINT: + return true; + default: + break; + } + return (m >= WM_MOUSEFIRST && m <= WM_MOUSELAST) || + (m >= WM_NCMOUSEMOVE && m <= WM_NCXBUTTONDBLCLK) || + (m >= WM_KEYFIRST && m <= WM_KEYLAST); + } + + static inline QByteArray nativeEventType() { + return QByteArrayLiteral("windows_generic_MSG"); + } + + // Send to QAbstractEventDispatcher + bool filterNativeEvent(MSG *msg, LRESULT *result) { + auto dispatcher = QAbstractEventDispatcher::instance(); + QT_NATIVE_EVENT_RESULT_TYPE filterResult = *result; + if (dispatcher && dispatcher->filterNativeEvent(nativeEventType(), msg, &filterResult)) { + *result = LRESULT(filterResult); + return true; + } + return false; + } + + // Send to QWindowSystemInterface + bool filterNativeEvent(QWindow *window, MSG *msg, LRESULT *result) { + QT_NATIVE_EVENT_RESULT_TYPE filterResult = *result; + if (QWindowSystemInterface::handleNativeEvent(window, nativeEventType(), msg, + &filterResult)) { + *result = LRESULT(filterResult); + return true; + } + return false; + } + + static inline bool forwardFilteredEvent(QWindow *window, HWND hWnd, UINT message, WPARAM wParam, + LPARAM lParam, LRESULT *result) { + MSG msg = createMessageBlock(hWnd, message, wParam, lParam); + + // https://github.com/qt/qtbase/blob/e26a87f1ecc40bc8c6aa5b889fce67410a57a702/src/plugins/platforms/windows/qwindowscontext.cpp#L1025 + // Do exact the same as what Qt Windows plugin does. + + // Run the native event filters. QTBUG-67095: Exclude input messages which are sent + // by QEventDispatcherWin32::processEvents() + if (!isInputMessage(msg.message) && filterNativeEvent(&msg, result)) + return true; + + auto platformWindow = window->handle(); + if (platformWindow && filterNativeEvent(platformWindow->window(), &msg, result)) + return true; + + return false; + } + // https://github.com/qt/qtbase/blob/e26a87f1ecc40bc8c6aa5b889fce67410a57a702/src/plugins/platforms/windows/qwindowscontext.cpp#L1556 // In QWindowsContext::windowsProc(), the messages will be passed to all global native event // filters, but because we have already filtered the messages in the hook WndProc function for @@ -378,17 +500,38 @@ return false; } - // https://github.com/qt/qtbase/blob/e26a87f1ecc40bc8c6aa5b889fce67410a57a702/src/plugins/platforms/windows/qwindowscontext.cpp#L1546 - // Qt needs to refer to the WM_NCCALCSIZE message data that hasn't been processed, so we - // have to process it after Qt acquires the initial data. auto msg = static_cast<const MSG *>(message); - if (msg->message == WM_NCCALCSIZE && lastMessageContext) { - LRESULT res; - if (lastMessageContext->nonClientCalcSizeHandler(msg->hwnd, msg->message, - msg->wParam, msg->lParam, &res)) { - *result = decltype(*result)(res); - return true; + switch (msg->message) { + case WM_NCCALCSIZE: { + // https://github.com/qt/qtbase/blob/e26a87f1ecc40bc8c6aa5b889fce67410a57a702/src/plugins/platforms/windows/qwindowscontext.cpp#L1546 + // Qt needs to refer to the WM_NCCALCSIZE message data that hasn't been + // processed, so we have to process it after Qt acquires the initial data. + if (lastMessageContext) { + LRESULT res; + if (lastMessageContext->nonClientCalcSizeHandler( + msg->hwnd, msg->message, msg->wParam, msg->lParam, &res)) { + *result = decltype(*result)(res); + return true; + } + } + break; } + + // case WM_NCHITTEST: { + // // The child window must return HTTRANSPARENT when processing + // WM_NCHITTEST for + // // the parent window to receive WM_NCHITTEST. + // if (!lastMessageContext) { + // auto rootHWnd = ::GetAncestor(msg->hwnd, GA_ROOT); + // if (rootHWnd != msg->hwnd) { + // if (auto ctx = g_wndProcHash->value(rootHWnd)) { + // *result = HTTRANSPARENT; + // return true; + // } + // } + // } + // break; + // } } return false; } @@ -477,18 +620,25 @@ return ::DefWindowProcW(hWnd, message, wParam, lParam); } + WindowsNativeEventFilter::lastMessageContext = ctx; + const auto &contextCleaner = qScopeGuard([]() { + WindowsNativeEventFilter::lastMessageContext = nullptr; // + }); + // Since Qt does the necessary processing of the WM_NCCALCSIZE message, we need to // forward it right away and process it in our native event filter. if (message == WM_NCCALCSIZE) { - WindowsNativeEventFilter::lastMessageContext = ctx; - LRESULT result = ::CallWindowProcW(g_qtWindowProc, hWnd, message, wParam, lParam); - WindowsNativeEventFilter::lastMessageContext = nullptr; - return result; + return ::CallWindowProcW(g_qtWindowProc, hWnd, message, wParam, lParam); } // Try hooked procedure and save result LRESULT result; if (ctx->windowProc(hWnd, message, wParam, lParam, &result)) { + // https://github.com/stdware/qwindowkit/issues/45 + // Forward the event to user-defined native event filters, there may be some messages + // that need to be processed by the user. + std::ignore = + forwardFilteredEvent(ctx->window(), hWnd, message, wParam, lParam, &result); return result; } @@ -497,18 +647,10 @@ } static inline void addManagedWindow(QWindow *window, HWND hWnd, Win32WindowContext *ctx) { - const auto margins = [hWnd]() -> QMargins { - const auto titleBarHeight = int(getTitleBarHeight(hWnd)); - if (isSystemBorderEnabled()) { - return {0, -titleBarHeight, 0, 0}; - } else { - const auto frameSize = int(getResizeBorderThickness(hWnd)); - return {-frameSize, -titleBarHeight, -frameSize, -frameSize}; - } - }(); - - // Inform Qt we want and have set custom margins - setInternalWindowFrameMargins(window, margins); + if (isSystemBorderEnabled()) { + // Inform Qt we want and have set custom margins + setInternalWindowFrameMargins(window, QMargins(0, -int(getTitleBarHeight(hWnd)), 0, 0)); + } // Store original window proc if (!g_qtWindowProc) { @@ -567,12 +709,12 @@ auto hWnd = reinterpret_cast<HWND>(m_windowId); #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) const QPoint nativeGlobalPos = - QHighDpi::toNativeGlobalPosition(pos, m_windowHandle); + QHighDpi::toNativeGlobalPosition(pos, m_windowHandle.data()); #else - const QPoint nativeGlobalPos = QHighDpi::toNativePixels(pos, m_windowHandle); + const QPoint nativeGlobalPos = QHighDpi::toNativePixels(pos, m_windowHandle.data()); #endif - showSystemMenu2(hWnd, qpoint2point(nativeGlobalPos), false, - m_delegate->isHostSizeFixed(m_host)); + std::ignore = showSystemMenu_sys(hWnd, qpoint2point(nativeGlobalPos), false, + isHostSizeFixed()); return; } @@ -631,7 +773,7 @@ } case DrawWindows10BorderHook2: { - if (!m_windowHandle) + if (!m_windowId) return; // https://github.com/microsoft/terminal/blob/71a6f26e6ece656084e87de1a528c4a8072eeabd/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp#L1025 @@ -663,7 +805,7 @@ QVariant Win32WindowContext::windowAttribute(const QString &key) const { if (key == QStringLiteral("window-rect")) { - if (!m_windowHandle) + if (!m_windowId) return {}; RECT frame{}; @@ -685,12 +827,19 @@ return isSystemBorderEnabled() && !isWin11OrGreater(); } + if (key == QStringLiteral("windows-system-border-enabled")) { + return isSystemBorderEnabled(); + } + if (key == QStringLiteral("border-thickness")) { - return m_windowHandle + return m_windowId ? int(getWindowFrameBorderThickness(reinterpret_cast<HWND>(m_windowId))) : 0; } + if (key == QStringLiteral("title-bar-height")) { + return m_windowId ? int(getTitleBarHeight(reinterpret_cast<HWND>(m_windowId))) : 0; + } return AbstractWindowContext::windowAttribute(key); } @@ -698,13 +847,13 @@ // Reset the context data mouseLeaveBlocked = false; lastHitTestResult = WindowPart::Outside; + lastHitTestResultRaw = HTNOWHERE; // If the original window id is valid, remove all resources related if (oldWinId) { removeManagedWindow(reinterpret_cast<HWND>(oldWinId)); } - - if (!m_windowHandle || !winId) { + if (!winId) { return; } @@ -774,13 +923,9 @@ // Forward to native event filter subscribers if (!m_nativeEventFilters.isEmpty()) { - MSG msg; - msg.hwnd = hWnd; - msg.message = message; - msg.wParam = wParam; - msg.lParam = lParam; + MSG msg = createMessageBlock(hWnd, message, wParam, lParam); QT_NATIVE_EVENT_RESULT_TYPE res = 0; - if (nativeDispatch(QByteArrayLiteral("windows_generic_MSG"), &msg, &res)) { + if (nativeDispatch(nativeEventType(), &msg, &res)) { *result = LRESULT(res); return true; } @@ -803,7 +948,7 @@ if (key == QStringLiteral("extra-margins")) { auto margins = qmargins2margins(attribute.value<QMargins>()); - return apis.pDwmExtendFrameIntoClientArea(hwnd, &margins) == S_OK; + return SUCCEEDED(apis.pDwmExtendFrameIntoClientArea(hwnd, &margins)); } if (key == QStringLiteral("dark-mode")) { @@ -817,12 +962,8 @@ } else { apis.pAllowDarkModeForApp(enable); } - for (const auto attr : { - _DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1, - _DWMWA_USE_IMMERSIVE_DARK_MODE, - }) { - apis.pDwmSetWindowAttribute(hwnd, attr, &enable, sizeof(enable)); - } + const auto attr = isWin1020H1OrGreater() ? _DWMWA_USE_IMMERSIVE_DARK_MODE : _DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1; + apis.pDwmSetWindowAttribute(hwnd, attr, &enable, sizeof(enable)); apis.pFlushMenuThemes(); return true; @@ -1130,7 +1271,7 @@ POINT screenPoint{GET_X_LPARAM(dwScreenPos), GET_Y_LPARAM(dwScreenPos)}; ::ScreenToClient(hWnd, &screenPoint); QPoint qtScenePos = QHighDpi::fromNativeLocalPosition(point2qpoint(screenPoint), - m_windowHandle); + m_windowHandle.data()); auto dummy = WindowAgentBase::Unknown; if (isInSystemButtons(qtScenePos, &dummy)) { // We must record whether the last WM_MOUSELEAVE was filtered, because if @@ -1175,9 +1316,8 @@ case WM_NCPOINTERUP: #endif case WM_NCMOUSEHOVER: { - const WindowPart currentWindowPart = lastHitTestResult; if (message == WM_NCMOUSEMOVE) { - if (currentWindowPart != WindowPart::ChromeButton) { + if (lastHitTestResult != WindowPart::ChromeButton) { // https://github.com/qt/qtbase/blob/e26a87f1ecc40bc8c6aa5b889fce67410a57a702/src/widgets/kernel/qwidgetwindow.cpp#L472 // When the mouse press event arrives, QWidgetWindow will implicitly grab // the top widget right under the mouse, and set `qt_button_down` to this @@ -1236,14 +1376,50 @@ } } - if (currentWindowPart == WindowPart::ChromeButton) { - emulateClientAreaMessage(hWnd, message, wParam, lParam); + if (lastHitTestResult == WindowPart::ChromeButton) { if (message == WM_NCMOUSEMOVE) { // ### FIXME FIXME FIXME // ### FIXME: Calling DefWindowProc() here is really dangerous, investigate // how to avoid doing this. // ### FIXME FIXME FIXME *result = ::DefWindowProcW(hWnd, WM_NCMOUSEMOVE, wParam, lParam); + emulateClientAreaMessage(hWnd, message, wParam, lParam); + return true; + } + + if (lastHitTestResultRaw == HTSYSMENU) { + switch (message) { + case WM_NCLBUTTONDOWN: + if (iconButtonClickLevel == 0) { + // A message of WM_SYSCOMMAND with SC_MOUSEMENU will be sent by + // Windows, and the current control flow will be blocked by the + // menu while Windows will create and execute a new event loop + // until the menu returns + iconButtonClickTime = ::GetTickCount64(); + *result = ::DefWindowProcW(hWnd, message, wParam, lParam); + iconButtonClickTime = 0; + if (iconButtonClickLevel & IconButtonTriggersClose) { + ::PostMessageW(hWnd, WM_SYSCOMMAND, SC_CLOSE, 0); + } + if (iconButtonClickLevel & IconButtonDoubleClicked) { + iconButtonClickLevel = 0; + } + // Otherwise, no need to reset `iconButtonClickLevel` if not to + // close, if it has value, there must be another incoming + // WM_NCLBUTTONDOWN + } else { + iconButtonClickLevel = 0; + } + break; + case WM_NCLBUTTONDBLCLK: + // A message of WM_SYSCOMMAND with SC_CLOSE will be sent by Windows + *result = ::DefWindowProcW(hWnd, message, wParam, lParam); + break; + default: + *result = FALSE; + emulateClientAreaMessage(hWnd, message, wParam, lParam); + break; + } } else { // According to MSDN, we should return non-zero for X button messages to // indicate we have handled these messages (due to historical reasons), for @@ -1252,6 +1428,7 @@ (((message >= WM_NCXBUTTONDOWN) && (message <= WM_NCXBUTTONDBLCLK)) ? TRUE : FALSE); + emulateClientAreaMessage(hWnd, message, wParam, lParam); } return true; } @@ -1381,7 +1558,8 @@ // color, our homemade top border can almost have exactly the same // appearance with the system's one. [[maybe_unused]] const auto &hitTestRecorder = qScopeGuard([this, result]() { - lastHitTestResult = getHitWindowPart(int(*result)); // + lastHitTestResultRaw = int(*result); + lastHitTestResult = getHitWindowPart(lastHitTestResultRaw); }); POINT nativeGlobalPos{GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; @@ -1393,15 +1571,24 @@ auto clientWidth = RECT_WIDTH(clientRect); auto clientHeight = RECT_HEIGHT(clientRect); - QPoint qtScenePos = - QHighDpi::fromNativeLocalPosition(point2qpoint(nativeLocalPos), m_windowHandle); + QPoint qtScenePos = QHighDpi::fromNativeLocalPosition(point2qpoint(nativeLocalPos), + m_windowHandle.data()); - bool isFixedSize = m_delegate->isHostSizeFixed(m_host); - bool isTitleBar = isInTitleBarDraggableArea(qtScenePos); + int frameSize = getResizeBorderThickness(hWnd); + + bool isFixedWidth = isHostWidthFixed(); + bool isFixedHeight = isHostHeightFixed(); + bool isFixedSize = isHostSizeFixed(); + bool isInLeftBorder = nativeLocalPos.x <= frameSize; + bool isInTopBorder = nativeLocalPos.y <= frameSize; + bool isInRightBorder = nativeLocalPos.x > clientWidth - frameSize; + bool isInBottomBorder = nativeLocalPos.y > clientHeight - frameSize; + bool isInTitleBar = isInTitleBarDraggableArea(qtScenePos); + WindowAgentBase::SystemButton sysButtonType = WindowAgentBase::Unknown; + bool isInCaptionButtons = isInSystemButtons(qtScenePos, &sysButtonType); bool dontOverrideCursor = false; // ### TODO - WindowAgentBase::SystemButton sysButtonType = WindowAgentBase::Unknown; - if (!isFixedSize && isInSystemButtons(qtScenePos, &sysButtonType)) { + if (isInCaptionButtons) { // Firstly, we set the hit test result to a default value to be able to detect // whether we have changed it or not afterwards. *result = HTNOWHERE; @@ -1411,29 +1598,41 @@ // window is not maximized/fullscreen/minimized, of course). if (isWindowNoState(hWnd)) { static constexpr const quint8 kBorderSize = 2; - bool isTop = (nativeLocalPos.y <= kBorderSize); + bool isTop = nativeLocalPos.y <= kBorderSize; bool isLeft = nativeLocalPos.x <= kBorderSize; - bool isRight = (nativeLocalPos.x >= (clientWidth - kBorderSize)); + bool isRight = nativeLocalPos.x > clientWidth - kBorderSize; if (isTop || isLeft || isRight) { - if (dontOverrideCursor) { + if (isFixedSize || dontOverrideCursor) { // The user doesn't want the window to be resized, so we tell // Windows we are in the client area so that the controls beneath // the mouse cursor can still be hovered or clicked. - *result = (isTitleBar ? HTCAPTION : HTCLIENT); + *result = isInTitleBar ? HTCAPTION : HTCLIENT; } else { if (isTop) { if (isLeft) { - *result = HTTOPLEFT; + if (isFixedWidth) { + *result = HTTOP; + } else if (isFixedHeight) { + *result = HTLEFT; + } else { + *result = HTTOPLEFT; + } } else if (isRight) { - *result = HTTOPRIGHT; + if (isFixedWidth) { + *result = HTTOP; + } else if (isFixedHeight) { + *result = HTRIGHT; + } else { + *result = HTTOPRIGHT; + } } else { - *result = HTTOP; + *result = isFixedHeight ? HTBORDER : HTTOP; } } else { - if (isLeft) { - *result = HTLEFT; + if (isFixedWidth) { + *result = HTBORDER; } else { - *result = HTRIGHT; + *result = isLeft ? HTLEFT : HTRIGHT; } } } @@ -1475,10 +1674,8 @@ // OK, we are not inside any chrome buttons, try to find out which part of the // window are we hitting. - bool max = IsMaximized(hWnd); + bool max = isMaximized(hWnd); bool full = isFullScreen(hWnd); - int frameSize = getResizeBorderThickness(hWnd); - bool isTop = (nativeLocalPos.y < frameSize); if (isSystemBorderEnabled()) { // This will handle the left, right and bottom parts of the frame @@ -1490,8 +1687,51 @@ // outside the window, that is, the three transparent window resize area. // Returning HTCLIENT will confuse Windows, we can't put our controls there // anyway. - *result = ((isFixedSize || dontOverrideCursor) ? HTBORDER - : originalHitTestResult); + *result = HTNOWHERE; // Make sure we can know we don't set any value + // explicitly later. + if (originalHitTestResult == HTCAPTION) { + } else if (isFixedSize || dontOverrideCursor) { + *result = HTBORDER; + } else if (isFixedWidth || isFixedHeight) { + if (originalHitTestResult == HTTOPLEFT) { + if (isFixedWidth) { + *result = HTTOP; + } else { + *result = HTLEFT; + } + } else if (originalHitTestResult == HTTOPRIGHT) { + if (isFixedWidth) { + *result = HTTOP; + } else { + *result = HTRIGHT; + } + } else if (originalHitTestResult == HTBOTTOMRIGHT) { + if (isFixedWidth) { + *result = HTBOTTOM; + } else { + *result = HTRIGHT; + } + } else if (originalHitTestResult == HTBOTTOMLEFT) { + if (isFixedWidth) { + *result = HTBOTTOM; + } else { + *result = HTLEFT; + } + } else if (originalHitTestResult == HTLEFT || + originalHitTestResult == HTRIGHT) { + if (isFixedWidth) { + *result = HTBORDER; + } + } else if (originalHitTestResult == HTTOP || + originalHitTestResult == HTBOTTOM) { + if (isFixedHeight) { + *result = HTBORDER; + } + } + } + if (*result == HTNOWHERE) { + *result = originalHitTestResult; + } return true; } if (full) { @@ -1499,7 +1739,7 @@ return true; } if (max) { - *result = (isTitleBar ? HTCAPTION : HTCLIENT); + *result = isInTitleBar ? HTCAPTION : HTCLIENT; return true; } // At this point, we know that the cursor is inside the client area, @@ -1507,16 +1747,25 @@ // title bar or the drag bar. Apparently, it must be the drag bar or // the little border at the top which the user can use to move or // resize the window. - if (isTop) { + if (isInTopBorder) { // Return HTCLIENT instead of HTBORDER here, because the mouse is // inside our homemade title bar now, return HTCLIENT to let our // title bar can still capture mouse events. - *result = ((isFixedSize || dontOverrideCursor) - ? (isTitleBar ? HTCAPTION : HTCLIENT) - : HTTOP); + *result = [&]() { + if (isFixedSize || isFixedHeight || dontOverrideCursor || + (isFixedWidth && (isInLeftBorder || isInRightBorder))) { + if (isInTitleBar) { + return HTCAPTION; + } else { + return HTCLIENT; + } + } else { + return HTTOP; + } + }(); return true; } - if (isTitleBar) { + if (isInTitleBar) { *result = HTCAPTION; return true; } @@ -1527,58 +1776,86 @@ *result = HTCLIENT; return true; } - if (max) { - *result = (isTitleBar ? HTCAPTION : HTCLIENT); + if (max || isFixedSize || dontOverrideCursor) { + *result = isInTitleBar ? HTCAPTION : HTCLIENT; return true; } - if (!isFixedSize) { - const bool isBottom = (nativeLocalPos.y >= (clientHeight - frameSize)); - // Make the border a little wider to let the user easy to resize on corners. - const auto scaleFactor = ((isTop || isBottom) ? qreal(2) : qreal(1)); - const int scaledFrameSize = std::round(qreal(frameSize) * scaleFactor); - const bool isLeft = (nativeLocalPos.x < scaledFrameSize); - const bool isRight = (nativeLocalPos.x >= (clientWidth - scaledFrameSize)); - if (dontOverrideCursor && (isTop || isBottom || isLeft || isRight)) { - // Return HTCLIENT instead of HTBORDER here, because the mouse is - // inside the window now, return HTCLIENT to let the controls - // inside our window can still capture mouse events. - *result = (isTitleBar ? HTCAPTION : HTCLIENT); - return true; + if (isFixedWidth || isFixedHeight) { + if (isInLeftBorder && isInTopBorder) { + if (isFixedWidth) { + *result = HTTOP; + } else { + *result = HTLEFT; + } + } else if (isInRightBorder && isInTopBorder) { + if (isFixedWidth) { + *result = HTTOP; + } else { + *result = HTRIGHT; + } + } else if (isInRightBorder && isInBottomBorder) { + if (isFixedWidth) { + *result = HTBOTTOM; + } else { + *result = HTRIGHT; + } + } else if (isInLeftBorder && isInBottomBorder) { + if (isFixedWidth) { + *result = HTBOTTOM; + } else { + *result = HTLEFT; + } + } else if (isInLeftBorder || isInRightBorder) { + if (isFixedWidth) { + *result = HTCLIENT; + } else { + *result = isInLeftBorder ? HTLEFT : HTRIGHT; + } + } else if (isInTopBorder || isInBottomBorder) { + if (isFixedHeight) { + *result = HTCLIENT; + } else { + *result = isInTopBorder ? HTTOP : HTBOTTOM; + } + } else { + *result = HTCLIENT; } - if (isTop) { - if (isLeft) { + return true; + } else { + if (isInTopBorder) { + if (isInLeftBorder) { *result = HTTOPLEFT; return true; } - if (isRight) { + if (isInRightBorder) { *result = HTTOPRIGHT; return true; } *result = HTTOP; return true; } - if (isBottom) { - if (isLeft) { + if (isInBottomBorder) { + if (isInLeftBorder) { *result = HTBOTTOMLEFT; return true; } - if (isRight) { + if (isInRightBorder) { *result = HTBOTTOMRIGHT; return true; } *result = HTBOTTOM; return true; } - if (isLeft) { + if (isInLeftBorder) { *result = HTLEFT; return true; } - if (isRight) { + if (isInRightBorder) { *result = HTRIGHT; return true; } } - if (isTitleBar) { + if (isInTitleBar) { *result = HTCAPTION; return true; } @@ -1771,9 +2048,9 @@ // that's also how most applications customize their title bars on Windows. It's // totally OK but since we want to preserve as much original frame as possible, // we can't use that solution. - const LRESULT hitTestResult = ::DefWindowProcW(hWnd, WM_NCCALCSIZE, wParam, lParam); - if ((hitTestResult != HTERROR) && (hitTestResult != HTNOWHERE)) { - *result = hitTestResult; + const LRESULT originalResult = ::DefWindowProcW(hWnd, WM_NCCALCSIZE, wParam, lParam); + if (originalResult != 0) { + *result = originalResult; return true; } // Re-apply the original top from before the size of the default frame was @@ -1787,7 +2064,7 @@ clientRect->top = originalTop; } - const bool max = IsMaximized(hWnd); + const bool max = isMaximized(hWnd); const bool full = isFullScreen(hWnd); // We don't need this correction when we're fullscreen. We will // have the WS_POPUP size, so we don't have to worry about @@ -1892,7 +2169,10 @@ // of the upper-left non-client area. It's confirmed that this issue exists // from Windows 7 to Windows 10. Not tested on Windows 11 yet. Don't know // whether it exists on Windows XP to Windows Vista or not. - *result = wParam ? WVR_REDRAW : FALSE; + + // https://github.com/chromium/chromium/blob/5d297da3cf2a642e9ace2b23fed097370bc70814/ui/views/win/hwnd_message_handler.cc#L2330 + // Do not return WVR_REDRAW otherwise child HWNDs will be mispositioned. + *result = FALSE; return true; } @@ -1902,7 +2182,7 @@ return {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; }; const auto getNativeGlobalPosFromKeyboard = [hWnd]() -> POINT { - const bool maxOrFull = IsMaximized(hWnd) || isFullScreen(hWnd); + const bool maxOrFull = isMaximized(hWnd) || isFullScreen(hWnd); const quint32 frameSize = getResizeBorderThickness(hWnd); const quint32 horizontalOffset = ((maxOrFull || !isSystemBorderEnabled()) ? 0 : frameSize); @@ -1930,12 +2210,16 @@ bool shouldShowSystemMenu = false; bool broughtByKeyboard = false; POINT nativeGlobalPos{}; + switch (message) { case WM_RBUTTONUP: { const POINT nativeLocalPos = getNativePosFromMouse(); - const QPoint qtScenePos = - QHighDpi::fromNativeLocalPosition(point2qpoint(nativeLocalPos), m_windowHandle); - if (isInTitleBarDraggableArea(qtScenePos)) { + const QPoint qtScenePos = QHighDpi::fromNativeLocalPosition( + point2qpoint(nativeLocalPos), m_windowHandle.data()); + WindowAgentBase::SystemButton sysButtonType = WindowAgentBase::Unknown; + if (isInTitleBarDraggableArea(qtScenePos) || + (isInSystemButtons(qtScenePos, &sysButtonType) && + sysButtonType == WindowAgentBase::WindowIcon)) { shouldShowSystemMenu = true; nativeGlobalPos = nativeLocalPos; ::ClientToScreen(hWnd, &nativeGlobalPos); @@ -1951,10 +2235,20 @@ } case WM_SYSCOMMAND: { const WPARAM filteredWParam = (wParam & 0xFFF0); - if ((filteredWParam == SC_KEYMENU) && (lParam == VK_SPACE)) { - shouldShowSystemMenu = true; - broughtByKeyboard = true; - nativeGlobalPos = getNativeGlobalPosFromKeyboard(); + switch (filteredWParam) { + case SC_MOUSEMENU: + shouldShowSystemMenu = true; + nativeGlobalPos = getNativeGlobalPosFromKeyboard(); + break; + case SC_KEYMENU: + if (lParam == VK_SPACE) { + shouldShowSystemMenu = true; + broughtByKeyboard = true; + nativeGlobalPos = getNativeGlobalPosFromKeyboard(); + } + break; + default: + break; } break; } @@ -1973,8 +2267,86 @@ break; } if (shouldShowSystemMenu) { - showSystemMenu2(hWnd, nativeGlobalPos, broughtByKeyboard, - m_delegate->isHostSizeFixed(m_host)); + static HHOOK mouseHook = nullptr; + static std::optional<POINT> mouseClickPos; + static bool mouseDoubleClicked = false; + bool mouseHookedLocal = false; + + // The menu is triggered by a click on icon button + if (iconButtonClickTime > 0) { + POINT menuPos{0, static_cast<LONG>(getTitleBarHeight(hWnd))}; + if (const auto tb = titleBar()) { + auto titleBarHeight = qreal(m_delegate->mapGeometryToScene(tb).height()); + titleBarHeight *= m_windowHandle->devicePixelRatio(); + menuPos.y = qRound(titleBarHeight); + } + ::ClientToScreen(hWnd, &menuPos); + nativeGlobalPos = menuPos; + + // Install mouse hook + if (!mouseHook) { + mouseHook = ::SetWindowsHookExW( + WH_MOUSE, + [](int nCode, WPARAM wParam, LPARAM lParam) { + if (nCode >= 0) { + switch (wParam) { + case WM_LBUTTONDBLCLK: + mouseDoubleClicked = true; + Q_FALLTHROUGH(); + + // case WM_POINTERDOWN: + + case WM_LBUTTONDOWN: { + auto pMouseStruct = + reinterpret_cast<MOUSEHOOKSTRUCT *>(lParam); + if (pMouseStruct) { + mouseClickPos = pMouseStruct->pt; + } + break; + } + default: + break; + } + } + return ::CallNextHookEx(nullptr, nCode, wParam, lParam); + }, + nullptr, ::GetCurrentThreadId()); + mouseHookedLocal = true; + } + } + + bool res = + showSystemMenu_sys(hWnd, nativeGlobalPos, broughtByKeyboard, isHostSizeFixed()); + + // Uninstall mouse hook and check if it's a double-click + if (mouseHookedLocal) { + ::UnhookWindowsHookEx(mouseHook); + + // Emulate the Windows icon button's behavior + if (!res && mouseClickPos.has_value()) { + POINT nativeLocalPos = mouseClickPos.value(); + ::ScreenToClient(hWnd, &nativeLocalPos); + QPoint qtScenePos = QHighDpi::fromNativeLocalPosition( + point2qpoint(nativeLocalPos), m_windowHandle.data()); + WindowAgentBase::SystemButton sysButtonType = WindowAgentBase::Unknown; + if (isInSystemButtons(qtScenePos, &sysButtonType) && + sysButtonType == WindowAgentBase::WindowIcon) { + iconButtonClickLevel |= IconButtonClicked; + if (::GetTickCount64() - iconButtonClickTime <= ::GetDoubleClickTime()) { + iconButtonClickLevel |= IconButtonTriggersClose; + } + } + } + + if (mouseDoubleClicked) { + iconButtonClickLevel |= IconButtonDoubleClicked; + } + + mouseHook = nullptr; + mouseClickPos.reset(); + mouseDoubleClicked = false; + } + // QPA's internal code will handle system menu events separately, and its // behavior is not what we would want to see because it doesn't know our // window doesn't have any window frame now, so return early here to avoid -- Gitblit v1.9.1