From 7a54badb4a5782c551deb2919f3c489fdb4fbc02 Mon Sep 17 00:00:00 2001
From: Sine Striker <trueful@163.com>
Date: 周日, 03 12月 2023 17:17:40 +0800
Subject: [PATCH] Add snap layout handling

---
 src/core/windowitemdelegate.h               |   14 +
 src/widgets/widgetitemdelegate.cpp          |   39 +++
 src/widgets/widgetitemdelegate_p.h          |    3 
 src/core/contexts/abstractwindowcontext.cpp |   17 +
 src/core/contexts/abstractwindowcontext_p.h |    1 
 src/quick/quickwindowagent_p.h              |    2 
 src/quick/quickitemdelegate.cpp             |   17 +
 src/widgets/widgetwindowagent_p.h           |    2 
 src/core/contexts/win32windowcontext.cpp    |  380 +++++++++++++++++++++++++++++++++++++
 src/core/windowitemdelegate.cpp             |   13 +
 src/core/contexts/win32windowcontext_p.h    |   20 +
 src/widgets/widgetwindowagent.cpp           |    8 
 src/core/CMakeLists.txt                     |    1 
 src/core/qwindowkit_windows.h               |    9 
 src/quick/quickitemdelegate_p.h             |    1 
 src/core/qwkcoreglobal_p.h                  |    9 
 src/quick/quickwindowagent.cpp              |    8 
 17 files changed, 507 insertions(+), 37 deletions(-)

diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 2ad4a0c..d5940f8 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -10,6 +10,7 @@
     corewindowagent_p.h
     corewindowagent.cpp
     windowitemdelegate.h
+    windowitemdelegate.cpp
     contexts/abstractwindowcontext_p.h
     contexts/abstractwindowcontext.cpp
 )
diff --git a/src/core/contexts/abstractwindowcontext.cpp b/src/core/contexts/abstractwindowcontext.cpp
index 920e210..5c75d28 100644
--- a/src/core/contexts/abstractwindowcontext.cpp
+++ b/src/core/contexts/abstractwindowcontext.cpp
@@ -84,4 +84,21 @@
         return hitTestVisibleShape;
     }
 
+    bool AbstractWindowContext::isInSystemButtons(const QPoint &pos,
+                                                  CoreWindowAgent::SystemButton *button) const {
+        *button = CoreWindowAgent::Unknown;
+        for (int i = CoreWindowAgent::WindowIcon; i <= CoreWindowAgent::Close; ++i) {
+            auto currentButton = m_systemButtons[i];
+            if (!currentButton || !m_delegate->isVisible(currentButton) ||
+                !m_delegate->isEnabled(currentButton)) {
+                continue;
+            }
+            if (m_delegate->mapGeometryToScene(currentButton).contains(pos)) {
+                *button = CoreWindowAgent::WindowIcon;
+                return true;
+            }
+        }
+        return false;
+    }
+
 }
\ No newline at end of file
diff --git a/src/core/contexts/abstractwindowcontext_p.h b/src/core/contexts/abstractwindowcontext_p.h
index c9b1374..83ad768 100644
--- a/src/core/contexts/abstractwindowcontext_p.h
+++ b/src/core/contexts/abstractwindowcontext_p.h
@@ -40,6 +40,7 @@
         void showSystemMenu(const QPoint &pos);
 
         QRegion hitTestShape() const;
+        bool isInSystemButtons(const QPoint &pos, CoreWindowAgent::SystemButton *button) const;
 
     protected:
         QWindow *m_windowHandle;
diff --git a/src/core/contexts/win32windowcontext.cpp b/src/core/contexts/win32windowcontext.cpp
index 73a8eb2..a2f885b 100644
--- a/src/core/contexts/win32windowcontext.cpp
+++ b/src/core/contexts/win32windowcontext.cpp
@@ -1,8 +1,12 @@
 #include "win32windowcontext_p.h"
 
+#include <optional>
+
 #include <QtCore/QHash>
 #include <QtCore/QAbstractNativeEventFilter>
 #include <QtCore/QCoreApplication>
+
+#include <QtGui/private/qhighdpiscaling_p.h>
 
 #include "qwkcoreglobal_p.h"
 
@@ -30,6 +34,76 @@
     };
 
     static WindowsNativeEventFilter *g_nativeFilter = nullptr;
+
+    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<HWND>(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#L1025
     // We can see from the source code that Qt will filter out some messages first and then send the
@@ -108,7 +182,7 @@
     }
 
     Win32WindowContext::Win32WindowContext(QWindow *window, WindowItemDelegate *delegate)
