diff --git a/dlls/winemac.drv/cocoa_app.h b/dlls/winemac.drv/cocoa_app.h index 35675956d1b..4e81e305dfc 100644 --- a/dlls/winemac.drv/cocoa_app.h +++ b/dlls/winemac.drv/cocoa_app.h @@ -59,7 +59,13 @@ @interface WineApplication : NSApplication NSTimer* cursorTimer; BOOL cursorHidden; + BOOL clippingCursor; + CGRect cursorClipRect; + CFMachPortRef cursorClippingEventTap; + NSMutableArray* warpRecords; + CGPoint synthesizedLocation; NSTimeInterval lastSetCursorPositionTime; + NSTimeInterval lastEventTapEventTime; } @property (nonatomic) CGEventSourceKeyboardType keyboardType; @@ -79,6 +85,8 @@ - (void) windowGotFocus:(WineWindow*)window; - (void) keyboardSelectionDidChange; + - (void) flipRect:(NSRect*)rect; + - (void) wineWindow:(WineWindow*)window ordered:(NSWindowOrderingMode)order relativeTo:(WineWindow*)otherWindow; diff --git a/dlls/winemac.drv/cocoa_app.m b/dlls/winemac.drv/cocoa_app.m index f1ad0bc427d..0d6913e6e75 100644 --- a/dlls/winemac.drv/cocoa_app.m +++ b/dlls/winemac.drv/cocoa_app.m @@ -19,6 +19,7 @@ */ #import +#include #import "cocoa_app.h" #import "cocoa_event.h" @@ -28,6 +29,27 @@ int macdrv_err_on; +@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 WineApplication () @property (readwrite, copy, nonatomic) NSEvent* lastFlagsChanged; @@ -56,8 +78,10 @@ - (id) init originalDisplayModes = [[NSMutableDictionary alloc] init]; + warpRecords = [[NSMutableArray alloc] init]; + if (!eventQueues || !eventQueuesLock || !keyWindows || !orderedWineWindows || - !originalDisplayModes) + !originalDisplayModes || !warpRecords) { [self release]; return nil; @@ -68,6 +92,7 @@ - (id) init - (void) dealloc { + [warpRecords release]; [cursorTimer release]; [cursorFrames release]; [originalDisplayModes release]; @@ -263,6 +288,14 @@ actually off by one (precisely because they were flipped from return point; } + - (void) flipRect:(NSRect*)rect + { + // We don't use -primaryScreenHeight here so there's no chance of having + // out-of-date cached info. This method is called infrequently enough + // that getting the screen height each time is not prohibitively expensive. + rect->origin.y = NSMaxY([[[NSScreen screens] objectAtIndex:0] frame]) - NSMaxY(*rect); + } + - (void) wineWindow:(WineWindow*)window ordered:(NSWindowOrderingMode)order relativeTo:(WineWindow*)otherWindow @@ -548,17 +581,283 @@ - (void) setCursorWithFrames:(NSArray*)frames } } + /* + * ---------- 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; + } + + 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) + { + WineApplication* app = refcon; + return [app eventTapWithProxy:proxy type:type event:event]; + } + + - (BOOL) installEventTap + { + ProcessSerialNumber psn; + OSErr err; + 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; + void* appServices; + OSErr (*pGetCurrentProcess)(ProcessSerialNumber* PSN); + + if (cursorClippingEventTap) + return TRUE; + + // We need to get the Mac GetCurrentProcess() from the ApplicationServices + // framework with dlsym() because the Win32 function of the same name + // obscures it. + appServices = dlopen("/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices", RTLD_LAZY); + if (!appServices) + return FALSE; + + pGetCurrentProcess = dlsym(appServices, "GetCurrentProcess"); + if (!pGetCurrentProcess) + { + dlclose(appServices); + return FALSE; + } + + err = pGetCurrentProcess(&psn); + dlclose(appServices); + if (err != noErr) + return FALSE; + + // 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; - ret = (CGWarpMouseCursorPosition(pos) == kCGErrorSuccess); + if (clippingCursor) + { + [self clipCursorLocation:&pos]; + + synthesizedLocation = pos; + ret = [self warpCursorTo:&synthesizedLocation from:NULL]; + 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 + { + ret = (CGWarpMouseCursorPosition(pos) == kCGErrorSuccess); + if (ret) + lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime]; + } + if (ret) { WineEventQueue* queue; - lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime]; - // Discard all pending mouse move events. [eventQueuesLock lock]; for (queue in eventQueues) @@ -573,6 +872,56 @@ - (BOOL) setCursorPosition:(CGPoint)pos return ret; } + - (void) activateCursorClipping + { + if (clippingCursor) + { + CGEventTapEnable(cursorClippingEventTap, TRUE); + [self setCursorPosition:NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]])]; + } + } + + - (void) deactivateCursorClipping + { + if (clippingCursor) + { + CGEventTapEnable(cursorClippingEventTap, FALSE); + [warpRecords removeAllObjects]; + lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime]; + } + } + + - (BOOL) startClippingCursor:(CGRect)rect + { + CGError err; + + if (!cursorClippingEventTap && ![self installEventTap]) + return FALSE; + + err = CGAssociateMouseAndMouseCursorPosition(false); + if (err != kCGErrorSuccess) + return FALSE; + + clippingCursor = TRUE; + cursorClipRect = rect; + if ([self isActive]) + [self activateCursorClipping]; + + return TRUE; + } + + - (BOOL) stopClippingCursor + { + CGError err = CGAssociateMouseAndMouseCursorPosition(true); + if (err != kCGErrorSuccess) + return FALSE; + + [self deactivateCursorClipping]; + clippingCursor = FALSE; + + return TRUE; + } + /* * ---------- NSApplication method overrides ---------- @@ -612,8 +961,9 @@ - (void) sendEvent:(NSEvent*)anEvent BOOL absolute = forceNextMouseMoveAbsolute || (targetWindow != lastTargetWindow); forceNextMouseMoveAbsolute = FALSE; - // If we recently warped the cursor, discard mouse move events until - // we see an event which is later than that time. + // If we recently warped the cursor (other than in our cursor-clipping + // event tap), discard mouse move events until we see an event which is + // later than that time. if (lastSetCursorPositionTime) { if ([anEvent timestamp] <= lastSetCursorPositionTime) @@ -651,6 +1001,8 @@ - (void) sendEvent:(NSEvent*)anEvent */ - (void)applicationDidBecomeActive:(NSNotification *)notification { + [self activateCursorClipping]; + [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){ WineWindow* window = obj; if ([window levelWhenActive] != [window level]) @@ -740,6 +1092,8 @@ - (void)applicationWillFinishLaunching:(NSNotification *)notification - (void)applicationWillResignActive:(NSNotification *)notification { + [self deactivateCursorClipping]; + [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){ WineWindow* window = obj; NSInteger level = window.floating ? NSFloatingWindowLevel : NSNormalWindowLevel; @@ -946,3 +1300,45 @@ int macdrv_set_cursor_position(CGPoint pos) return ret; } + +/*********************************************************************** + * macdrv_clip_cursor + * + * Sets the cursor cursor clipping rectangle. If the rectangle is equal + * to or larger than the whole desktop region, the cursor is unclipped. + * Returns zero on failure, non-zero on success. + */ +int macdrv_clip_cursor(CGRect rect) +{ + __block int ret; + + OnMainThread(^{ + BOOL clipping = FALSE; + + if (!CGRectIsInfinite(rect)) + { + NSRect nsrect = NSRectFromCGRect(rect); + NSScreen* screen; + + /* Convert the rectangle from top-down coords to bottom-up. */ + [NSApp flipRect:&nsrect]; + + clipping = FALSE; + for (screen in [NSScreen screens]) + { + if (!NSContainsRect(nsrect, [screen frame])) + { + clipping = TRUE; + break; + } + } + } + + if (clipping) + ret = [NSApp startClippingCursor:rect]; + else + ret = [NSApp stopClippingCursor]; + }); + + return ret; +} diff --git a/dlls/winemac.drv/cocoa_window.m b/dlls/winemac.drv/cocoa_window.m index 204a0bcd9fe..dab570f6569 100644 --- a/dlls/winemac.drv/cocoa_window.m +++ b/dlls/winemac.drv/cocoa_window.m @@ -144,8 +144,6 @@ @interface WineWindow () @property (readwrite, nonatomic) NSInteger levelWhenActive; - + (void) flipRect:(NSRect*)rect; - @end @@ -255,7 +253,7 @@ + (WineWindow*) createWindowWithFeatures:(const struct macdrv_window_features*)w WineContentView* contentView; NSTrackingArea* trackingArea; - [self flipRect:&window_frame]; + [NSApp flipRect:&window_frame]; window = [[[self alloc] initWithContentRect:window_frame styleMask:style_mask_for_features(wf) @@ -309,11 +307,6 @@ - (void) dealloc [super dealloc]; } - + (void) flipRect:(NSRect*)rect - { - rect->origin.y = NSMaxY([[[NSScreen screens] objectAtIndex:0] frame]) - NSMaxY(*rect); - } - - (void) adjustFeaturesForState { NSUInteger style = normalStyleMask; @@ -524,7 +517,7 @@ - (BOOL) setFrameIfOnScreen:(NSRect)contentRect /* Origin is (left, top) in a top-down space. Need to convert it to (left, bottom) in a bottom-up space. */ - [[self class] flipRect:&contentRect]; + [NSApp flipRect:&contentRect]; if (on_screen) { @@ -1094,7 +1087,7 @@ - (void)windowDidResize:(NSNotification *)notification macdrv_event event; NSRect frame = [self contentRectForFrameRect:[self frame]]; - [[self class] flipRect:&frame]; + [NSApp flipRect:&frame]; /* Coalesce events by discarding any previous ones still in the queue. */ [queue discardEventsMatchingMask:event_mask_for_type(WINDOW_FRAME_CHANGED) @@ -1314,7 +1307,7 @@ void macdrv_get_cocoa_window_frame(macdrv_window w, CGRect* out_frame) NSRect frame; frame = [window contentRectForFrameRect:[window frame]]; - [[window class] flipRect:&frame]; + [NSApp flipRect:&frame]; *out_frame = NSRectToCGRect(frame); }); } diff --git a/dlls/winemac.drv/macdrv_cocoa.h b/dlls/winemac.drv/macdrv_cocoa.h index 16f55951d7d..67a5e54c0ea 100644 --- a/dlls/winemac.drv/macdrv_cocoa.h +++ b/dlls/winemac.drv/macdrv_cocoa.h @@ -122,6 +122,7 @@ extern void macdrv_set_cursor(CFStringRef name, CFArrayRef frames) DECLSPEC_HIDDEN; extern int macdrv_get_cursor_position(CGPoint *pos) DECLSPEC_HIDDEN; extern int macdrv_set_cursor_position(CGPoint pos) DECLSPEC_HIDDEN; +extern int macdrv_clip_cursor(CGRect rect) DECLSPEC_HIDDEN; /* display */ diff --git a/dlls/winemac.drv/mouse.c b/dlls/winemac.drv/mouse.c index d38a3163976..cfbb27471ac 100644 --- a/dlls/winemac.drv/mouse.c +++ b/dlls/winemac.drv/mouse.c @@ -735,6 +735,32 @@ void CDECL macdrv_DestroyCursorIcon(HCURSOR cursor) } +/*********************************************************************** + * ClipCursor (MACDRV.@) + * + * Set the cursor clipping rectangle. + */ +BOOL CDECL macdrv_ClipCursor(LPCRECT clip) +{ + CGRect rect; + + TRACE("%s\n", wine_dbgstr_rect(clip)); + + if (clip) + { + rect = CGRectMake(clip->left, clip->top, max(1, clip->right - clip->left), + max(1, clip->bottom - clip->top)); + } + else + rect = CGRectInfinite; + + /* FIXME: This needs to be done not just in this process but in all of the + ones for this WINEPREFIX. Broadcast a message to do that. */ + + return macdrv_clip_cursor(rect); +} + + /*********************************************************************** * GetCursorPos (MACDRV.@) */ diff --git a/dlls/winemac.drv/winemac.drv.spec b/dlls/winemac.drv/winemac.drv.spec index 87cd0ee6db3..ef50c65e4d7 100644 --- a/dlls/winemac.drv/winemac.drv.spec +++ b/dlls/winemac.drv/winemac.drv.spec @@ -7,6 +7,7 @@ @ cdecl ActivateKeyboardLayout(long long) macdrv_ActivateKeyboardLayout @ cdecl Beep() macdrv_Beep @ cdecl ChangeDisplaySettingsEx(ptr ptr long long long) macdrv_ChangeDisplaySettingsEx +@ cdecl ClipCursor(ptr) macdrv_ClipCursor @ cdecl CreateDesktopWindow(long) macdrv_CreateDesktopWindow @ cdecl CreateWindow(long) macdrv_CreateWindow @ cdecl DestroyCursorIcon(long) macdrv_DestroyCursorIcon