From a5cd2550f488723a31af363b5ed3b9b309d5e5b4 Mon Sep 17 00:00:00 2001 From: Sine Striker Date: Mon, 28 Aug 2023 06:31:10 +0800 Subject: [PATCH] Use better workaround to emulate mouse events --- src/core/framelesshelper_win.cpp | 174 +++++++++++++------------------ 1 file changed, 75 insertions(+), 99 deletions(-) diff --git a/src/core/framelesshelper_win.cpp b/src/core/framelesshelper_win.cpp index 7b5e444a..0ba2f120 100644 --- a/src/core/framelesshelper_win.cpp +++ b/src/core/framelesshelper_win.cpp @@ -103,7 +103,14 @@ enum class WindowPart : quint8 struct FramelessWin32HelperData { SystemParameters params = {}; - std::pair, std::optional> hitTestResult = {}; + + // Store the last result of WM_NCHITTEST, it's helpful to handle WM_MOUSEMOVE and WM_NCMOUSELEAVE + WindowPart lastHitTestResult = WindowPart::Outside; + + // Store true if we blocked a WM_MOUSELEAVE when mouse moves on chrome button, reset when a + // WM_MOUSELEAVE comes or we manually call TrackMouseEvent + bool mouseLeaveBlocked = false; + Dpi dpi = {}; #if (QT_VERSION < QT_VERSION_CHECK(6, 5, 1)) QRect restoreGeometry = {}; @@ -360,9 +367,6 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me const auto emulateClientAreaMessage = [hWnd, uMsg, wParam, lParam](const std::optional &overrideMessage = std::nullopt) -> void { const int myMsg = overrideMessage.value_or(uMsg); const auto wparam = [myMsg, wParam]() -> WPARAM { - if (myMsg == WM_NCMOUSELEAVE) { - return kMessageTag; - } const quint64 keyState = Utils::getKeyState(); if ((myMsg >= WM_NCXBUTTONDOWN) && (myMsg <= WM_NCXBUTTONDBLCLK)) { const auto xButtonMask = GET_XBUTTON_WPARAM(wParam); @@ -370,10 +374,7 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me } return keyState; }(); - const auto lparam = [myMsg, lParam, hWnd]() -> LPARAM { - if (myMsg == WM_NCMOUSELEAVE) { - return 0; - } + const auto lparam = [lParam, hWnd]() -> LPARAM { const auto screenPos = POINT{ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; POINT clientPos = screenPos; if (::ScreenToClient(hWnd, &clientPos) == FALSE) { @@ -438,40 +439,31 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me SEND_MESSAGE(hWnd, WM_MOUSEHOVER, wparam, lparam); break; case WM_NCMOUSELEAVE: - SEND_MESSAGE(hWnd, WM_MOUSELEAVE, wparam, lparam); + SEND_MESSAGE(hWnd, WM_MOUSELEAVE, kMessageTag, 0); break; default: Q_UNREACHABLE(); } }; - const auto clearWindowPartCache = [&data, &muData, &emulateClientAreaMessage]() -> void { - if (getHittedWindowPart(data.hitTestResult.second.value_or(HTNOWHERE)) == WindowPart::ChromeButton) { - emulateClientAreaMessage(WM_NCMOUSELEAVE); - muData.hitTestResult = {}; - } - }; - - if ((uMsg == WM_MOUSELEAVE) && !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. - const QPoint qtScenePos = Utils::fromNativeLocalPosition(window, QPoint{ msg->pt.x, msg->pt.y }); - SystemButtonType dummy = SystemButtonType::Unknown; - if (data.params.isInsideSystemButtons(qtScenePos, &dummy)) { - *result = FALSE; - return true; - } - } - - if (uMsg == WM_SIZE) { - if ((wParam == SIZE_MAXIMIZED) || (wParam == SIZE_MINIMIZED)) { - clearWindowPartCache(); + if (uMsg == 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. + const QPoint qtScenePos = Utils::fromNativeLocalPosition(window, QPoint{ msg->pt.x, msg->pt.y }); + SystemButtonType dummy = SystemButtonType::Unknown; + if (data.params.isInsideSystemButtons(qtScenePos, &dummy)) { + muData.mouseLeaveBlocked = true; + *result = FALSE; + return true; + } } + muData.mouseLeaveBlocked = false; } switch (uMsg) { @@ -810,14 +802,9 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me // appearance with the system's one. const auto hitTestRecorder = qScopeGuard([&muData, &result](){ - auto &first = std::get<0>(muData.hitTestResult); - auto &second = std::get<1>(muData.hitTestResult); - if (second.has_value()) { - first = second; - } else if (!first.has_value()){ - first = HTNOWHERE; - } - second = *result; + muData.lastHitTestResult = getHittedWindowPart(*result); + + qDebug() << "NCHITTEST" << *result; }); const auto nativeGlobalPos = POINT{ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; @@ -1000,9 +987,8 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me return true; } case WM_MOUSEMOVE: { - const WindowPart previousWindowPart = getHittedWindowPart(data.hitTestResult.first.value_or(HTNOWHERE)); - const WindowPart currentWindowPart = getHittedWindowPart(data.hitTestResult.second.value_or(HTNOWHERE)); - if ((previousWindowPart == WindowPart::ChromeButton) && (currentWindowPart == WindowPart::ClientArea)) { + if (data.lastHitTestResult != WindowPart::ChromeButton && data.mouseLeaveBlocked) { + muData.mouseLeaveBlocked = false; std::ignore = requestForMouseLeaveMessage(hWnd, false); } } break; @@ -1024,22 +1010,54 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me case WM_NCPOINTERDOWN: case WM_NCPOINTERUP: #endif - case WM_NCMOUSEHOVER: + case WM_NCMOUSEHOVER: { + const WindowPart currentWindowPart = data.lastHitTestResult; + if (uMsg == WM_NCMOUSEMOVE) { + if (currentWindowPart != WindowPart::ChromeButton) { + std::ignore = data.params.resetQtGrabbedControl(); + if (muData.mouseLeaveBlocked) { + emulateClientAreaMessage(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`. + muData.lastHitTestResult = WindowPart::Outside; + } + + if (currentWindowPart == WindowPart::ChromeButton) { + emulateClientAreaMessage(); + if (uMsg == WM_NCMOUSEMOVE) { + *result = ::DefWindowProcW(hWnd, WM_NCMOUSEMOVE, wParam, lParam); + } else { + *result = (((uMsg >= WM_NCXBUTTONDOWN) && (uMsg <= WM_NCXBUTTONDBLCLK)) ? TRUE : FALSE); + } + return true; + } + } break; case WM_NCMOUSELEAVE: { - const WindowPart previousWindowPart = getHittedWindowPart(data.hitTestResult.first.value_or(HTNOWHERE)); - const WindowPart currentWindowPart = getHittedWindowPart(data.hitTestResult.second.value_or(HTNOWHERE)); - if (uMsg == WM_NCMOUSELEAVE) { - if ((previousWindowPart == WindowPart::ChromeButton) && (currentWindowPart == WindowPart::Outside)) { - // If current window part is chrome button, it indicates that we must have clicked - // the minimize button or maximize button, we also should send the client leave - // message to Qt. + const WindowPart currentWindowPart = data.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 (muData.mouseLeaveBlocked) { + muData.mouseLeaveBlocked = false; + std::ignore = requestForMouseLeaveMessage(hWnd, false); + } + } else { + if (data.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(WM_NCMOUSELEAVE); } if (currentWindowPart == WindowPart::Outside) { - // The mouse is leaving the window from the non-client area, clear window part cache. - muData.hitTestResult = {}; - // 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, @@ -1047,40 +1065,6 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me std::ignore = data.params.resetQtGrabbedControl(); } - } else { - if (uMsg == WM_NCMOUSEMOVE) { - if (currentWindowPart != WindowPart::ChromeButton) { - std::ignore = data.params.resetQtGrabbedControl(); - } - if ((previousWindowPart == WindowPart::ChromeButton) - && ((currentWindowPart == WindowPart::TitleBar) - || (currentWindowPart == WindowPart::ResizeBorder) - || (currentWindowPart == WindowPart::FixedBorder))) { - emulateClientAreaMessage(WM_NCMOUSELEAVE); - } - - // We need to make sure we get the correct window part when a WM_NCMOUSELEAVE come, - // so we reset current window part to null when we receive a WM_NCMOUSEMOVE. - - // If the mouse is entering the client area, there must be a WM_NCHITTEST setting current - // window part to NCCLIENT before the WM_NCMOUSELEAVE comes; - // If the mouse is leaving the window, current window part remains as null. - auto &hitTestResult = muData.hitTestResult; - if (hitTestResult.second.has_value()) { - hitTestResult.first = hitTestResult.second; - hitTestResult.second.reset(); - } - } - - if (currentWindowPart == WindowPart::ChromeButton) { - emulateClientAreaMessage(); - if (uMsg == WM_NCMOUSEMOVE) { - *result = ::DefWindowProcW(hWnd, WM_NCMOUSEMOVE, wParam, lParam); - } else { - *result = (((uMsg >= WM_NCXBUTTONDOWN) && (uMsg <= WM_NCXBUTTONDBLCLK)) ? TRUE : FALSE); - } - return true; - } } } break; #if (QT_VERSION < QT_VERSION_CHECK(6, 2, 2)) // I contributed this small technique to upstream Qt since 6.2.2 @@ -1156,14 +1140,6 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me // Re-apply the custom window frame if recovered from the basic theme. std::ignore = Utils::updateWindowFrameMargins(windowId, false); } break; - case WM_ACTIVATE: - if (LOWORD(wParam) == WA_INACTIVE) { - clearWindowPartCache(); - } - break; - case WM_INITMENU: - clearWindowPartCache(); - break; #if (QT_VERSION < QT_VERSION_CHECK(6, 5, 1)) case WM_ENTERSIZEMOVE: // Sent to a window when the user drags the title bar or the resize border. case WM_EXITSIZEMOVE: // Sent to a window when the user releases the mouse button (from dragging the title bar or the resize border).