[PATCH 1/5] winemac.drv: Factor out cursor clipping code to its own class.

Tim Clem tclem at codeweavers.com
Wed Jan 19 13:40:24 CST 2022


Signed-off-by: Tim Clem <tclem at codeweavers.com>
---
 dlls/winemac.drv/Makefile.in            |   1 +
 dlls/winemac.drv/cocoa_app.h            |  11 +-
 dlls/winemac.drv/cocoa_app.m            | 326 ++--------------------
 dlls/winemac.drv/cocoa_cursorclipping.h |  46 +++
 dlls/winemac.drv/cocoa_cursorclipping.m | 353 ++++++++++++++++++++++++
 5 files changed, 430 insertions(+), 307 deletions(-)
 create mode 100644 dlls/winemac.drv/cocoa_cursorclipping.h
 create mode 100644 dlls/winemac.drv/cocoa_cursorclipping.m

diff --git a/dlls/winemac.drv/Makefile.in b/dlls/winemac.drv/Makefile.in
index fc3dddbdae71..c43fcd46ed3d 100644
--- a/dlls/winemac.drv/Makefile.in
+++ b/dlls/winemac.drv/Makefile.in
@@ -25,6 +25,7 @@ C_SRCS = \
 OBJC_SRCS = \
 	cocoa_app.m \
 	cocoa_clipboard.m \
+	cocoa_cursorclipping.m \
 	cocoa_display.m \
 	cocoa_event.m \
 	cocoa_main.m \
diff --git a/dlls/winemac.drv/cocoa_app.h b/dlls/winemac.drv/cocoa_app.h
index 0b70a2fd55b5..4ddf2fb3babb 100644
--- a/dlls/winemac.drv/cocoa_app.h
+++ b/dlls/winemac.drv/cocoa_app.h
@@ -67,6 +67,7 @@
 
 
 @class WineEventQueue;
+ at class WineEventTapClipCursorHandler;
 @class WineWindow;
 
 
@@ -118,13 +119,9 @@ @interface WineApplicationController : NSObject <NSApplicationDelegate>
     BOOL        cursorHidden;
     BOOL        clientWantsCursorHidden;
 
-    BOOL clippingCursor;
-    CGRect cursorClipRect;
-    CFMachPortRef cursorClippingEventTap;
-    NSMutableArray* warpRecords;
-    CGPoint synthesizedLocation;
     NSTimeInterval lastSetCursorPositionTime;
-    NSTimeInterval lastEventTapEventTime;
+
+    WineEventTapClipCursorHandler* clipCursorHandler;
 
     NSImage* applicationIcon;
 
@@ -139,6 +136,7 @@ @interface WineApplicationController : NSObject <NSApplicationDelegate>
 @property (readonly, nonatomic) BOOL areDisplaysCaptured;
 
 @property (readonly) BOOL clippingCursor;
+ at property (nonatomic) NSTimeInterval lastSetCursorPositionTime;
 
     + (WineApplicationController*) sharedController;
 
@@ -160,6 +158,7 @@ - (void) window:(WineWindow*)window isBeingDragged:(BOOL)dragged;
     - (void) windowWillOrderOut:(WineWindow*)window;
 
     - (void) flipRect:(NSRect*)rect;
+    - (NSPoint) flippedMouseLocation:(NSPoint)point;
 
     - (WineWindow*) frontWineWindow;
     - (void) adjustWindowLevels;
diff --git a/dlls/winemac.drv/cocoa_app.m b/dlls/winemac.drv/cocoa_app.m
index 1bb752d20b78..b2d54bce9087 100644
--- a/dlls/winemac.drv/cocoa_app.m
+++ b/dlls/winemac.drv/cocoa_app.m
@@ -21,6 +21,7 @@
 #import <Carbon/Carbon.h>
 
 #import "cocoa_app.h"
+#import "cocoa_cursorclipping.h"
 #import "cocoa_event.h"
 #import "cocoa_window.h"
 
@@ -80,27 +81,6 @@ - (void) setWineController:(WineApplicationController*)newController
 @end
 
 
- at interface WarpRecord : NSObject
-{
-    CGEventTimestamp timeBefore, timeAfter;
-    CGPoint from, to;
-}
-
- at property (nonatomic) CGEventTimestamp timeBefore;
- at property (nonatomic) CGEventTimestamp timeAfter;
- at property (nonatomic) CGPoint from;
- at property (nonatomic) CGPoint to;
-
- at end
-
-
- at implementation WarpRecord
-
- at synthesize timeBefore, timeAfter, from, to;
-
- at end;
-
-
 @interface WineApplicationController ()
 
 @property (readwrite, copy, nonatomic) NSEvent* lastFlagsChanged;
