The Mac driver has historically used session-wide CGEventTaps to implement ClipCursor. In macOS Mojave, such event taps started requiring Accessibility permission from the user. That's problematic on two levels:
First, it's a hassle. Unlike, say, microphone permissions, the user can't just click "Approve" on a dialog box. Instead they have to go to the System Preferences app, enter their password, and click a checkbox (which will have the name of the preloader). This is especially bad because ClipCursor is frequently called by fullscreen games, which means the permission prompt will be hidden.
Second, many apps misbehave if ClipCursor fails. For instance, some Unity games (e.g. Raft, The Forest) exhibit erratic mouse movement in that case. But apps are likely to end up in that state since Accessibility permission is deny-by-default and there is no blocking call to check for the permission. The attempt to install an event tap immediately fails until the permission is granted.
As of 10.13, there is an undocumented NSWindow method -setMouseConfinementRect: that is in many ways a better option. It does not support certain uncommon scenarios (clipping to regions outside of a window), but it is far simpler and requires fewer workarounds than the event tap. It also does not require permission from the user.
This series of patches factors the existing clipping code out of cocoa_app.m into its own class. It then adds a second implementation of that interface that uses -setMouseConfinementRect:. Finally it enables that approach by default in 10.13+, adding a registry key to enable the old behavior if needed.
I'd welcome any feedback or suggestions. If you know of any apps that clip the cursor to a region outside of a single frontmost window, I'd also love to hear about those.
Tim Clem (5): winemac.drv: Factor out cursor clipping code to its own class. winemac.drv: Create a protocol to represent a cursor clipping handler. winemac.drv: Factor common cursor clipping methods into functions. winemac.drv: Add a cursor clipping implementation using -setMouseConfinementRect:. winemac.drv: Use -setMouseConfinementRect: for cursor clipping by default.
dlls/winemac.drv/Makefile.in | 1 + dlls/winemac.drv/cocoa_app.h | 11 +- dlls/winemac.drv/cocoa_app.m | 333 ++-------------- dlls/winemac.drv/cocoa_cursorclipping.h | 73 ++++ dlls/winemac.drv/cocoa_cursorclipping.m | 510 ++++++++++++++++++++++++ dlls/winemac.drv/macdrv_cocoa.h | 1 + dlls/winemac.drv/macdrv_main.c | 4 + 7 files changed, 626 insertions(+), 307 deletions(-) create mode 100644 dlls/winemac.drv/cocoa_cursorclipping.h create mode 100644 dlls/winemac.drv/cocoa_cursorclipping.m
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
Signed-off-by: Tim Clem tclem@codeweavers.com --- dlls/winemac.drv/cocoa_app.h | 4 ++-- dlls/winemac.drv/cocoa_app.m | 5 ++++- dlls/winemac.drv/cocoa_cursorclipping.h | 29 +++++++++++++++++-------- 3 files changed, 26 insertions(+), 12 deletions(-)
diff --git a/dlls/winemac.drv/cocoa_app.h b/dlls/winemac.drv/cocoa_app.h index 4ddf2fb3babb..c1ddf079d47e 100644 --- a/dlls/winemac.drv/cocoa_app.h +++ b/dlls/winemac.drv/cocoa_app.h @@ -67,8 +67,8 @@
@class WineEventQueue; -@class WineEventTapClipCursorHandler; @class WineWindow; +@protocol WineClipCursorHandler;
@interface WineApplicationController : NSObject <NSApplicationDelegate> @@ -121,7 +121,7 @@ @interface WineApplicationController : NSObject <NSApplicationDelegate>
NSTimeInterval lastSetCursorPositionTime;
- WineEventTapClipCursorHandler* clipCursorHandler; + id<WineClipCursorHandler> clipCursorHandler;
NSImage* applicationIcon;
diff --git a/dlls/winemac.drv/cocoa_app.m b/dlls/winemac.drv/cocoa_app.m index b2d54bce9087..ac52a7425dd5 100644 --- a/dlls/winemac.drv/cocoa_app.m +++ b/dlls/winemac.drv/cocoa_app.m @@ -1145,10 +1145,13 @@ - (BOOL) setCursorPosition:(CGPoint)pos
if ([windowsBeingDragged count]) ret = FALSE; - else if (self.clippingCursor) + else if (self.clippingCursor && [clipCursorHandler respondsToSelector:@selector(setCursorPosition:)]) ret = [clipCursorHandler setCursorPosition:pos]; else { + if (self.clippingCursor) + [clipCursorHandler clipCursorLocation:&pos]; + // Annoyingly, CGWarpMouseCursorPosition() effectively disassociates // the mouse from the cursor position for 0.25 seconds. This means // that mouse movement during that interval doesn't move the cursor diff --git a/dlls/winemac.drv/cocoa_cursorclipping.h b/dlls/winemac.drv/cocoa_cursorclipping.h index 132527e10396..7ce0529a1575 100644 --- a/dlls/winemac.drv/cocoa_cursorclipping.h +++ b/dlls/winemac.drv/cocoa_cursorclipping.h @@ -21,15 +21,8 @@
#import <AppKit/AppKit.h>
-@interface WineEventTapClipCursorHandler : NSObject -{ - BOOL clippingCursor; - CGRect cursorClipRect; - CFMachPortRef cursorClippingEventTap; - NSMutableArray* warpRecords; - CGPoint synthesizedLocation; - NSTimeInterval lastEventTapEventTime; -} + +@protocol WineClipCursorHandler <NSObject>
@property (readonly, nonatomic) BOOL clippingCursor; @property (readonly, nonatomic) CGRect cursorClipRect; @@ -41,6 +34,24 @@ - (void) clipCursorLocation:(CGPoint*)location;
- (void) setRetinaMode:(int)mode;
+ @optional + /* If provided, should reposition the cursor as needed given the current + * clipping rect. If not provided, the location will be clipped by + * -clipCursorLocation, and the cursor will be warped normally. + */ - (BOOL) setCursorPosition:(CGPoint)pos;
@end + + +@interface WineEventTapClipCursorHandler : NSObject <WineClipCursorHandler> +{ + BOOL clippingCursor; + CGRect cursorClipRect; + CFMachPortRef cursorClippingEventTap; + NSMutableArray* warpRecords; + CGPoint synthesizedLocation; + NSTimeInterval lastEventTapEventTime; +} + +@end
Signed-off-by: Tim Clem tclem@codeweavers.com --- dlls/winemac.drv/cocoa_cursorclipping.m | 46 ++++++++++++++++--------- 1 file changed, 29 insertions(+), 17 deletions(-)
diff --git a/dlls/winemac.drv/cocoa_cursorclipping.m b/dlls/winemac.drv/cocoa_cursorclipping.m index 7c0b53e47d93..eaa243ae1a59 100644 --- a/dlls/winemac.drv/cocoa_cursorclipping.m +++ b/dlls/winemac.drv/cocoa_cursorclipping.m @@ -67,6 +67,29 @@ @implementation WarpRecord @end;
+static void clip_cursor_location(CGRect cursorClipRect, 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; +} + + +static void scale_rect_for_retina_mode(int mode, CGRect *cursorClipRect) +{ + double scale = mode ? 0.5 : 2.0; + cursorClipRect->origin.x *= scale; + cursorClipRect->origin.y *= scale; + cursorClipRect->size.width *= scale; + cursorClipRect->size.height *= scale; +} + + @implementation WineEventTapClipCursorHandler
@synthesize clippingCursor, cursorClipRect; @@ -88,18 +111,6 @@ - (void) dealloc [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; @@ -341,13 +352,14 @@ - (BOOL) stopClippingCursor return TRUE; }
+ - (void) clipCursorLocation:(CGPoint*)location + { + clip_cursor_location(cursorClipRect, location); + } + - (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; + scale_rect_for_retina_mode(mode, &cursorClipRect); }
@end
This 10.13+ API is far simpler than the CGEventTap approach, and does not require Accessibility permissions. It is not currently not enabled.
Signed-off-by: Tim Clem tclem@codeweavers.com --- dlls/winemac.drv/cocoa_cursorclipping.h | 16 +++ dlls/winemac.drv/cocoa_cursorclipping.m | 141 ++++++++++++++++++++++++ 2 files changed, 157 insertions(+)
diff --git a/dlls/winemac.drv/cocoa_cursorclipping.h b/dlls/winemac.drv/cocoa_cursorclipping.h index 7ce0529a1575..bec7b384d870 100644 --- a/dlls/winemac.drv/cocoa_cursorclipping.h +++ b/dlls/winemac.drv/cocoa_cursorclipping.h @@ -55,3 +55,19 @@ @interface WineEventTapClipCursorHandler : NSObject <WineClipCursorHandler> }
@end + + +@interface WineConfinementClipCursorHandler : NSObject <WineClipCursorHandler> +{ + BOOL clippingCursor; + CGRect cursorClipRect; + /* The number of the window that "owns" the clipping (i.e., the one with a + * mouseConfinementRect set). Using this rather than a WineWindow* to avoid + * tricky retain situations. */ + NSInteger clippingWindowNumber; +} + + /* Returns true if the API in use by this handler is available. */ + + (BOOL) isAvailable; + +@end diff --git a/dlls/winemac.drv/cocoa_cursorclipping.m b/dlls/winemac.drv/cocoa_cursorclipping.m index eaa243ae1a59..bbaa896099b3 100644 --- a/dlls/winemac.drv/cocoa_cursorclipping.m +++ b/dlls/winemac.drv/cocoa_cursorclipping.m @@ -21,9 +21,24 @@
#import "cocoa_app.h" #import "cocoa_cursorclipping.h" +#import "cocoa_window.h"
/* Neither Quartz nor Cocoa has an exact analog for Win32 cursor clipping. + * + * Historically, we've used a CGEventTap and the + * CGAssociateMouseAndMouseCursorPosition function, as implemented in + * the WineEventTapClipCursorHandler class. + * + * As of macOS 10.13, there is an undocumented alternative, + * -[NSWindow setMouseConfinementRect:]. It comes with its own drawbacks, + * but is generally far simpler. It is described and implemented in + * the WineConfinementClipCursorHandler class. + */ + + +/* Clipping via CGEventTap and CGAssociateMouseAndMouseCursorPosition: + * * 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 @@ -363,3 +378,129 @@ - (void) setRetinaMode:(int)mode }
@end + + +/* Clipping via mouse confinement rects: + * + * The undocumented -[NSWindow setMouseConfinementRect:] method is almost + * perfect for our needs. It has two main drawbacks compared to the CGEventTap + * approach: + * 1. It requires macOS 10.13+ + * 2. A mouse confinement rect is tied to a region of a particular window. If + * an app calls ClipCursor with a rect that is outside the bounds of a + * window, the best we can do is intersect that rect with the window's bounds + * and clip to the result. If no windows are visible in the app, we can't do + * any clipping. Switching between windows in the same app while clipping is + * active is likewise impossible. + * + * But it has two major benefits: + * 1. The code is far simpler. + * 2. CGEventTap started requiring Accessibility permissions from macOS in + * Catalina. It's a hassle to enable, and if it's triggered while an app is + * fullscreen (which is often the case with clipping), it's easy to miss. + */ + + +@interface NSWindow (UndocumentedMouseConfinement) + /* Confines the system's mouse location to the provided window-relative rect + * while the app is frontmost and the window is key or a child of the key + * window. Confinement rects will be unioned among the key window and its + * children. The app should invoke this any time internal window geometry + * changes to keep the region up to date. Set NSZeroRect to remove mouse + * location confinement. + * + * These have been available since 10.13. + */ + - (NSRect) mouseConfinementRect; + - (void) setMouseConfinementRect:(NSRect)mouseConfinementRect; +@end + + +@implementation WineConfinementClipCursorHandler + +@synthesize clippingCursor, cursorClipRect; + + + (BOOL) isAvailable + { + if ([NSProcessInfo instancesRespondToSelector:@selector(isOperatingSystemAtLeastVersion:)]) + { + NSOperatingSystemVersion requiredVersion = { 10, 13, 0 }; + return [[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:requiredVersion] && + [NSWindow instancesRespondToSelector:@selector(setMouseConfinementRect:)]; + } + + return FALSE; + } + + /* Returns the region of the given rect that intersects with the given + * window. The rect should be in screen coordinates. The result will be in + * window-relative coordinates. + * + * Returns NSZeroRect if the rect lies entirely outside the window. + */ + + (NSRect) rectForScreenRect:(CGRect)rect inWindow:(NSWindow*)window + { + NSRect flippedRect = NSRectFromCGRect(rect); + [[WineApplicationController sharedController] flipRect:&flippedRect]; + + NSRect intersection = NSIntersectionRect([window frame], flippedRect); + + if (NSIsEmptyRect(intersection)) + return NSZeroRect; + + return [window convertRectFromScreen:intersection]; + } + + - (BOOL) startClippingCursor:(CGRect)rect + { + if (clippingCursor && ![self stopClippingCursor]) + return FALSE; + + WineWindow *ownerWindow = [[WineApplicationController sharedController] frontWineWindow]; + if (!ownerWindow) + { + /* There's nothing we can do here in this case, since confinement + * rects must be tied to a window. */ + return FALSE; + } + + NSRect clipRectInWindowCoords = [WineConfinementClipCursorHandler rectForScreenRect:rect + inWindow:ownerWindow]; + + if (NSIsEmptyRect(clipRectInWindowCoords)) + { + /* If the clip region is entirely outside of the bounds of the + * window, there's again nothing we can do. */ + return FALSE; + } + + [ownerWindow setMouseConfinementRect:clipRectInWindowCoords]; + + clippingWindowNumber = ownerWindow.windowNumber; + cursorClipRect = rect; + clippingCursor = TRUE; + + return TRUE; + } + + - (BOOL) stopClippingCursor + { + NSWindow *ownerWindow = [NSApp windowWithWindowNumber:clippingWindowNumber]; + [ownerWindow setMouseConfinementRect:NSZeroRect]; + + clippingCursor = FALSE; + + return TRUE; + } + + - (void) clipCursorLocation:(CGPoint*)location + { + clip_cursor_location(cursorClipRect, location); + } + + - (void) setRetinaMode:(int)mode + { + scale_rect_for_retina_mode(mode, &cursorClipRect); + } + +@end
On macOS 10.13+, use this private NSWindow method for ClipCursor calls. The old behavior can be restored by setting the per-app Mac Driver registry key UseConfinementCursorClipping to N.
Signed-off-by: Tim Clem tclem@codeweavers.com --- dlls/winemac.drv/cocoa_app.m | 8 ++++++-- dlls/winemac.drv/cocoa_cursorclipping.m | 4 ++++ dlls/winemac.drv/macdrv_cocoa.h | 1 + dlls/winemac.drv/macdrv_main.c | 4 ++++ 4 files changed, 15 insertions(+), 2 deletions(-)
diff --git a/dlls/winemac.drv/cocoa_app.m b/dlls/winemac.drv/cocoa_app.m index ac52a7425dd5..16d446111191 100644 --- a/dlls/winemac.drv/cocoa_app.m +++ b/dlls/winemac.drv/cocoa_app.m @@ -1208,8 +1208,12 @@ - (void) updateWindowsForCursorClipping
- (BOOL) startClippingCursor:(CGRect)rect { - if (!clipCursorHandler) - clipCursorHandler = [[WineEventTapClipCursorHandler alloc] init]; + if (!clipCursorHandler) { + if (use_confinement_cursor_clipping && [WineConfinementClipCursorHandler isAvailable]) + clipCursorHandler = [[WineConfinementClipCursorHandler alloc] init]; + else + clipCursorHandler = [[WineEventTapClipCursorHandler alloc] init]; + }
if (self.clippingCursor && CGRectEqualToRect(rect, clipCursorHandler.cursorClipRect)) return TRUE; diff --git a/dlls/winemac.drv/cocoa_cursorclipping.m b/dlls/winemac.drv/cocoa_cursorclipping.m index bbaa896099b3..81b53c2703cb 100644 --- a/dlls/winemac.drv/cocoa_cursorclipping.m +++ b/dlls/winemac.drv/cocoa_cursorclipping.m @@ -34,6 +34,10 @@ * -[NSWindow setMouseConfinementRect:]. It comes with its own drawbacks, * but is generally far simpler. It is described and implemented in * the WineConfinementClipCursorHandler class. + * + * On macOS 10.13+, WineConfinementClipCursorHandler is the default. + * The Mac driver registry key UseConfinementCursorClipping can be set + * to "n" to use the event tap implementation. */
diff --git a/dlls/winemac.drv/macdrv_cocoa.h b/dlls/winemac.drv/macdrv_cocoa.h index 2c903bfb12a3..ef4e0b9205e1 100644 --- a/dlls/winemac.drv/macdrv_cocoa.h +++ b/dlls/winemac.drv/macdrv_cocoa.h @@ -162,6 +162,7 @@ extern int left_command_is_ctrl DECLSPEC_HIDDEN; extern int right_command_is_ctrl DECLSPEC_HIDDEN; extern int allow_immovable_windows DECLSPEC_HIDDEN; +extern int use_confinement_cursor_clipping DECLSPEC_HIDDEN; extern int cursor_clipping_locks_windows DECLSPEC_HIDDEN; extern int use_precise_scrolling DECLSPEC_HIDDEN; extern int gl_surface_mode DECLSPEC_HIDDEN; diff --git a/dlls/winemac.drv/macdrv_main.c b/dlls/winemac.drv/macdrv_main.c index 63c6a8199e06..a6a7f73e0401 100644 --- a/dlls/winemac.drv/macdrv_main.c +++ b/dlls/winemac.drv/macdrv_main.c @@ -57,6 +57,7 @@ int right_command_is_ctrl = 0; BOOL allow_software_rendering = FALSE; BOOL disable_window_decorations = FALSE; int allow_immovable_windows = TRUE; +int use_confinement_cursor_clipping = TRUE; int cursor_clipping_locks_windows = TRUE; int use_precise_scrolling = TRUE; int gl_surface_mode = GL_SURFACE_IN_FRONT_OPAQUE; @@ -194,6 +195,9 @@ static void setup_options(void) if (!get_config_key(hkey, appkey, "AllowImmovableWindows", buffer, sizeof(buffer))) allow_immovable_windows = IS_OPTION_TRUE(buffer[0]);
+ if (!get_config_key(hkey, appkey, "UseConfinementCursorClipping", buffer, sizeof(buffer))) + use_confinement_cursor_clipping = IS_OPTION_TRUE(buffer[0]); + if (!get_config_key(hkey, appkey, "CursorClippingLocksWindows", buffer, sizeof(buffer))) cursor_clipping_locks_windows = IS_OPTION_TRUE(buffer[0]);
I was hoping Brendon or Chip would give some feedback on this but guessing there busy and not noticed this yet.
This looks good to me and has been tested with a couple of games.
On Wed, Jan 19, 2022 at 2:40 PM Tim Clem tclem@codeweavers.com wrote:
The Mac driver has historically used session-wide CGEventTaps to implement ClipCursor. In macOS Mojave, such event taps started requiring Accessibility permission from the user. That's problematic on two levels:
First, it's a hassle. Unlike, say, microphone permissions, the user can't just click "Approve" on a dialog box. Instead they have to go to the System Preferences app, enter their password, and click a checkbox (which will have the name of the preloader). This is especially bad because ClipCursor is frequently called by fullscreen games, which means the permission prompt will be hidden.
Second, many apps misbehave if ClipCursor fails. For instance, some Unity games (e.g. Raft, The Forest) exhibit erratic mouse movement in that case. But apps are likely to end up in that state since Accessibility permission is deny-by-default and there is no blocking call to check for the permission. The attempt to install an event tap immediately fails until the permission is granted.
As of 10.13, there is an undocumented NSWindow method -setMouseConfinementRect: that is in many ways a better option. It does not support certain uncommon scenarios (clipping to regions outside of a window), but it is far simpler and requires fewer workarounds than the event tap. It also does not require permission from the user.
This series of patches factors the existing clipping code out of cocoa_app.m into its own class. It then adds a second implementation of that interface that uses -setMouseConfinementRect:. Finally it enables that approach by default in 10.13+, adding a registry key to enable the old behavior if needed.
I'd welcome any feedback or suggestions. If you know of any apps that clip the cursor to a region outside of a single frontmost window, I'd also love to hear about those.
Tim Clem (5): winemac.drv: Factor out cursor clipping code to its own class. winemac.drv: Create a protocol to represent a cursor clipping handler. winemac.drv: Factor common cursor clipping methods into functions. winemac.drv: Add a cursor clipping implementation using -setMouseConfinementRect:. winemac.drv: Use -setMouseConfinementRect: for cursor clipping by default.
dlls/winemac.drv/Makefile.in | 1 + dlls/winemac.drv/cocoa_app.h | 11 +- dlls/winemac.drv/cocoa_app.m | 333 ++-------------- dlls/winemac.drv/cocoa_cursorclipping.h | 73 ++++ dlls/winemac.drv/cocoa_cursorclipping.m | 510 ++++++++++++++++++++++++ dlls/winemac.drv/macdrv_cocoa.h | 1 + dlls/winemac.drv/macdrv_main.c | 4 + 7 files changed, 626 insertions(+), 307 deletions(-) create mode 100644 dlls/winemac.drv/cocoa_cursorclipping.h create mode 100644 dlls/winemac.drv/cocoa_cursorclipping.m
-- 2.34.1