#include "win32windowcontext_p.h" #include #include #include #include #include #include "qwkcoreglobal_p.h" namespace QWK { using WndProcHash = QHash; // hWnd -> context Q_GLOBAL_STATIC(WndProcHash, g_wndProcHash); static WNDPROC g_qtWindowProc = nullptr; // Original Qt window proc function static inline QPoint fromNativeLocalPosition(const QWindow *window, const QPoint &point) { #if 1 return QHighDpi::fromNativeLocalPosition(point, window); #else return QPointF(QPointF(point) / window->devicePixelRatio()).toPoint(); #endif } static inline Win32WindowContext::WindowPart getHitWindowPart(int hitTestResult) { switch (hitTestResult) { case HTCLIENT: return Win32WindowContext::ClientArea; case HTCAPTION: return Win32WindowContext::TitleBar; case HTSYSMENU: case HTHELP: case HTREDUCE: case HTZOOM: case HTCLOSE: return Win32WindowContext::ChromeButton; case HTLEFT: case HTRIGHT: case HTTOP: case HTTOPLEFT: case HTTOPRIGHT: case HTBOTTOM: case HTBOTTOMLEFT: case HTBOTTOMRIGHT: return Win32WindowContext::ResizeBorder; case HTBORDER: return Win32WindowContext::FixedBorder; default: break; } return Win32WindowContext::Outside; } static bool isValidWindow(WId windowId, bool checkVisible, bool checkTopLevel) { const auto hwnd = reinterpret_cast(windowId); if (::IsWindow(hwnd) == FALSE) { return false; } const LONG_PTR styles = ::GetWindowLongPtrW(hwnd, GWL_STYLE); if ((styles == 0) || (styles & WS_DISABLED)) { return false; } const LONG_PTR exStyles = ::GetWindowLongPtrW(hwnd, GWL_EXSTYLE); if ((exStyles == 0) || (exStyles & WS_EX_TOOLWINDOW)) { return false; } RECT rect = {0, 0, 0, 0}; if (::GetWindowRect(hwnd, &rect) == FALSE) { return false; } if ((rect.left >= rect.right) || (rect.top >= rect.bottom)) { return false; } if (checkVisible) { if (::IsWindowVisible(hwnd) == FALSE) { return false; } } if (checkTopLevel) { if (::GetAncestor(hwnd, GA_ROOT) != hwnd) { return false; } } return true; } // https://github.com/qt/qtbase/blob/e26a87f1ecc40bc8c6aa5b889fce67410a57a702/src/plugins/platforms/windows/qwindowscontext.cpp#L1556 // In QWindowsContext::windowsProc(), the messages will be passed to all global native event // filters, but because we have already filtered the messages in the hook WndProc function for // convenience, Qt does not know we may have already process the messages and thus will call // DefWindowProc(). Consequently, we have to add a global native filter that forwards the result // of the hook function, telling Qt whether we have filtered the events before. Since Qt only // handles Windows window messages in the main thread, it is safe to do so. class WindowsNativeEventFilter : public QAbstractNativeEventFilter { public: bool nativeEventFilter(const QByteArray &eventType, void *message, QT_NATIVE_EVENT_RESULT_TYPE *result) override { if (lastMessageHandled) { *result = static_cast(lastMessageResult); return true; } return false; } static bool lastMessageHandled; static LRESULT lastMessageResult; static WindowsNativeEventFilter *instance; static inline void install() { instance = new WindowsNativeEventFilter(); qApp->installNativeEventFilter(instance); } static inline void uninstall() { qApp->removeNativeEventFilter(instance); delete instance; instance = nullptr; } }; bool WindowsNativeEventFilter::lastMessageHandled = false; LRESULT WindowsNativeEventFilter::lastMessageResult = 0; WindowsNativeEventFilter *WindowsNativeEventFilter::instance = nullptr; // https://github.com/qt/qtbase/blob/e26a87f1ecc40bc8c6aa5b889fce67410a57a702/src/plugins/platforms/windows/qwindowscontext.cpp#L1025 // We can see from the source code that Qt will filter out some messages first and then send the // unfiltered messages to the event dispatcher. To activate the Snap Layout feature on Windows // 11, we must process some non-client area messages ourselves, but unfortunately these messages // have been filtered out already in that line, and thus we'll never have the chance to process // them ourselves. This is Qt's low level platform specific code, so we don't have any official // ways to change this behavior. But luckily we can replace the window procedure function of // Qt's windows, and in this hooked window procedure function, we finally have the chance to // process window messages before Qt touches them. So we reconstruct the MSG structure and send // it to our own custom native event filter to do all the magic works. But since the system menu // feature doesn't necessarily belong to the native implementation, we seperate the handling // code and always process the system menu part in this function for both implementations. // // Original event flow: // [Entry] Windows Message Queue // | // [Qt Window Proc] qwindowscontext.cpp#L1547: qWindowsWndProc() // ``` // const bool handled = QWindowsContext::instance()->windowsProc // (hwnd, message, et, wParam, lParam, &result, // &platformWindow); // ``` // | // [Non-Input Filter] qwindowscontext.cpp#L1025: QWindowsContext::windowsProc() // ``` // if (!isInputMessage(msg.message) && // filterNativeEvent(&msg, result)) // return true; // ``` // | // [User Filter] qwindowscontext.cpp#L1588: QWindowsContext::windowsProc() // ``` // QAbstractEventDispatcher *dispatcher = // QAbstractEventDispatcher::instance(); // qintptr filterResult = 0; // if (dispatcher && // dispatcher->filterNativeEvent(nativeEventType(), msg, // &filterResult)) { // *result = LRESULT(filterResult); // return true; // } // ``` // | // [Extra work] The rest of QWindowsContext::windowsProc() and qWindowsWndProc() // // Notice: Only non-input messages will be processed by the user-defined global native event // filter!!! These events are then passed to the widget class's own overridden // QWidget::nativeEvent() as a local filter, where all native events can be handled, but we must // create a new class derived from QWidget which we don't intend to. Therefore, we don't expect // to process events from the global native event filter, but instead hook Qt's window // procedure. extern "C" LRESULT QT_WIN_CALLBACK QWKHookedWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { Q_ASSERT(hWnd); if (!hWnd) { return FALSE; } // Search window context auto ctx = g_wndProcHash->value(hWnd); if (!ctx) { return ::DefWindowProcW(hWnd, message, wParam, lParam); } // Try hooked procedure and save result auto &handled = WindowsNativeEventFilter::lastMessageHandled; auto &result = WindowsNativeEventFilter::lastMessageResult; handled = ctx->windowProc(hWnd, message, wParam, lParam, &result); // TODO: Determine whether to show system menu // ... // Since Qt does the necessary processing of the message afterward, we still need to // continue dispatching it. return ::CallWindowProcW(g_qtWindowProc, hWnd, message, wParam, lParam); } Win32WindowContext::Win32WindowContext(QWindow *window, WindowItemDelegate *delegate) : AbstractWindowContext(window, delegate) { } Win32WindowContext::~Win32WindowContext() { // Remove window handle mapping if (auto hWnd = reinterpret_cast(windowId); hWnd) { g_wndProcHash->remove(hWnd); // Remove event filter if the all windows has been destroyed if (g_wndProcHash->empty()) { WindowsNativeEventFilter::uninstall(); } } } bool Win32WindowContext::setup() { auto winId = m_windowHandle->winId(); // Install window hook auto hWnd = reinterpret_cast(winId); // Store original window proc if (!g_qtWindowProc) { g_qtWindowProc = reinterpret_cast(::GetWindowLongPtrW(hWnd, GWLP_WNDPROC)); } // Hook window proc ::SetWindowLongPtrW(hWnd, GWLP_WNDPROC, reinterpret_cast(QWKHookedWndProc)); // Install global native event filter if (!WindowsNativeEventFilter::instance) { WindowsNativeEventFilter::install(); } // Cache window ID windowId = winId; // Save window handle mapping g_wndProcHash->insert(hWnd, this); return true; } bool Win32WindowContext::windowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, LRESULT *result) { *result = FALSE; // We should skip these messages otherwise we will get crashes. // NOTE: WM_QUIT won't be posted to the WindowProc function. switch (message) { case WM_CLOSE: case WM_DESTROY: case WM_NCDESTROY: // Undocumented messages: case WM_UAHDESTROYWINDOW: case WM_UNREGISTER_WINDOW_SERVICES: return false; default: break; } if (!isValidWindow(windowId, false, true)) { return false; } // Test snap layout if (snapLayoutHandler(hWnd, message, wParam, lParam, result)) { return true; } // TODO: Uncomment and do something // bool frameBorderVisible = Utils::isWindowFrameBorderVisible(); // TODO: Implement // ... return false; // Not handled } static constexpr const auto kMessageTag = WPARAM(0x97CCEA99); static inline constexpr bool isTaggedMessage(WPARAM wParam) { return (wParam == kMessageTag); } static quint64 getKeyState() { quint64 result = 0; const auto &get = [](const int virtualKey) -> bool { return (::GetAsyncKeyState(virtualKey) < 0); }; const bool buttonSwapped = (::GetSystemMetrics(SM_SWAPBUTTON) != FALSE); if (get(VK_LBUTTON)) { result |= (buttonSwapped ? MK_RBUTTON : MK_LBUTTON); } if (get(VK_RBUTTON)) { result |= (buttonSwapped ? MK_LBUTTON : MK_RBUTTON); } if (get(VK_SHIFT)) { result |= MK_SHIFT; } if (get(VK_CONTROL)) { result |= MK_CONTROL; } if (get(VK_MBUTTON)) { result |= MK_MBUTTON; } if (get(VK_XBUTTON1)) { result |= MK_XBUTTON1; } if (get(VK_XBUTTON2)) { result |= MK_XBUTTON2; } return result; } static void emulateClientAreaMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, const std::optional &overrideMessage = std::nullopt) { const int myMsg = overrideMessage.value_or(message); const auto wParamNew = [myMsg, wParam]() -> WPARAM { if (myMsg == WM_NCMOUSELEAVE) { // wParam is always ignored in mouse leave messages, but here we // give them a special tag to be able to distinguish which messages // are sent by ourselves. return kMessageTag; } const quint64 keyState = getKeyState(); if ((myMsg >= WM_NCXBUTTONDOWN) && (myMsg <= WM_NCXBUTTONDBLCLK)) { const auto xButtonMask = GET_XBUTTON_WPARAM(wParam); return MAKEWPARAM(keyState, xButtonMask); } return keyState; }(); const auto lParamNew = [myMsg, lParam, hWnd]() -> LPARAM { if (myMsg == WM_NCMOUSELEAVE) { // lParam is always ignored in mouse leave messages. return 0; } const auto screenPos = POINT{GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; POINT clientPos = screenPos; if (::ScreenToClient(hWnd, &clientPos) == FALSE) { return 0; } return MAKELPARAM(clientPos.x, clientPos.y); }(); #if 0 # define SEND_MESSAGE ::SendMessageW #else # define SEND_MESSAGE ::PostMessageW #endif switch (myMsg) { case WM_NCHITTEST: // Treat hit test messages as mouse move events. case WM_NCMOUSEMOVE: SEND_MESSAGE(hWnd, WM_MOUSEMOVE, wParamNew, lParamNew); break; case WM_NCLBUTTONDOWN: SEND_MESSAGE(hWnd, WM_LBUTTONDOWN, wParamNew, lParamNew); break; case WM_NCLBUTTONUP: SEND_MESSAGE(hWnd, WM_LBUTTONUP, wParamNew, lParamNew); break; case WM_NCLBUTTONDBLCLK: SEND_MESSAGE(hWnd, WM_LBUTTONDBLCLK, wParamNew, lParamNew); break; case WM_NCRBUTTONDOWN: SEND_MESSAGE(hWnd, WM_RBUTTONDOWN, wParamNew, lParamNew); break; case WM_NCRBUTTONUP: SEND_MESSAGE(hWnd, WM_RBUTTONUP, wParamNew, lParamNew); break; case WM_NCRBUTTONDBLCLK: SEND_MESSAGE(hWnd, WM_RBUTTONDBLCLK, wParamNew, lParamNew); break; case WM_NCMBUTTONDOWN: SEND_MESSAGE(hWnd, WM_MBUTTONDOWN, wParamNew, lParamNew); break; case WM_NCMBUTTONUP: SEND_MESSAGE(hWnd, WM_MBUTTONUP, wParamNew, lParamNew); break; case WM_NCMBUTTONDBLCLK: SEND_MESSAGE(hWnd, WM_MBUTTONDBLCLK, wParamNew, lParamNew); break; case WM_NCXBUTTONDOWN: SEND_MESSAGE(hWnd, WM_XBUTTONDOWN, wParamNew, lParamNew); break; case WM_NCXBUTTONUP: SEND_MESSAGE(hWnd, WM_XBUTTONUP, wParamNew, lParamNew); break; case WM_NCXBUTTONDBLCLK: SEND_MESSAGE(hWnd, WM_XBUTTONDBLCLK, wParamNew, lParamNew); break; #if 0 // ### TODO: How to handle touch events? case WM_NCPOINTERUPDATE: case WM_NCPOINTERDOWN: case WM_NCPOINTERUP: break; #endif case WM_NCMOUSEHOVER: SEND_MESSAGE(hWnd, WM_MOUSEHOVER, wParamNew, lParamNew); break; case WM_NCMOUSELEAVE: SEND_MESSAGE(hWnd, WM_MOUSELEAVE, wParamNew, lParamNew); break; default: break; } #undef SEND_MESSAGE } static bool requestForMouseLeaveMessage(HWND hWnd, bool nonClient) { TRACKMOUSEEVENT tme; SecureZeroMemory(&tme, sizeof(tme)); tme.cbSize = sizeof(tme); tme.dwFlags = TME_LEAVE; if (nonClient) { tme.dwFlags |= TME_NONCLIENT; } tme.hwndTrack = hWnd; tme.dwHoverTime = HOVER_DEFAULT; if (::TrackMouseEvent(&tme) == FALSE) { return false; } return true; } bool Win32WindowContext::snapLayoutHandler(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, LRESULT *result) { switch (message) { case 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. DWORD dwScreenPos = ::GetMessagePos(); QPoint qtScenePos = fromNativeLocalPosition(m_windowHandle, QPoint(GET_X_LPARAM(dwScreenPos), GET_Y_LPARAM(dwScreenPos))); auto dummy = CoreWindowAgent::Unknown; if (isInSystemButtons(qtScenePos, &dummy)) { // We must record whether the last WM_MOUSELEAVE was filtered, because if // Qt does not receive this message it will not call TrackMouseEvent() // again, resulting in the client area not responding to any mouse event. mouseLeaveBlocked = true; *result = FALSE; return true; } } mouseLeaveBlocked = false; break; } case WM_MOUSEMOVE: { // At appropriate time, we will call TrackMouseEvent() for Qt. Simultaneously, // we unset `mouseLeaveBlocked` mark and pretend as if Qt has received // WM_MOUSELEAVE. if (lastHitTestResult != WindowPart::ChromeButton && mouseLeaveBlocked) { mouseLeaveBlocked = false; std::ignore = requestForMouseLeaveMessage(hWnd, false); } break; } case WM_NCMOUSEMOVE: case WM_NCLBUTTONDOWN: case WM_NCLBUTTONUP: case WM_NCLBUTTONDBLCLK: case WM_NCRBUTTONDOWN: case WM_NCRBUTTONUP: case WM_NCRBUTTONDBLCLK: case WM_NCMBUTTONDOWN: case WM_NCMBUTTONUP: case WM_NCMBUTTONDBLCLK: case WM_NCXBUTTONDOWN: case WM_NCXBUTTONUP: case WM_NCXBUTTONDBLCLK: #if 0 // ### TODO: How to handle touch events? case WM_NCPOINTERUPDATE: case WM_NCPOINTERDOWN: case WM_NCPOINTERUP: #endif case WM_NCMOUSEHOVER: { const WindowPart currentWindowPart = lastHitTestResult; if (message == WM_NCMOUSEMOVE) { if (currentWindowPart != WindowPart::ChromeButton) { std::ignore = m_delegate->resetQtGrabbedControl(); if (mouseLeaveBlocked) { emulateClientAreaMessage(hWnd, message, wParam, lParam, 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`. lastHitTestResult = WindowPart::Outside; } if (currentWindowPart == WindowPart::ChromeButton) { emulateClientAreaMessage(hWnd, message, wParam, lParam); 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); } 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 // all other messages we should return zero instead. *result = (((message >= WM_NCXBUTTONDOWN) && (message <= WM_NCXBUTTONDBLCLK)) ? TRUE : FALSE); } return true; } break; } case WM_NCMOUSELEAVE: { const WindowPart currentWindowPart = 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 (mouseLeaveBlocked) { mouseLeaveBlocked = false; std::ignore = requestForMouseLeaveMessage(hWnd, false); } } else { if (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(hWnd, message, wParam, lParam, WM_NCMOUSELEAVE); } if (currentWindowPart == WindowPart::Outside) { // 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, but it has no bad effect. std::ignore = m_delegate->resetQtGrabbedControl(); } } break; } default: break; } return false; } }