include: Add IFACEMETHOD macros.
[wine.git] / dlls / winemac.drv / cocoa_cursorclipping.m
blobcfb7ff529e766ed3daf976ec3c4bcf19e1ffc799
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"
26 #pragma GCC diagnostic ignored "-Wdeclaration-after-statement"
29 /* Neither Quartz nor Cocoa has an exact analog for Win32 cursor clipping.
30  *
31  * Historically, we've used a CGEventTap and the
32  * CGAssociateMouseAndMouseCursorPosition function, as implemented in
33  * the WineEventTapClipCursorHandler class.
34  *
35  * As of macOS 10.13, there is an undocumented alternative,
36  * -[NSWindow setMouseConfinementRect:]. It comes with its own drawbacks,
37  * but is generally far simpler. It is described and implemented in
38  * the WineConfinementClipCursorHandler class.
39  *
40  * On macOS 10.13+, WineConfinementClipCursorHandler is the default.
41  * The Mac driver registry key UseConfinementCursorClipping can be set
42  * to "n" to use the event tap implementation.
43  */
46 /* Clipping via CGEventTap and CGAssociateMouseAndMouseCursorPosition:
47  *
48  * For one simple case, clipping to a 1x1 rectangle, Quartz does have an
49  * equivalent: CGAssociateMouseAndMouseCursorPosition(false).  For the
50  * general case, we leverage that.  We disassociate mouse movements from
51  * the cursor position and then move the cursor manually, keeping it within
52  * the clipping rectangle.
53  *
54  * Moving the cursor manually isn't enough.  We need to modify the event
55  * stream so that the events have the new location, too.  We need to do
56  * this at a point before the events enter Cocoa, so that Cocoa will assign
57  * the correct window to the event.  So, we install a Quartz event tap to
58  * do that.
59  *
60  * Also, there's a complication when we move the cursor.  We use
61  * CGWarpMouseCursorPosition().  That doesn't generate mouse movement
62  * events, but the change of cursor position is incorporated into the
63  * deltas of the next mouse move event.  When the mouse is disassociated
64  * from the cursor position, we need the deltas to only reflect actual
65  * device movement, not programmatic changes.  So, the event tap cancels
66  * out the change caused by our calls to CGWarpMouseCursorPosition().
67  */
70 @interface WarpRecord : NSObject
72     CGEventTimestamp timeBefore, timeAfter;
73     CGPoint from, to;
76 @property (nonatomic) CGEventTimestamp timeBefore;
77 @property (nonatomic) CGEventTimestamp timeAfter;
78 @property (nonatomic) CGPoint from;
79 @property (nonatomic) CGPoint to;
81 @end
84 @implementation WarpRecord
86 @synthesize timeBefore, timeAfter, from, to;
88 @end;
91 static void clip_cursor_location(CGRect cursorClipRect, CGPoint *location)
93     if (location->x < CGRectGetMinX(cursorClipRect))
94         location->x = CGRectGetMinX(cursorClipRect);
95     if (location->y < CGRectGetMinY(cursorClipRect))
96         location->y = CGRectGetMinY(cursorClipRect);
97     if (location->x > CGRectGetMaxX(cursorClipRect) - 1)
98         location->x = CGRectGetMaxX(cursorClipRect) - 1;
99     if (location->y > CGRectGetMaxY(cursorClipRect) - 1)
100         location->y = CGRectGetMaxY(cursorClipRect) - 1;
104 static void scale_rect_for_retina_mode(int mode, CGRect *cursorClipRect)
106     double scale = mode ? 0.5 : 2.0;
107     cursorClipRect->origin.x *= scale;
108     cursorClipRect->origin.y *= scale;
109     cursorClipRect->size.width *= scale;
110     cursorClipRect->size.height *= scale;
114 @implementation WineEventTapClipCursorHandler
116 @synthesize clippingCursor, cursorClipRect;
118     - (id) init
119     {
120         self = [super init];
121         if (self)
122         {
123             warpRecords = [[NSMutableArray alloc] init];
124         }
126         return self;
127     }
129     - (void) dealloc
130     {
131         [warpRecords release];
132         [super dealloc];
133     }
135     - (BOOL) warpCursorTo:(CGPoint*)newLocation from:(const CGPoint*)currentLocation
136     {
137         CGPoint oldLocation;
139         if (currentLocation)
140             oldLocation = *currentLocation;
141         else
142             oldLocation = NSPointToCGPoint([[WineApplicationController sharedController] flippedMouseLocation:[NSEvent mouseLocation]]);
144         if (!CGPointEqualToPoint(oldLocation, *newLocation))
145         {
146             WarpRecord* warpRecord = [[[WarpRecord alloc] init] autorelease];
147             CGError err;
149             warpRecord.from = oldLocation;
150             warpRecord.timeBefore = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
152             /* Actually move the cursor. */
153             err = CGWarpMouseCursorPosition(*newLocation);
154             if (err != kCGErrorSuccess)
155                 return FALSE;
157             warpRecord.timeAfter = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
158             *newLocation = NSPointToCGPoint([[WineApplicationController sharedController] flippedMouseLocation:[NSEvent mouseLocation]]);
160             if (!CGPointEqualToPoint(oldLocation, *newLocation))
161             {
162                 warpRecord.to = *newLocation;
163                 [warpRecords addObject:warpRecord];
164             }
165         }
167         return TRUE;
168     }
170     - (BOOL) isMouseMoveEventType:(CGEventType)type
171     {
172         switch(type)
173         {
174         case kCGEventMouseMoved:
175         case kCGEventLeftMouseDragged:
176         case kCGEventRightMouseDragged:
177         case kCGEventOtherMouseDragged:
178             return TRUE;
179         default:
180             return FALSE;
181         }
182     }
184     - (int) warpsFinishedByEventTime:(CGEventTimestamp)eventTime location:(CGPoint)eventLocation
185     {
186         int warpsFinished = 0;
187         for (WarpRecord* warpRecord in warpRecords)
188         {
189             if (warpRecord.timeAfter < eventTime ||
190                 (warpRecord.timeBefore <= eventTime && CGPointEqualToPoint(eventLocation, warpRecord.to)))
191                 warpsFinished++;
192             else
193                 break;
194         }
196         return warpsFinished;
197     }
199     - (CGEventRef) eventTapWithProxy:(CGEventTapProxy)proxy
200                                 type:(CGEventType)type
201                                event:(CGEventRef)event
202     {
203         CGEventTimestamp eventTime;
204         CGPoint eventLocation, cursorLocation;
206         if (type == kCGEventTapDisabledByUserInput)
207             return event;
208         if (type == kCGEventTapDisabledByTimeout)
209         {
210             CGEventTapEnable(cursorClippingEventTap, TRUE);
211             return event;
212         }
214         if (!clippingCursor)
215             return event;
217         eventTime = CGEventGetTimestamp(event);
218         lastEventTapEventTime = eventTime / (double)NSEC_PER_SEC;
220         eventLocation = CGEventGetLocation(event);
222         cursorLocation = NSPointToCGPoint([[WineApplicationController sharedController] flippedMouseLocation:[NSEvent mouseLocation]]);
224         if ([self isMouseMoveEventType:type])
225         {
226             double deltaX, deltaY;
227             int warpsFinished = [self warpsFinishedByEventTime:eventTime location:eventLocation];
228             int i;
230             deltaX = CGEventGetDoubleValueField(event, kCGMouseEventDeltaX);
231             deltaY = CGEventGetDoubleValueField(event, kCGMouseEventDeltaY);
233             for (i = 0; i < warpsFinished; i++)
234             {
235                 WarpRecord* warpRecord = warpRecords[0];
236                 deltaX -= warpRecord.to.x - warpRecord.from.x;
237                 deltaY -= warpRecord.to.y - warpRecord.from.y;
238                 [warpRecords removeObjectAtIndex:0];
239             }
241             if (warpsFinished)
242             {
243                 CGEventSetDoubleValueField(event, kCGMouseEventDeltaX, deltaX);
244                 CGEventSetDoubleValueField(event, kCGMouseEventDeltaY, deltaY);
245             }
247             synthesizedLocation.x += deltaX;
248             synthesizedLocation.y += deltaY;
249         }
251         // If the event is destined for another process, don't clip it.  This may
252         // happen if the user activates Exposé or Mission Control.  In that case,
253         // our app does not resign active status, so clipping is still in effect,
254         // but the cursor should not actually be clipped.
255         //
256         // In addition, the fact that mouse moves may have been delivered to a
257         // different process means we have to treat the next one we receive as
258         // absolute rather than relative.
259         if (CGEventGetIntegerValueField(event, kCGEventTargetUnixProcessID) == getpid())
260             [self clipCursorLocation:&synthesizedLocation];
261         else
262             [WineApplicationController sharedController].lastSetCursorPositionTime = lastEventTapEventTime;
264         [self warpCursorTo:&synthesizedLocation from:&cursorLocation];
265         if (!CGPointEqualToPoint(eventLocation, synthesizedLocation))
266             CGEventSetLocation(event, synthesizedLocation);
268         return event;
269     }
271     CGEventRef WineAppEventTapCallBack(CGEventTapProxy proxy, CGEventType type,
272                                        CGEventRef event, void *refcon)
273     {
274         WineEventTapClipCursorHandler* handler = refcon;
275         return [handler eventTapWithProxy:proxy type:type event:event];
276     }
278     - (BOOL) installEventTap
279     {
280         CGEventMask mask = CGEventMaskBit(kCGEventLeftMouseDown)        |
281                            CGEventMaskBit(kCGEventLeftMouseUp)          |
282                            CGEventMaskBit(kCGEventRightMouseDown)       |
283                            CGEventMaskBit(kCGEventRightMouseUp)         |
284                            CGEventMaskBit(kCGEventMouseMoved)           |
285                            CGEventMaskBit(kCGEventLeftMouseDragged)     |
286                            CGEventMaskBit(kCGEventRightMouseDragged)    |
287                            CGEventMaskBit(kCGEventOtherMouseDown)       |
288                            CGEventMaskBit(kCGEventOtherMouseUp)         |
289                            CGEventMaskBit(kCGEventOtherMouseDragged)    |
290                            CGEventMaskBit(kCGEventScrollWheel);
291         CFRunLoopSourceRef source;
293         if (cursorClippingEventTap)
294             return TRUE;
296         // We create an annotated session event tap rather than a process-specific
297         // event tap because we need to programmatically move the cursor even when
298         // mouse moves are directed to other processes.  We disable our tap when
299         // other processes are active, but things like Exposé are handled by other
300         // processes even when we remain active.
301         cursorClippingEventTap = CGEventTapCreate(kCGAnnotatedSessionEventTap, kCGHeadInsertEventTap,
302             kCGEventTapOptionDefault, mask, WineAppEventTapCallBack, self);
303         if (!cursorClippingEventTap)
304             return FALSE;
306         CGEventTapEnable(cursorClippingEventTap, FALSE);
308         source = CFMachPortCreateRunLoopSource(NULL, cursorClippingEventTap, 0);
309         if (!source)
310         {
311             CFRelease(cursorClippingEventTap);
312             cursorClippingEventTap = NULL;
313             return FALSE;
314         }
316         CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
317         CFRelease(source);
318         return TRUE;
319     }
321     - (BOOL) setCursorPosition:(CGPoint)pos
322     {
323         BOOL ret;
325         [self clipCursorLocation:&pos];
327         ret = [self warpCursorTo:&pos from:NULL];
328         synthesizedLocation = pos;
329         if (ret)
330         {
331             // We want to discard mouse-move events that have already been
332             // through the event tap, because it's too late to account for
333             // the setting of the cursor position with them.  However, the
334             // events that may be queued with times after that but before
335             // the above warp can still be used.  So, use the last event
336             // tap event time so that -sendEvent: doesn't discard them.
337             [WineApplicationController sharedController].lastSetCursorPositionTime = lastEventTapEventTime;
338         }
340         return ret;
341     }
343     - (BOOL) startClippingCursor:(CGRect)rect
344     {
345         CGError err;
347         if (!cursorClippingEventTap && ![self installEventTap])
348             return FALSE;
350         err = CGAssociateMouseAndMouseCursorPosition(false);
351         if (err != kCGErrorSuccess)
352             return FALSE;
354         clippingCursor = TRUE;
355         cursorClipRect = rect;
357         CGEventTapEnable(cursorClippingEventTap, TRUE);
359         return TRUE;
360     }
362     - (BOOL) stopClippingCursor
363     {
364         CGError err = CGAssociateMouseAndMouseCursorPosition(true);
365         if (err != kCGErrorSuccess)
366             return FALSE;
368         clippingCursor = FALSE;
370         CGEventTapEnable(cursorClippingEventTap, FALSE);
371         [warpRecords removeAllObjects];
373         return TRUE;
374     }
376     - (void) clipCursorLocation:(CGPoint*)location
377     {
378         clip_cursor_location(cursorClipRect, location);
379     }
381     - (void) setRetinaMode:(int)mode
382     {
383         scale_rect_for_retina_mode(mode, &cursorClipRect);
384     }
386 @end
389 /* Clipping via mouse confinement rects:
391  * The undocumented -[NSWindow setMouseConfinementRect:] method is almost
392  * perfect for our needs. It has two main drawbacks compared to the CGEventTap
393  * approach:
394  * 1. It requires macOS 10.13+
395  * 2. A mouse confinement rect is tied to a region of a particular window. If
396  *    an app calls ClipCursor with a rect that is outside the bounds of a
397  *    window, the best we can do is intersect that rect with the window's bounds
398  *    and clip to the result. If no windows are visible in the app, we can't do
399  *    any clipping. Switching between windows in the same app while clipping is
400  *    active is likewise impossible.
402  * But it has two major benefits:
403  * 1. The code is far simpler.
404  * 2. CGEventTap started requiring Accessibility permissions from macOS in
405  *    Catalina. It's a hassle to enable, and if it's triggered while an app is
406  *    fullscreen (which is often the case with clipping), it's easy to miss.
407  */
410 @interface NSWindow (UndocumentedMouseConfinement)
411     /* Confines the system's mouse location to the provided window-relative rect
412      * while the app is frontmost and the window is key or a child of the key
413      * window. Confinement rects will be unioned among the key window and its
414      * children. The app should invoke this any time internal window geometry
415      * changes to keep the region up to date. Set NSZeroRect to remove mouse
416      * location confinement.
417      *
418      * These have been available since 10.13.
419      */
420     - (NSRect) mouseConfinementRect;
421     - (void) setMouseConfinementRect:(NSRect)mouseConfinementRect;
422 @end
425 @implementation WineConfinementClipCursorHandler
427 @synthesize clippingCursor, cursorClipRect;
429     + (BOOL) isAvailable
430     {
431         if ([NSProcessInfo instancesRespondToSelector:@selector(isOperatingSystemAtLeastVersion:)])
432         {
433             NSOperatingSystemVersion requiredVersion = { 10, 13, 0 };
434             return [[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:requiredVersion] &&
435                    [NSWindow instancesRespondToSelector:@selector(setMouseConfinementRect:)];
436         }
438         return FALSE;
439     }
441     /* Returns the region of the given rect that intersects with the given
442      * window. The rect should be in screen coordinates. The result will be in
443      * window-relative coordinates.
444      *
445      * Returns NSZeroRect if the rect lies entirely outside the window.
446      */
447     + (NSRect) rectForScreenRect:(CGRect)rect inWindow:(NSWindow*)window
448     {
449         NSRect flippedRect = NSRectFromCGRect(rect);
450         [[WineApplicationController sharedController] flipRect:&flippedRect];
452         NSRect intersection = NSIntersectionRect([window frame], flippedRect);
454         if (NSIsEmptyRect(intersection))
455             return NSZeroRect;
457         return [window convertRectFromScreen:intersection];
458     }
460     - (BOOL) startClippingCursor:(CGRect)rect
461     {
462         if (clippingCursor && ![self stopClippingCursor])
463             return FALSE;
465         WineWindow *ownerWindow = [[WineApplicationController sharedController] frontWineWindow];
466         if (!ownerWindow)
467         {
468             /* There's nothing we can do here in this case, since confinement
469              * rects must be tied to a window. */
470             return FALSE;
471         }
473         NSRect clipRectInWindowCoords = [WineConfinementClipCursorHandler rectForScreenRect:rect
474                                                                                    inWindow:ownerWindow];
476         if (NSIsEmptyRect(clipRectInWindowCoords))
477         {
478             /* If the clip region is entirely outside of the bounds of the
479              * window, there's again nothing we can do. */
480             return FALSE;
481         }
483         [ownerWindow setMouseConfinementRect:clipRectInWindowCoords];
485         clippingWindowNumber = ownerWindow.windowNumber;
486         cursorClipRect = rect;
487         clippingCursor = TRUE;
489         return TRUE;
490     }
492     - (BOOL) stopClippingCursor
493     {
494         NSWindow *ownerWindow = [NSApp windowWithWindowNumber:clippingWindowNumber];
495         [ownerWindow setMouseConfinementRect:NSZeroRect];
497         clippingCursor = FALSE;
499         return TRUE;
500     }
502     - (void) clipCursorLocation:(CGPoint*)location
503     {
504         clip_cursor_location(cursorClipRect, location);
505     }
507     - (void) setRetinaMode:(int)mode
508     {
509         scale_rect_for_retina_mode(mode, &cursorClipRect);
510     }
512 @end