@@ -125,8 +105,7 @@ @implementation WineApplicationController
     @synthesize applicationIcon;
     @synthesize cursorFrames, cursorTimer, cursor;
     @synthesize mouseCaptureWindow;
-
-    @synthesize clippingCursor;
+    @synthesize lastSetCursorPositionTime;
 
     + (void) initialize
     {
@@ -183,8 +162,6 @@ - (id) init
             originalDisplayModes = [[NSMutableDictionary alloc] init];
             latentDisplayModes = [[NSMutableDictionary alloc] init];
 
-            warpRecords = [[NSMutableArray alloc] init];
-
             windowsBeingDragged = [[NSMutableSet alloc] init];
 
             // On macOS 10.12+, use notifications to more reliably detect when windows are being dragged.
@@ -197,7 +174,7 @@ - (id) init
                 useDragNotifications = NO;
 
             if (!requests || !requestsManipQueue || !eventQueues || !eventQueuesLock ||
-                !keyWindows || !originalDisplayModes || !latentDisplayModes || !warpRecords)
+                !keyWindows || !originalDisplayModes || !latentDisplayModes)
             {
                 [self release];
                 return nil;
@@ -219,7 +196,7 @@ - (void) dealloc
         [cursor release];
         [screenFrameCGRects release];
         [applicationIcon release];
-        [warpRecords release];
+        [clipCursorHandler release];
         [cursorTimer release];
         [cursorFrames release];
         [latentDisplayModes release];
@@ -1162,251 +1139,14 @@ - (void) handleCommandTab
         }
     }
 
