From 230896e7dae9e97e3363947e72e70cca1f89911d Mon Sep 17 00:00:00 2001
From: Zhao Yuhang <2546789017@qq.com>
Date: 周五, 08 12月 2023 22:47:07 +0800
Subject: [PATCH] port WIP system menu

---
 src/core/contexts/win32windowcontext.cpp |  296 +++++++++++++++++++++++++++++++++++++++++++++++++++-------
 1 files changed, 257 insertions(+), 39 deletions(-)

diff --git a/src/core/contexts/win32windowcontext.cpp b/src/core/contexts/win32windowcontext.cpp
index af62e4c..9581b94 100644
--- a/src/core/contexts/win32windowcontext.cpp
+++ b/src/core/contexts/win32windowcontext.cpp
@@ -5,7 +5,6 @@
 
 #include <QtCore/QHash>
 #include <QtCore/QAbstractNativeEventFilter>
-#include <QtCore/QOperatingSystemVersion>
 #include <QtCore/QScopeGuard>
 #include <QtGui/QGuiApplication>
 
@@ -24,19 +23,26 @@
 #include <shellscalingapi.h>
 #include <dwmapi.h>
 #include <timeapi.h>
+#include <versionhelpers.h>
 
 Q_DECLARE_METATYPE(QMargins)
 
 namespace QWK {
 
-    static constexpr const auto kAutoHideTaskBarThickness =
-        quint8{2}; // The thickness of an auto-hide taskbar in pixels.
+    // The thickness of an auto-hide taskbar in pixels.
+    static constexpr const auto kAutoHideTaskBarThickness = quint8{2};
 
-    using WndProcHash = QHash<HWND, Win32WindowContext *>; // hWnd -> context
+    // hWnd -> context
+    using WndProcHash = QHash<HWND, Win32WindowContext *>;
     Q_GLOBAL_STATIC(WndProcHash, g_wndProcHash)
 
-    static WNDPROC g_qtWindowProc = nullptr; // Original Qt window proc function
+    // Original Qt window proc function
+    static WNDPROC g_qtWindowProc = nullptr;
 
+    // ### FIXME FIXME FIXME
+    // ### FIXME: Tell the user to call in the documentation, instead of automatically
+    // calling it directly.
+    // ### FIXME FIXME FIXME
     static struct QWK_Hook {
         QWK_Hook() {
             qApp->setAttribute(Qt::AA_DontCreateNativeWidgetSiblings);
@@ -44,37 +50,56 @@
     } g_hook{};
 
     struct DynamicApis {
-        decltype(&::DwmFlush) pDwmFlush = nullptr;
-        decltype(&::DwmIsCompositionEnabled) pDwmIsCompositionEnabled = nullptr;
-        decltype(&::DwmGetCompositionTimingInfo) pDwmGetCompositionTimingInfo = nullptr;
-        decltype(&::GetDpiForWindow) pGetDpiForWindow = nullptr;
-        decltype(&::GetSystemMetricsForDpi) pGetSystemMetricsForDpi = nullptr;
-        decltype(&::GetDpiForMonitor) pGetDpiForMonitor = nullptr;
-        decltype(&::timeGetDevCaps) ptimeGetDevCaps = nullptr;
-        decltype(&::timeBeginPeriod) ptimeBeginPeriod = nullptr;
-        decltype(&::timeEndPeriod) ptimeEndPeriod = nullptr;
+//        template <typename T>
+//        struct DefaultFunc;
+//
+//        template <typename Return, typename... Args>
+//        struct DefaultFunc<Return(QT_WIN_CALLBACK *)(Args...)> {
+//            static Return STDAPICALLTYPE func(Args...) {
+//                return Return{};
+//            }
+//        };
+//
+// #define DWM_API_DECLARE(NAME) decltype(&::NAME) p##NAME = DefaultFunc<decltype(&::NAME)>::func
+#define DWM_API_DECLARE(NAME) decltype(&::NAME) p##NAME = nullptr
+
+        DWM_API_DECLARE(DwmFlush);
+        DWM_API_DECLARE(DwmIsCompositionEnabled);
+        DWM_API_DECLARE(DwmGetCompositionTimingInfo);
+        DWM_API_DECLARE(GetDpiForWindow);
+        DWM_API_DECLARE(GetSystemMetricsForDpi);
+        DWM_API_DECLARE(GetDpiForMonitor);
+        DWM_API_DECLARE(timeGetDevCaps);
+        DWM_API_DECLARE(timeBeginPeriod);
+        DWM_API_DECLARE(timeEndPeriod);
+
+#undef DWM_API_DECLARE
 
         DynamicApis() {
-            QSystemLibrary user32(QStringLiteral("user32.dll"));
+            QSystemLibrary user32(QStringLiteral("user32"));
             pGetDpiForWindow =
                 reinterpret_cast<decltype(pGetDpiForWindow)>(user32.resolve("GetDpiForWindow"));
             pGetSystemMetricsForDpi = reinterpret_cast<decltype(pGetSystemMetricsForDpi)>(
                 user32.resolve("GetSystemMetricsForDpi"));
 
-            QSystemLibrary shcore(QStringLiteral("shcore.dll"));
+            QSystemLibrary shcore(QStringLiteral("shcore"));
             pGetDpiForMonitor =
                 reinterpret_cast<decltype(pGetDpiForMonitor)>(shcore.resolve("GetDpiForMonitor"));
 
-            QSystemLibrary dwmapi(QStringLiteral("dwmapi.dll"));
+            QSystemLibrary dwmapi(QStringLiteral("dwmapi"));
             pDwmFlush = reinterpret_cast<decltype(pDwmFlush)>(dwmapi.resolve("DwmFlush"));
             pDwmIsCompositionEnabled = reinterpret_cast<decltype(pDwmIsCompositionEnabled)>(
                 dwmapi.resolve("DwmIsCompositionEnabled"));
-            pDwmGetCompositionTimingInfo = reinterpret_cast<decltype(pDwmGetCompositionTimingInfo)>(dwmapi.resolve("DwmGetCompositionTimingInfo"));
+            pDwmGetCompositionTimingInfo = reinterpret_cast<decltype(pDwmGetCompositionTimingInfo)>(
+                dwmapi.resolve("DwmGetCompositionTimingInfo"));
 
-            QSystemLibrary winmm(QStringLiteral("winmm.dll"));
-            ptimeGetDevCaps = reinterpret_cast<decltype(ptimeGetDevCaps)>(winmm.resolve("timeGetDevCaps"));
-            ptimeBeginPeriod = reinterpret_cast<decltype(ptimeBeginPeriod)>(winmm.resolve("timeBeginPeriod"));
-            ptimeEndPeriod = reinterpret_cast<decltype(ptimeEndPeriod)>(winmm.resolve("timeEndPeriod"));
+            QSystemLibrary winmm(QStringLiteral("winmm"));
+            ptimeGetDevCaps =
+                reinterpret_cast<decltype(ptimeGetDevCaps)>(winmm.resolve("timeGetDevCaps"));
+            ptimeBeginPeriod =
+                reinterpret_cast<decltype(ptimeBeginPeriod)>(winmm.resolve("timeBeginPeriod"));
+            ptimeEndPeriod =
+                reinterpret_cast<decltype(ptimeEndPeriod)>(winmm.resolve("timeEndPeriod"));
         }
 
         ~DynamicApis() = default;
@@ -169,20 +194,22 @@
     }
 
     static inline bool isWin8OrGreater() {
-        static const bool result =
-            QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows8;
+        static const bool result = ::IsWindows8OrGreater();
         return result;
     }
 
     static inline bool isWin8Point1OrGreater() {
-        static const bool result =
-            QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows8_1;
+        static const bool result = ::IsWindows8Point1OrGreater();
         return result;
     }
 
     static inline bool isWin10OrGreater() {
-        static const bool result =
-            QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows10;
+        static const bool result = ::IsWindows10OrGreater();
+        return result;
+    }
+
+    static inline bool isWin11OrGreater() {
+        static const bool result = ::IsWindowsVersionOrGreater(HIBYTE(_WIN32_WINNT_WIN10), LOBYTE(_WIN32_WINNT_WIN10), 22000);
         return result;
     }
 
@@ -210,8 +237,8 @@
             return apis.pGetDpiForWindow(hwnd);
         } else if (apis.pGetDpiForMonitor) { // Win8.1
             HMONITOR monitor = ::MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
-            UINT dpiX{USER_DEFAULT_SCREEN_DPI};
-            UINT dpiY{USER_DEFAULT_SCREEN_DPI};
+            UINT dpiX{0};
+            UINT dpiY{0};
             apis.pGetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &dpiX, &dpiY);
             return dpiX;
         } else { // Win2K
@@ -358,6 +385,117 @@
         apis.ptimeEndPeriod(ms_granularity);
     }
 
