| | |
| | | #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 implementation contradicts with QTBUG-113736 patch. |
| | | #if !QWINDOWKIT_CONFIG(ENABLE_WINDOWS_SYSTEM_BORDERS) && QT_VERSION >= QT_VERSION_CHECK(6, 6, 1) |
| | | # define QTBUG_113736_WORKAROUND |
| | | #endif |
| | | |
| | | 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; |
| | |
| | | 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); |
| | |
| | | // 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 |
| | |
| | | |
| | | 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) { |
| | |
| | | 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; |
| | | } |
| | |
| | | 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 |
| | |
| | | } |
| | | |
| | | 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) { |
| | |
| | | #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; |
| | | } |
| | | |
| | |
| | | ? int(getWindowFrameBorderThickness(reinterpret_cast<HWND>(m_windowId))) |
| | | : 0; |
| | | } |
| | | |
| | | return AbstractWindowContext::windowAttribute(key); |
| | | } |
| | | |
| | |
| | | // 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) { |
| | |
| | | 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 |
| | |
| | | } |
| | | } |
| | | |
| | | 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); |
| | | 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 |
| | |
| | | (((message >= WM_NCXBUTTONDOWN) && (message <= WM_NCXBUTTONDBLCLK)) |
| | | ? TRUE |
| | | : FALSE); |
| | | emulateClientAreaMessage(hWnd, message, wParam, lParam); |
| | | } |
| | | return true; |
| | | } |
| | |
| | | // 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)}; |
| | |
| | | bool shouldShowSystemMenu = false; |
| | | bool broughtByKeyboard = false; |
| | | POINT nativeGlobalPos{}; |
| | | |
| | | switch (message) { |
| | | case WM_RBUTTONUP: { |
| | | const POINT nativeLocalPos = getNativePosFromMouse(); |
| | |
| | | } |
| | | 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; |
| | | } |
| | |
| | | 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, |
| | | 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 |= IconButtonClicked; |
| | | if (::GetTickCount64() - iconButtonClickTime <= doubleClickTime) { |
| | | 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 |