From a0bede6ff6a700a6eea9702c49d378f07de22f63 Mon Sep 17 00:00:00 2001
From: Sine Striker <trueful@163.com>
Date: 周三, 06 12月 2023 12:11:37 +0800
Subject: [PATCH] Fix title bar hover bug

---
 src/core/contexts/win32windowcontext.cpp |  294 ++++++++++++++++++++++++++++++++++++++++++++++++----------
 1 files changed, 240 insertions(+), 54 deletions(-)

diff --git a/src/core/contexts/win32windowcontext.cpp b/src/core/contexts/win32windowcontext.cpp
index 09c2b83..8800a7f 100644
--- a/src/core/contexts/win32windowcontext.cpp
+++ b/src/core/contexts/win32windowcontext.cpp
@@ -1,19 +1,31 @@
 #include "win32windowcontext_p.h"
+#include "qwkcoreglobal_p.h"
 
 #include <optional>
 
 #include <QtCore/QHash>
 #include <QtCore/QAbstractNativeEventFilter>
-#include <QtCore/QCoreApplication>
 #include <QtCore/QOperatingSystemVersion>
+#include <QtCore/QScopeGuard>
+#include <QtCore/QTimer>
+#include <QtGui/QGuiApplication>
 
 #include <QtCore/private/qsystemlibrary_p.h>
 #include <QtGui/private/qhighdpiscaling_p.h>
-
-#include "qwkcoreglobal_p.h"
+#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+#  include <QtGui/private/qguiapplication_p.h>
+#endif
+#include <QtGui/qpa/qplatformwindow.h>
+#if QT_VERSION < QT_VERSION_CHECK(6, 2, 0)
+#  include <QtGui/qpa/qplatformnativeinterface.h>
+#else
+#  include <QtGui/qpa/qplatformwindow_p.h>
+#endif
 
 #include <shellscalingapi.h>
 #include <dwmapi.h>
+
+Q_DECLARE_METATYPE(QMargins)
 
 namespace QWK {
 
@@ -25,8 +37,15 @@
 
     static WNDPROC g_qtWindowProc = nullptr; // Original Qt window proc function
 
+    static struct QWK_Hook {
+        QWK_Hook() {
+            qApp->setAttribute(Qt::AA_DontCreateNativeWidgetSiblings);
+        }
+    } g_hook{};
+
     struct DynamicApis {
         decltype(&::DwmFlush) pDwmFlush = nullptr;
+        decltype(&::DwmIsCompositionEnabled) pDwmIsCompositionEnabled = nullptr;
         decltype(&::GetDpiForWindow) pGetDpiForWindow = nullptr;
         decltype(&::GetSystemMetricsForDpi) pGetSystemMetricsForDpi = nullptr;
         decltype(&::GetDpiForMonitor) pGetDpiForMonitor = nullptr;
@@ -44,6 +63,8 @@
 
             QSystemLibrary dwmapi(QStringLiteral("dwmapi.dll"));
             pDwmFlush = reinterpret_cast<decltype(pDwmFlush)>(dwmapi.resolve("DwmFlush"));
+            pDwmIsCompositionEnabled = reinterpret_cast<decltype(pDwmIsCompositionEnabled)>(
+                dwmapi.resolve("DwmIsCompositionEnabled"));
         }
 
         ~DynamicApis() = default;
@@ -137,6 +158,12 @@
         return hwnd2str(reinterpret_cast<WId>(hwnd));
     }
 
+    static inline bool isWin8OrGreater() {
+        static const bool result =
+            QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows8;
+        return result;
+    }
+
     static inline bool isWin8Point1OrGreater() {
         static const bool result =
             QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows8_1;
@@ -149,10 +176,32 @@
         return result;
     }
 
