Sine Striker
2023-12-03 7a54badb4a5782c551deb2919f3c489fdb4fbc02
src/core/contexts/win32windowcontext.cpp
@@ -1,8 +1,12 @@
#include "win32windowcontext_p.h"
#include <optional>
#include <QtCore/QHash>
#include <QtCore/QAbstractNativeEventFilter>
#include <QtCore/QCoreApplication>
#include <QtGui/private/qhighdpiscaling_p.h>
#include "qwkcoreglobal_p.h"
@@ -30,6 +34,76 @@
    };
    static WindowsNativeEventFilter *g_nativeFilter = nullptr;
    static inline QPoint fromNativeLocalPosition(const QWindow *window, const QPoint &point) {
#if 1
        return QHighDpi::fromNativeLocalPosition(point, window);
#else
        return QPointF(QPointF(point) / window->devicePixelRatio()).toPoint();
#endif
    }
    static inline Win32WindowContext::WindowPart getHitWindowPart(int hitTestResult) {
        switch (hitTestResult) {
            case HTCLIENT:
                return Win32WindowContext::ClientArea;
            case HTCAPTION:
                return Win32WindowContext::TitleBar;
            case HTSYSMENU:
            case HTHELP:
            case HTREDUCE:
            case HTZOOM:
            case HTCLOSE:
                return Win32WindowContext::ChromeButton;
            case HTLEFT:
            case HTRIGHT:
            case HTTOP:
            case HTTOPLEFT:
            case HTTOPRIGHT:
            case HTBOTTOM:
            case HTBOTTOMLEFT:
            case HTBOTTOMRIGHT:
                return Win32WindowContext::ResizeBorder;
            case HTBORDER:
                return Win32WindowContext::FixedBorder;
            default:
                break;
        }
        return Win32WindowContext::Outside;
    }
    static bool isValidWindow(WId windowId, bool checkVisible, bool checkTopLevel) {
        const auto hwnd = reinterpret_cast<HWND>(windowId);
        if (::IsWindow(hwnd) == FALSE) {
            return false;
        }
        const LONG_PTR styles = ::GetWindowLongPtrW(hwnd, GWL_STYLE);
        if ((styles == 0) || (styles & WS_DISABLED)) {
            return false;
        }
        const LONG_PTR exStyles = ::GetWindowLongPtrW(hwnd, GWL_EXSTYLE);
        if ((exStyles == 0) || (exStyles & WS_EX_TOOLWINDOW)) {
            return false;
        }
        RECT rect = {0, 0, 0, 0};
        if (::GetWindowRect(hwnd, &rect) == FALSE) {
            return false;
        }
        if ((rect.left >= rect.right) || (rect.top >= rect.bottom)) {
            return false;
        }
        if (checkVisible) {
            if (::IsWindowVisible(hwnd) == FALSE) {
                return false;
            }
        }
        if (checkTopLevel) {
            if (::GetAncestor(hwnd, GA_ROOT) != hwnd) {
                return false;
            }
        }
        return true;
    }
    // https://github.com/qt/qtbase/blob/e26a87f1ecc40bc8c6aa5b889fce67410a57a702/src/plugins/platforms/windows/qwindowscontext.cpp#L1025
    // We can see from the source code that Qt will filter out some messages first and then send the