-        : AbstractWindowContext(window, delegate), windowId(0) {
+        : AbstractWindowContext(window, delegate) {
     }
 
     Win32WindowContext::~Win32WindowContext() {
@@ -158,7 +232,31 @@
                                         LRESULT *result) {
         *result = FALSE;
 
-        Q_UNUSED(windowId)
+        // 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
         // ...
@@ -166,4 +264,282 @@
         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<int> &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)) {
+                        mouseLeaveBlocked = true;
+                        *result = FALSE;
+                        return true;
+                    }
+                }
+                mouseLeaveBlocked = false;
+                break;
+            }
+
+            case WM_MOUSEMOVE: {
+                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;
+    }
+
 }
\ No newline at end of file
diff --git a/src/core/contexts/win32windowcontext_p.h b/src/core/contexts/win32windowcontext_p.h
index b218497..52f4433 100644
--- a/src/core/contexts/win32windowcontext_p.h
+++ b/src/core/contexts/win32windowcontext_p.h
@@ -12,13 +12,31 @@
         Win32WindowContext(QWindow *window, WindowItemDelegate *delegate);
         ~Win32WindowContext() override;
 
+        enum WindowPart {
+            Outside,
+            ClientArea,
+            ChromeButton,
+            ResizeBorder,
+            FixedBorder,
+            TitleBar,
+        };
+
     public:
         bool setup() override;
 
         bool windowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, LRESULT *result);
+        bool snapLayoutHandler(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam,
+                               LRESULT *result);
 
     protected:
-        WId windowId;
+        WId windowId = 0;
+
+        // Store the last hit test result, it's helpful to handle WM_MOUSEMOVE and WM_NCMOUSELEAVE.
+        WindowPart lastHitTestResult = WindowPart::Outside;
+
+        // True if we blocked a WM_MOUSELEAVE when mouse moves on chrome button, false when a
+        // WM_MOUSELEAVE comes or we manually call TrackMouseEvent().
+        bool mouseLeaveBlocked = false;
     };
 
 }
diff --git a/src/core/qwindowkit_windows.h b/src/core/qwindowkit_windows.h
index ff7ff2b..1c47ccf 100644
--- a/src/core/qwindowkit_windows.h
+++ b/src/core/qwindowkit_windows.h
@@ -21,4 +21,13 @@
 #  define GET_Y_LPARAM(lp) (static_cast<int>(static_cast<short>(HIWORD(lp))))
 #endif
 
+// Maybe undocumented Windows messages
+#ifndef WM_UAHDESTROYWINDOW
+#  define WM_UAHDESTROYWINDOW (0x0090)
+#endif
+
+#ifndef WM_UNREGISTER_WINDOW_SERVICES
+#  define WM_UNREGISTER_WINDOW_SERVICES (0x0272)
+#endif
+
 #endif // QWINDOWKIT_WINDOWS_H
diff --git a/src/core/qwkcoreglobal_p.h b/src/core/qwkcoreglobal_p.h
index 5f76437..bde882b 100644
--- a/src/core/qwkcoreglobal_p.h
+++ b/src/core/qwkcoreglobal_p.h
@@ -24,13 +24,4 @@
 #  define QWK_FATAL qCFatal(qWindowKitLog)
 #endif
 
-// MOC can't handle C++ attributes before 5.15.
-#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
-#  define Q_NODISCARD    [[nodiscard]]
-#  define Q_MAYBE_UNUSED [[maybe_unused]]
-#else
-#  define Q_NODISCARD
-#  define Q_MAYBE_UNUSED
-#endif
-
 #endif // QWKCOREGLOBAL_P_H
diff --git a/src/core/windowitemdelegate.cpp b/src/core/windowitemdelegate.cpp
new file mode 100644
index 0000000..552310d
--- /dev/null
+++ b/src/core/windowitemdelegate.cpp
@@ -0,0 +1,13 @@
+#include "windowitemdelegate.h"
+
+namespace QWK {
+
+    WindowItemDelegate::WindowItemDelegate() = default;
+
+    WindowItemDelegate::~WindowItemDelegate() = default;
+
+    bool WindowItemDelegate::resetQtGrabbedControl() const {
+        return true;
+    }
+
+}
\ No newline at end of file
diff --git a/src/core/windowitemdelegate.h b/src/core/windowitemdelegate.h
index fe9a252..5794851 100644
--- a/src/core/windowitemdelegate.h
+++ b/src/core/windowitemdelegate.h
@@ -2,22 +2,28 @@
 #define WINDOWITEMDELEGATE_H
 
 #include <QtCore/QObject>
+#include <QtCore/QPoint>
 #include <QtGui/QWindow>
 
