2 * MACDRV Cocoa application class
4 * Copyright 2011, 2012, 2013 Ken Thomases for CodeWeavers Inc.
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
21 #import <Carbon/Carbon.h>
25 #import "cocoa_event.h"
26 #import "cocoa_window.h"
32 @interface WarpRecord : NSObject
34 CGEventTimestamp timeBefore, timeAfter;
38 @property (nonatomic) CGEventTimestamp timeBefore;
39 @property (nonatomic) CGEventTimestamp timeAfter;
40 @property (nonatomic) CGPoint from;
41 @property (nonatomic) CGPoint to;
46 @implementation WarpRecord
48 @synthesize timeBefore, timeAfter, from, to;
53 @interface WineApplication ()
55 @property (readwrite, copy, nonatomic) NSEvent* lastFlagsChanged;
56 @property (copy, nonatomic) NSArray* cursorFrames;
57 @property (retain, nonatomic) NSTimer* cursorTimer;
62 @implementation WineApplication
64 @synthesize keyboardType, lastFlagsChanged;
65 @synthesize orderedWineWindows;
66 @synthesize cursorFrames, cursorTimer;
73 eventQueues = [[NSMutableArray alloc] init];
74 eventQueuesLock = [[NSLock alloc] init];
76 keyWindows = [[NSMutableArray alloc] init];
77 orderedWineWindows = [[NSMutableArray alloc] init];
79 originalDisplayModes = [[NSMutableDictionary alloc] init];
81 warpRecords = [[NSMutableArray alloc] init];
83 if (!eventQueues || !eventQueuesLock || !keyWindows || !orderedWineWindows ||
84 !originalDisplayModes || !warpRecords)
95 [warpRecords release];
96 [cursorTimer release];
97 [cursorFrames release];
98 [originalDisplayModes release];
99 [orderedWineWindows release];
100 [keyWindows release];
101 [eventQueues release];
102 [eventQueuesLock release];
106 - (void) transformProcessToForeground
108 if ([self activationPolicy] != NSApplicationActivationPolicyRegular)
112 NSString* bundleName;
116 [self setActivationPolicy:NSApplicationActivationPolicyRegular];
117 [self activateIgnoringOtherApps:YES];
119 mainMenu = [[[NSMenu alloc] init] autorelease];
121 submenu = [[[NSMenu alloc] initWithTitle:@"Wine"] autorelease];
122 bundleName = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString*)kCFBundleNameKey];
123 if ([bundleName length])
124 title = [NSString stringWithFormat:@"Quit %@", bundleName];
127 item = [submenu addItemWithTitle:title action:@selector(terminate:) keyEquivalent:@"q"];
128 [item setKeyEquivalentModifierMask:NSCommandKeyMask | NSAlternateKeyMask];
129 item = [[[NSMenuItem alloc] init] autorelease];
130 [item setTitle:@"Wine"];
131 [item setSubmenu:submenu];
132 [mainMenu addItem:item];
134 submenu = [[[NSMenu alloc] initWithTitle:@"Window"] autorelease];
135 [submenu addItemWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@""];
136 [submenu addItemWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""];
137 [submenu addItem:[NSMenuItem separatorItem]];
138 [submenu addItemWithTitle:@"Bring All to Front" action:@selector(arrangeInFront:) keyEquivalent:@""];
139 item = [[[NSMenuItem alloc] init] autorelease];
140 [item setTitle:@"Window"];
141 [item setSubmenu:submenu];
142 [mainMenu addItem:item];
144 [self setMainMenu:mainMenu];
145 [self setWindowsMenu:submenu];
149 - (BOOL) registerEventQueue:(WineEventQueue*)queue
151 [eventQueuesLock lock];
152 [eventQueues addObject:queue];
153 [eventQueuesLock unlock];
157 - (void) unregisterEventQueue:(WineEventQueue*)queue
159 [eventQueuesLock lock];
160 [eventQueues removeObjectIdenticalTo:queue];
161 [eventQueuesLock unlock];
164 - (void) computeEventTimeAdjustmentFromTicks:(unsigned long long)tickcount uptime:(uint64_t)uptime_ns
166 eventTimeAdjustment = (tickcount / 1000.0) - (uptime_ns / (double)NSEC_PER_SEC);
169 - (double) ticksForEventTime:(NSTimeInterval)eventTime
171 return (eventTime + eventTimeAdjustment) * 1000;
174 /* Invalidate old focus offers across all queues. */
175 - (void) invalidateGotFocusEvents
177 WineEventQueue* queue;
181 [eventQueuesLock lock];
182 for (queue in eventQueues)
184 [queue discardEventsMatchingMask:event_mask_for_type(WINDOW_GOT_FOCUS)
187 [eventQueuesLock unlock];
190 - (void) windowGotFocus:(WineWindow*)window
194 [NSApp invalidateGotFocusEvents];
196 event.type = WINDOW_GOT_FOCUS;
197 event.window = (macdrv_window)[window retain];
198 event.window_got_focus.serial = windowFocusSerial;
200 event.window_got_focus.tried_windows = [triedWindows retain];
202 event.window_got_focus.tried_windows = [[NSMutableSet alloc] init];
203 [window.queue postEvent:&event];
206 - (void) windowRejectedFocusEvent:(const macdrv_event*)event
208 if (event->window_got_focus.serial == windowFocusSerial)
210 triedWindows = (NSMutableSet*)event->window_got_focus.tried_windows;
211 [triedWindows addObject:(WineWindow*)event->window];
212 for (NSWindow* window in [keyWindows arrayByAddingObjectsFromArray:[self orderedWindows]])
214 if (![triedWindows containsObject:window] && [window canBecomeKeyWindow])
216 [window makeKeyWindow];
224 - (void) keyboardSelectionDidChange
226 TISInputSourceRef inputSource;
228 inputSource = TISCopyCurrentKeyboardLayoutInputSource();
232 uchr = TISGetInputSourceProperty(inputSource,
233 kTISPropertyUnicodeKeyLayoutData);
237 WineEventQueue* queue;
239 event.type = KEYBOARD_CHANGED;
241 event.keyboard_changed.keyboard_type = self.keyboardType;
242 event.keyboard_changed.iso_keyboard = (KBGetLayoutType(self.keyboardType) == kKeyboardISO);
243 event.keyboard_changed.uchr = CFDataCreateCopy(NULL, uchr);
245 if (event.keyboard_changed.uchr)
247 [eventQueuesLock lock];
249 for (queue in eventQueues)
251 CFRetain(event.keyboard_changed.uchr);
252 [queue postEvent:&event];
255 [eventQueuesLock unlock];
257 CFRelease(event.keyboard_changed.uchr);
261 CFRelease(inputSource);
265 - (CGFloat) primaryScreenHeight
267 if (!primaryScreenHeightValid)
269 NSArray* screens = [NSScreen screens];
272 primaryScreenHeight = NSHeight([[screens objectAtIndex:0] frame]);
273 primaryScreenHeightValid = TRUE;
276 return 1280; /* arbitrary value */
279 return primaryScreenHeight;
282 - (NSPoint) flippedMouseLocation:(NSPoint)point
284 /* This relies on the fact that Cocoa's mouse location points are
285 actually off by one (precisely because they were flipped from
286 Quartz screen coordinates using this same technique). */
287 point.y = [self primaryScreenHeight] - point.y;
291 - (void) flipRect:(NSRect*)rect
293 // We don't use -primaryScreenHeight here so there's no chance of having
294 // out-of-date cached info. This method is called infrequently enough
295 // that getting the screen height each time is not prohibitively expensive.
296 rect->origin.y = NSMaxY([[[NSScreen screens] objectAtIndex:0] frame]) - NSMaxY(*rect);
299 - (void) wineWindow:(WineWindow*)window
300 ordered:(NSWindowOrderingMode)order
301 relativeTo:(WineWindow*)otherWindow
309 [orderedWineWindows removeObjectIdenticalTo:window];
312 index = [orderedWineWindows indexOfObjectIdenticalTo:otherWindow];
313 if (index == NSNotFound)
319 for (otherWindow in orderedWineWindows)
321 if ([otherWindow levelWhenActive] <= [window levelWhenActive])
326 [orderedWineWindows insertObject:window atIndex:index];
331 [orderedWineWindows removeObjectIdenticalTo:window];
334 index = [orderedWineWindows indexOfObjectIdenticalTo:otherWindow];
335 if (index == NSNotFound)
336 index = [orderedWineWindows count];
341 for (otherWindow in orderedWineWindows)
343 if ([otherWindow levelWhenActive] < [window levelWhenActive])
348 [orderedWineWindows insertObject:window atIndex:index];
357 - (void) sendDisplaysChanged:(BOOL)activating
360 WineEventQueue* queue;
362 event.type = DISPLAYS_CHANGED;
364 event.displays_changed.activating = activating;
366 [eventQueuesLock lock];
367 for (queue in eventQueues)
368 [queue postEvent:&event];
369 [eventQueuesLock unlock];
372 // We can compare two modes directly using CFEqual, but that may require that
373 // they are identical to a level that we don't need. In particular, when the
374 // OS switches between the integrated and discrete GPUs, the set of display
375 // modes can change in subtle ways. We're interested in whether two modes
376 // match in their most salient features, even if they aren't identical.
377 - (BOOL) mode:(CGDisplayModeRef)mode1 matchesMode:(CGDisplayModeRef)mode2
379 NSString *encoding1, *encoding2;
380 uint32_t ioflags1, ioflags2, different;
381 double refresh1, refresh2;
383 if (CGDisplayModeGetWidth(mode1) != CGDisplayModeGetWidth(mode2)) return FALSE;
384 if (CGDisplayModeGetHeight(mode1) != CGDisplayModeGetHeight(mode2)) return FALSE;
386 encoding1 = [(NSString*)CGDisplayModeCopyPixelEncoding(mode1) autorelease];
387 encoding2 = [(NSString*)CGDisplayModeCopyPixelEncoding(mode2) autorelease];
388 if (![encoding1 isEqualToString:encoding2]) return FALSE;
390 ioflags1 = CGDisplayModeGetIOFlags(mode1);
391 ioflags2 = CGDisplayModeGetIOFlags(mode2);
392 different = ioflags1 ^ ioflags2;
393 if (different & (kDisplayModeValidFlag | kDisplayModeSafeFlag | kDisplayModeStretchedFlag |
394 kDisplayModeInterlacedFlag | kDisplayModeTelevisionFlag))
397 refresh1 = CGDisplayModeGetRefreshRate(mode1);
398 if (refresh1 == 0) refresh1 = 60;
399 refresh2 = CGDisplayModeGetRefreshRate(mode2);
400 if (refresh2 == 0) refresh2 = 60;
401 if (fabs(refresh1 - refresh2) > 0.1) return FALSE;
406 - (CGDisplayModeRef)modeMatchingMode:(CGDisplayModeRef)mode forDisplay:(CGDirectDisplayID)displayID
408 CGDisplayModeRef ret = NULL;
409 NSArray *modes = [(NSArray*)CGDisplayCopyAllDisplayModes(displayID, NULL) autorelease];
410 for (id candidateModeObject in modes)
412 CGDisplayModeRef candidateMode = (CGDisplayModeRef)candidateModeObject;
413 if ([self mode:candidateMode matchesMode:mode])
422 - (BOOL) setMode:(CGDisplayModeRef)mode forDisplay:(CGDirectDisplayID)displayID
425 NSNumber* displayIDKey = [NSNumber numberWithUnsignedInt:displayID];
426 CGDisplayModeRef currentMode, originalMode;
428 currentMode = CGDisplayCopyDisplayMode(displayID);
429 if (!currentMode) // Invalid display ID
432 if ([self mode:mode matchesMode:currentMode]) // Already there!
434 CGDisplayModeRelease(currentMode);
438 mode = [self modeMatchingMode:mode forDisplay:displayID];
441 CGDisplayModeRelease(currentMode);
445 originalMode = (CGDisplayModeRef)[originalDisplayModes objectForKey:displayIDKey];
447 originalMode = currentMode;
449 if ([self mode:mode matchesMode:originalMode])
451 if ([originalDisplayModes count] == 1) // If this is the last changed display, do a blanket reset
453 CGRestorePermanentDisplayConfiguration();
454 CGReleaseAllDisplays();
455 [originalDisplayModes removeAllObjects];
458 else // ... otherwise, try to restore just the one display
460 if (CGDisplaySetDisplayMode(displayID, mode, NULL) == CGDisplayNoErr)
462 [originalDisplayModes removeObjectForKey:displayIDKey];
469 if ([originalDisplayModes count] || CGCaptureAllDisplays() == CGDisplayNoErr)
471 if (CGDisplaySetDisplayMode(displayID, mode, NULL) == CGDisplayNoErr)
473 [originalDisplayModes setObject:(id)originalMode forKey:displayIDKey];
476 else if (![originalDisplayModes count])
478 CGRestorePermanentDisplayConfiguration();
479 CGReleaseAllDisplays();
484 CGDisplayModeRelease(currentMode);
488 [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
489 [(WineWindow*)obj adjustWindowLevel];
496 - (BOOL) areDisplaysCaptured
498 return ([originalDisplayModes count] > 0);
510 - (void) unhideCursor
515 cursorHidden = FALSE;
521 NSDictionary* frame = [cursorFrames objectAtIndex:cursorFrame];
522 CGImageRef cgimage = (CGImageRef)[frame objectForKey:@"image"];
523 NSImage* image = [[NSImage alloc] initWithCGImage:cgimage size:NSZeroSize];
524 CFDictionaryRef hotSpotDict = (CFDictionaryRef)[frame objectForKey:@"hotSpot"];
528 if (!CGPointMakeWithDictionaryRepresentation(hotSpotDict, &hotSpot))
529 hotSpot = CGPointZero;
530 cursor = [[NSCursor alloc] initWithImage:image hotSpot:NSPointFromCGPoint(hotSpot)];
537 - (void) nextCursorFrame:(NSTimer*)theTimer
540 NSTimeInterval duration;
544 if (cursorFrame >= [cursorFrames count])
548 frame = [cursorFrames objectAtIndex:cursorFrame];
549 duration = [[frame objectForKey:@"duration"] doubleValue];
550 date = [[theTimer fireDate] dateByAddingTimeInterval:duration];
551 [cursorTimer setFireDate:date];
554 - (void) setCursorWithFrames:(NSArray*)frames
556 if (self.cursorFrames == frames)
559 self.cursorFrames = frames;
561 [cursorTimer invalidate];
562 self.cursorTimer = nil;
566 if ([frames count] > 1)
568 NSDictionary* frame = [frames objectAtIndex:0];
569 NSTimeInterval duration = [[frame objectForKey:@"duration"] doubleValue];
570 NSDate* date = [NSDate dateWithTimeIntervalSinceNow:duration];
571 self.cursorTimer = [[[NSTimer alloc] initWithFireDate:date
574 selector:@selector(nextCursorFrame:)
576 repeats:YES] autorelease];
577 [[NSRunLoop currentRunLoop] addTimer:cursorTimer forMode:NSRunLoopCommonModes];
585 * ---------- Cursor clipping methods ----------
587 * Neither Quartz nor Cocoa has an exact analog for Win32 cursor clipping.
588 * For one simple case, clipping to a 1x1 rectangle, Quartz does have an
589 * equivalent: CGAssociateMouseAndMouseCursorPosition(false). For the
590 * general case, we leverage that. We disassociate mouse movements from
591 * the cursor position and then move the cursor manually, keeping it within
592 * the clipping rectangle.
594 * Moving the cursor manually isn't enough. We need to modify the event
595 * stream so that the events have the new location, too. We need to do
596 * this at a point before the events enter Cocoa, so that Cocoa will assign
597 * the correct window to the event. So, we install a Quartz event tap to
600 * Also, there's a complication when we move the cursor. We use
601 * CGWarpMouseCursorPosition(). That doesn't generate mouse movement
602 * events, but the change of cursor position is incorporated into the
603 * deltas of the next mouse move event. When the mouse is disassociated
604 * from the cursor position, we need the deltas to only reflect actual
605 * device movement, not programmatic changes. So, the event tap cancels
606 * out the change caused by our calls to CGWarpMouseCursorPosition().
608 - (void) clipCursorLocation:(CGPoint*)location
610 if (location->x < CGRectGetMinX(cursorClipRect))
611 location->x = CGRectGetMinX(cursorClipRect);
612 if (location->y < CGRectGetMinY(cursorClipRect))
613 location->y = CGRectGetMinY(cursorClipRect);
614 if (location->x > CGRectGetMaxX(cursorClipRect) - 1)
615 location->x = CGRectGetMaxX(cursorClipRect) - 1;
616 if (location->y > CGRectGetMaxY(cursorClipRect) - 1)
617 location->y = CGRectGetMaxY(cursorClipRect) - 1;
620 - (BOOL) warpCursorTo:(CGPoint*)newLocation from:(const CGPoint*)currentLocation
625 oldLocation = *currentLocation;
627 oldLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
629 if (!CGPointEqualToPoint(oldLocation, *newLocation))
631 WarpRecord* warpRecord = [[[WarpRecord alloc] init] autorelease];
634 warpRecord.from = oldLocation;
635 warpRecord.timeBefore = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
637 /* Actually move the cursor. */
638 err = CGWarpMouseCursorPosition(*newLocation);
639 if (err != kCGErrorSuccess)
642 warpRecord.timeAfter = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
643 *newLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
645 if (!CGPointEqualToPoint(oldLocation, *newLocation))
647 warpRecord.to = *newLocation;
648 [warpRecords addObject:warpRecord];
655 - (BOOL) isMouseMoveEventType:(CGEventType)type
659 case kCGEventMouseMoved:
660 case kCGEventLeftMouseDragged:
661 case kCGEventRightMouseDragged:
662 case kCGEventOtherMouseDragged:
669 - (int) warpsFinishedByEventTime:(CGEventTimestamp)eventTime location:(CGPoint)eventLocation
671 int warpsFinished = 0;
672 for (WarpRecord* warpRecord in warpRecords)
674 if (warpRecord.timeAfter < eventTime ||
675 (warpRecord.timeBefore <= eventTime && CGPointEqualToPoint(eventLocation, warpRecord.to)))
681 return warpsFinished;
684 - (CGEventRef) eventTapWithProxy:(CGEventTapProxy)proxy
685 type:(CGEventType)type
686 event:(CGEventRef)event
688 CGEventTimestamp eventTime;
689 CGPoint eventLocation, cursorLocation;
691 if (type == kCGEventTapDisabledByUserInput)
693 if (type == kCGEventTapDisabledByTimeout)
695 CGEventTapEnable(cursorClippingEventTap, TRUE);
702 eventTime = CGEventGetTimestamp(event);
703 lastEventTapEventTime = eventTime / (double)NSEC_PER_SEC;
705 eventLocation = CGEventGetLocation(event);
707 cursorLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
709 if ([self isMouseMoveEventType:type])
711 double deltaX, deltaY;
712 int warpsFinished = [self warpsFinishedByEventTime:eventTime location:eventLocation];
715 deltaX = CGEventGetDoubleValueField(event, kCGMouseEventDeltaX);
716 deltaY = CGEventGetDoubleValueField(event, kCGMouseEventDeltaY);
718 for (i = 0; i < warpsFinished; i++)
720 WarpRecord* warpRecord = [warpRecords objectAtIndex:0];
721 deltaX -= warpRecord.to.x - warpRecord.from.x;
722 deltaY -= warpRecord.to.y - warpRecord.from.y;
723 [warpRecords removeObjectAtIndex:0];
728 CGEventSetDoubleValueField(event, kCGMouseEventDeltaX, deltaX);
729 CGEventSetDoubleValueField(event, kCGMouseEventDeltaY, deltaY);
732 synthesizedLocation.x += deltaX;
733 synthesizedLocation.y += deltaY;
736 // If the event is destined for another process, don't clip it. This may
737 // happen if the user activates Exposé or Mission Control. In that case,
738 // our app does not resign active status, so clipping is still in effect,
739 // but the cursor should not actually be clipped.
741 // In addition, the fact that mouse moves may have been delivered to a
742 // different process means we have to treat the next one we receive as
743 // absolute rather than relative.
744 if (CGEventGetIntegerValueField(event, kCGEventTargetUnixProcessID) == getpid())
745 [self clipCursorLocation:&synthesizedLocation];
747 lastSetCursorPositionTime = lastEventTapEventTime;
749 [self warpCursorTo:&synthesizedLocation from:&cursorLocation];
750 if (!CGPointEqualToPoint(eventLocation, synthesizedLocation))
751 CGEventSetLocation(event, synthesizedLocation);
756 CGEventRef WineAppEventTapCallBack(CGEventTapProxy proxy, CGEventType type,
757 CGEventRef event, void *refcon)
759 WineApplication* app = refcon;
760 return [app eventTapWithProxy:proxy type:type event:event];
763 - (BOOL) installEventTap
765 ProcessSerialNumber psn;
767 CGEventMask mask = CGEventMaskBit(kCGEventLeftMouseDown) |
768 CGEventMaskBit(kCGEventLeftMouseUp) |
769 CGEventMaskBit(kCGEventRightMouseDown) |
770 CGEventMaskBit(kCGEventRightMouseUp) |
771 CGEventMaskBit(kCGEventMouseMoved) |
772 CGEventMaskBit(kCGEventLeftMouseDragged) |
773 CGEventMaskBit(kCGEventRightMouseDragged) |
774 CGEventMaskBit(kCGEventOtherMouseDown) |
775 CGEventMaskBit(kCGEventOtherMouseUp) |
776 CGEventMaskBit(kCGEventOtherMouseDragged) |
777 CGEventMaskBit(kCGEventScrollWheel);
778 CFRunLoopSourceRef source;
780 OSErr (*pGetCurrentProcess)(ProcessSerialNumber* PSN);
782 if (cursorClippingEventTap)
785 // We need to get the Mac GetCurrentProcess() from the ApplicationServices
786 // framework with dlsym() because the Win32 function of the same name
788 appServices = dlopen("/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices", RTLD_LAZY);
792 pGetCurrentProcess = dlsym(appServices, "GetCurrentProcess");
793 if (!pGetCurrentProcess)
795 dlclose(appServices);
799 err = pGetCurrentProcess(&psn);
800 dlclose(appServices);
804 // We create an annotated session event tap rather than a process-specific
805 // event tap because we need to programmatically move the cursor even when
806 // mouse moves are directed to other processes. We disable our tap when
807 // other processes are active, but things like Exposé are handled by other
808 // processes even when we remain active.
809 cursorClippingEventTap = CGEventTapCreate(kCGAnnotatedSessionEventTap, kCGHeadInsertEventTap,
810 kCGEventTapOptionDefault, mask, WineAppEventTapCallBack, self);
811 if (!cursorClippingEventTap)
814 CGEventTapEnable(cursorClippingEventTap, FALSE);
816 source = CFMachPortCreateRunLoopSource(NULL, cursorClippingEventTap, 0);
819 CFRelease(cursorClippingEventTap);
820 cursorClippingEventTap = NULL;
824 CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
829 - (BOOL) setCursorPosition:(CGPoint)pos
835 [self clipCursorLocation:&pos];
837 synthesizedLocation = pos;
838 ret = [self warpCursorTo:&synthesizedLocation from:NULL];
841 // We want to discard mouse-move events that have already been
842 // through the event tap, because it's too late to account for
843 // the setting of the cursor position with them. However, the
844 // events that may be queued with times after that but before
845 // the above warp can still be used. So, use the last event
846 // tap event time so that -sendEvent: doesn't discard them.
847 lastSetCursorPositionTime = lastEventTapEventTime;
852 ret = (CGWarpMouseCursorPosition(pos) == kCGErrorSuccess);
854 lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
859 WineEventQueue* queue;
861 // Discard all pending mouse move events.
862 [eventQueuesLock lock];
863 for (queue in eventQueues)
865 [queue discardEventsMatchingMask:event_mask_for_type(MOUSE_MOVED) |
866 event_mask_for_type(MOUSE_MOVED_ABSOLUTE)
869 [eventQueuesLock unlock];
875 - (void) activateCursorClipping
879 CGEventTapEnable(cursorClippingEventTap, TRUE);
880 [self setCursorPosition:NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]])];
884 - (void) deactivateCursorClipping
888 CGEventTapEnable(cursorClippingEventTap, FALSE);
889 [warpRecords removeAllObjects];
890 lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
894 - (BOOL) startClippingCursor:(CGRect)rect
898 if (!cursorClippingEventTap && ![self installEventTap])
901 err = CGAssociateMouseAndMouseCursorPosition(false);
902 if (err != kCGErrorSuccess)
905 clippingCursor = TRUE;
906 cursorClipRect = rect;
908 [self activateCursorClipping];
913 - (BOOL) stopClippingCursor
915 CGError err = CGAssociateMouseAndMouseCursorPosition(true);
916 if (err != kCGErrorSuccess)
919 [self deactivateCursorClipping];
920 clippingCursor = FALSE;
927 * ---------- NSApplication method overrides ----------
929 - (void) sendEvent:(NSEvent*)anEvent
931 NSEventType type = [anEvent type];
932 if (type == NSFlagsChanged)
933 self.lastFlagsChanged = anEvent;
935 [super sendEvent:anEvent];
937 if (type == NSMouseMoved || type == NSLeftMouseDragged ||
938 type == NSRightMouseDragged || type == NSOtherMouseDragged)
940 WineWindow* targetWindow;
942 /* Because of the way -[NSWindow setAcceptsMouseMovedEvents:] works, the
943 event indicates its window is the main window, even if the cursor is
944 over a different window. Find the actual WineWindow that is under the
945 cursor and post the event as being for that window. */
946 if (type == NSMouseMoved)
948 CGPoint cgpoint = CGEventGetLocation([anEvent CGEvent]);
949 NSPoint point = [self flippedMouseLocation:NSPointFromCGPoint(cgpoint)];
950 NSInteger windowUnderNumber;
952 windowUnderNumber = [NSWindow windowNumberAtPoint:point
953 belowWindowWithWindowNumber:0];
954 targetWindow = (WineWindow*)[self windowWithWindowNumber:windowUnderNumber];
957 targetWindow = (WineWindow*)[anEvent window];
959 if ([targetWindow isKindOfClass:[WineWindow class]])
961 BOOL absolute = forceNextMouseMoveAbsolute || (targetWindow != lastTargetWindow);
962 forceNextMouseMoveAbsolute = FALSE;
964 // If we recently warped the cursor (other than in our cursor-clipping
965 // event tap), discard mouse move events until we see an event which is
966 // later than that time.
967 if (lastSetCursorPositionTime)
969 if ([anEvent timestamp] <= lastSetCursorPositionTime)
972 lastSetCursorPositionTime = 0;
976 [targetWindow postMouseMovedEvent:anEvent absolute:absolute];
977 lastTargetWindow = targetWindow;
979 else if (lastTargetWindow)
981 [[NSCursor arrowCursor] set];
983 lastTargetWindow = nil;
986 else if (type == NSLeftMouseDown || type == NSLeftMouseUp ||
987 type == NSRightMouseDown || type == NSRightMouseUp ||
988 type == NSOtherMouseDown || type == NSOtherMouseUp ||
989 type == NSScrollWheel)
991 // Since mouse button and scroll wheel events deliver absolute cursor
992 // position, the accumulating delta from move events is invalidated.
993 // Make sure next mouse move event starts over from an absolute baseline.
994 forceNextMouseMoveAbsolute = TRUE;
1000 * ---------- NSApplicationDelegate methods ----------
1002 - (void)applicationDidBecomeActive:(NSNotification *)notification
1004 [self activateCursorClipping];
1006 [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
1007 WineWindow* window = obj;
1008 if ([window levelWhenActive] != [window level])
1009 [window setLevel:[window levelWhenActive]];
1012 // If a Wine process terminates abruptly while it has the display captured
1013 // and switched to a different resolution, Mac OS X will uncapture the
1014 // displays and switch their resolutions back. However, the other Wine
1015 // processes won't have their notion of the desktop rect changed back.
1016 // This can lead them to refuse to draw or acknowledge clicks in certain
1017 // portions of their windows.
1019 // To solve this, we synthesize a displays-changed event whenever we're
1020 // activated. This will provoke a re-synchronization of Wine's notion of
1021 // the desktop rect with the actual state.
1022 [self sendDisplaysChanged:TRUE];
1024 // The cursor probably moved while we were inactive. Accumulated mouse
1025 // movement deltas are invalidated. Make sure the next mouse move event
1026 // starts over from an absolute baseline.
1027 forceNextMouseMoveAbsolute = TRUE;
1030 - (void)applicationDidChangeScreenParameters:(NSNotification *)notification
1032 primaryScreenHeightValid = FALSE;
1033 [self sendDisplaysChanged:FALSE];
1035 // When the display configuration changes, the cursor position may jump.
1036 // Accumulated mouse movement deltas are invalidated. Make sure the next
1037 // mouse move event starts over from an absolute baseline.
1038 forceNextMouseMoveAbsolute = TRUE;
1041 - (void)applicationDidResignActive:(NSNotification *)notification
1044 WineEventQueue* queue;
1046 [self invalidateGotFocusEvents];
1048 event.type = APP_DEACTIVATED;
1049 event.window = NULL;
1051 [eventQueuesLock lock];
1052 for (queue in eventQueues)
1053 [queue postEvent:&event];
1054 [eventQueuesLock unlock];
1057 - (void)applicationWillFinishLaunching:(NSNotification *)notification
1059 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
1061 [nc addObserverForName:NSWindowDidBecomeKeyNotification
1064 usingBlock:^(NSNotification *note){
1065 NSWindow* window = [note object];
1066 [keyWindows removeObjectIdenticalTo:window];
1067 [keyWindows insertObject:window atIndex:0];
1070 [nc addObserverForName:NSWindowWillCloseNotification
1072 queue:[NSOperationQueue mainQueue]
1073 usingBlock:^(NSNotification *note){
1074 NSWindow* window = [note object];
1075 [keyWindows removeObjectIdenticalTo:window];
1076 [orderedWineWindows removeObjectIdenticalTo:window];
1077 if (window == lastTargetWindow)
1078 lastTargetWindow = nil;
1081 [nc addObserver:self
1082 selector:@selector(keyboardSelectionDidChange)
1083 name:NSTextInputContextKeyboardSelectionDidChangeNotification
1086 /* The above notification isn't sent unless the NSTextInputContext
1087 class has initialized itself. Poke it. */
1088 [NSTextInputContext self];
1090 self.keyboardType = LMGetKbdType();
1093 - (void)applicationWillResignActive:(NSNotification *)notification
1095 [self deactivateCursorClipping];
1097 [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
1098 WineWindow* window = obj;
1099 NSInteger level = window.floating ? NSFloatingWindowLevel : NSNormalWindowLevel;
1100 if ([window level] > level)
1101 [window setLevel:level];
1107 /***********************************************************************
1110 * Run a block on the main thread synchronously.
1112 void OnMainThread(dispatch_block_t block)
1114 dispatch_sync(dispatch_get_main_queue(), block);
1117 /***********************************************************************
1120 * Run a block on the main thread asynchronously.
1122 void OnMainThreadAsync(dispatch_block_t block)
1124 dispatch_async(dispatch_get_main_queue(), block);
1127 /***********************************************************************
1130 void LogError(const char* func, NSString* format, ...)
1133 va_start(args, format);
1134 LogErrorv(func, format, args);
1138 /***********************************************************************
1141 void LogErrorv(const char* func, NSString* format, va_list args)
1143 NSString* message = [[NSString alloc] initWithFormat:format arguments:args];
1144 fprintf(stderr, "err:%s:%s", func, [message UTF8String]);
1148 /***********************************************************************
1149 * macdrv_window_rejected_focus
1151 * Pass focus to the next window that hasn't already rejected this same
1152 * WINDOW_GOT_FOCUS event.
1154 void macdrv_window_rejected_focus(const macdrv_event *event)
1157 [NSApp windowRejectedFocusEvent:event];
1161 /***********************************************************************
1162 * macdrv_get_keyboard_layout
1164 * Returns the keyboard layout uchr data.
1166 CFDataRef macdrv_copy_keyboard_layout(CGEventSourceKeyboardType* keyboard_type, int* is_iso)
1168 __block CFDataRef result = NULL;
1171 TISInputSourceRef inputSource;
1173 inputSource = TISCopyCurrentKeyboardLayoutInputSource();
1176 CFDataRef uchr = TISGetInputSourceProperty(inputSource,
1177 kTISPropertyUnicodeKeyLayoutData);
1178 result = CFDataCreateCopy(NULL, uchr);
1179 CFRelease(inputSource);
1181 *keyboard_type = ((WineApplication*)NSApp).keyboardType;
1182 *is_iso = (KBGetLayoutType(*keyboard_type) == kKeyboardISO);
1189 /***********************************************************************
1192 * Play the beep sound configured by the user in System Preferences.
1194 void macdrv_beep(void)
1196 OnMainThreadAsync(^{
1201 /***********************************************************************
1202 * macdrv_set_display_mode
1204 int macdrv_set_display_mode(const struct macdrv_display* display,
1205 CGDisplayModeRef display_mode)
1210 ret = [NSApp setMode:display_mode forDisplay:display->displayID];
1216 /***********************************************************************
1221 * If name is non-NULL, it is a selector for a class method on NSCursor
1222 * identifying the cursor to set. In that case, frames is ignored. If
1223 * name is NULL, then frames is used.
1225 * frames is an array of dictionaries. Each dictionary is a frame of
1226 * an animated cursor. Under the key "image" is a CGImage for the
1227 * frame. Under the key "duration" is a CFNumber time interval, in
1228 * seconds, for how long that frame is presented before proceeding to
1229 * the next frame. Under the key "hotSpot" is a CFDictionary encoding a
1230 * CGPoint, to be decoded using CGPointMakeWithDictionaryRepresentation().
1231 * This is the hot spot, measured in pixels down and to the right of the
1232 * top-left corner of the image.
1234 * If the array has exactly 1 element, the cursor is static, not
1235 * animated. If frames is NULL or has 0 elements, the cursor is hidden.
1237 void macdrv_set_cursor(CFStringRef name, CFArrayRef frames)
1241 sel = NSSelectorFromString((NSString*)name);
1244 OnMainThreadAsync(^{
1245 NSCursor* cursor = [NSCursor performSelector:sel];
1246 [NSApp setCursorWithFrames:nil];
1248 [NSApp unhideCursor];
1253 NSArray* nsframes = (NSArray*)frames;
1254 if ([nsframes count])
1256 OnMainThreadAsync(^{
1257 [NSApp setCursorWithFrames:nsframes];
1262 OnMainThreadAsync(^{
1263 [NSApp setCursorWithFrames:nil];
1270 /***********************************************************************
1271 * macdrv_get_cursor_position
1273 * Obtains the current cursor position. Returns zero on failure,
1274 * non-zero on success.
1276 int macdrv_get_cursor_position(CGPoint *pos)
1279 NSPoint location = [NSEvent mouseLocation];
1280 location = [NSApp flippedMouseLocation:location];
1281 *pos = NSPointToCGPoint(location);
1287 /***********************************************************************
1288 * macdrv_set_cursor_position
1290 * Sets the cursor position without generating events. Returns zero on
1291 * failure, non-zero on success.
1293 int macdrv_set_cursor_position(CGPoint pos)
1298 ret = [NSApp setCursorPosition:pos];
1304 /***********************************************************************
1305 * macdrv_clip_cursor
1307 * Sets the cursor cursor clipping rectangle. If the rectangle is equal
1308 * to or larger than the whole desktop region, the cursor is unclipped.
1309 * Returns zero on failure, non-zero on success.
1311 int macdrv_clip_cursor(CGRect rect)
1316 BOOL clipping = FALSE;
1318 if (!CGRectIsInfinite(rect))
1320 NSRect nsrect = NSRectFromCGRect(rect);
1323 /* Convert the rectangle from top-down coords to bottom-up. */
1324 [NSApp flipRect:&nsrect];
1327 for (screen in [NSScreen screens])
1329 if (!NSContainsRect(nsrect, [screen frame]))
1338 ret = [NSApp startClippingCursor:rect];
1340 ret = [NSApp stopClippingCursor];