@@ -108,7 +182,7 @@
    }
    Win32WindowContext::Win32WindowContext(QWindow *window, WindowItemDelegate *delegate)
        : AbstractWindowContext(window, delegate), windowId(0) {
        : AbstractWindowContext(window, delegate) {
    }
    Win32WindowContext::~Win32WindowContext() {
@@ -158,7 +232,31 @@
                                        LRESULT *result) {
        *result = FALSE;
        Q_UNUSED(windowId)
        // We should skip these messages otherwise we will get crashes.
        // NOTE: WM_QUIT won't be posted to the WindowProc function.
        switch (message) {
            case WM_CLOSE:
            case WM_DESTROY:
            case WM_NCDESTROY:
            // Undocumented messages:
            case WM_UAHDESTROYWINDOW:
            case WM_UNREGISTER_WINDOW_SERVICES:
                return false;
            default:
                break;
        }
        if (!isValidWindow(windowId, false, true)) {
            return false;
        }
        // Test snap layout
        if (snapLayoutHandler(hWnd, message, wParam, lParam, result)) {
            return true;
        }
        // TODO: Uncomment and do something
        // bool frameBorderVisible = Utils::isWindowFrameBorderVisible();
        // TODO: Implement
        // ...
@@ -166,4 +264,282 @@
        return false; // Not handled
    }
    static constexpr const auto kMessageTag = WPARAM(0x97CCEA99);
    static inline constexpr bool isTaggedMessage(WPARAM wParam) {
        return (wParam == kMessageTag);
    }
    static quint64 getKeyState() {
        quint64 result = 0;
        const auto &get = [](const int virtualKey) -> bool {
            return (::GetAsyncKeyState(virtualKey) < 0);
        };
        const bool buttonSwapped = (::GetSystemMetrics(SM_SWAPBUTTON) != FALSE);
        if (get(VK_LBUTTON)) {
            result |= (buttonSwapped ? MK_RBUTTON : MK_LBUTTON);
        }
        if (get(VK_RBUTTON)) {
            result |= (buttonSwapped ? MK_LBUTTON : MK_RBUTTON);
        }
        if (get(VK_SHIFT)) {
            result |= MK_SHIFT;
        }
        if (get(VK_CONTROL)) {
            result |= MK_CONTROL;
        }
        if (get(VK_MBUTTON)) {
            result |= MK_MBUTTON;
        }
        if (get(VK_XBUTTON1)) {
            result |= MK_XBUTTON1;
        }
        if (get(VK_XBUTTON2)) {
            result |= MK_XBUTTON2;
        }
        return result;
    }
    static void emulateClientAreaMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam,
                                         const std::optional<int> &overrideMessage = std::nullopt) {
        const int myMsg = overrideMessage.value_or(message);
        const auto wParamNew = [myMsg, wParam]() -> WPARAM {
            if (myMsg == WM_NCMOUSELEAVE) {
                // wParam is always ignored in mouse leave messages, but here we
                // give them a special tag to be able to distinguish which messages
                // are sent by ourselves.
                return kMessageTag;
            }
            const quint64 keyState = getKeyState();
            if ((myMsg >= WM_NCXBUTTONDOWN) && (myMsg <= WM_NCXBUTTONDBLCLK)) {
                const auto xButtonMask = GET_XBUTTON_WPARAM(wParam);
                return MAKEWPARAM(keyState, xButtonMask);
            }
            return keyState;
        }();
        const auto lParamNew = [myMsg, lParam, hWnd]() -> LPARAM {
            if (myMsg == WM_NCMOUSELEAVE) {
                // lParam is always ignored in mouse leave messages.
                return 0;
            }
            const auto screenPos = POINT{GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
            POINT clientPos = screenPos;
            if (::ScreenToClient(hWnd, &clientPos) == FALSE) {
                return 0;
            }
            return MAKELPARAM(clientPos.x, clientPos.y);
        }();
#if 0
#  define SEND_MESSAGE ::SendMessageW
#else
#  define SEND_MESSAGE ::PostMessageW
#endif
        switch (myMsg) {
            case WM_NCHITTEST: // Treat hit test messages as mouse move events.
            case WM_NCMOUSEMOVE:
                SEND_MESSAGE(hWnd, WM_MOUSEMOVE, wParamNew, lParamNew);
                break;
            case WM_NCLBUTTONDOWN:
                SEND_MESSAGE(hWnd, WM_LBUTTONDOWN, wParamNew, lParamNew);
                break;
            case WM_NCLBUTTONUP:
                SEND_MESSAGE(hWnd, WM_LBUTTONUP, wParamNew, lParamNew);
                break;
            case WM_NCLBUTTONDBLCLK:
                SEND_MESSAGE(hWnd, WM_LBUTTONDBLCLK, wParamNew, lParamNew);
                break;
            case WM_NCRBUTTONDOWN:
                SEND_MESSAGE(hWnd, WM_RBUTTONDOWN, wParamNew, lParamNew);
                break;
            case WM_NCRBUTTONUP:
                SEND_MESSAGE(hWnd, WM_RBUTTONUP, wParamNew, lParamNew);
                break;
            case WM_NCRBUTTONDBLCLK:
                SEND_MESSAGE(hWnd, WM_RBUTTONDBLCLK, wParamNew, lParamNew);
                break;
            case WM_NCMBUTTONDOWN:
                SEND_MESSAGE(hWnd, WM_MBUTTONDOWN, wParamNew, lParamNew);
                break;
            case WM_NCMBUTTONUP:
                SEND_MESSAGE(hWnd, WM_MBUTTONUP, wParamNew, lParamNew);
                break;
            case WM_NCMBUTTONDBLCLK:
                SEND_MESSAGE(hWnd, WM_MBUTTONDBLCLK, wParamNew, lParamNew);
                break;
            case WM_NCXBUTTONDOWN:
                SEND_MESSAGE(hWnd, WM_XBUTTONDOWN, wParamNew, lParamNew);
                break;
            case WM_NCXBUTTONUP:
                SEND_MESSAGE(hWnd, WM_XBUTTONUP, wParamNew, lParamNew);
                break;
            case WM_NCXBUTTONDBLCLK:
                SEND_MESSAGE(hWnd, WM_XBUTTONDBLCLK, wParamNew, lParamNew);
                break;
#if 0 // ### TODO: How to handle touch events?
        case WM_NCPOINTERUPDATE:
        case WM_NCPOINTERDOWN:
        case WM_NCPOINTERUP:
            break;
#endif
            case WM_NCMOUSEHOVER:
                SEND_MESSAGE(hWnd, WM_MOUSEHOVER, wParamNew, lParamNew);
                break;
            case WM_NCMOUSELEAVE:
                SEND_MESSAGE(hWnd, WM_MOUSELEAVE, wParamNew, lParamNew);
                break;
            default:
                break;
        }
#undef SEND_MESSAGE
    }
    static bool requestForMouseLeaveMessage(HWND hWnd, bool nonClient) {
        TRACKMOUSEEVENT tme;
        SecureZeroMemory(&tme, sizeof(tme));
        tme.cbSize = sizeof(tme);
        tme.dwFlags = TME_LEAVE;
        if (nonClient) {
            tme.dwFlags |= TME_NONCLIENT;
        }
        tme.hwndTrack = hWnd;
        tme.dwHoverTime = HOVER_DEFAULT;
        if (::TrackMouseEvent(&tme) == FALSE) {
            return false;
        }
        return true;
    }
    bool Win32WindowContext::snapLayoutHandler(HWND hWnd, UINT message, WPARAM wParam,
                                                     LPARAM lParam, LRESULT *result) {
        switch (message) {
            case WM_MOUSELEAVE: {
                if (!isTaggedMessage(wParam)) {
                    // Qt will call TrackMouseEvent() to get the WM_MOUSELEAVE message when it
                    // receives WM_MOUSEMOVE messages, and since we are converting every
                    // WM_NCMOUSEMOVE message to WM_MOUSEMOVE message and send it back to the window
                    // to be able to hover our controls, we also get lots of WM_MOUSELEAVE messages
                    // at the same time because of the reason above, and these superfluous mouse
                    // leave events cause Qt to think the mouse has left the control, and thus we
                    // actually lost the hover state. So we filter out these superfluous mouse leave
                    // events here to avoid this issue.
                    DWORD dwScreenPos = ::GetMessagePos();
                    QPoint qtScenePos =
                        fromNativeLocalPosition(m_windowHandle, QPoint(GET_X_LPARAM(dwScreenPos),
                                                                       GET_Y_LPARAM(dwScreenPos)));
                    auto dummy = CoreWindowAgent::Unknown;
                    if (isInSystemButtons(qtScenePos, &dummy)) {
                        mouseLeaveBlocked = true;
                        *result = FALSE;
                        return true;
                    }
                }
                mouseLeaveBlocked = false;
                break;
            }
            case WM_MOUSEMOVE: {
                if ((lastHitTestResult != WindowPart::ChromeButton) && mouseLeaveBlocked) {
                    mouseLeaveBlocked = false;
                    std::ignore = requestForMouseLeaveMessage(hWnd, false);
                }
                break;
            }
            case WM_NCMOUSEMOVE:
            case WM_NCLBUTTONDOWN:
            case WM_NCLBUTTONUP:
            case WM_NCLBUTTONDBLCLK:
            case WM_NCRBUTTONDOWN:
            case WM_NCRBUTTONUP:
            case WM_NCRBUTTONDBLCLK:
            case WM_NCMBUTTONDOWN:
            case WM_NCMBUTTONUP:
            case WM_NCMBUTTONDBLCLK:
            case WM_NCXBUTTONDOWN:
            case WM_NCXBUTTONUP:
            case WM_NCXBUTTONDBLCLK:
#if 0 // ### TODO: How to handle touch events?
    case WM_NCPOINTERUPDATE:
    case WM_NCPOINTERDOWN:
    case WM_NCPOINTERUP:
#endif
            case WM_NCMOUSEHOVER: {
                const WindowPart currentWindowPart = lastHitTestResult;
                if (message == WM_NCMOUSEMOVE) {
                    if (currentWindowPart != WindowPart::ChromeButton) {
                        std::ignore = m_delegate->resetQtGrabbedControl();
                        if (mouseLeaveBlocked) {
                            emulateClientAreaMessage(hWnd, message, wParam, lParam,
                                                     WM_NCMOUSELEAVE);
                        }
                    }
                    // We need to make sure we get the right hit-test result when a WM_NCMOUSELEAVE
                    // comes, so we reset it when we receive a WM_NCMOUSEMOVE.
                    // If the mouse is entering the client area, there must be a WM_NCHITTEST
                    // setting it to `Client` before the WM_NCMOUSELEAVE comes; If the mouse is
                    // leaving the window, current window part remains as `Outside`.
                    lastHitTestResult = WindowPart::Outside;
                }
                if (currentWindowPart == WindowPart::ChromeButton) {
                    emulateClientAreaMessage(hWnd, message, wParam, lParam);
                    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);
                    } 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
                        // all other messages we should return zero instead.
                        *result =
                            (((message >= WM_NCXBUTTONDOWN) && (message <= WM_NCXBUTTONDBLCLK))
                                 ? TRUE
                                 : FALSE);
                    }
                    return true;
                }
                break;
            }
            case WM_NCMOUSELEAVE: {
                const WindowPart currentWindowPart = lastHitTestResult;
                if (currentWindowPart == WindowPart::ChromeButton) {
                    // If we press on the chrome button and move mouse, Windows will take the
                    // pressing area as HTCLIENT which maybe because of our former retransmission of
                    // WM_NCLBUTTONDOWN, as a result, a WM_NCMOUSELEAVE will come immediately and a
                    // lot of WM_MOUSEMOVE will come if we move the mouse, we should track the mouse
                    // in advance.
                    if (mouseLeaveBlocked) {
                        mouseLeaveBlocked = false;
                        std::ignore = requestForMouseLeaveMessage(hWnd, false);
                    }
                } else {
                    if (mouseLeaveBlocked) {
                        // The mouse is moving from the chrome button to other non-client area, we
                        // should emulate a WM_MOUSELEAVE message to reset the button state.
                        emulateClientAreaMessage(hWnd, message, wParam, lParam, WM_NCMOUSELEAVE);
                    }
                    if (currentWindowPart == WindowPart::Outside) {
                        // Notice: we're not going to clear window part cache when the mouse leaves
                        // window from client area, which means we will get previous window part as
                        // HTCLIENT if the mouse leaves window from client area and enters window
                        // from non-client area, but it has no bad effect.
                        std::ignore = m_delegate->resetQtGrabbedControl();
                    }
                }
                break;
            }
            default:
                break;
        }
        return false;
    }
}