-#include <QWKCore/qwkcoreglobal.h>
+#include <QWKCore/corewindowagent.h>
 
 namespace QWK {
 
-    class WindowItemDelegate {
+    class QWK_CORE_EXPORT WindowItemDelegate {
     public:
-        WindowItemDelegate() = default;
-        virtual ~WindowItemDelegate() = default;
+        WindowItemDelegate();
+        virtual ~WindowItemDelegate();
 
     public:
         virtual QWindow *window(QObject *obj) const = 0;
 
+        // Property query
         virtual bool isEnabled(QObject *obj) const = 0;
         virtual bool isVisible(QObject *obj) const = 0;
+        virtual QRect mapGeometryToScene(const QObject *obj) const = 0;
+
+        // Callbacks
+        virtual bool resetQtGrabbedControl() const;
 
     private:
         Q_DISABLE_COPY_MOVE(WindowItemDelegate)
diff --git a/src/quick/quickitemdelegate.cpp b/src/quick/quickitemdelegate.cpp
index 0483f44..1dff54d 100644
--- a/src/quick/quickitemdelegate.cpp
+++ b/src/quick/quickitemdelegate.cpp
@@ -11,15 +11,26 @@
     QuickItemDelegate::~QuickItemDelegate() = default;
 
     QWindow *QuickItemDelegate::window(QObject *obj) const {
-        return qobject_cast<QQuickItem *>(obj)->window();
+        return static_cast<QQuickItem *>(obj)->window();
     }
 
     bool QuickItemDelegate::isEnabled(QObject *obj) const {
-        return qobject_cast<QQuickItem *>(obj)->isEnabled();
+        return static_cast<QQuickItem *>(obj)->isEnabled();
     }
 
     bool QuickItemDelegate::isVisible(QObject *obj) const {
-        return qobject_cast<QQuickItem *>(obj)->isVisible();
+        return static_cast<QQuickItem *>(obj)->isVisible();
+    }
+
+    QRect QuickItemDelegate::mapGeometryToScene(const QObject *obj) const {
+        auto item = static_cast<const QQuickItem *>(obj);
+        const QPointF originPoint = item->mapToScene(QPointF(0.0, 0.0));
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
+        const QSizeF size = item->size();
+#else
+        const QSizeF size = {item->width(), item->height()};
+#endif
+        return QRectF(originPoint, size).toRect();
     }
 
 }
\ No newline at end of file
diff --git a/src/quick/quickitemdelegate_p.h b/src/quick/quickitemdelegate_p.h
index 7cd3473..51463ee 100644
--- a/src/quick/quickitemdelegate_p.h
+++ b/src/quick/quickitemdelegate_p.h
@@ -19,6 +19,7 @@
 
         bool isEnabled(QObject *obj) const override;
         bool isVisible(QObject *obj) const override;
+        QRect mapGeometryToScene(const QObject *obj) const override;
     };
 
 }
diff --git a/src/quick/quickwindowagent.cpp b/src/quick/quickwindowagent.cpp
index 3e22883..83a7e9b 100644
--- a/src/quick/quickwindowagent.cpp
+++ b/src/quick/quickwindowagent.cpp
@@ -30,14 +30,14 @@
         }
 
         Q_D(QuickWindowAgent);
-        if (d->host) {
+        if (d->hostWindow) {
             return false;
         }
 
         if (!d->setup(window, new QuickItemDelegate())) {
             return true;
         }
-        d->host = window;
+        d->hostWindow = window;
         return true;
     }
 
@@ -58,7 +58,7 @@
 
     QQuickItem *QuickWindowAgent::systemButton(SystemButton button) const {
         Q_D(const QuickWindowAgent);
-        return qobject_cast<QQuickItem *>(d->eventHandler->systemButton(button));
+        return static_cast<QQuickItem *>(d->eventHandler->systemButton(button));
     }
 
     void QuickWindowAgent::setSystemButton(SystemButton button, QQuickItem *item) {
@@ -71,7 +71,7 @@
 
     QQuickItem *QuickWindowAgent::titleBar() const {
         Q_D(const QuickWindowAgent);
-        return qobject_cast<QQuickItem *>(d->eventHandler->titleBar());
+        return static_cast<QQuickItem *>(d->eventHandler->titleBar());
     }
 
     void QuickWindowAgent::setTitleBar(QQuickItem *item) {
diff --git a/src/quick/quickwindowagent_p.h b/src/quick/quickwindowagent_p.h
index 4f56140..9043c5c 100644
--- a/src/quick/quickwindowagent_p.h
+++ b/src/quick/quickwindowagent_p.h
@@ -15,7 +15,7 @@
         void init();
 
         // Host
-        QQuickWindow *host{};
+        QQuickWindow *hostWindow{};
     };
 
 }
diff --git a/src/widgets/widgetitemdelegate.cpp b/src/widgets/widgetitemdelegate.cpp
index ad8b44b..08b3804 100644
--- a/src/widgets/widgetitemdelegate.cpp
+++ b/src/widgets/widgetitemdelegate.cpp
@@ -1,25 +1,48 @@
 #include "widgetitemdelegate_p.h"
 
