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"
29 static NSString* const WineAppWaitQueryResponseMode = @"WineAppWaitQueryResponseMode";
35 @interface WarpRecord : NSObject
37 CGEventTimestamp timeBefore, timeAfter;
41 @property (nonatomic) CGEventTimestamp timeBefore;
42 @property (nonatomic) CGEventTimestamp timeAfter;
43 @property (nonatomic) CGPoint from;
44 @property (nonatomic) CGPoint to;
49 @implementation WarpRecord
51 @synthesize timeBefore, timeAfter, from, to;
56 @interface WineApplication ()
58 @property (readwrite, copy, nonatomic) NSEvent* lastFlagsChanged;
59 @property (copy, nonatomic) NSArray* cursorFrames;
60 @property (retain, nonatomic) NSTimer* cursorTimer;
62 static void PerformRequest(void *info);
67 @implementation WineApplication
69 @synthesize keyboardType, lastFlagsChanged;
70 @synthesize orderedWineWindows;
71 @synthesize cursorFrames, cursorTimer;
78 CFRunLoopSourceContext context = { 0 };
79 context.perform = PerformRequest;
80 requestSource = CFRunLoopSourceCreate(NULL, 0, &context);
86 CFRunLoopAddSource(CFRunLoopGetMain(), requestSource, kCFRunLoopCommonModes);
87 CFRunLoopAddSource(CFRunLoopGetMain(), requestSource, (CFStringRef)WineAppWaitQueryResponseMode);
89 requests = [[NSMutableArray alloc] init];
90 requestsManipQueue = dispatch_queue_create("org.winehq.WineAppRequestManipQueue", NULL);
92 eventQueues = [[NSMutableArray alloc] init];
93 eventQueuesLock = [[NSLock alloc] init];
95 keyWindows = [[NSMutableArray alloc] init];
96 orderedWineWindows = [[NSMutableArray alloc] init];
98 originalDisplayModes = [[NSMutableDictionary alloc] init];
100 warpRecords = [[NSMutableArray alloc] init];
102 if (!requests || !requestsManipQueue || !eventQueues || !eventQueuesLock ||
103 !keyWindows || !orderedWineWindows || !originalDisplayModes || !warpRecords)
114 [warpRecords release];
115 [cursorTimer release];
116 [cursorFrames release];
117 [originalDisplayModes release];
118 [orderedWineWindows release];
119 [keyWindows release];
120 [eventQueues release];
121 [eventQueuesLock release];
122 if (requestsManipQueue) dispatch_release(requestsManipQueue);
126 CFRunLoopSourceInvalidate(requestSource);
127 CFRelease(requestSource);
132 - (void) transformProcessToForeground
134 if ([self activationPolicy] != NSApplicationActivationPolicyRegular)
138 NSString* bundleName;
142 [self setActivationPolicy:NSApplicationActivationPolicyRegular];
143 [self activateIgnoringOtherApps:YES];
145 mainMenu = [[[NSMenu alloc] init] autorelease];
147 submenu = [[[NSMenu alloc] initWithTitle:@"Wine"] autorelease];
148 bundleName = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString*)kCFBundleNameKey];
149 if ([bundleName length])
150 title = [NSString stringWithFormat:@"Quit %@", bundleName];
153 item = [submenu addItemWithTitle:title action:@selector(terminate:) keyEquivalent:@"q"];
154 [item setKeyEquivalentModifierMask:NSCommandKeyMask | NSAlternateKeyMask];
155 item = [[[NSMenuItem alloc] init] autorelease];
156 [item setTitle:@"Wine"];
157 [item setSubmenu:submenu];
158 [mainMenu addItem:item];
160 submenu = [[[NSMenu alloc] initWithTitle:@"Window"] autorelease];
161 [submenu addItemWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@""];
162 [submenu addItemWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""];
163 [submenu addItem:[NSMenuItem separatorItem]];
164 [submenu addItemWithTitle:@"Bring All to Front" action:@selector(arrangeInFront:) keyEquivalent:@""];
165 item = [[[NSMenuItem alloc] init] autorelease];
166 [item setTitle:@"Window"];
167 [item setSubmenu:submenu];
168 [mainMenu addItem:item];
170 [self setMainMenu:mainMenu];
171 [self setWindowsMenu:submenu];
175 - (BOOL) waitUntilQueryDone:(int*)done timeout:(NSDate*)timeout
177 PerformRequest(NULL);
181 [[NSRunLoop currentRunLoop] runMode:WineAppWaitQueryResponseMode beforeDate:timeout];
182 } while (!*done && [timeout timeIntervalSinceNow] >= 0);
187 - (BOOL) registerEventQueue:(WineEventQueue*)queue
189 [eventQueuesLock lock];
190 [eventQueues addObject:queue];
191 [eventQueuesLock unlock];
195 - (void) unregisterEventQueue:(WineEventQueue*)queue
197 [eventQueuesLock lock];
198 [eventQueues removeObjectIdenticalTo:queue];
199 [eventQueuesLock unlock];
202 - (void) computeEventTimeAdjustmentFromTicks:(unsigned long long)tickcount uptime:(uint64_t)uptime_ns
204 eventTimeAdjustment = (tickcount / 1000.0) - (uptime_ns / (double)NSEC_PER_SEC);
207 - (double) ticksForEventTime:(NSTimeInterval)eventTime
209 return (eventTime + eventTimeAdjustment) * 1000;
212 /* Invalidate old focus offers across all queues. */
213 - (void) invalidateGotFocusEvents
215 WineEventQueue* queue;
219 [eventQueuesLock lock];
220 for (queue in eventQueues)
222 [queue discardEventsMatchingMask:event_mask_for_type(WINDOW_GOT_FOCUS)
225 [eventQueuesLock unlock];
228 - (void) windowGotFocus:(WineWindow*)window
232 [NSApp invalidateGotFocusEvents];
234 event.type = WINDOW_GOT_FOCUS;
235 event.window = (macdrv_window)[window retain];
236 event.window_got_focus.serial = windowFocusSerial;
238 event.window_got_focus.tried_windows = [triedWindows retain];
240 event.window_got_focus.tried_windows = [[NSMutableSet alloc] init];
241 [window.queue postEvent:&event];
244 - (void) windowRejectedFocusEvent:(const macdrv_event*)event
246 if (event->window_got_focus.serial == windowFocusSerial)
248 triedWindows = (NSMutableSet*)event->window_got_focus.tried_windows;
249 [triedWindows addObject:(WineWindow*)event->window];
250 for (NSWindow* window in [keyWindows arrayByAddingObjectsFromArray:[self orderedWindows]])
252 if (![triedWindows containsObject:window] && [window canBecomeKeyWindow])
254 [window makeKeyWindow];
262 - (void) keyboardSelectionDidChange
264 TISInputSourceRef inputSource;
266 inputSource = TISCopyCurrentKeyboardLayoutInputSource();
270 uchr = TISGetInputSourceProperty(inputSource,
271 kTISPropertyUnicodeKeyLayoutData);
275 WineEventQueue* queue;
277 event.type = KEYBOARD_CHANGED;
279 event.keyboard_changed.keyboard_type = self.keyboardType;
280 event.keyboard_changed.iso_keyboard = (KBGetLayoutType(self.keyboardType) == kKeyboardISO);
281 event.keyboard_changed.uchr = CFDataCreateCopy(NULL, uchr);
283 if (event.keyboard_changed.uchr)
285 [eventQueuesLock lock];
287 for (queue in eventQueues)
289 CFRetain(event.keyboard_changed.uchr);
290 [queue postEvent:&event];
293 [eventQueuesLock unlock];
295 CFRelease(event.keyboard_changed.uchr);
299 CFRelease(inputSource);
303 - (CGFloat) primaryScreenHeight
305 if (!primaryScreenHeightValid)
307 NSArray* screens = [NSScreen screens];
310 primaryScreenHeight = NSHeight([[screens objectAtIndex:0] frame]);
311 primaryScreenHeightValid = TRUE;
314 return 1280; /* arbitrary value */
317 return primaryScreenHeight;
320 - (NSPoint) flippedMouseLocation:(NSPoint)point
322 /* This relies on the fact that Cocoa's mouse location points are
323 actually off by one (precisely because they were flipped from
324 Quartz screen coordinates using this same technique). */
325 point.y = [self primaryScreenHeight] - point.y;
329 - (void) flipRect:(NSRect*)rect
331 // We don't use -primaryScreenHeight here so there's no chance of having
332 // out-of-date cached info. This method is called infrequently enough
333 // that getting the screen height each time is not prohibitively expensive.
334 rect->origin.y = NSMaxY([[[NSScreen screens] objectAtIndex:0] frame]) - NSMaxY(*rect);
337 - (void) wineWindow:(WineWindow*)window
338 ordered:(NSWindowOrderingMode)order
339 relativeTo:(WineWindow*)otherWindow
347 [orderedWineWindows removeObjectIdenticalTo:window];
350 index = [orderedWineWindows indexOfObjectIdenticalTo:otherWindow];
351 if (index == NSNotFound)
357 for (otherWindow in orderedWineWindows)
359 if ([otherWindow levelWhenActive] <= [window levelWhenActive])
364 [orderedWineWindows insertObject:window atIndex:index];
369 [orderedWineWindows removeObjectIdenticalTo:window];
372 index = [orderedWineWindows indexOfObjectIdenticalTo:otherWindow];
373 if (index == NSNotFound)
374 index = [orderedWineWindows count];
379 for (otherWindow in orderedWineWindows)
381 if ([otherWindow levelWhenActive] < [window levelWhenActive])
386 [orderedWineWindows insertObject:window atIndex:index];
395 - (void) sendDisplaysChanged:(BOOL)activating
398 WineEventQueue* queue;
400 event.type = DISPLAYS_CHANGED;
402 event.displays_changed.activating = activating;
404 [eventQueuesLock lock];
405 for (queue in eventQueues)
406 [queue postEvent:&event];
407 [eventQueuesLock unlock];
410 // We can compare two modes directly using CFEqual, but that may require that
411 // they are identical to a level that we don't need. In particular, when the
412 // OS switches between the integrated and discrete GPUs, the set of display
413 // modes can change in subtle ways. We're interested in whether two modes
414 // match in their most salient features, even if they aren't identical.
415 - (BOOL) mode:(CGDisplayModeRef)mode1 matchesMode:(CGDisplayModeRef)mode2
417 NSString *encoding1, *encoding2;
418 uint32_t ioflags1, ioflags2, different;
419 double refresh1, refresh2;
421 if (CGDisplayModeGetWidth(mode1) != CGDisplayModeGetWidth(mode2)) return FALSE;
422 if (CGDisplayModeGetHeight(mode1) != CGDisplayModeGetHeight(mode2)) return FALSE;
424 encoding1 = [(NSString*)CGDisplayModeCopyPixelEncoding(mode1) autorelease];
425 encoding2 = [(NSString*)CGDisplayModeCopyPixelEncoding(mode2) autorelease];
426 if (![encoding1 isEqualToString:encoding2]) return FALSE;
428 ioflags1 = CGDisplayModeGetIOFlags(mode1);
429 ioflags2 = CGDisplayModeGetIOFlags(mode2);
430 different = ioflags1 ^ ioflags2;
431 if (different & (kDisplayModeValidFlag | kDisplayModeSafeFlag | kDisplayModeStretchedFlag |
432 kDisplayModeInterlacedFlag | kDisplayModeTelevisionFlag))
435 refresh1 = CGDisplayModeGetRefreshRate(mode1);
436 if (refresh1 == 0) refresh1 = 60;
437 refresh2 = CGDisplayModeGetRefreshRate(mode2);
438 if (refresh2 == 0) refresh2 = 60;
439 if (fabs(refresh1 - refresh2) > 0.1) return FALSE;
444 - (CGDisplayModeRef)modeMatchingMode:(CGDisplayModeRef)mode forDisplay:(CGDirectDisplayID)displayID
446 CGDisplayModeRef ret = NULL;
447 NSArray *modes = [(NSArray*)CGDisplayCopyAllDisplayModes(displayID, NULL) autorelease];
448 for (id candidateModeObject in modes)
450 CGDisplayModeRef candidateMode = (CGDisplayModeRef)candidateModeObject;
451 if ([self mode:candidateMode matchesMode:mode])
460 - (BOOL) setMode:(CGDisplayModeRef)mode forDisplay:(CGDirectDisplayID)displayID
463 NSNumber* displayIDKey = [NSNumber numberWithUnsignedInt:displayID];
464 CGDisplayModeRef currentMode, originalMode;
466 currentMode = CGDisplayCopyDisplayMode(displayID);
467 if (!currentMode) // Invalid display ID
470 if ([self mode:mode matchesMode:currentMode]) // Already there!
472 CGDisplayModeRelease(currentMode);
476 mode = [self modeMatchingMode:mode forDisplay:displayID];
479 CGDisplayModeRelease(currentMode);
483 originalMode = (CGDisplayModeRef)[originalDisplayModes objectForKey:displayIDKey];
485 originalMode = currentMode;
487 if ([self mode:mode matchesMode:originalMode])
489 if ([originalDisplayModes count] == 1) // If this is the last changed display, do a blanket reset
491 CGRestorePermanentDisplayConfiguration();
492 CGReleaseAllDisplays();
493 [originalDisplayModes removeAllObjects];
496 else // ... otherwise, try to restore just the one display
498 if (CGDisplaySetDisplayMode(displayID, mode, NULL) == CGDisplayNoErr)
500 [originalDisplayModes removeObjectForKey:displayIDKey];
507 if ([originalDisplayModes count] || CGCaptureAllDisplays() == CGDisplayNoErr)
509 if (CGDisplaySetDisplayMode(displayID, mode, NULL) == CGDisplayNoErr)
511 [originalDisplayModes setObject:(id)originalMode forKey:displayIDKey];
514 else if (![originalDisplayModes count])
516 CGRestorePermanentDisplayConfiguration();
517 CGReleaseAllDisplays();
522 CGDisplayModeRelease(currentMode);
526 [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
527 [(WineWindow*)obj adjustWindowLevel];
534 - (BOOL) areDisplaysCaptured
536 return ([originalDisplayModes count] > 0);
548 - (void) unhideCursor
553 cursorHidden = FALSE;
559 NSDictionary* frame = [cursorFrames objectAtIndex:cursorFrame];
560 CGImageRef cgimage = (CGImageRef)[frame objectForKey:@"image"];
561 NSImage* image = [[NSImage alloc] initWithCGImage:cgimage size:NSZeroSize];
562 CFDictionaryRef hotSpotDict = (CFDictionaryRef)[frame objectForKey:@"hotSpot"];
566 if (!CGPointMakeWithDictionaryRepresentation(hotSpotDict, &hotSpot))
567 hotSpot = CGPointZero;
568 cursor = [[NSCursor alloc] initWithImage:image hotSpot:NSPointFromCGPoint(hotSpot)];
575 - (void) nextCursorFrame:(NSTimer*)theTimer
578 NSTimeInterval duration;
582 if (cursorFrame >= [cursorFrames count])
586 frame = [cursorFrames objectAtIndex:cursorFrame];
587 duration = [[frame objectForKey:@"duration"] doubleValue];
588 date = [[theTimer fireDate] dateByAddingTimeInterval:duration];
589 [cursorTimer setFireDate:date];
592 - (void) setCursorWithFrames:(NSArray*)frames
594 if (self.cursorFrames == frames)
597 self.cursorFrames = frames;
599 [cursorTimer invalidate];
600 self.cursorTimer = nil;
604 if ([frames count] > 1)
606 NSDictionary* frame = [frames objectAtIndex:0];
607 NSTimeInterval duration = [[frame objectForKey:@"duration"] doubleValue];
608 NSDate* date = [NSDate dateWithTimeIntervalSinceNow:duration];
609 self.cursorTimer = [[[NSTimer alloc] initWithFireDate:date
612 selector:@selector(nextCursorFrame:)
614 repeats:YES] autorelease];
615 [[NSRunLoop currentRunLoop] addTimer:cursorTimer forMode:NSRunLoopCommonModes];
623 * ---------- Cursor clipping methods ----------
625 * Neither Quartz nor Cocoa has an exact analog for Win32 cursor clipping.
626 * For one simple case, clipping to a 1x1 rectangle, Quartz does have an
627 * equivalent: CGAssociateMouseAndMouseCursorPosition(false). For the
628 * general case, we leverage that. We disassociate mouse movements from
629 * the cursor position and then move the cursor manually, keeping it within
630 * the clipping rectangle.
632 * Moving the cursor manually isn't enough. We need to modify the event
633 * stream so that the events have the new location, too. We need to do
634 * this at a point before the events enter Cocoa, so that Cocoa will assign
635 * the correct window to the event. So, we install a Quartz event tap to
638 * Also, there's a complication when we move the cursor. We use
639 * CGWarpMouseCursorPosition(). That doesn't generate mouse movement
640 * events, but the change of cursor position is incorporated into the
641 * deltas of the next mouse move event. When the mouse is disassociated
642 * from the cursor position, we need the deltas to only reflect actual
643 * device movement, not programmatic changes. So, the event tap cancels
644 * out the change caused by our calls to CGWarpMouseCursorPosition().
646 - (void) clipCursorLocation:(CGPoint*)location
648 if (location->x < CGRectGetMinX(cursorClipRect))
649 location->x = CGRectGetMinX(cursorClipRect);
650 if (location->y < CGRectGetMinY(cursorClipRect))
651 location->y = CGRectGetMinY(cursorClipRect);
652 if (location->x > CGRectGetMaxX(cursorClipRect) - 1)
653 location->x = CGRectGetMaxX(cursorClipRect) - 1;
654 if (location->y > CGRectGetMaxY(cursorClipRect) - 1)
655 location->y = CGRectGetMaxY(cursorClipRect) - 1;
658 - (BOOL) warpCursorTo:(CGPoint*)newLocation from:(const CGPoint*)currentLocation
663 oldLocation = *currentLocation;
665 oldLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
667 if (!CGPointEqualToPoint(oldLocation, *newLocation))
669 WarpRecord* warpRecord = [[[WarpRecord alloc] init] autorelease];
672 warpRecord.from = oldLocation;
673 warpRecord.timeBefore = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
675 /* Actually move the cursor. */
676 err = CGWarpMouseCursorPosition(*newLocation);
677 if (err != kCGErrorSuccess)
680 warpRecord.timeAfter = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
681 *newLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
683 if (!CGPointEqualToPoint(oldLocation, *newLocation))
685 warpRecord.to = *newLocation;
686 [warpRecords addObject:warpRecord];
693 - (BOOL) isMouseMoveEventType:(CGEventType)type
697 case kCGEventMouseMoved:
698 case kCGEventLeftMouseDragged:
699 case kCGEventRightMouseDragged:
700 case kCGEventOtherMouseDragged:
707 - (int) warpsFinishedByEventTime:(CGEventTimestamp)eventTime location:(CGPoint)eventLocation
709 int warpsFinished = 0;
710 for (WarpRecord* warpRecord in warpRecords)
712 if (warpRecord.timeAfter < eventTime ||
713 (warpRecord.timeBefore <= eventTime && CGPointEqualToPoint(eventLocation, warpRecord.to)))
719 return warpsFinished;
722 - (CGEventRef) eventTapWithProxy:(CGEventTapProxy)proxy
723 type:(CGEventType)type
724 event:(CGEventRef)event
726 CGEventTimestamp eventTime;
727 CGPoint eventLocation, cursorLocation;
729 if (type == kCGEventTapDisabledByUserInput)
731 if (type == kCGEventTapDisabledByTimeout)
733 CGEventTapEnable(cursorClippingEventTap, TRUE);
740 eventTime = CGEventGetTimestamp(event);
741 lastEventTapEventTime = eventTime / (double)NSEC_PER_SEC;
743 eventLocation = CGEventGetLocation(event);
745 cursorLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
747 if ([self isMouseMoveEventType:type])
749 double deltaX, deltaY;
750 int warpsFinished = [self warpsFinishedByEventTime:eventTime location:eventLocation];
753 deltaX = CGEventGetDoubleValueField(event, kCGMouseEventDeltaX);
754 deltaY = CGEventGetDoubleValueField(event, kCGMouseEventDeltaY);
756 for (i = 0; i < warpsFinished; i++)
758 WarpRecord* warpRecord = [warpRecords objectAtIndex:0];
759 deltaX -= warpRecord.to.x - warpRecord.from.x;
760 deltaY -= warpRecord.to.y - warpRecord.from.y;
761 [warpRecords removeObjectAtIndex:0];
766 CGEventSetDoubleValueField(event, kCGMouseEventDeltaX, deltaX);
767 CGEventSetDoubleValueField(event, kCGMouseEventDeltaY, deltaY);
770 synthesizedLocation.x += deltaX;
771 synthesizedLocation.y += deltaY;
774 // If the event is destined for another process, don't clip it. This may
775 // happen if the user activates Exposé or Mission Control. In that case,
776 // our app does not resign active status, so clipping is still in effect,
777 // but the cursor should not actually be clipped.
779 // In addition, the fact that mouse moves may have been delivered to a
780 // different process means we have to treat the next one we receive as
781 // absolute rather than relative.
782 if (CGEventGetIntegerValueField(event, kCGEventTargetUnixProcessID) == getpid())
783 [self clipCursorLocation:&synthesizedLocation];
785 lastSetCursorPositionTime = lastEventTapEventTime;
787 [self warpCursorTo:&synthesizedLocation from:&cursorLocation];
788 if (!CGPointEqualToPoint(eventLocation, synthesizedLocation))
789 CGEventSetLocation(event, synthesizedLocation);
794 CGEventRef WineAppEventTapCallBack(CGEventTapProxy proxy, CGEventType type,
795 CGEventRef event, void *refcon)
797 WineApplication* app = refcon;
798 return [app eventTapWithProxy:proxy type:type event:event];
801 - (BOOL) installEventTap
803 ProcessSerialNumber psn;
805 CGEventMask mask = CGEventMaskBit(kCGEventLeftMouseDown) |
806 CGEventMaskBit(kCGEventLeftMouseUp) |
807 CGEventMaskBit(kCGEventRightMouseDown) |
808 CGEventMaskBit(kCGEventRightMouseUp) |
809 CGEventMaskBit(kCGEventMouseMoved) |
810 CGEventMaskBit(kCGEventLeftMouseDragged) |
811 CGEventMaskBit(kCGEventRightMouseDragged) |
812 CGEventMaskBit(kCGEventOtherMouseDown) |
813 CGEventMaskBit(kCGEventOtherMouseUp) |
814 CGEventMaskBit(kCGEventOtherMouseDragged) |
815 CGEventMaskBit(kCGEventScrollWheel);
816 CFRunLoopSourceRef source;
818 OSErr (*pGetCurrentProcess)(ProcessSerialNumber* PSN);
820 if (cursorClippingEventTap)
823 // We need to get the Mac GetCurrentProcess() from the ApplicationServices
824 // framework with dlsym() because the Win32 function of the same name
826 appServices = dlopen("/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices", RTLD_LAZY);
830 pGetCurrentProcess = dlsym(appServices, "GetCurrentProcess");
831 if (!pGetCurrentProcess)
833 dlclose(appServices);
837 err = pGetCurrentProcess(&psn);
838 dlclose(appServices);
842 // We create an annotated session event tap rather than a process-specific
843 // event tap because we need to programmatically move the cursor even when
844 // mouse moves are directed to other processes. We disable our tap when
845 // other processes are active, but things like Exposé are handled by other
846 // processes even when we remain active.
847 cursorClippingEventTap = CGEventTapCreate(kCGAnnotatedSessionEventTap, kCGHeadInsertEventTap,
848 kCGEventTapOptionDefault, mask, WineAppEventTapCallBack, self);
849 if (!cursorClippingEventTap)
852 CGEventTapEnable(cursorClippingEventTap, FALSE);
854 source = CFMachPortCreateRunLoopSource(NULL, cursorClippingEventTap, 0);
857 CFRelease(cursorClippingEventTap);
858 cursorClippingEventTap = NULL;
862 CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
867 - (BOOL) setCursorPosition:(CGPoint)pos
873 [self clipCursorLocation:&pos];
875 synthesizedLocation = pos;
876 ret = [self warpCursorTo:&synthesizedLocation from:NULL];
879 // We want to discard mouse-move events that have already been
880 // through the event tap, because it's too late to account for
881 // the setting of the cursor position with them. However, the
882 // events that may be queued with times after that but before
883 // the above warp can still be used. So, use the last event
884 // tap event time so that -sendEvent: doesn't discard them.
885 lastSetCursorPositionTime = lastEventTapEventTime;
890 ret = (CGWarpMouseCursorPosition(pos) == kCGErrorSuccess);
892 lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
897 WineEventQueue* queue;
899 // Discard all pending mouse move events.
900 [eventQueuesLock lock];
901 for (queue in eventQueues)
903 [queue discardEventsMatchingMask:event_mask_for_type(MOUSE_MOVED) |
904 event_mask_for_type(MOUSE_MOVED_ABSOLUTE)
907 [eventQueuesLock unlock];
913 - (void) activateCursorClipping
917 CGEventTapEnable(cursorClippingEventTap, TRUE);
918 [self setCursorPosition:NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]])];
922 - (void) deactivateCursorClipping
926 CGEventTapEnable(cursorClippingEventTap, FALSE);
927 [warpRecords removeAllObjects];
928 lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
932 - (BOOL) startClippingCursor:(CGRect)rect
936 if (!cursorClippingEventTap && ![self installEventTap])
939 err = CGAssociateMouseAndMouseCursorPosition(false);
940 if (err != kCGErrorSuccess)
943 clippingCursor = TRUE;
944 cursorClipRect = rect;
946 [self activateCursorClipping];
951 - (BOOL) stopClippingCursor
953 CGError err = CGAssociateMouseAndMouseCursorPosition(true);
954 if (err != kCGErrorSuccess)
957 [self deactivateCursorClipping];
958 clippingCursor = FALSE;
965 * ---------- NSApplication method overrides ----------
967 - (void) sendEvent:(NSEvent*)anEvent
969 NSEventType type = [anEvent type];
970 if (type == NSFlagsChanged)
971 self.lastFlagsChanged = anEvent;
973 [super sendEvent:anEvent];
975 if (type == NSMouseMoved || type == NSLeftMouseDragged ||
976 type == NSRightMouseDragged || type == NSOtherMouseDragged)
978 WineWindow* targetWindow;
980 /* Because of the way -[NSWindow setAcceptsMouseMovedEvents:] works, the
981 event indicates its window is the main window, even if the cursor is
982 over a different window. Find the actual WineWindow that is under the
983 cursor and post the event as being for that window. */
984 if (type == NSMouseMoved)
986 CGPoint cgpoint = CGEventGetLocation([anEvent CGEvent]);
987 NSPoint point = [self flippedMouseLocation:NSPointFromCGPoint(cgpoint)];
988 NSInteger windowUnderNumber;
990 windowUnderNumber = [NSWindow windowNumberAtPoint:point
991 belowWindowWithWindowNumber:0];
992 targetWindow = (WineWindow*)[self windowWithWindowNumber:windowUnderNumber];
995 targetWindow = (WineWindow*)[anEvent window];
997 if ([targetWindow isKindOfClass:[WineWindow class]])
999 BOOL absolute = forceNextMouseMoveAbsolute || (targetWindow != lastTargetWindow);
1000 forceNextMouseMoveAbsolute = FALSE;
1002 // If we recently warped the cursor (other than in our cursor-clipping
1003 // event tap), discard mouse move events until we see an event which is
1004 // later than that time.
1005 if (lastSetCursorPositionTime)
1007 if ([anEvent timestamp] <= lastSetCursorPositionTime)
1010 lastSetCursorPositionTime = 0;
1014 [targetWindow postMouseMovedEvent:anEvent absolute:absolute];
1015 lastTargetWindow = targetWindow;
1017 else if (lastTargetWindow)
1019 [[NSCursor arrowCursor] set];
1020 [self unhideCursor];
1021 lastTargetWindow = nil;
1024 else if (type == NSLeftMouseDown || type == NSLeftMouseUp ||
1025 type == NSRightMouseDown || type == NSRightMouseUp ||
1026 type == NSOtherMouseDown || type == NSOtherMouseUp ||
1027 type == NSScrollWheel)
1029 // Since mouse button and scroll wheel events deliver absolute cursor
1030 // position, the accumulating delta from move events is invalidated.
1031 // Make sure next mouse move event starts over from an absolute baseline.
1032 forceNextMouseMoveAbsolute = TRUE;
1038 * ---------- NSApplicationDelegate methods ----------
1040 - (void)applicationDidBecomeActive:(NSNotification *)notification
1042 [self activateCursorClipping];
1044 [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
1045 WineWindow* window = obj;
1046 if ([window levelWhenActive] != [window level])
1047 [window setLevel:[window levelWhenActive]];
1050 // If a Wine process terminates abruptly while it has the display captured
1051 // and switched to a different resolution, Mac OS X will uncapture the
1052 // displays and switch their resolutions back. However, the other Wine
1053 // processes won't have their notion of the desktop rect changed back.
1054 // This can lead them to refuse to draw or acknowledge clicks in certain
1055 // portions of their windows.
1057 // To solve this, we synthesize a displays-changed event whenever we're
1058 // activated. This will provoke a re-synchronization of Wine's notion of
1059 // the desktop rect with the actual state.
1060 [self sendDisplaysChanged:TRUE];
1062 // The cursor probably moved while we were inactive. Accumulated mouse
1063 // movement deltas are invalidated. Make sure the next mouse move event
1064 // starts over from an absolute baseline.
1065 forceNextMouseMoveAbsolute = TRUE;
1068 - (void)applicationDidChangeScreenParameters:(NSNotification *)notification
1070 primaryScreenHeightValid = FALSE;
1071 [self sendDisplaysChanged:FALSE];
1073 // When the display configuration changes, the cursor position may jump.
1074 // Accumulated mouse movement deltas are invalidated. Make sure the next
1075 // mouse move event starts over from an absolute baseline.
1076 forceNextMouseMoveAbsolute = TRUE;
1079 - (void)applicationDidResignActive:(NSNotification *)notification
1082 WineEventQueue* queue;
1084 [self invalidateGotFocusEvents];
1086 event.type = APP_DEACTIVATED;
1087 event.window = NULL;
1089 [eventQueuesLock lock];
1090 for (queue in eventQueues)
1091 [queue postEvent:&event];
1092 [eventQueuesLock unlock];
1095 - (void)applicationWillFinishLaunching:(NSNotification *)notification
1097 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
1099 [nc addObserverForName:NSWindowDidBecomeKeyNotification
1102 usingBlock:^(NSNotification *note){
1103 NSWindow* window = [note object];
1104 [keyWindows removeObjectIdenticalTo:window];
1105 [keyWindows insertObject:window atIndex:0];
1108 [nc addObserverForName:NSWindowWillCloseNotification
1110 queue:[NSOperationQueue mainQueue]
1111 usingBlock:^(NSNotification *note){
1112 NSWindow* window = [note object];
1113 [keyWindows removeObjectIdenticalTo:window];
1114 [orderedWineWindows removeObjectIdenticalTo:window];
1115 if (window == lastTargetWindow)
1116 lastTargetWindow = nil;
1119 [nc addObserver:self
1120 selector:@selector(keyboardSelectionDidChange)
1121 name:NSTextInputContextKeyboardSelectionDidChangeNotification
1124 /* The above notification isn't sent unless the NSTextInputContext
1125 class has initialized itself. Poke it. */
1126 [NSTextInputContext self];
1128 self.keyboardType = LMGetKbdType();
1131 - (void)applicationWillResignActive:(NSNotification *)notification
1133 [self deactivateCursorClipping];
1135 [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
1136 WineWindow* window = obj;
1137 NSInteger level = window.floating ? NSFloatingWindowLevel : NSNormalWindowLevel;
1138 if ([window level] > level)
1139 [window setLevel:level];
1143 /***********************************************************************
1146 * Run-loop-source perform callback. Pull request blocks from the
1147 * array of queued requests and invoke them.
1149 static void PerformRequest(void *info)
1151 WineApplication* app = (WineApplication*)NSApp;
1155 __block dispatch_block_t block;
1157 dispatch_sync(app->requestsManipQueue, ^{
1158 if ([app->requests count])
1160 block = (dispatch_block_t)[[app->requests objectAtIndex:0] retain];
1161 [app->requests removeObjectAtIndex:0];
1175 /***********************************************************************
1178 * Run a block on the main thread asynchronously.
1180 void OnMainThreadAsync(dispatch_block_t block)
1182 WineApplication* app = (WineApplication*)NSApp;
1184 block = [block copy];
1185 dispatch_sync(app->requestsManipQueue, ^{
1186 [app->requests addObject:block];
1189 CFRunLoopSourceSignal(app->requestSource);
1190 CFRunLoopWakeUp(CFRunLoopGetMain());
1195 /***********************************************************************
1198 void LogError(const char* func, NSString* format, ...)
1201 va_start(args, format);
1202 LogErrorv(func, format, args);
1206 /***********************************************************************
1209 void LogErrorv(const char* func, NSString* format, va_list args)
1211 NSString* message = [[NSString alloc] initWithFormat:format arguments:args];
1212 fprintf(stderr, "err:%s:%s", func, [message UTF8String]);
1216 /***********************************************************************
1217 * macdrv_window_rejected_focus
1219 * Pass focus to the next window that hasn't already rejected this same
1220 * WINDOW_GOT_FOCUS event.
1222 void macdrv_window_rejected_focus(const macdrv_event *event)
1225 [NSApp windowRejectedFocusEvent:event];
1229 /***********************************************************************
1230 * macdrv_get_keyboard_layout
1232 * Returns the keyboard layout uchr data.
1234 CFDataRef macdrv_copy_keyboard_layout(CGEventSourceKeyboardType* keyboard_type, int* is_iso)
1236 __block CFDataRef result = NULL;
1239 TISInputSourceRef inputSource;
1241 inputSource = TISCopyCurrentKeyboardLayoutInputSource();
1244 CFDataRef uchr = TISGetInputSourceProperty(inputSource,
1245 kTISPropertyUnicodeKeyLayoutData);
1246 result = CFDataCreateCopy(NULL, uchr);
1247 CFRelease(inputSource);
1249 *keyboard_type = ((WineApplication*)NSApp).keyboardType;
1250 *is_iso = (KBGetLayoutType(*keyboard_type) == kKeyboardISO);
1257 /***********************************************************************
1260 * Play the beep sound configured by the user in System Preferences.
1262 void macdrv_beep(void)
1264 OnMainThreadAsync(^{
1269 /***********************************************************************
1270 * macdrv_set_display_mode
1272 int macdrv_set_display_mode(const struct macdrv_display* display,
1273 CGDisplayModeRef display_mode)
1278 ret = [NSApp setMode:display_mode forDisplay:display->displayID];
1284 /***********************************************************************
1289 * If name is non-NULL, it is a selector for a class method on NSCursor
1290 * identifying the cursor to set. In that case, frames is ignored. If
1291 * name is NULL, then frames is used.
1293 * frames is an array of dictionaries. Each dictionary is a frame of
1294 * an animated cursor. Under the key "image" is a CGImage for the
1295 * frame. Under the key "duration" is a CFNumber time interval, in
1296 * seconds, for how long that frame is presented before proceeding to
1297 * the next frame. Under the key "hotSpot" is a CFDictionary encoding a
1298 * CGPoint, to be decoded using CGPointMakeWithDictionaryRepresentation().
1299 * This is the hot spot, measured in pixels down and to the right of the
1300 * top-left corner of the image.
1302 * If the array has exactly 1 element, the cursor is static, not
1303 * animated. If frames is NULL or has 0 elements, the cursor is hidden.
1305 void macdrv_set_cursor(CFStringRef name, CFArrayRef frames)
1309 sel = NSSelectorFromString((NSString*)name);
1312 OnMainThreadAsync(^{
1313 NSCursor* cursor = [NSCursor performSelector:sel];
1314 [NSApp setCursorWithFrames:nil];
1316 [NSApp unhideCursor];
1321 NSArray* nsframes = (NSArray*)frames;
1322 if ([nsframes count])
1324 OnMainThreadAsync(^{
1325 [NSApp setCursorWithFrames:nsframes];
1330 OnMainThreadAsync(^{
1331 [NSApp setCursorWithFrames:nil];
1338 /***********************************************************************
1339 * macdrv_get_cursor_position
1341 * Obtains the current cursor position. Returns zero on failure,
1342 * non-zero on success.
1344 int macdrv_get_cursor_position(CGPoint *pos)
1347 NSPoint location = [NSEvent mouseLocation];
1348 location = [NSApp flippedMouseLocation:location];
1349 *pos = NSPointToCGPoint(location);
1355 /***********************************************************************
1356 * macdrv_set_cursor_position
1358 * Sets the cursor position without generating events. Returns zero on
1359 * failure, non-zero on success.
1361 int macdrv_set_cursor_position(CGPoint pos)
1366 ret = [NSApp setCursorPosition:pos];
1372 /***********************************************************************
1373 * macdrv_clip_cursor
1375 * Sets the cursor cursor clipping rectangle. If the rectangle is equal
1376 * to or larger than the whole desktop region, the cursor is unclipped.
1377 * Returns zero on failure, non-zero on success.
1379 int macdrv_clip_cursor(CGRect rect)
1384 BOOL clipping = FALSE;
1386 if (!CGRectIsInfinite(rect))
1388 NSRect nsrect = NSRectFromCGRect(rect);
1391 /* Convert the rectangle from top-down coords to bottom-up. */
1392 [NSApp flipRect:&nsrect];
1395 for (screen in [NSScreen screens])
1397 if (!NSContainsRect(nsrect, [screen frame]))
1406 ret = [NSApp startClippingCursor:rect];
1408 ret = [NSApp stopClippingCursor];