Use better workaround to emulate mouse events (#273)

This commit is contained in:
SineStriker 2023-08-28 11:01:24 +08:00 committed by GitHub
parent 7d09a6b9c9
commit 0ce47469a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 72 additions and 92 deletions

View File

@ -103,7 +103,14 @@ enum class WindowPart : quint8
struct FramelessWin32HelperData
{
SystemParameters params = {};
std::pair<std::optional<int>, std::optional<int>> 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 = {};
@ -445,35 +452,26 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me
}
};
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_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;
}
if (uMsg == WM_SIZE) {
if ((wParam == SIZE_MAXIMIZED) || (wParam == SIZE_MINIMIZED)) {
clearWindowPartCache();
}
}
switch (uMsg) {
#if (QT_VERSION < QT_VERSION_CHECK(5, 9, 0)) // Qt has done this for us since 5.9.0
case WM_NCCREATE: {
@ -810,14 +808,7 @@ 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);
});
const auto nativeGlobalPos = POINT{ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
@ -1000,9 +991,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 +1014,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 +1069,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 +1144,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).