+#include <QtGui/QMouseEvent>
 #include <QtWidgets/QWidget>
+#include <QtWidgets/QApplication>
+
+extern Q_WIDGETS_EXPORT QWidget *qt_button_down;
 
 namespace QWK {
-    
-    WidgetItemDelegate::WidgetItemDelegate() {
-    }
 
-    WidgetItemDelegate::~WidgetItemDelegate() {
-    }
+    WidgetItemDelegate::WidgetItemDelegate() = default;
+
+    WidgetItemDelegate::~WidgetItemDelegate() = default;
 
     QWindow *WidgetItemDelegate::window(QObject *obj) const {
-        return qobject_cast<QWidget *>(obj)->windowHandle();
+        return static_cast<QWidget *>(obj)->windowHandle();
     }
 
     bool WidgetItemDelegate::isEnabled(QObject *obj) const {
-        return qobject_cast<QWidget *>(obj)->isEnabled();
+        return static_cast<QWidget *>(obj)->isEnabled();
     }
 
     bool WidgetItemDelegate::isVisible(QObject *obj) const {
-        return qobject_cast<QWidget *>(obj)->isVisible();
+        return static_cast<QWidget *>(obj)->isVisible();
+    }
+
+    QRect WidgetItemDelegate::mapGeometryToScene(const QObject *obj) const {
+        auto widget = static_cast<const QWidget *>(obj);
+        const QPoint originPoint = widget->mapTo(widget->window(), QPoint(0, 0));
+        const QSize size = widget->size();
+        return {originPoint, size};
+    }
+
+    bool WidgetItemDelegate::resetQtGrabbedControl() const {
+        if (qt_button_down) {
+            static constexpr const auto invalidPos = QPoint{-99999, -99999};
+            const auto event =
+                new QMouseEvent(QEvent::MouseButtonRelease, invalidPos, invalidPos, invalidPos,
+                                Qt::LeftButton, QGuiApplication::mouseButtons() ^ Qt::LeftButton,
+                                QGuiApplication::keyboardModifiers());
+            QApplication::postEvent(qt_button_down, event);
+            qt_button_down = nullptr;
+            return true;
+        }
+        return false;
     }
 
 }
\ No newline at end of file
diff --git a/src/widgets/widgetitemdelegate_p.h b/src/widgets/widgetitemdelegate_p.h
index a993975..cc11c54 100644
--- a/src/widgets/widgetitemdelegate_p.h
+++ b/src/widgets/widgetitemdelegate_p.h
@@ -19,6 +19,9 @@
 
         bool isEnabled(QObject *obj) const override;
         bool isVisible(QObject *obj) const override;
+        QRect mapGeometryToScene(const QObject *obj) const override;
+
+        bool resetQtGrabbedControl() const override;
     };
 
 }
diff --git a/src/widgets/widgetwindowagent.cpp b/src/widgets/widgetwindowagent.cpp
index 59ee504..1bf0b4e 100644
--- a/src/widgets/widgetwindowagent.cpp
+++ b/src/widgets/widgetwindowagent.cpp
@@ -28,7 +28,7 @@
         }
 
         Q_D(WidgetWindowAgent);
-        if (d->host) {
+        if (d->hostWidget) {
             return false;
         }
 
@@ -36,7 +36,7 @@
         if (!d->setup(w->windowHandle(), new WidgetItemDelegate())) {
             return false;
         }
-        d->host = w;
+        d->hostWidget = w;
         return true;
     }
 
@@ -57,7 +57,7 @@
 
     QWidget *WidgetWindowAgent::systemButton(CoreWindowAgent::SystemButton button) const {
         Q_D(const WidgetWindowAgent);
-        return qobject_cast<QWidget *>(d->eventHandler->systemButton(button));
+        return static_cast<QWidget *>(d->eventHandler->systemButton(button));
     }
 
     void WidgetWindowAgent::setSystemButton(CoreWindowAgent::SystemButton button, QWidget *w) {
@@ -70,7 +70,7 @@
 
     QWidget *WidgetWindowAgent::titleBar() const {
         Q_D(const WidgetWindowAgent);
-        return qobject_cast<QWidget *>(d->eventHandler->titleBar());
+        return static_cast<QWidget *>(d->eventHandler->titleBar());
     }
 
     void WidgetWindowAgent::setTitleBar(QWidget *w) {
diff --git a/src/widgets/widgetwindowagent_p.h b/src/widgets/widgetwindowagent_p.h
index 4d8f2a5..225f567 100644
--- a/src/widgets/widgetwindowagent_p.h
+++ b/src/widgets/widgetwindowagent_p.h
@@ -15,7 +15,7 @@
         void init();
 
         // Host
-        QWidget *host{};
+        QWidget *hostWidget{};
     };
 
 }

--
Gitblit v1.9.1