-    /*
-     * ---------- Cursor clipping methods ----------
-     *
-     * Neither Quartz nor Cocoa has an exact analog for Win32 cursor clipping.
-     * For one simple case, clipping to a 1x1 rectangle, Quartz does have an
-     * equivalent: CGAssociateMouseAndMouseCursorPosition(false).  For the
-     * general case, we leverage that.  We disassociate mouse movements from
-     * the cursor position and then move the cursor manually, keeping it within
-     * the clipping rectangle.
-     *
-     * Moving the cursor manually isn't enough.  We need to modify the event
-     * stream so that the events have the new location, too.  We need to do
-     * this at a point before the events enter Cocoa, so that Cocoa will assign
-     * the correct window to the event.  So, we install a Quartz event tap to
-     * do that.
-     *
-     * Also, there's a complication when we move the cursor.  We use
-     * CGWarpMouseCursorPosition().  That doesn't generate mouse movement
-     * events, but the change of cursor position is incorporated into the
-     * deltas of the next mouse move event.  When the mouse is disassociated
-     * from the cursor position, we need the deltas to only reflect actual
-     * device movement, not programmatic changes.  So, the event tap cancels
-     * out the change caused by our calls to CGWarpMouseCursorPosition().
-     */
-    - (void) clipCursorLocation:(CGPoint*)location
-    {
-        if (location->x < CGRectGetMinX(cursorClipRect))
-            location->x = CGRectGetMinX(cursorClipRect);
-        if (location->y < CGRectGetMinY(cursorClipRect))
-            location->y = CGRectGetMinY(cursorClipRect);
-        if (location->x > CGRectGetMaxX(cursorClipRect) - 1)
-            location->x = CGRectGetMaxX(cursorClipRect) - 1;
-        if (location->y > CGRectGetMaxY(cursorClipRect) - 1)
-            location->y = CGRectGetMaxY(cursorClipRect) - 1;
-    }
-
-    - (BOOL) warpCursorTo:(CGPoint*)newLocation from:(const CGPoint*)currentLocation
-    {
-        CGPoint oldLocation;
-
-        if (currentLocation)
-            oldLocation = *currentLocation;
-        else
-            oldLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
-
-        if (!CGPointEqualToPoint(oldLocation, *newLocation))
-        {
-            WarpRecord* warpRecord = [[[WarpRecord alloc] init] autorelease];
-            CGError err;
-
-            warpRecord.from = oldLocation;
-            warpRecord.timeBefore = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
-
-            /* Actually move the cursor. */
-            err = CGWarpMouseCursorPosition(*newLocation);
-            if (err != kCGErrorSuccess)
-                return FALSE;
-
-            warpRecord.timeAfter = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
-            *newLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
-
-            if (!CGPointEqualToPoint(oldLocation, *newLocation))
-            {
-                warpRecord.to = *newLocation;
-                [warpRecords addObject:warpRecord];
-            }
-        }
-
-        return TRUE;
-    }
-
-    - (BOOL) isMouseMoveEventType:(CGEventType)type
-    {
-        switch(type)
-        {
-        case kCGEventMouseMoved:
-        case kCGEventLeftMouseDragged:
-        case kCGEventRightMouseDragged:
-        case kCGEventOtherMouseDragged:
-            return TRUE;
-        default:
-            return FALSE;
-        }
-    }
-
-    - (int) warpsFinishedByEventTime:(CGEventTimestamp)eventTime location:(CGPoint)eventLocation
-    {
-        int warpsFinished = 0;
-        for (WarpRecord* warpRecord in warpRecords)
-        {
-            if (warpRecord.timeAfter < eventTime ||
-                (warpRecord.timeBefore <= eventTime && CGPointEqualToPoint(eventLocation, warpRecord.to)))
-                warpsFinished++;
-            else
-                break;
-        }
-
-        return warpsFinished;
-    }
-
-    - (CGEventRef) eventTapWithProxy:(CGEventTapProxy)proxy
-                                type:(CGEventType)type
-                               event:(CGEventRef)event
-    {
-        CGEventTimestamp eventTime;
-        CGPoint eventLocation, cursorLocation;
-
-        if (type == kCGEventTapDisabledByUserInput)
-            return event;
-        if (type == kCGEventTapDisabledByTimeout)
-        {
-            CGEventTapEnable(cursorClippingEventTap, TRUE);
-            return event;
-        }
-
-        if (!clippingCursor)
-            return event;
-
-        eventTime = CGEventGetTimestamp(event);
-        lastEventTapEventTime = eventTime / (double)NSEC_PER_SEC;
-
-        eventLocation = CGEventGetLocation(event);
-
-        cursorLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
-
-        if ([self isMouseMoveEventType:type])
-        {
-            double deltaX, deltaY;
-            int warpsFinished = [self warpsFinishedByEventTime:eventTime location:eventLocation];
-            int i;
-
-            deltaX = CGEventGetDoubleValueField(event, kCGMouseEventDeltaX);
-            deltaY = CGEventGetDoubleValueField(event, kCGMouseEventDeltaY);
-
-            for (i = 0; i < warpsFinished; i++)
-            {
-                WarpRecord* warpRecord = [warpRecords objectAtIndex:0];
-                deltaX -= warpRecord.to.x - warpRecord.from.x;
-                deltaY -= warpRecord.to.y - warpRecord.from.y;
-                [warpRecords removeObjectAtIndex:0];
-            }
-
-            if (warpsFinished)
-            {
-                CGEventSetDoubleValueField(event, kCGMouseEventDeltaX, deltaX);
-                CGEventSetDoubleValueField(event, kCGMouseEventDeltaY, deltaY);
-            }
-
-            synthesizedLocation.x += deltaX;
-            synthesizedLocation.y += deltaY;
-        }
-
-        // If the event is destined for another process, don't clip it.  This may
-        // happen if the user activates Exposé or Mission Control.  In that case,
-        // our app does not resign active status, so clipping is still in effect,
-        // but the cursor should not actually be clipped.
-        //
-        // In addition, the fact that mouse moves may have been delivered to a
-        // different process means we have to treat the next one we receive as
-        // absolute rather than relative.
-        if (CGEventGetIntegerValueField(event, kCGEventTargetUnixProcessID) == getpid())
-            [self clipCursorLocation:&synthesizedLocation];
-        else
-            lastSetCursorPositionTime = lastEventTapEventTime;
-
-        [self warpCursorTo:&synthesizedLocation from:&cursorLocation];
-        if (!CGPointEqualToPoint(eventLocation, synthesizedLocation))
-            CGEventSetLocation(event, synthesizedLocation);
-
-        return event;
-    }
-
-    CGEventRef WineAppEventTapCallBack(CGEventTapProxy proxy, CGEventType type,
-                                       CGEventRef event, void *refcon)
-    {
-        WineApplicationController* controller = refcon;
-        return [controller eventTapWithProxy:proxy type:type event:event];
-    }
-
-    - (BOOL) installEventTap
-    {
-        CGEventMask mask = CGEventMaskBit(kCGEventLeftMouseDown)        |
-                           CGEventMaskBit(kCGEventLeftMouseUp)          |
-                           CGEventMaskBit(kCGEventRightMouseDown)       |
-                           CGEventMaskBit(kCGEventRightMouseUp)         |
-                           CGEventMaskBit(kCGEventMouseMoved)           |
-                           CGEventMaskBit(kCGEventLeftMouseDragged)     |
-                           CGEventMaskBit(kCGEventRightMouseDragged)    |
-                           CGEventMaskBit(kCGEventOtherMouseDown)       |
-                           CGEventMaskBit(kCGEventOtherMouseUp)         |
-                           CGEventMaskBit(kCGEventOtherMouseDragged)    |
-                           CGEventMaskBit(kCGEventScrollWheel);
-        CFRunLoopSourceRef source;
-
-        if (cursorClippingEventTap)
-            return TRUE;
-
-        // We create an annotated session event tap rather than a process-specific
-        // event tap because we need to programmatically move the cursor even when
-        // mouse moves are directed to other processes.  We disable our tap when
-        // other processes are active, but things like Exposé are handled by other
-        // processes even when we remain active.
-        cursorClippingEventTap = CGEventTapCreate(kCGAnnotatedSessionEventTap, kCGHeadInsertEventTap,
-            kCGEventTapOptionDefault, mask, WineAppEventTapCallBack, self);
-        if (!cursorClippingEventTap)
-            return FALSE;
-
-        CGEventTapEnable(cursorClippingEventTap, FALSE);
-
-        source = CFMachPortCreateRunLoopSource(NULL, cursorClippingEventTap, 0);
-        if (!source)
-        {
-            CFRelease(cursorClippingEventTap);
-            cursorClippingEventTap = NULL;
-            return FALSE;
-        }
-
-        CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
-        CFRelease(source);
-        return TRUE;
-    }
-
     - (BOOL) setCursorPosition:(CGPoint)pos
     {
         BOOL ret;
 
         if ([windowsBeingDragged count])
             ret = FALSE;
-        else if (clippingCursor)
-        {
-            [self clipCursorLocation:&pos];
-
-            ret = [self warpCursorTo:&pos from:NULL];
-            synthesizedLocation = pos;
-            if (ret)
-            {
-                // We want to discard mouse-move events that have already been
-                // through the event tap, because it's too late to account for
-                // the setting of the cursor position with them.  However, the
-                // events that may be queued with times after that but before
-                // the above warp can still be used.  So, use the last event
-                // tap event time so that -sendEvent: doesn't discard them.
-                lastSetCursorPositionTime = lastEventTapEventTime;
-            }
-        }
+        else if (self.clippingCursor)
+            ret = [clipCursorHandler setCursorPosition:pos];
         else
         {
             // Annoyingly, CGWarpMouseCursorPosition() effectively disassociates
@@ -1465,22 +1205,15 @@ - (void) updateWindowsForCursorClipping
 
     - (BOOL) startClippingCursor:(CGRect)rect
     {
-        CGError err;
+        if (!clipCursorHandler)
+            clipCursorHandler = [[WineEventTapClipCursorHandler alloc] init];
 
-        if (!cursorClippingEventTap && ![self installEventTap])
-            return FALSE;
-
-        if (clippingCursor && CGRectEqualToRect(rect, cursorClipRect))
+        if (self.clippingCursor && CGRectEqualToRect(rect, clipCursorHandler.cursorClipRect))
             return TRUE;
 
-        err = CGAssociateMouseAndMouseCursorPosition(false);
-        if (err != kCGErrorSuccess)
+        if (![clipCursorHandler startClippingCursor:rect])
             return FALSE;
 
-        clippingCursor = TRUE;
-        cursorClipRect = rect;
-
-        CGEventTapEnable(cursorClippingEventTap, TRUE);
         [self setCursorPosition:NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]])];
 
         [self updateWindowsForCursorClipping];
