diff --git a/dlls/winemac.drv/Makefile.in b/dlls/winemac.drv/Makefile.in index fc3dddbdae7..c43fcd46ed3 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 0689c22ec32..cf01844dd84 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 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 @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 8b6a1779b84..5443893b835 100644 --- a/dlls/winemac.drv/cocoa_app.m +++ b/dlls/winemac.drv/cocoa_app.m @@ -21,6 +21,7 @@ #import #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]; @@ -1165,251 +1142,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 @@ -1468,22 +1208,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]; @@ -1493,19 +1226,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]; @@ -1513,6 +1239,11 @@ - (BOOL) stopClippingCursor return TRUE; } + - (BOOL) clippingCursor + { + return clipCursorHandler.clippingCursor; + } + - (BOOL) isKeyPressed:(uint16_t)keyCode { int bits = sizeof(pressedKeyCodes[0]) * 8; @@ -1662,7 +1393,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; @@ -1686,8 +1417,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); @@ -1779,8 +1510,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) { @@ -1897,8 +1628,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; @@ -2265,14 +1996,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 00000000000..132527e1039 --- /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 + +@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 00000000000..7c0b53e47d9 --- /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