+    static inline bool isDwmCompositionEnabled() {
+        if (isWin8OrGreater()) {
+            return true;
+        }
+        const DynamicApis &apis = DynamicApis::instance();
+        if (!apis.pDwmIsCompositionEnabled) {
+            return false;
+        }
+        BOOL enabled = FALSE;
+        return SUCCEEDED(apis.pDwmIsCompositionEnabled(&enabled)) && enabled;
+    }
+
+    static inline void triggerFrameChange(HWND hwnd) {
+        Q_ASSERT(hwnd);
+        if (!hwnd) {
+            return;
+        }
+        ::SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
+                       SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER |
+                           SWP_FRAMECHANGED);
+    }
+
     static inline quint32 getDpiForWindow(HWND hwnd) {
         Q_ASSERT(hwnd);
         if (!hwnd) {
-            return USER_DEFAULT_SCREEN_DPI;
+            return 0;
         }
         const DynamicApis &apis = DynamicApis::instance();
         if (apis.pGetDpiForWindow) {         // Win10
@@ -187,6 +236,55 @@
         }
     }
 
+    static inline quint32 getTitleBarHeight(HWND hwnd) {
+        Q_ASSERT(hwnd);
+        if (!hwnd) {
+            return 0;
+        }
+        const auto captionHeight = [hwnd]() -> int {
+            const DynamicApis &apis = DynamicApis::instance();
+            if (apis.pGetSystemMetricsForDpi) {
+                const quint32 dpi = getDpiForWindow(hwnd);
+                return apis.pGetSystemMetricsForDpi(SM_CYCAPTION, dpi);
+            } else {
+                return ::GetSystemMetrics(SM_CYCAPTION);
+            }
+        }();
+        return captionHeight + getResizeBorderThickness(hwnd);
+    }
+
+    static inline void updateInternalWindowFrameMargins(HWND hwnd, QWindow *window) {
+        Q_ASSERT(hwnd);
+        Q_ASSERT(window);
+        if (!hwnd || !window) {
+            return;
+        }
+        const auto margins = [hwnd]() -> QMargins {
+            const int titleBarHeight = getTitleBarHeight(hwnd);
+            if (isWin10OrGreater()) {
+                return {0, -titleBarHeight, 0, 0};
+            } else {
+                const int frameSize = getResizeBorderThickness(hwnd);
+                return {-frameSize, -titleBarHeight, -frameSize, -frameSize};
+            }
+        }();
+        const QVariant marginsVar = QVariant::fromValue(margins);
+        window->setProperty("_q_windowsCustomMargins", marginsVar);
+#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
+        if (QPlatformWindow *platformWindow = window->handle()) {
+            if (const auto ni = QGuiApplication::platformNativeInterface()) {
+                ni->setWindowProperty(platformWindow, QStringLiteral("_q_windowsCustomMargins"),
+                                      marginsVar);
+            }
+        }
+#else
+        if (const auto platformWindow =
+                dynamic_cast<QNativeInterface::Private::QWindowsWindow *>(window->handle())) {
+            platformWindow->setCustomMargins(margins);
+        }
+#endif
+    }
+
     static inline std::optional<MONITORINFOEXW> getMonitorForWindow(HWND hwnd) {
         Q_ASSERT(hwnd);
         if (!hwnd) {
@@ -213,8 +311,7 @@
         return (windowRect == mi.value_or(MONITORINFOEXW{}).rcMonitor);
     }
 
-    static inline bool isWindowNoState(HWND hwnd)
-    {
+    static inline bool isWindowNoState(HWND hwnd) {
         Q_ASSERT(hwnd);
         if (!hwnd) {
             return false;
@@ -266,7 +363,7 @@
             case HTBORDER:
                 return Win32WindowContext::FixedBorder;
             default:
-                break;
+                break; // unreachable
         }
         return Win32WindowContext::Outside;
     }
@@ -314,11 +411,14 @@
     public:
         bool nativeEventFilter(const QByteArray &eventType, void *message,
                                QT_NATIVE_EVENT_RESULT_TYPE *result) override {
+            Q_UNUSED(eventType);
+
             // It has been observed that the pointer that Qt gives us is sometimes null on some
             // machines. We need to guard against it in such scenarios.
             if (!result) {
                 return false;
             }
+
             if (lastMessageHandled) {
                 *result = static_cast<QT_NATIVE_EVENT_RESULT_TYPE>(lastMessageResult);
                 return true;
@@ -332,11 +432,11 @@
 
         static inline void install() {
             instance = new WindowsNativeEventFilter();
-            qApp->installNativeEventFilter(instance);
+            installNativeEventFilter(instance);
         }
 
         static inline void uninstall() {
-            qApp->removeNativeEventFilter(instance);
+            removeNativeEventFilter(instance);
             delete instance;
             instance = nullptr;
         }
@@ -419,13 +519,16 @@
         // TODO: Determine whether to show system menu
         // ...
 
-        // Since Qt does the necessary processing of the message afterward, we still need to
+        // Since Qt does the necessary processing of the WM_NCCALCSIZE afterward, we still need to
         // continue dispatching it.
+        if (message != WM_NCCALCSIZE && handled) {
+            return result;
+        }
         return ::CallWindowProcW(g_qtWindowProc, hWnd, message, wParam, lParam);
     }
 
-    Win32WindowContext::Win32WindowContext(QWindow *window, WindowItemDelegate *delegate)
-        : AbstractWindowContext(window, delegate) {
+    Win32WindowContext::Win32WindowContext(QObject *host, WindowItemDelegate *delegate)
+        : AbstractWindowContext(host, delegate) {
     }
 
     Win32WindowContext::~Win32WindowContext() {
@@ -441,10 +544,16 @@
     }
 
     bool Win32WindowContext::setup() {
-        auto winId = m_windowHandle->winId();
+        if (!m_windowHandle) {
+            return false;
+        }
 
         // Install window hook
+        auto winId = m_windowHandle->winId();
         auto hWnd = reinterpret_cast<HWND>(winId);
+
+        // Inform Qt we want and have set custom margins
+        updateInternalWindowFrameMargins(hWnd, m_windowHandle);
 
         // Store original window proc
         if (!g_qtWindowProc) {
@@ -464,6 +573,8 @@
 
         // Save window handle mapping
         g_wndProcHash->insert(hWnd, this);
+
+        QTimer::singleShot(0, m_windowHandle, [hWnd]() { triggerFrameChange(hWnd); });
 
         return true;
     }
@@ -657,9 +768,10 @@
                     // actually lost the hover state. So we filter out these superfluous mouse leave
                     // events here to avoid this issue.
                     DWORD dwScreenPos = ::GetMessagePos();
+                    POINT screenPoint{GET_X_LPARAM(dwScreenPos), GET_Y_LPARAM(dwScreenPos)};
+                    ::ScreenToClient(hWnd, &screenPoint);
                     QPoint qtScenePos =
-                        fromNativeLocalPosition(m_windowHandle, QPoint(GET_X_LPARAM(dwScreenPos),
-                                                                       GET_Y_LPARAM(dwScreenPos)));
+                        fromNativeLocalPosition(m_windowHandle, {screenPoint.x, screenPoint.y});
                     auto dummy = CoreWindowAgent::Unknown;
                     if (isInSystemButtons(qtScenePos, &dummy)) {
                         // We must record whether the last WM_MOUSELEAVE was filtered, because if
@@ -1087,45 +1199,50 @@
                 // window can behave just like a normal Win32 window even if we now
                 // doesn't have a title bar at all. Most importantly, the flicker and
                 // jitter during window resizing is totally gone now. The disadvantage
-                // is we have to draw a top frame border ourself. Previously I thought
+                // is we have to draw a top frame border ourselves. Previously I thought
                 // we have to do the black magic in WM_PAINT just like what Windows
                 // Terminal does, however, later I found that if we choose a proper
                 // color, our homemade top border can almost have exactly the same
                 // appearance with the system's one.
 
-                const auto nativeGlobalPos = POINT{ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
+                [[maybe_unused]] const auto &hitTestRecorder = qScopeGuard([this, result]() {
+                    lastHitTestResult = getHitWindowPart(int(*result)); //
+                });
+
+                POINT nativeGlobalPos{GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
                 POINT nativeLocalPos = nativeGlobalPos;
                 ::ScreenToClient(hWnd, &nativeLocalPos);
 
-                auto clientRect = RECT{ 0, 0, 0, 0 };
+                RECT clientRect{0, 0, 0, 0};
                 ::GetClientRect(hWnd, &clientRect);
-                const auto clientWidth = RECT_WIDTH(clientRect);
-                const auto clientHeight = RECT_HEIGHT(clientRect);
+                auto clientWidth = RECT_WIDTH(clientRect);
+                auto clientHeight = RECT_HEIGHT(clientRect);
 
-                const QPoint qtScenePos = fromNativeLocalPosition(m_windowHandle, QPoint(nativeLocalPos.x, nativeLocalPos.y));
+                QPoint qtScenePos = fromNativeLocalPosition(
+                    m_windowHandle, QPoint(nativeLocalPos.x, nativeLocalPos.y));
 
-                const bool isFixedSize = /*isWindowFixedSize()*/false; // ### FIXME
-                const bool isTitleBar = isInTitleBarDraggableArea(qtScenePos);
-                const bool dontOverrideCursor = false; // ### TODO
+                bool isFixedSize = m_delegate->isHostSizeFixed(m_host);
+                bool isTitleBar = isInTitleBarDraggableArea(qtScenePos);
+                bool dontOverrideCursor = false; // ### TODO
 
                 CoreWindowAgent::SystemButton sysButtonType = CoreWindowAgent::Unknown;
                 if (!isFixedSize && isInSystemButtons(qtScenePos, &sysButtonType)) {
-                    // Firstly, we set the hit test result to a default value to be able to detect whether we
-                    // have changed it or not afterwards.
+                    // 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;
-                    // Even if the mouse is inside the chrome button area now, we should still allow the user
-                    // to be able to resize the window with the top or right window border, this is also the
-                    // normal behavior of a native Win32 window (but only when the window is not maximized/
-                    // fullscreened/minimized, of course).
+                    // Even if the mouse is inside the chrome button area now, we should still allow
+                    // the user to be able to resize the window with the top or right window border,
+                    // this is also the normal behavior of a native Win32 window (but only when the
+                    // window is not maximized/fullscreen/minimized, of course).
                     if (isWindowNoState(hWnd)) {
                         static constexpr const int kBorderSize = 2;
-                        const bool isTop = (nativeLocalPos.y <= kBorderSize);
-                        const bool isRight = (nativeLocalPos.x >= (clientWidth - kBorderSize));
+                        bool isTop = (nativeLocalPos.y <= kBorderSize);
+                        bool isRight = (nativeLocalPos.x >= (clientWidth - kBorderSize));
                         if (isTop || isRight) {
                             if (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.
+                                // 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);
                             } else {
                                 if (isTop && isRight) {
@@ -1139,8 +1256,9 @@
                         }
                     }
                     if (*result == HTNOWHERE) {
-                        // OK, we are now really inside one of the chrome buttons, tell Windows the exact role of our button.
-                        // The Snap Layout feature introduced in Windows 11 won't work without this.
+                        // OK, we are now really inside one of the chrome buttons, tell Windows the
+                        // exact role of our button. The Snap Layout feature introduced in Windows
+                        // 11 won't work without this.
                         switch (sysButtonType) {
                             case CoreWindowAgent::WindowIcon:
                                 *result = HTSYSMENU;
@@ -1159,33 +1277,38 @@
                                 break;
                             case CoreWindowAgent::Unknown:
                                 break;
+                            default:
+                                break; // unreachable
                         }
                     }
                     if (*result == HTNOWHERE) {
-                        // OK, it seems we are not inside the window resize area, nor inside the chrome buttons,
-                        // tell Windows we are in the client area to let Qt handle this event.
+                        // OK, it seems we are not inside the window resize area, nor inside the
+                        // chrome buttons, tell Windows we are in the client area to let Qt handle
+                        // this event.
                         *result = HTCLIENT;
                     }
                     return true;
                 }
-                // OK, we are not inside any chrome buttons, try to find out which part of the window
-                // are we hitting.
+                // OK, we are not inside any chrome buttons, try to find out which part of the
+                // window are we hitting.
 
-                const bool max = IsMaximized(hWnd);
-                const bool full = isFullScreen(hWnd);
-                const int frameSize = getResizeBorderThickness(hWnd);
-                const bool isTop = (nativeLocalPos.y < frameSize);
+                bool max = IsMaximized(hWnd);
+                bool full = isFullScreen(hWnd);
+                int frameSize = getResizeBorderThickness(hWnd);
+                bool isTop = (nativeLocalPos.y < frameSize);
 
                 if (isWin10OrGreater()) {
                     // This will handle the left, right and bottom parts of the frame
                     // because we didn't change them.
-                    const LRESULT originalHitTestResult = ::DefWindowProcW(hWnd, WM_NCHITTEST, 0, lParam);
+                    LRESULT originalHitTestResult = ::DefWindowProcW(hWnd, WM_NCHITTEST, 0, lParam);
                     if (originalHitTestResult != HTCLIENT) {
-                        // Even if the window is not resizable, we still can't return HTCLIENT here because
-                        // when we enter this code path, it means the mouse cursor is 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);
+                        // Even if the window is not resizable, we still can't return HTCLIENT here
+                        // because when we enter this code path, it means the mouse cursor is
+                        // 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);
                         return true;
                     }
                     if (full) {
@@ -1205,7 +1328,9 @@
                         // 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 = ((isFixedSize || dontOverrideCursor)
+                                       ? (isTitleBar ? HTCAPTION : HTCLIENT)
+                                       : HTTOP);
                         return true;
                     }
                     if (isTitleBar) {
@@ -1213,6 +1338,7 @@
                         return true;
                     }
                     *result = HTCLIENT;
+                    return true;
                 } else {
                     if (full) {
                         *result = HTCLIENT;
@@ -1274,13 +1400,73 @@
                         return true;
                     }
                     *result = HTCLIENT;
+                    return true;
                 }
-                return true;
             }
             default:
                 break;
         }
+        if (!isWin10OrGreater()) {
+            switch (message) {
+                case WM_NCUAHDRAWCAPTION:
+                case WM_NCUAHDRAWFRAME: {
+                    // These undocumented messages are sent to draw themed window
+                    // borders. Block them to prevent drawing borders over the client
+                    // area.
+                    *result = FALSE;
+                    return true;
+                }
+                case WM_NCPAINT: {
+                    // 杈规闃村奖澶勪簬闈炲鎴峰尯鐨勮寖鍥达紝鍥犳濡傛灉鐩存帴闃绘闈炲鎴峰尯鐨勭粯鍒讹紝浼氬鑷磋竟妗嗛槾褰变涪澶�
+
+                    if (!isDwmCompositionEnabled()) {
+                        // Only block WM_NCPAINT when DWM composition is disabled. If
+                        // it's blocked when DWM composition is enabled, the frame
+                        // shadow won't be drawn.
+                        *result = FALSE;
+                        return true;
+                    } else {
+                        break;
+                    }
+                }
+                case WM_NCACTIVATE: {
+                    if (isDwmCompositionEnabled()) {
+                        // DefWindowProc won't repaint the window border if lParam (normally a HRGN)
+                        // is -1. See the following link's "lParam" section:
+                        // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-ncactivate
+                        // Don't use "*result = 0" here, otherwise the window won't respond to the
+                        // window activation state change.
+                        *result = ::DefWindowProcW(hWnd, WM_NCACTIVATE, wParam, -1);
+                    } else {
+                        if (wParam == FALSE) {
+                            *result = TRUE;
+                        } else {
+                            *result = FALSE;
+                        }
+                    }
+                    return true;
+                }
+                case WM_SETICON:
+                case WM_SETTEXT: {
+                    // Disable painting while these messages are handled to prevent them
+                    // from drawing a window caption over the client area.
+                    const auto oldStyle = static_cast<DWORD>(::GetWindowLongPtrW(hWnd, GWL_STYLE));
+                    // Prevent Windows from drawing the default title bar by temporarily
+                    // toggling the WS_VISIBLE style.
+                    const DWORD newStyle = (oldStyle & ~WS_VISIBLE);
+                    ::SetWindowLongPtrW(hWnd, GWL_STYLE, static_cast<LONG_PTR>(newStyle));
+                    triggerFrameChange(hWnd);
+                    const LRESULT originalResult = ::DefWindowProcW(hWnd, message, wParam, lParam);
+                    ::SetWindowLongPtrW(hWnd, GWL_STYLE, static_cast<LONG_PTR>(oldStyle));
+                    triggerFrameChange(hWnd);
+                    *result = originalResult;
+                    return true;
+                }
+                default:
+                    break;
+            }
+        }
         return false;
     }
 
-}
\ No newline at end of file
+}

--
Gitblit v1.9.1