Signed-off-by: Tim Clem tclem@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; +@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; +@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
-@interface WarpRecord : NSObject -{ - CGEventTimestamp timeBefore, timeAfter; - CGPoint from, to; -} - -@property (nonatomic) CGEventTimestamp timeBefore; -@property (nonatomic) CGEventTimestamp timeAfter; -@property (nonatomic) CGPoint from; -@property (nonatomic) CGPoint to; - -@end - - -@implementation WarpRecord - -@synthesize timeBefore, timeAfter, from, to; - -@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> + +@interface WineEventTapClipCursorHandler : NSObject +{ + BOOL clippingCursor; + CGRect cursorClipRect; + CFMachPortRef cursorClippingEventTap; + NSMutableArray* warpRecords; + CGPoint synthesizedLocation; + NSTimeInterval lastEventTapEventTime; +} + +@property (readonly, nonatomic) BOOL clippingCursor; +@property (readonly, nonatomic) CGRect cursorClipRect; + + - (BOOL) startClippingCursor:(CGRect)rect; + - (BOOL) stopClippingCursor; + + - (void) clipCursorLocation:(CGPoint*)location; + + - (void) setRetinaMode:(int)mode; + + - (BOOL) setCursorPosition:(CGPoint)pos; + +@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(). + */ + + +@interface WarpRecord : NSObject +{ + CGEventTimestamp timeBefore, timeAfter; + CGPoint from, to; +} + +@property (nonatomic) CGEventTimestamp timeBefore; +@property (nonatomic) CGEventTimestamp timeAfter; +@property (nonatomic) CGPoint from; +@property (nonatomic) CGPoint to; + +@end + + +@implementation WarpRecord + +@synthesize timeBefore, timeAfter, from, to; + +@end; + + +@implementation WineEventTapClipCursorHandler + +@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; + } + +@end