Sine Striker
2024-05-08 2d223770ee33ddf7d93d83bc2cb538bfd66d38d9
src/core/contexts/win32windowcontext.cpp
@@ -33,14 +33,6 @@
#include "qwkglobal_p.h"
#include "qwkwindowsextra_p.h"
// https://github.com/qt/qtbase/blob/6.6.1/src/plugins/platforms/windows/qwindowswindow.cpp#L2791
// https://github.com/qt/qtbase/blob/6.6.1/src/plugins/platforms/windows/qwindowswindow.cpp#L3321
// This issue exists only in Qt 6.6.1, and was introduced by QTBUG-113736 patch and fixed by
// QTBUG-117704 patch.
#if !QWINDOWKIT_CONFIG(ENABLE_WINDOWS_SYSTEM_BORDERS) && QT_VERSION == QT_VERSION_CHECK(6, 6, 1)
#  define QTBUG_113736_WORKAROUND
#endif
namespace QWK {
    // The thickness of an auto-hide taskbar in pixels.
@@ -241,14 +233,15 @@
        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);
@@ -292,7 +285,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
@@ -301,11 +295,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) {
@@ -490,17 +485,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;
        }
@@ -589,13 +605,15 @@
            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
@@ -613,20 +631,10 @@
    }
    static inline void addManagedWindow(QWindow *window, HWND hWnd, Win32WindowContext *ctx) {
#ifndef QTBUG_113736_WORKAROUND
        const auto margins = [](HWND 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};
            }
        }(hWnd);
        // Inform Qt we want and have set custom margins
        setInternalWindowFrameMargins(window, margins);
#endif
        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) {
@@ -689,8 +697,8 @@
#else
                const QPoint nativeGlobalPos = QHighDpi::toNativePixels(pos, m_windowHandle);
#endif
                showSystemMenu2(hWnd, qpoint2point(nativeGlobalPos), false,
                                m_delegate->isHostSizeFixed(m_host));
                std::ignore = showSystemMenu_sys(hWnd, qpoint2point(nativeGlobalPos), false,
                                                 m_delegate->isHostSizeFixed(m_host));
                return;
            }
@@ -808,7 +816,6 @@
                       ? int(getWindowFrameBorderThickness(reinterpret_cast<HWND>(m_windowId)))
                       : 0;
        }
        return AbstractWindowContext::windowAttribute(key);
    }
@@ -816,11 +823,12 @@
        // Reset the context data
        mouseLeaveBlocked = false;
        lastHitTestResult = WindowPart::Outside;
        lastHitTestResultRaw = HTNOWHERE;
#ifdef QTBUG_113736_WORKAROUND
        m_delegate->setWindowFlags(m_host,
                                   m_delegate->getWindowFlags(m_host) | Qt::FramelessWindowHint);
#endif
        if (!isSystemBorderEnabled()) {
            m_delegate->setWindowFlags(m_host, m_delegate->getWindowFlags(m_host) |
                                                   Qt::FramelessWindowHint);
        }
        // If the original window id is valid, remove all resources related
        if (oldWinId) {
@@ -1294,9 +1302,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
@@ -1355,14 +1362,46 @@
                    }
                }
                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, WM_NCLBUTTONDOWN, wParam, lParam);
                                    if (iconButtonClickLevel == 2) {
                                        ::PostMessageW(hWnd, WM_SYSCOMMAND, SC_CLOSE, 0);
                                    }
                                    // No need to reset `iconButtonClickLevel`, 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
@@ -1371,6 +1410,7 @@
                            (((message >= WM_NCXBUTTONDOWN) && (message <= WM_NCXBUTTONDBLCLK))
                                 ? TRUE
                                 : FALSE);
                        emulateClientAreaMessage(hWnd, message, wParam, lParam);
                    }
                    return true;
                }
@@ -1500,7 +1540,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)};
@@ -2049,6 +2090,7 @@
        bool shouldShowSystemMenu = false;
        bool broughtByKeyboard = false;
        POINT nativeGlobalPos{};
        switch (message) {
            case WM_RBUTTONUP: {
                const POINT nativeLocalPos = getNativePosFromMouse();
@@ -2070,10 +2112,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;
            }
@@ -2092,8 +2144,69 @@
                break;
        }
        if (shouldShowSystemMenu) {
            showSystemMenu2(hWnd, nativeGlobalPos, broughtByKeyboard,
                            m_delegate->isHostSizeFixed(m_host));
            static HHOOK mouseHook = nullptr;
            static std::optional<POINT> mouseClickPos;
            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) {
                                if (wParam == WM_LBUTTONDOWN || wParam == WM_LBUTTONDBLCLK) {
                                    auto pMouseStruct = reinterpret_cast<MOUSEHOOKSTRUCT *>(lParam);
                                    if (pMouseStruct) {
                                        mouseClickPos = pMouseStruct->pt;
                                    }
                                }
                            }
                            return ::CallNextHookEx(nullptr, nCode, wParam, lParam);
                        },
                        nullptr, ::GetCurrentThreadId());
                    mouseHookedLocal = true;
                }
            }
            bool res = showSystemMenu_sys(hWnd, nativeGlobalPos, broughtByKeyboard,
                                          m_delegate->isHostSizeFixed(m_host));
            // Uninstall mouse hook and check if it's a double-click
            if (mouseHookedLocal) {
                ::UnhookWindowsHookEx(mouseHook);
                // Emulate the Windows icon button's behavior
                static uint32_t doubleClickTime = ::GetDoubleClickTime();
                if (!res && mouseClickPos.has_value()) {
                    POINT nativeLocalPos = mouseClickPos.value();
                    ::ScreenToClient(hWnd, &nativeLocalPos);
                    QPoint qtScenePos = QHighDpi::fromNativeLocalPosition(
                        point2qpoint(nativeLocalPos), m_windowHandle);
                    WindowAgentBase::SystemButton sysButtonType = WindowAgentBase::Unknown;
                    if (isInSystemButtons(qtScenePos, &sysButtonType) &&
                        sysButtonType == WindowAgentBase::WindowIcon) {
                        iconButtonClickLevel = 1;
                        if (::GetTickCount64() - iconButtonClickTime <= doubleClickTime) {
                            iconButtonClickLevel = 2;
                        }
                    }
                }
                mouseHook = nullptr;
                mouseClickPos.reset();
            }
            // 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