makefiles: Always use the global SOURCES variable for .l files.
[wine.git] / dlls / winemac.drv / cocoa_cursorclipping.m
blobfaefa5030e71b56ed6d1b989db65871de8969086
1 /*
2  * MACDRV CGEventTap-based cursor clipping class
3  *
4  * Copyright 2011, 2012, 2013 Ken Thomases for CodeWeavers Inc.
5  * Copyright 2021 Tim Clem for CodeWeavers Inc.
6  *
7  * This library is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Lesser General Public
9  * License as published by the Free Software Foundation; either
10  * version 2.1 of the License, or (at your option) any later version.
11  *
12  * This library is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Lesser General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public
18  * License along with this library; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
20  */
22 #import "cocoa_app.h"
23 #import "cocoa_cursorclipping.h"
24 #import "cocoa_window.h"
27 /* Neither Quartz nor Cocoa has an exact analog for Win32 cursor clipping.
28  *
29  * Historically, we've used a CGEventTap and the
30  * CGAssociateMouseAndMouseCursorPosition function, as implemented in
31  * the WineEventTapClipCursorHandler class.
32  *
33  * As of macOS 10.13, there is an undocumented alternative,
34  * -[NSWindow setMouseConfinementRect:]. It comes with its own drawbacks,
35  * but is generally far simpler. It is described and implemented in
36  * the WineConfinementClipCursorHandler class.
37  *
38  * On macOS 10.13+, WineConfinementClipCursorHandler is the default.
39  * The Mac driver registry key UseConfinementCursorClipping can be set
40  * to "n" to use the event tap implementation.
41  */
44 /* Clipping via CGEventTap and CGAssociateMouseAndMouseCursorPosition:
45  *
46  * For one simple case, clipping to a 1x1 rectangle, Quartz does have an
47  * equivalent: CGAssociateMouseAndMouseCursorPosition(false).  For the
48  * general case, we leverage that.  We disassociate mouse movements from
49  * the cursor position and then move the cursor manually, keeping it within
50  * the clipping rectangle.
51  *
52  * Moving the cursor manually isn't enough.  We need to modify the event
53  * stream so that the events have the new location, too.  We need to do
54  * this at a point before the events enter Cocoa, so that Cocoa will assign
55  * the correct window to the event.  So, we install a Quartz event tap to
56  * do that.
57  *
58  * Also, there's a complication when we move the cursor.  We use
59  * CGWarpMouseCursorPosition().  That doesn't generate mouse movement
60  * events, but the change of cursor position is incorporated into the
61  * deltas of the next mouse move event.  When the mouse is disassociated
62  * from the cursor position, we need the deltas to only reflect actual
63  * device movement, not programmatic changes.  So, the event tap cancels
64  * out the change caused by our calls to CGWarpMouseCursorPosition().
65  */
68 @interface WarpRecord : NSObject
70     CGEventTimestamp timeBefore, timeAfter;
71     CGPoint from, to;
74 @property (nonatomic) CGEventTimestamp timeBefore;
75 @property (nonatomic) CGEventTimestamp timeAfter;
76 @property (nonatomic) CGPoint from;
77 @property (nonatomic) CGPoint to;
79 @end
82 @implementation WarpRecord
84 @synthesize timeBefore, timeAfter, from, to;
86 @end;
89 static void clip_cursor_location(CGRect cursorClipRect, CGPoint *location)
91     if (location->x < CGRectGetMinX(cursorClipRect))
92         location->x = CGRectGetMinX(cursorClipRect);
93     if (location->y < CGRectGetMinY(cursorClipRect))
94         location->y = CGRectGetMinY(cursorClipRect);
95     if (location->x > CGRectGetMaxX(cursorClipRect) - 1)
96         location->x = CGRectGetMaxX(cursorClipRect) - 1;
97     if (location->y > CGRectGetMaxY(cursorClipRect) - 1)
98         location->y = CGRectGetMaxY(cursorClipRect) - 1;
102 static void scale_rect_for_retina_mode(int mode, CGRect *cursorClipRect)
104     double scale = mode ? 0.5 : 2.0;
105     cursorClipRect->origin.x *= scale;
106     cursorClipRect->origin.y *= scale;
107     cursorClipRect->size.width *= scale;
108     cursorClipRect->size.height *= scale;
112 @implementation WineEventTapClipCursorHandler
114 @synthesize clippingCursor, cursorClipRect;
116     - (id) init
117     {
118         self = [super init];
119         if (self)
120         {
121             warpRecords = [[NSMutableArray alloc] init];
122         }
124         return self;
125     }
127     - (void) dealloc
128     {
129         [warpRecords release];
130         [super dealloc];
131     }
133     - (BOOL) warpCursorTo:(CGPoint*)newLocation from:(const CGPoint*)currentLocation
134     {
135         CGPoint oldLocation;
137         if (currentLocation)
138             oldLocation = *currentLocation;
139         else
140             oldLocation = NSPointToCGPoint([[WineApplicationController sharedController] flippedMouseLocation:[NSEvent mouseLocation]]);
142         if (!CGPointEqualToPoint(oldLocation, *newLocation))
143         {
144             WarpRecord* warpRecord = [[[WarpRecord alloc] init] autorelease];
145             CGError err;
147             warpRecord.from = oldLocation;
148             warpRecord.timeBefore = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
150             /* Actually move the cursor. */
151             err = CGWarpMouseCursorPosition(*newLocation);
152             if (err != kCGErrorSuccess)
153                 return FALSE;
155             warpRecord.timeAfter = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
156             *newLocation = NSPointToCGPoint([[WineApplicationController sharedController] flippedMouseLocation:[NSEvent mouseLocation]]);
158             if (!CGPointEqualToPoint(oldLocation, *newLocation))
159             {
160                 warpRecord.to = *newLocation;
161                 [warpRecords addObject:warpRecord];
162             }
163         }
165         return TRUE;
166     }
168     - (BOOL) isMouseMoveEventType:(CGEventType)type
169     {
170         switch(type)
171         {
172         case kCGEventMouseMoved:
173         case kCGEventLeftMouseDragged:
174         case kCGEventRightMouseDragged:
175         case kCGEventOtherMouseDragged:
176             return TRUE;
177         default:
178             return FALSE;
179         }
180     }
182     - (int) warpsFinishedByEventTime:(CGEventTimestamp)eventTime location:(CGPoint)eventLocation
183     {
184         int warpsFinished = 0;
185         for (WarpRecord* warpRecord in warpRecords)
186         {
187             if (warpRecord.timeAfter < eventTime ||
188                 (warpRecord.timeBefore <= eventTime && CGPointEqualToPoint(eventLocation, warpRecord.to)))
189                 warpsFinished++;
190             else
191                 break;
192         }
194         return warpsFinished;
195     }
197     - (CGEventRef) eventTapWithProxy:(CGEventTapProxy)proxy
198                                 type:(CGEventType)type
199                                event:(CGEventRef)event
200     {
201         CGEventTimestamp eventTime;
202         CGPoint eventLocation, cursorLocation;
204         if (type == kCGEventTapDisabledByUserInput)
205             return event;
206         if (type == kCGEventTapDisabledByTimeout)
207         {
208             CGEventTapEnable(cursorClippingEventTap, TRUE);
209             return event;
210         }
212         if (!clippingCursor)
213             return event;
215         eventTime = CGEventGetTimestamp(event);
216         lastEventTapEventTime = eventTime / (double)NSEC_PER_SEC;
218         eventLocation = CGEventGetLocation(event);
220         cursorLocation = NSPointToCGPoint([[WineApplicationController sharedController] flippedMouseLocation:[NSEvent mouseLocation]]);
222         if ([self isMouseMoveEventType:type])
223         {
224             double deltaX, deltaY;
225             int warpsFinished = [self warpsFinishedByEventTime:eventTime location:eventLocation];
226             int i;
228             deltaX = CGEventGetDoubleValueField(event, kCGMouseEventDeltaX);
229             deltaY = CGEventGetDoubleValueField(event, kCGMouseEventDeltaY);
231             for (i = 0; i < warpsFinished; i++)
232             {
233                 WarpRecord* warpRecord = warpRecords[0];
234                 deltaX -= warpRecord.to.x - warpRecord.from.x;
235                 deltaY -= warpRecord.to.y - warpRecord.from.y;
236                 [warpRecords removeObjectAtIndex:0];
237             }
239             if (warpsFinished)
240             {
241                 CGEventSetDoubleValueField(event, kCGMouseEventDeltaX, deltaX);
242                 CGEventSetDoubleValueField(event, kCGMouseEventDeltaY, deltaY);
243             }
245             synthesizedLocation.x += deltaX;
246             synthesizedLocation.y += deltaY;
247         }
249         // If the event is destined for another process, don't clip it.  This may
250         // happen if the user activates Exposé or Mission Control.  In that case,
251         // our app does not resign active status, so clipping is still in effect,
252         // but the cursor should not actually be clipped.
253         //
254         // In addition, the fact that mouse moves may have been delivered to a
255         // different process means we have to treat the next one we receive as
256         // absolute rather than relative.
257         if (CGEventGetIntegerValueField(event, kCGEventTargetUnixProcessID) == getpid())
258             [self clipCursorLocation:&synthesizedLocation];
259         else
260             [WineApplicationController sharedController].lastSetCursorPositionTime = lastEventTapEventTime;
262         [self warpCursorTo:&synthesizedLocation from:&cursorLocation];
263         if (!CGPointEqualToPoint(eventLocation, synthesizedLocation))
264             CGEventSetLocation(event, synthesizedLocation);
266         return event;
267     }
269     CGEventRef WineAppEventTapCallBack(CGEventTapProxy proxy, CGEventType type,
270                                        CGEventRef event, void *refcon)
271     {
272         WineEventTapClipCursorHandler* handler = refcon;
273         return [handler eventTapWithProxy:proxy type:type event:event];
274     }
276     - (BOOL) installEventTap
277     {
278         CGEventMask mask = CGEventMaskBit(kCGEventLeftMouseDown)        |
279                            CGEventMaskBit(kCGEventLeftMouseUp)          |
280                            CGEventMaskBit(kCGEventRightMouseDown)       |
281                            CGEventMaskBit(kCGEventRightMouseUp)         |
282                            CGEventMaskBit(kCGEventMouseMoved)           |
283                            CGEventMaskBit(kCGEventLeftMouseDragged)     |
284                            CGEventMaskBit(kCGEventRightMouseDragged)    |
285                            CGEventMaskBit(kCGEventOtherMouseDown)       |
286                            CGEventMaskBit(kCGEventOtherMouseUp)         |
287                            CGEventMaskBit(kCGEventOtherMouseDragged)    |
288                            CGEventMaskBit(kCGEventScrollWheel);
289         CFRunLoopSourceRef source;
291         if (cursorClippingEventTap)
292             return TRUE;
294         // We create an annotated session event tap rather than a process-specific
295         // event tap because we need to programmatically move the cursor even when
296         // mouse moves are directed to other processes.  We disable our tap when
297         // other processes are active, but things like Exposé are handled by other
298         // processes even when we remain active.
299         cursorClippingEventTap = CGEventTapCreate(kCGAnnotatedSessionEventTap, kCGHeadInsertEventTap,
300             kCGEventTapOptionDefault, mask, WineAppEventTapCallBack, self);
301         if (!cursorClippingEventTap)
302             return FALSE;
304         CGEventTapEnable(cursorClippingEventTap, FALSE);
306         source = CFMachPortCreateRunLoopSource(NULL, cursorClippingEventTap, 0);
307         if (!source)
308         {
309             CFRelease(cursorClippingEventTap);
310             cursorClippingEventTap = NULL;
311             return FALSE;
312         }
314         CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
315         CFRelease(source);
316         return TRUE;
317     }
319     - (BOOL) setCursorPosition:(CGPoint)pos
320     {
321         BOOL ret;
323         [self clipCursorLocation:&pos];
325         ret = [self warpCursorTo:&pos from:NULL];
326         synthesizedLocation = pos;
327         if (ret)
328         {
329             // We want to discard mouse-move events that have already been
330             // through the event tap, because it's too late to account for
331             // the setting of the cursor position with them.  However, the
332             // events that may be queued with times after that but before
333             // the above warp can still be used.  So, use the last event
334             // tap event time so that -sendEvent: doesn't discard them.
335             [WineApplicationController sharedController].lastSetCursorPositionTime = lastEventTapEventTime;
336         }
338         return ret;
339     }
341     - (BOOL) startClippingCursor:(CGRect)rect
342     {
343         CGError err;
345         if (!cursorClippingEventTap && ![self installEventTap])
346             return FALSE;
348         err = CGAssociateMouseAndMouseCursorPosition(false);
349         if (err != kCGErrorSuccess)
350             return FALSE;
352         clippingCursor = TRUE;
353         cursorClipRect = rect;
355         CGEventTapEnable(cursorClippingEventTap, TRUE);
357         return TRUE;
358     }
360     - (BOOL) stopClippingCursor
361     {
362         CGError err = CGAssociateMouseAndMouseCursorPosition(true);
363         if (err != kCGErrorSuccess)
364             return FALSE;
366         clippingCursor = FALSE;
368         CGEventTapEnable(cursorClippingEventTap, FALSE);
369         [warpRecords removeAllObjects];
371         return TRUE;
372     }
374     - (void) clipCursorLocation:(CGPoint*)location
375     {
376         clip_cursor_location(cursorClipRect, location);
377     }
379     - (void) setRetinaMode:(int)mode
380     {
381         scale_rect_for_retina_mode(mode, &cursorClipRect);
382     }
384 @end
387 /* Clipping via mouse confinement rects:
389  * The undocumented -[NSWindow setMouseConfinementRect:] method is almost
390  * perfect for our needs. It has two main drawbacks compared to the CGEventTap
391  * approach:
392  * 1. It requires macOS 10.13+
393  * 2. A mouse confinement rect is tied to a region of a particular window. If
394  *    an app calls ClipCursor with a rect that is outside the bounds of a
395  *    window, the best we can do is intersect that rect with the window's bounds
396  *    and clip to the result. If no windows are visible in the app, we can't do
397  *    any clipping. Switching between windows in the same app while clipping is
398  *    active is likewise impossible.
400  * But it has two major benefits:
401  * 1. The code is far simpler.
402  * 2. CGEventTap started requiring Accessibility permissions from macOS in
403  *    Catalina. It's a hassle to enable, and if it's triggered while an app is
404  *    fullscreen (which is often the case with clipping), it's easy to miss.
405  */
408 @interface NSWindow (UndocumentedMouseConfinement)
409     /* Confines the system's mouse location to the provided window-relative rect
410      * while the app is frontmost and the window is key or a child of the key
411      * window. Confinement rects will be unioned among the key window and its
412      * children. The app should invoke this any time internal window geometry
413      * changes to keep the region up to date. Set NSZeroRect to remove mouse
414      * location confinement.
415      *
416      * These have been available since 10.13.
417      */
418     - (NSRect) mouseConfinementRect;
419     - (void) setMouseConfinementRect:(NSRect)mouseConfinementRect;
420 @end
423 @implementation WineConfinementClipCursorHandler
425 @synthesize clippingCursor, cursorClipRect;
427     + (BOOL) isAvailable
428     {
429         if ([NSProcessInfo instancesRespondToSelector:@selector(isOperatingSystemAtLeastVersion:)])
430         {
431             NSOperatingSystemVersion requiredVersion = { 10, 13, 0 };
432             return [[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:requiredVersion] &&
433                    [NSWindow instancesRespondToSelector:@selector(setMouseConfinementRect:)];
434         }
436         return FALSE;
437     }
439     /* Returns the region of the given rect that intersects with the given
440      * window. The rect should be in screen coordinates. The result will be in
441      * window-relative coordinates.
442      *
443      * Returns NSZeroRect if the rect lies entirely outside the window.
444      */
445     + (NSRect) rectForScreenRect:(CGRect)rect inWindow:(NSWindow*)window
446     {
447         NSRect flippedRect = NSRectFromCGRect(rect);
448         [[WineApplicationController sharedController] flipRect:&flippedRect];
450         NSRect intersection = NSIntersectionRect([window frame], flippedRect);
452         if (NSIsEmptyRect(intersection))
453             return NSZeroRect;
455         return [window convertRectFromScreen:intersection];
456     }
458     - (BOOL) startClippingCursor:(CGRect)rect
459     {
460         if (clippingCursor && ![self stopClippingCursor])
461             return FALSE;
463         WineWindow *ownerWindow = [[WineApplicationController sharedController] frontWineWindow];
464         if (!ownerWindow)
465         {
466             /* There's nothing we can do here in this case, since confinement
467              * rects must be tied to a window. */
468             return FALSE;
469         }
471         NSRect clipRectInWindowCoords = [WineConfinementClipCursorHandler rectForScreenRect:rect
472                                                                                    inWindow:ownerWindow];
474         if (NSIsEmptyRect(clipRectInWindowCoords))
475         {
476             /* If the clip region is entirely outside of the bounds of the
477              * window, there's again nothing we can do. */
478             return FALSE;
479         }
481         [ownerWindow setMouseConfinementRect:clipRectInWindowCoords];
483         clippingWindowNumber = ownerWindow.windowNumber;
484         cursorClipRect = rect;
485         clippingCursor = TRUE;
487         return TRUE;
488     }
490     - (BOOL) stopClippingCursor
491     {
492         NSWindow *ownerWindow = [NSApp windowWithWindowNumber:clippingWindowNumber];
493         [ownerWindow setMouseConfinementRect:NSZeroRect];
495         clippingCursor = FALSE;
497         return TRUE;
498     }
500     - (void) clipCursorLocation:(CGPoint*)location
501     {
502         clip_cursor_location(cursorClipRect, location);
503     }
505     - (void) setRetinaMode:(int)mode
506     {
507         scale_rect_for_retina_mode(mode, &cursorClipRect);
508     }
510 @end