+    static inline void showSystemMenu2(HWND hWnd, const POINT &pos, const bool selectFirstEntry)
+    {
+        const 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;
+        }
+
+        // Tweak the menu items according to the current window status and user settings.
+        const bool disableClose = /*data->callbacks->getProperty(kSysMenuDisableCloseVar, false).toBool()*/false;
+        const bool disableRestore = /*data->callbacks->getProperty(kSysMenuDisableRestoreVar, false).toBool()*/false;
+        const bool disableMinimize = /*data->callbacks->getProperty(kSysMenuDisableMinimizeVar, false).toBool()*/false;
+        const bool disableMaximize = /*data->callbacks->getProperty(kSysMenuDisableMaximizeVar, false).toBool()*/false;
+        const bool disableSize = /*data->callbacks->getProperty(kSysMenuDisableSizeVar, false).toBool()*/false;
+        const bool disableMove = /*data->callbacks->getProperty(kSysMenuDisableMoveVar, false).toBool()*/false;
+        const bool removeClose = /*data->callbacks->getProperty(kSysMenuRemoveCloseVar, false).toBool()*/false;
+        const bool removeSeparator = /*data->callbacks->getProperty(kSysMenuRemoveSeparatorVar, false).toBool()*/false;
+        const bool removeRestore = /*data->callbacks->getProperty(kSysMenuRemoveRestoreVar, false).toBool()*/false;
+        const bool removeMinimize = /*data->callbacks->getProperty(kSysMenuRemoveMinimizeVar, false).toBool()*/false;
+        const bool removeMaximize = /*data->callbacks->getProperty(kSysMenuRemoveMaximizeVar, false).toBool()*/false;
+        const bool removeSize = /*data->callbacks->getProperty(kSysMenuRemoveSizeVar, false).toBool()*/false;
+        const bool removeMove = /*data->callbacks->getProperty(kSysMenuRemoveMoveVar, false).toBool()*/false;
+        const bool maxOrFull = IsMaximized(hWnd) || isFullScreen(hWnd);
+        const bool fixedSize = /*data->callbacks->isWindowFixedSize()*/false;
+        if (removeClose) {
+            ::DeleteMenu(hMenu, SC_CLOSE, MF_BYCOMMAND);
+        } else {
+            ::EnableMenuItem(hMenu, SC_CLOSE, (MF_BYCOMMAND | (disableClose ? MFS_DISABLED : MFS_ENABLED)));
+        }
+        if (removeSeparator) {
+            // Looks like we must use 0 for the second parameter here, otherwise we can't remove the separator.
+            ::DeleteMenu(hMenu, 0, MFT_SEPARATOR);
+        }
+        if (removeMaximize) {
+            ::DeleteMenu(hMenu, SC_MAXIMIZE, MF_BYCOMMAND);
+        } else {
+            ::EnableMenuItem(hMenu, SC_MAXIMIZE, (MF_BYCOMMAND | ((maxOrFull || fixedSize || disableMaximize) ? MFS_DISABLED : MFS_ENABLED)));
+        }
+        if (removeRestore) {
+            ::DeleteMenu(hMenu, SC_RESTORE, MF_BYCOMMAND);
+        } else {
+            ::EnableMenuItem(hMenu, SC_RESTORE, (MF_BYCOMMAND | ((maxOrFull && !fixedSize && !disableRestore) ? MFS_ENABLED : MFS_DISABLED)));
+            // The first menu item should be selected by default if the menu is brought
+            // up by keyboard. I don't know how to pre-select a menu item but it seems
+            // highlight can do the job. However, there's an annoying issue if we do
+            // this manually: the highlighted menu item is really only highlighted,
+            // not selected, so even if the mouse cursor hovers on other menu items
+            // or the user navigates to other menu items through keyboard, the original
+            // highlight bar will not move accordingly, the OS will generate another
+            // highlight bar to indicate the current selected menu item, which will make
+            // the menu look kind of weird. Currently I don't know how to fix this issue.
+            ::HiliteMenuItem(hWnd, hMenu, SC_RESTORE, (MF_BYCOMMAND | (selectFirstEntry ? MFS_HILITE : MFS_UNHILITE)));
+        }
+        if (removeMinimize) {
+            ::DeleteMenu(hMenu, SC_MINIMIZE, MF_BYCOMMAND);
+        } else {
+            ::EnableMenuItem(hMenu, SC_MINIMIZE, (MF_BYCOMMAND | (disableMinimize ? MFS_DISABLED : MFS_ENABLED)));
+        }
+        if (removeSize) {
+            ::DeleteMenu(hMenu, SC_SIZE, MF_BYCOMMAND);
+        } else {
+            ::EnableMenuItem(hMenu, SC_SIZE, (MF_BYCOMMAND | ((maxOrFull || fixedSize || disableSize || disableMinimize || disableMaximize) ? MFS_DISABLED : MFS_ENABLED)));
+        }
+        if (removeMove) {
+            ::DeleteMenu(hMenu, SC_MOVE, MF_BYCOMMAND);
+        } else {
+            ::EnableMenuItem(hMenu, SC_MOVE, (MF_BYCOMMAND | ((maxOrFull || disableMove) ? MFS_DISABLED : MFS_ENABLED)));
+        }
+
+        // The default menu item will appear in bold font. There can only be one default
+        // menu item per menu at most. Set the item ID to "UINT_MAX" (or simply "-1")
+        // can clear the default item for the given menu.
+        std::optional<UINT> defaultItemId = std::nullopt;
+        if (isWin11OrGreater()) {
+            if (maxOrFull) {
+                if (!removeRestore) {
+                    defaultItemId = SC_RESTORE;
+                }
+            } else {
+                if (!removeMaximize) {
+                    defaultItemId = SC_MAXIMIZE;
+                }
+            }
+        }
+        if (!(defaultItemId.has_value() || removeClose)) {
+            defaultItemId = SC_CLOSE;
+        }
+        ::SetMenuDefaultItem(hMenu, defaultItemId.value_or(UINT_MAX), FALSE);
+
+        ::DrawMenuBar(hWnd);
+
+        // Popup the system menu at the required position.
+        const auto result = ::TrackPopupMenu(hMenu, (TPM_RETURNCMD | (QGuiApplication::isRightToLeft() ? TPM_RIGHTALIGN : TPM_LEFTALIGN)), pos.x, pos.y, 0, hWnd, nullptr);
+
+        if (!removeRestore) {
+            // Unhighlight the first menu item after the popup menu is closed, otherwise it will keep
+            // highlighting until we unhighlight it manually.
+            ::HiliteMenuItem(hWnd, hMenu, SC_RESTORE, (MF_BYCOMMAND | MFS_UNHILITE));
+        }
+
+        if (!result) {
+            // The user canceled the menu, no need to continue.
+            return;
+        }
+
+        // Send the command that the user chooses to the corresponding window.
+        ::PostMessageW(hWnd, WM_SYSCOMMAND, result, 0);
+    }
+
     static inline Win32WindowContext::WindowPart getHitWindowPart(int hitTestResult) {
         switch (hitTestResult) {
             case HTCLIENT:
@@ -440,7 +578,7 @@
 
             // 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 acquired the initial data.
+            // 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;
@@ -553,12 +691,7 @@
 
         // Try hooked procedure and save result
         LRESULT result;
-        bool handled = ctx->windowProc(hWnd, message, wParam, lParam, &result);
-
-        // TODO: Determine whether to show system menu
-        // ...
-
-        if (handled) {
+        if (ctx->windowProc(hWnd, message, wParam, lParam, &result)) {
             return result;
         }
 
@@ -629,6 +762,10 @@
 
         if (!isValidWindow(hWnd, false, true)) {
             return false;
+        }
+
+        if (systemMenuHandler(hWnd, message, wParam, lParam, result)) {
+            return true;
         }
 
         // Test snap layout
@@ -800,8 +937,8 @@
                     DWORD dwScreenPos = ::GetMessagePos();
                     POINT screenPoint{GET_X_LPARAM(dwScreenPos), GET_Y_LPARAM(dwScreenPos)};
                     ::ScreenToClient(hWnd, &screenPoint);
-                    QPoint qtScenePos =
-                        QHighDpi::fromNativeLocalPosition(QPoint{screenPoint.x, screenPoint.y}, m_windowHandle);
+                    QPoint qtScenePos = QHighDpi::fromNativeLocalPosition(
+                        QPoint{screenPoint.x, screenPoint.y}, m_windowHandle);
                     auto dummy = CoreWindowAgent::Unknown;
                     if (isInSystemButtons(qtScenePos, &dummy)) {
                         // We must record whether the last WM_MOUSELEAVE was filtered, because if
@@ -1285,6 +1422,7 @@
     bool Win32WindowContext::nonClientCalcSizeHandler(HWND hWnd, UINT message, WPARAM wParam,
                                                       LPARAM lParam, LRESULT *result) {
         Q_UNUSED(message)
+        Q_UNUSED(this)
 
         // Windows鏄牴鎹繖涓秷鎭殑杩斿洖鍊兼潵璁剧疆绐楀彛鐨勫鎴峰尯锛堢獥鍙d腑鐪熸鏄剧ず鐨勫唴瀹癸級
         // 鍜岄潪瀹㈡埛鍖猴紙鏍囬鏍忋�佺獥鍙h竟妗嗐�佽彍鍗曟爮鍜岀姸鎬佹爮绛塛indows绯荤粺鑷鎻愪緵鐨勯儴鍒�
@@ -1511,6 +1649,86 @@
 
     bool Win32WindowContext::systemMenuHandler(HWND hWnd, UINT message, WPARAM wParam,
                                                LPARAM lParam, LRESULT *result) {
+        const auto getNativePosFromMouse = [lParam]() -> POINT {
+            return {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
+        };
+        const auto getNativeGlobalPosFromKeyboard = [hWnd]() -> POINT {
+            const bool maxOrFull = IsMaximized(hWnd) || isFullScreen(hWnd);
+            const quint32 frameSize = getResizeBorderThickness(hWnd);
+            const quint32 horizontalOffset = ((maxOrFull || !isWin10OrGreater()) ? 0 : frameSize);
+            const auto verticalOffset = [hWnd, maxOrFull, frameSize]() -> quint32 {
+                const quint32 titleBarHeight = getTitleBarHeight(hWnd);
+                if (!isWin10OrGreater()) {
+                    return titleBarHeight;
+                }
+                if (isWin11OrGreater()) {
+                    if (maxOrFull) {
+                        return (titleBarHeight + frameSize);
+                    }
+                    return titleBarHeight;
+                }
+                if (maxOrFull) {
+                    return titleBarHeight;
+                }
+                return titleBarHeight - frameSize;
+            }();
+            RECT windowPos{};
+            ::GetWindowRect(hWnd, &windowPos);
+            return {static_cast<LONG>(windowPos.left + horizontalOffset), static_cast<LONG>(windowPos.top + verticalOffset)};
+        };
+        bool shouldShowSystemMenu = false;
+        bool broughtByKeyboard = false;
+        POINT nativeGlobalPos{};
+        switch (message) {
+            case WM_RBUTTONUP: {
+                const POINT nativeLocalPos = getNativePosFromMouse();
+                const QPoint qtScenePos = QHighDpi::fromNativeLocalPosition(QPoint(nativeLocalPos.x, nativeLocalPos.y), m_windowHandle);
+                if (isInTitleBarDraggableArea(qtScenePos)) {
+                    shouldShowSystemMenu = true;
+                    nativeGlobalPos = nativeLocalPos;
+                    ::ClientToScreen(hWnd, &nativeGlobalPos);
+                }
+                break;
+            }
+            case WM_NCRBUTTONUP: {
+                if (wParam == HTCAPTION) {
+                    shouldShowSystemMenu = true;
+                    nativeGlobalPos = getNativePosFromMouse();
+                }
+                break;
+            }
+            case WM_SYSCOMMAND: {
+                const WPARAM filteredWParam = (wParam & 0xFFF0);
+                if ((filteredWParam == SC_KEYMENU) && (lParam == VK_SPACE)) {
+                    shouldShowSystemMenu = true;
+                    broughtByKeyboard = true;
+                    nativeGlobalPos = getNativeGlobalPosFromKeyboard();
+                }
+                break;
+            }
+            case WM_KEYDOWN:
+            case WM_SYSKEYDOWN: {
+                const bool altPressed = ((wParam == VK_MENU) || (::GetKeyState(VK_MENU) < 0));
+                const bool spacePressed = ((wParam == VK_SPACE) || (::GetKeyState(VK_SPACE) < 0));
+                if (altPressed && spacePressed) {
+                    shouldShowSystemMenu = true;
+                    broughtByKeyboard = true;
+                    nativeGlobalPos = getNativeGlobalPosFromKeyboard();
+                }
+                break;
+            }
+            default:
+                break;
+        }
+        if (shouldShowSystemMenu) {
+            showSystemMenu2(hWnd, nativeGlobalPos, broughtByKeyboard);
+            // 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
+            // entering Qt's own handling logic.
+            *result = FALSE;
+            return true;
+        }
         return false;
     }
 

--
Gitblit v1.9.1