@@ -1490,19 +1223,12 @@ - (BOOL) startClippingCursor:(CGRect)rect
 
     - (BOOL) stopClippingCursor
     {
-        CGError err;
-
-        if (!clippingCursor)
+        if (!self.clippingCursor)
             return TRUE;
 
-        err = CGAssociateMouseAndMouseCursorPosition(true);
-        if (err != kCGErrorSuccess)
+        if (![clipCursorHandler stopClippingCursor])
             return FALSE;
 
-        clippingCursor = FALSE;
-
-        CGEventTapEnable(cursorClippingEventTap, FALSE);
-        [warpRecords removeAllObjects];
         lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
 
         [self updateWindowsForCursorClipping];
@@ -1510,6 +1236,11 @@ - (BOOL) stopClippingCursor
         return TRUE;
     }
 
+    - (BOOL) clippingCursor
+    {
+        return clipCursorHandler.clippingCursor;
+    }
+
     - (BOOL) isKeyPressed:(uint16_t)keyCode
     {
         int bits = sizeof(pressedKeyCodes[0]) * 8;
@@ -1659,7 +1390,7 @@ - (void) handleMouseMove:(NSEvent*)anEvent
 
                 // Assume cursor is pinned for now
                 absolute = FALSE;
-                if (!clippingCursor || CGRectContainsPoint(cursorClipRect, computedPoint))
+                if (!self.clippingCursor || CGRectContainsPoint(clipCursorHandler.cursorClipRect, computedPoint))
                 {
                     const CGRect* rects;
                     NSUInteger count, i;
@@ -1683,8 +1414,8 @@ - (void) handleMouseMove:(NSEvent*)anEvent
 
             if (absolute)
             {
-                if (clippingCursor)
-                    [self clipCursorLocation:&point];
+                if (self.clippingCursor)
+                    [clipCursorHandler clipCursorLocation:&point];
                 point = cgpoint_win_from_mac(point);
 
                 event = macdrv_create_event(MOUSE_MOVED_ABSOLUTE, targetWindow);
@@ -1776,8 +1507,8 @@ - (void) handleMouseButton:(NSEvent*)theEvent
                             type == NSEventTypeOtherMouseDown);
             CGPoint pt = CGEventGetLocation([theEvent CGEvent]);
 
-            if (clippingCursor)
-                [self clipCursorLocation:&pt];
+            if (self.clippingCursor)
+                [clipCursorHandler clipCursorLocation:&pt];
 
             if (pressed)
             {
@@ -1894,8 +1625,8 @@ - (void) handleScrollWheel:(NSEvent*)theEvent
             CGPoint pt = CGEventGetLocation(cgevent);
             BOOL process;
 
-            if (clippingCursor)
-                [self clipCursorLocation:&pt];
+            if (self.clippingCursor)
+                [clipCursorHandler clipCursorLocation:&pt];
 
             if (mouseCaptureWindow)
                 process = TRUE;
@@ -2262,14 +1993,7 @@ - (void) setRetinaMode:(int)mode
     {
         retina_on = mode;
 
-        if (clippingCursor)
-        {
-            double scale = mode ? 0.5 : 2.0;
-            cursorClipRect.origin.x *= scale;
-            cursorClipRect.origin.y *= scale;
-            cursorClipRect.size.width *= scale;
-            cursorClipRect.size.height *= scale;
-        }
+        [clipCursorHandler setRetinaMode:mode];
 
         for (WineWindow* window in [NSApp windows])
         {
diff --git a/dlls/winemac.drv/cocoa_cursorclipping.h b/dlls/winemac.drv/cocoa_cursorclipping.h
new file mode 100644
index 000000000000..132527e10396
--- /dev/null
+++ b/dlls/winemac.drv/cocoa_cursorclipping.h
@@ -0,0 +1,46 @@
+/*
+ * MACDRV CGEventTap-based cursor clipping class declaration
+ *
+ * Copyright 2011, 2012, 2013 Ken Thomases for CodeWeavers Inc.
+ * Copyright 2021 Tim Clem for CodeWeavers Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+#import <AppKit/AppKit.h>
+
+ at interface WineEventTapClipCursorHandler : NSObject
+{
+    BOOL clippingCursor;
+    CGRect cursorClipRect;
+    CFMachPortRef cursorClippingEventTap;
+    NSMutableArray* warpRecords;
+    CGPoint synthesizedLocation;
+    NSTimeInterval lastEventTapEventTime;
+}
+
+ at property (readonly, nonatomic) BOOL clippingCursor;
+ at property (readonly, nonatomic) CGRect cursorClipRect;
+
+    - (BOOL) startClippingCursor:(CGRect)rect;
+    - (BOOL) stopClippingCursor;
+
+    - (void) clipCursorLocation:(CGPoint*)location;
+
+    - (void) setRetinaMode:(int)mode;
+
+    - (BOOL) setCursorPosition:(CGPoint)pos;
+
+ at end
diff --git a/dlls/winemac.drv/cocoa_cursorclipping.m b/dlls/winemac.drv/cocoa_cursorclipping.m
new file mode 100644
index 000000000000..7c0b53e47d93
--- /dev/null
+++ b/dlls/winemac.drv/cocoa_cursorclipping.m
@@ -0,0 +1,353 @@
+/*
+ * MACDRV CGEventTap-based cursor clipping class
+ *
+ * Copyright 2011, 2012, 2013 Ken Thomases for CodeWeavers Inc.
+ * Copyright 2021 Tim Clem for CodeWeavers Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+#import "cocoa_app.h"
+#import "cocoa_cursorclipping.h"
+
+
+/* Neither Quartz nor Cocoa has an exact analog for Win32 cursor clipping.
+ * For one simple case, clipping to a 1x1 rectangle, Quartz does have an
+ * equivalent: CGAssociateMouseAndMouseCursorPosition(false).  For the
+ * general case, we leverage that.  We disassociate mouse movements from
+ * the cursor position and then move the cursor manually, keeping it within
+ * the clipping rectangle.
+ *
+ * Moving the cursor manually isn't enough.  We need to modify the event
+ * stream so that the events have the new location, too.  We need to do
+ * this at a point before the events enter Cocoa, so that Cocoa will assign
+ * the correct window to the event.  So, we install a Quartz event tap to
+ * do that.
+ *
+ * Also, there's a complication when we move the cursor.  We use
+ * CGWarpMouseCursorPosition().  That doesn't generate mouse movement
+ * events, but the change of cursor position is incorporated into the
+ * deltas of the next mouse move event.  When the mouse is disassociated
+ * from the cursor position, we need the deltas to only reflect actual
+ * device movement, not programmatic changes.  So, the event tap cancels
+ * out the change caused by our calls to CGWarpMouseCursorPosition().
+ */
+
+
+ at interface WarpRecord : NSObject
+{
+    CGEventTimestamp timeBefore, timeAfter;
+    CGPoint from, to;
+}
+
+ at property (nonatomic) CGEventTimestamp timeBefore;
+ at property (nonatomic) CGEventTimestamp timeAfter;
+ at property (nonatomic) CGPoint from;
+ at property (nonatomic) CGPoint to;
+
+ at end
+
+
+ at implementation WarpRecord
+
+ at synthesize timeBefore, timeAfter, from, to;
+
+ at end;
+
+
+ at implementation WineEventTapClipCursorHandler
+
+ at synthesize clippingCursor, cursorClipRect;
+
+    - (id) init
+    {
+        self = [super init];
+        if (self)
+        {
+            warpRecords = [[NSMutableArray alloc] init];
+        }
+
+        return self;
+    }
+
+    - (void) dealloc
+    {
+        [warpRecords release];
+        [super dealloc];
+    }
+
+    - (void) clipCursorLocation:(CGPoint*)location
+    {
+        if (location->x < CGRectGetMinX(cursorClipRect))
+            location->x = CGRectGetMinX(cursorClipRect);
+        if (location->y < CGRectGetMinY(cursorClipRect))
+            location->y = CGRectGetMinY(cursorClipRect);
+        if (location->x > CGRectGetMaxX(cursorClipRect) - 1)
+            location->x = CGRectGetMaxX(cursorClipRect) - 1;
+        if (location->y > CGRectGetMaxY(cursorClipRect) - 1)
+            location->y = CGRectGetMaxY(cursorClipRect) - 1;
+    }
+
+    - (BOOL) warpCursorTo:(CGPoint*)newLocation from:(const CGPoint*)currentLocation
+    {
+        CGPoint oldLocation;
+
+        if (currentLocation)
+            oldLocation = *currentLocation;
+        else
+            oldLocation = NSPointToCGPoint([[WineApplicationController sharedController] flippedMouseLocation:[NSEvent mouseLocation]]);
+
+        if (!CGPointEqualToPoint(oldLocation, *newLocation))
+        {
+            WarpRecord* warpRecord = [[[WarpRecord alloc] init] autorelease];
+            CGError err;
+
+            warpRecord.from = oldLocation;
+            warpRecord.timeBefore = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
+
+            /* Actually move the cursor. */
+            err = CGWarpMouseCursorPosition(*newLocation);
+            if (err != kCGErrorSuccess)
+                return FALSE;
+
+            warpRecord.timeAfter = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
+            *newLocation = NSPointToCGPoint([[WineApplicationController sharedController] flippedMouseLocation:[NSEvent mouseLocation]]);
+
+            if (!CGPointEqualToPoint(oldLocation, *newLocation))
+            {
+                warpRecord.to = *newLocation;
+                [warpRecords addObject:warpRecord];
+            }
+        }
+
+        return TRUE;
+    }
+
+    - (BOOL) isMouseMoveEventType:(CGEventType)type
+    {
+        switch(type)
+        {
+        case kCGEventMouseMoved:
+        case kCGEventLeftMouseDragged:
+        case kCGEventRightMouseDragged:
+        case kCGEventOtherMouseDragged:
+            return TRUE;
+        default:
+            return FALSE;
+        }
+    }
+
+    - (int) warpsFinishedByEventTime:(CGEventTimestamp)eventTime location:(CGPoint)eventLocation
+    {
+        int warpsFinished = 0;
+        for (WarpRecord* warpRecord in warpRecords)
+        {
+            if (warpRecord.timeAfter < eventTime ||
+                (warpRecord.timeBefore <= eventTime && CGPointEqualToPoint(eventLocation, warpRecord.to)))
+                warpsFinished++;
+            else
+                break;
+        }
+
+        return warpsFinished;
+    }
+
+    - (CGEventRef) eventTapWithProxy:(CGEventTapProxy)proxy
+                                type:(CGEventType)type
+                               event:(CGEventRef)event
+    {
+        CGEventTimestamp eventTime;
+        CGPoint eventLocation, cursorLocation;
+
+        if (type == kCGEventTapDisabledByUserInput)
+            return event;
+        if (type == kCGEventTapDisabledByTimeout)
+        {
+            CGEventTapEnable(cursorClippingEventTap, TRUE);
+            return event;
+        }
+
+        if (!clippingCursor)
+            return event;
+
+        eventTime = CGEventGetTimestamp(event);
+        lastEventTapEventTime = eventTime / (double)NSEC_PER_SEC;
+
+        eventLocation = CGEventGetLocation(event);
+
+        cursorLocation = NSPointToCGPoint([[WineApplicationController sharedController] flippedMouseLocation:[NSEvent mouseLocation]]);
+
+        if ([self isMouseMoveEventType:type])
+        {
+            double deltaX, deltaY;
+            int warpsFinished = [self warpsFinishedByEventTime:eventTime location:eventLocation];
+            int i;
+
+            deltaX = CGEventGetDoubleValueField(event, kCGMouseEventDeltaX);
+            deltaY = CGEventGetDoubleValueField(event, kCGMouseEventDeltaY);
+
+            for (i = 0; i < warpsFinished; i++)
+            {
+                WarpRecord* warpRecord = [warpRecords objectAtIndex:0];
+                deltaX -= warpRecord.to.x - warpRecord.from.x;
+                deltaY -= warpRecord.to.y - warpRecord.from.y;
+                [warpRecords removeObjectAtIndex:0];
+            }
+
+            if (warpsFinished)
+            {
+                CGEventSetDoubleValueField(event, kCGMouseEventDeltaX, deltaX);
+                CGEventSetDoubleValueField(event, kCGMouseEventDeltaY, deltaY);
+            }
+
+            synthesizedLocation.x += deltaX;
+            synthesizedLocation.y += deltaY;
+        }
+
+        // If the event is destined for another process, don't clip it.  This may
+        // happen if the user activates Exposé or Mission Control.  In that case,
+        // our app does not resign active status, so clipping is still in effect,
+        // but the cursor should not actually be clipped.
+        //
+        // In addition, the fact that mouse moves may have been delivered to a
+        // different process means we have to treat the next one we receive as
+        // absolute rather than relative.
+        if (CGEventGetIntegerValueField(event, kCGEventTargetUnixProcessID) == getpid())
+            [self clipCursorLocation:&synthesizedLocation];
+        else
+            [WineApplicationController sharedController].lastSetCursorPositionTime = lastEventTapEventTime;
+
+        [self warpCursorTo:&synthesizedLocation from:&cursorLocation];
+        if (!CGPointEqualToPoint(eventLocation, synthesizedLocation))
+            CGEventSetLocation(event, synthesizedLocation);
+
+        return event;
+    }
+
+    CGEventRef WineAppEventTapCallBack(CGEventTapProxy proxy, CGEventType type,
+                                       CGEventRef event, void *refcon)
+    {
+        WineEventTapClipCursorHandler* handler = refcon;
+        return [handler eventTapWithProxy:proxy type:type event:event];
+    }
+
+    - (BOOL) installEventTap
+    {
+        CGEventMask mask = CGEventMaskBit(kCGEventLeftMouseDown)        |
+                           CGEventMaskBit(kCGEventLeftMouseUp)          |
+                           CGEventMaskBit(kCGEventRightMouseDown)       |
+                           CGEventMaskBit(kCGEventRightMouseUp)         |
+                           CGEventMaskBit(kCGEventMouseMoved)           |
+                           CGEventMaskBit(kCGEventLeftMouseDragged)     |
+                           CGEventMaskBit(kCGEventRightMouseDragged)    |
+                           CGEventMaskBit(kCGEventOtherMouseDown)       |
+                           CGEventMaskBit(kCGEventOtherMouseUp)         |
+                           CGEventMaskBit(kCGEventOtherMouseDragged)    |
+                           CGEventMaskBit(kCGEventScrollWheel);
+        CFRunLoopSourceRef source;
+
+        if (cursorClippingEventTap)
+            return TRUE;
+
+        // We create an annotated session event tap rather than a process-specific
+        // event tap because we need to programmatically move the cursor even when
+        // mouse moves are directed to other processes.  We disable our tap when
+        // other processes are active, but things like Exposé are handled by other
+        // processes even when we remain active.
+        cursorClippingEventTap = CGEventTapCreate(kCGAnnotatedSessionEventTap, kCGHeadInsertEventTap,
+            kCGEventTapOptionDefault, mask, WineAppEventTapCallBack, self);
+        if (!cursorClippingEventTap)
+            return FALSE;
+
+        CGEventTapEnable(cursorClippingEventTap, FALSE);
+
+        source = CFMachPortCreateRunLoopSource(NULL, cursorClippingEventTap, 0);
+        if (!source)
+        {
+            CFRelease(cursorClippingEventTap);
+            cursorClippingEventTap = NULL;
+            return FALSE;
+        }
+
+        CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
+        CFRelease(source);
+        return TRUE;
+    }
+
+    - (BOOL) setCursorPosition:(CGPoint)pos
+    {
+        BOOL ret;
+
+        [self clipCursorLocation:&pos];
+
+        ret = [self warpCursorTo:&pos from:NULL];
+        synthesizedLocation = pos;
+        if (ret)
+        {
+            // We want to discard mouse-move events that have already been
+            // through the event tap, because it's too late to account for
+            // the setting of the cursor position with them.  However, the
+            // events that may be queued with times after that but before
+            // the above warp can still be used.  So, use the last event
+            // tap event time so that -sendEvent: doesn't discard them.
+            [WineApplicationController sharedController].lastSetCursorPositionTime = lastEventTapEventTime;
+        }
+
+        return ret;
+    }
+
+    - (BOOL) startClippingCursor:(CGRect)rect
+    {
+        CGError err;
+
+        if (!cursorClippingEventTap && ![self installEventTap])
+            return FALSE;
+
+        err = CGAssociateMouseAndMouseCursorPosition(false);
+        if (err != kCGErrorSuccess)
+            return FALSE;
+
+        clippingCursor = TRUE;
+        cursorClipRect = rect;
+
+        CGEventTapEnable(cursorClippingEventTap, TRUE);
+
+        return TRUE;
+    }
+
+    - (BOOL) stopClippingCursor
+    {
+        CGError err = CGAssociateMouseAndMouseCursorPosition(true);
+        if (err != kCGErrorSuccess)
+            return FALSE;
+
+        clippingCursor = FALSE;
+
+        CGEventTapEnable(cursorClippingEventTap, FALSE);
+        [warpRecords removeAllObjects];
+
+        return TRUE;
+    }
+
+    - (void) setRetinaMode:(int)mode
+    {
+        double scale = mode ? 0.5 : 2.0;
+        cursorClipRect.origin.x *= scale;
+        cursorClipRect.origin.y *= scale;
+        cursorClipRect.size.width *= scale;
+        cursorClipRect.size.height *= scale;
+    }
+
+ at end
-- 
2.34.1




More information about the wine-devel mailing list