winemac.drv: Factor out cursor clipping code to its own class.
Signed-off-by: Tim Clem <tclem@codeweavers.com> Signed-off-by: Alexandre Julliard <julliard@winehq.org>
This commit is contained in:
parent
bdea89242c
commit
1c80eb5e5b
|
@ -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 \
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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];
|
||||
|
@ -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])
|
||||
{
|
||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue