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 @implementation WineApplication
37 @synthesize wineController;
39 - (void) sendEvent:(NSEvent*)anEvent
41 if (![wineController handleEvent:anEvent])
43 [super sendEvent:anEvent];
44 [wineController didSendEvent:anEvent];
48 - (void) setWineController:(WineApplicationController*)newController
50 wineController = newController;
51 [self setDelegate:wineController];
57 @interface WarpRecord : NSObject
59 CGEventTimestamp timeBefore, timeAfter;
63 @property (nonatomic) CGEventTimestamp timeBefore;
64 @property (nonatomic) CGEventTimestamp timeAfter;
65 @property (nonatomic) CGPoint from;
66 @property (nonatomic) CGPoint to;
71 @implementation WarpRecord
73 @synthesize timeBefore, timeAfter, from, to;
78 @interface WineApplicationController ()
80 @property (readwrite, copy, nonatomic) NSEvent* lastFlagsChanged;
81 @property (copy, nonatomic) NSArray* cursorFrames;
82 @property (retain, nonatomic) NSTimer* cursorTimer;
83 @property (retain, nonatomic) NSImage* applicationIcon;
84 @property (readonly, nonatomic) BOOL inputSourceIsInputMethod;
86 - (void) setupObservations;
87 - (void) applicationDidBecomeActive:(NSNotification *)notification;
89 static void PerformRequest(void *info);
94 @implementation WineApplicationController
96 @synthesize keyboardType, lastFlagsChanged;
97 @synthesize orderedWineWindows, applicationIcon;
98 @synthesize cursorFrames, cursorTimer;
102 if (self == [WineApplicationController class])
104 NSDictionary* defaults = [NSDictionary dictionaryWithObjectsAndKeys:
105 @"", @"NSQuotedKeystrokeBinding",
106 @"", @"NSRepeatCountBinding",
107 [NSNumber numberWithBool:NO], @"ApplePressAndHoldEnabled",
109 [[NSUserDefaults standardUserDefaults] registerDefaults:defaults];
113 + (WineApplicationController*) sharedController
115 static WineApplicationController* sharedController;
116 static dispatch_once_t once;
118 dispatch_once(&once, ^{
119 sharedController = [[self alloc] init];
122 return sharedController;
130 CFRunLoopSourceContext context = { 0 };
131 context.perform = PerformRequest;
132 requestSource = CFRunLoopSourceCreate(NULL, 0, &context);
138 CFRunLoopAddSource(CFRunLoopGetMain(), requestSource, kCFRunLoopCommonModes);
139 CFRunLoopAddSource(CFRunLoopGetMain(), requestSource, (CFStringRef)WineAppWaitQueryResponseMode);
141 requests = [[NSMutableArray alloc] init];
142 requestsManipQueue = dispatch_queue_create("org.winehq.WineAppRequestManipQueue", NULL);
144 eventQueues = [[NSMutableArray alloc] init];
145 eventQueuesLock = [[NSLock alloc] init];
147 keyWindows = [[NSMutableArray alloc] init];
148 orderedWineWindows = [[NSMutableArray alloc] init];
150 originalDisplayModes = [[NSMutableDictionary alloc] init];
152 warpRecords = [[NSMutableArray alloc] init];
154 if (!requests || !requestsManipQueue || !eventQueues || !eventQueuesLock ||
155 !keyWindows || !orderedWineWindows || !originalDisplayModes || !warpRecords)
161 [self setupObservations];
163 keyboardType = LMGetKbdType();
165 if ([NSApp isActive])
166 [self applicationDidBecomeActive:nil];
173 [applicationIcon release];
174 [warpRecords release];
175 [cursorTimer release];
176 [cursorFrames release];
177 [originalDisplayModes release];
178 [orderedWineWindows release];
179 [keyWindows release];
180 [eventQueues release];
181 [eventQueuesLock release];
182 if (requestsManipQueue) dispatch_release(requestsManipQueue);
186 CFRunLoopSourceInvalidate(requestSource);
187 CFRelease(requestSource);
192 - (void) transformProcessToForeground
194 if ([NSApp activationPolicy] != NSApplicationActivationPolicyRegular)
198 NSString* bundleName;
202 [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
203 [NSApp activateIgnoringOtherApps:YES];
205 mainMenu = [[[NSMenu alloc] init] autorelease];
207 submenu = [[[NSMenu alloc] initWithTitle:@"Wine"] autorelease];
208 bundleName = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString*)kCFBundleNameKey];
209 if ([bundleName length])
210 title = [NSString stringWithFormat:@"Quit %@", bundleName];
213 item = [submenu addItemWithTitle:title action:@selector(terminate:) keyEquivalent:@"q"];
214 [item setKeyEquivalentModifierMask:NSCommandKeyMask | NSAlternateKeyMask];
215 item = [[[NSMenuItem alloc] init] autorelease];
216 [item setTitle:@"Wine"];
217 [item setSubmenu:submenu];
218 [mainMenu addItem:item];
220 submenu = [[[NSMenu alloc] initWithTitle:@"Window"] autorelease];
221 [submenu addItemWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@""];
222 [submenu addItemWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""];
223 [submenu addItem:[NSMenuItem separatorItem]];
224 [submenu addItemWithTitle:@"Bring All to Front" action:@selector(arrangeInFront:) keyEquivalent:@""];
225 item = [[[NSMenuItem alloc] init] autorelease];
226 [item setTitle:@"Window"];
227 [item setSubmenu:submenu];
228 [mainMenu addItem:item];
230 [NSApp setMainMenu:mainMenu];
231 [NSApp setWindowsMenu:submenu];
233 [NSApp setApplicationIconImage:self.applicationIcon];
237 - (BOOL) waitUntilQueryDone:(int*)done timeout:(NSDate*)timeout processEvents:(BOOL)processEvents
239 PerformRequest(NULL);
245 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
246 NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask
248 inMode:NSDefaultRunLoopMode
251 [NSApp sendEvent:event];
255 [[NSRunLoop currentRunLoop] runMode:WineAppWaitQueryResponseMode beforeDate:timeout];
256 } while (!*done && [timeout timeIntervalSinceNow] >= 0);
261 - (BOOL) registerEventQueue:(WineEventQueue*)queue
263 [eventQueuesLock lock];
264 [eventQueues addObject:queue];
265 [eventQueuesLock unlock];
269 - (void) unregisterEventQueue:(WineEventQueue*)queue
271 [eventQueuesLock lock];
272 [eventQueues removeObjectIdenticalTo:queue];
273 [eventQueuesLock unlock];
276 - (void) computeEventTimeAdjustmentFromTicks:(unsigned long long)tickcount uptime:(uint64_t)uptime_ns
278 eventTimeAdjustment = (tickcount / 1000.0) - (uptime_ns / (double)NSEC_PER_SEC);
281 - (double) ticksForEventTime:(NSTimeInterval)eventTime
283 return (eventTime + eventTimeAdjustment) * 1000;
286 /* Invalidate old focus offers across all queues. */
287 - (void) invalidateGotFocusEvents
289 WineEventQueue* queue;
293 [eventQueuesLock lock];
294 for (queue in eventQueues)
296 [queue discardEventsMatchingMask:event_mask_for_type(WINDOW_GOT_FOCUS)
299 [eventQueuesLock unlock];
302 - (void) windowGotFocus:(WineWindow*)window
306 [self invalidateGotFocusEvents];
308 event = macdrv_create_event(WINDOW_GOT_FOCUS, window);
309 event->window_got_focus.serial = windowFocusSerial;
311 event->window_got_focus.tried_windows = [triedWindows retain];
313 event->window_got_focus.tried_windows = [[NSMutableSet alloc] init];
314 [window.queue postEvent:event];
315 macdrv_release_event(event);
318 - (void) windowRejectedFocusEvent:(const macdrv_event*)event
320 if (event->window_got_focus.serial == windowFocusSerial)
322 triedWindows = (NSMutableSet*)event->window_got_focus.tried_windows;
323 [triedWindows addObject:(WineWindow*)event->window];
324 for (NSWindow* window in [keyWindows arrayByAddingObjectsFromArray:[self orderedWineWindows]])
326 if (![triedWindows containsObject:window] && [window canBecomeKeyWindow])
328 [window makeKeyWindow];
336 - (void) keyboardSelectionDidChange
338 TISInputSourceRef inputSource;
340 inputSourceIsInputMethodValid = FALSE;
342 inputSource = TISCopyCurrentKeyboardLayoutInputSource();
346 uchr = TISGetInputSourceProperty(inputSource,
347 kTISPropertyUnicodeKeyLayoutData);
351 WineEventQueue* queue;
353 event = macdrv_create_event(KEYBOARD_CHANGED, nil);
354 event->keyboard_changed.keyboard_type = self.keyboardType;
355 event->keyboard_changed.iso_keyboard = (KBGetLayoutType(self.keyboardType) == kKeyboardISO);
356 event->keyboard_changed.uchr = CFDataCreateCopy(NULL, uchr);
358 if (event->keyboard_changed.uchr)
360 [eventQueuesLock lock];
362 for (queue in eventQueues)
363 [queue postEvent:event];
365 [eventQueuesLock unlock];
368 macdrv_release_event(event);
371 CFRelease(inputSource);
375 - (CGFloat) primaryScreenHeight
377 if (!primaryScreenHeightValid)
379 NSArray* screens = [NSScreen screens];
382 primaryScreenHeight = NSHeight([[screens objectAtIndex:0] frame]);
383 primaryScreenHeightValid = TRUE;
386 return 1280; /* arbitrary value */
389 return primaryScreenHeight;
392 - (NSPoint) flippedMouseLocation:(NSPoint)point
394 /* This relies on the fact that Cocoa's mouse location points are
395 actually off by one (precisely because they were flipped from
396 Quartz screen coordinates using this same technique). */
397 point.y = [self primaryScreenHeight] - point.y;
401 - (void) flipRect:(NSRect*)rect
403 // We don't use -primaryScreenHeight here so there's no chance of having
404 // out-of-date cached info. This method is called infrequently enough
405 // that getting the screen height each time is not prohibitively expensive.
406 rect->origin.y = NSMaxY([[[NSScreen screens] objectAtIndex:0] frame]) - NSMaxY(*rect);
409 - (void) wineWindow:(WineWindow*)window
410 ordered:(NSWindowOrderingMode)order
411 relativeTo:(WineWindow*)otherWindow
419 [orderedWineWindows removeObjectIdenticalTo:window];
422 index = [orderedWineWindows indexOfObjectIdenticalTo:otherWindow];
423 if (index == NSNotFound)
429 for (otherWindow in orderedWineWindows)
431 if ([otherWindow levelWhenActive] <= [window levelWhenActive])
436 [orderedWineWindows insertObject:window atIndex:index];
441 [orderedWineWindows removeObjectIdenticalTo:window];
444 index = [orderedWineWindows indexOfObjectIdenticalTo:otherWindow];
445 if (index == NSNotFound)
446 index = [orderedWineWindows count];
451 for (otherWindow in orderedWineWindows)
453 if ([otherWindow levelWhenActive] < [window levelWhenActive])
458 [orderedWineWindows insertObject:window atIndex:index];
467 - (void) sendDisplaysChanged:(BOOL)activating
470 WineEventQueue* queue;
472 event = macdrv_create_event(DISPLAYS_CHANGED, nil);
473 event->displays_changed.activating = activating;
475 [eventQueuesLock lock];
477 // If we're activating, then we just need one of our threads to get the
478 // event, so it can send it directly to the desktop window. Otherwise,
479 // we need all of the threads to get it because we don't know which owns
480 // the desktop window and only that one will do anything with it.
481 if (activating) event->deliver = 1;
483 for (queue in eventQueues)
484 [queue postEvent:event];
485 [eventQueuesLock unlock];
487 macdrv_release_event(event);
490 // We can compare two modes directly using CFEqual, but that may require that
491 // they are identical to a level that we don't need. In particular, when the
492 // OS switches between the integrated and discrete GPUs, the set of display
493 // modes can change in subtle ways. We're interested in whether two modes
494 // match in their most salient features, even if they aren't identical.
495 - (BOOL) mode:(CGDisplayModeRef)mode1 matchesMode:(CGDisplayModeRef)mode2
497 NSString *encoding1, *encoding2;
498 uint32_t ioflags1, ioflags2, different;
499 double refresh1, refresh2;
501 if (CGDisplayModeGetWidth(mode1) != CGDisplayModeGetWidth(mode2)) return FALSE;
502 if (CGDisplayModeGetHeight(mode1) != CGDisplayModeGetHeight(mode2)) return FALSE;
504 encoding1 = [(NSString*)CGDisplayModeCopyPixelEncoding(mode1) autorelease];
505 encoding2 = [(NSString*)CGDisplayModeCopyPixelEncoding(mode2) autorelease];
506 if (![encoding1 isEqualToString:encoding2]) return FALSE;
508 ioflags1 = CGDisplayModeGetIOFlags(mode1);
509 ioflags2 = CGDisplayModeGetIOFlags(mode2);
510 different = ioflags1 ^ ioflags2;
511 if (different & (kDisplayModeValidFlag | kDisplayModeSafeFlag | kDisplayModeStretchedFlag |
512 kDisplayModeInterlacedFlag | kDisplayModeTelevisionFlag))
515 refresh1 = CGDisplayModeGetRefreshRate(mode1);
516 if (refresh1 == 0) refresh1 = 60;
517 refresh2 = CGDisplayModeGetRefreshRate(mode2);
518 if (refresh2 == 0) refresh2 = 60;
519 if (fabs(refresh1 - refresh2) > 0.1) return FALSE;
524 - (CGDisplayModeRef)modeMatchingMode:(CGDisplayModeRef)mode forDisplay:(CGDirectDisplayID)displayID
526 CGDisplayModeRef ret = NULL;
527 NSArray *modes = [(NSArray*)CGDisplayCopyAllDisplayModes(displayID, NULL) autorelease];
528 for (id candidateModeObject in modes)
530 CGDisplayModeRef candidateMode = (CGDisplayModeRef)candidateModeObject;
531 if ([self mode:candidateMode matchesMode:mode])
540 - (BOOL) setMode:(CGDisplayModeRef)mode forDisplay:(CGDirectDisplayID)displayID
543 NSNumber* displayIDKey = [NSNumber numberWithUnsignedInt:displayID];
544 CGDisplayModeRef currentMode, originalMode;
546 currentMode = CGDisplayCopyDisplayMode(displayID);
547 if (!currentMode) // Invalid display ID
550 if ([self mode:mode matchesMode:currentMode]) // Already there!
552 CGDisplayModeRelease(currentMode);
556 mode = [self modeMatchingMode:mode forDisplay:displayID];
559 CGDisplayModeRelease(currentMode);
563 originalMode = (CGDisplayModeRef)[originalDisplayModes objectForKey:displayIDKey];
565 originalMode = currentMode;
567 if ([self mode:mode matchesMode:originalMode])
569 if ([originalDisplayModes count] == 1) // If this is the last changed display, do a blanket reset
571 CGRestorePermanentDisplayConfiguration();
572 CGReleaseAllDisplays();
573 [originalDisplayModes removeAllObjects];
576 else // ... otherwise, try to restore just the one display
578 if (CGDisplaySetDisplayMode(displayID, mode, NULL) == CGDisplayNoErr)
580 [originalDisplayModes removeObjectForKey:displayIDKey];
587 if ([originalDisplayModes count] || CGCaptureAllDisplays() == CGDisplayNoErr)
589 if (CGDisplaySetDisplayMode(displayID, mode, NULL) == CGDisplayNoErr)
591 [originalDisplayModes setObject:(id)originalMode forKey:displayIDKey];
594 else if (![originalDisplayModes count])
596 CGRestorePermanentDisplayConfiguration();
597 CGReleaseAllDisplays();
602 CGDisplayModeRelease(currentMode);
606 [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
607 [(WineWindow*)obj adjustWindowLevel];
614 - (BOOL) areDisplaysCaptured
616 return ([originalDisplayModes count] > 0);
628 - (void) unhideCursor
633 cursorHidden = FALSE;
639 NSDictionary* frame = [cursorFrames objectAtIndex:cursorFrame];
640 CGImageRef cgimage = (CGImageRef)[frame objectForKey:@"image"];
641 NSImage* image = [[NSImage alloc] initWithCGImage:cgimage size:NSZeroSize];
642 CFDictionaryRef hotSpotDict = (CFDictionaryRef)[frame objectForKey:@"hotSpot"];
646 if (!CGPointMakeWithDictionaryRepresentation(hotSpotDict, &hotSpot))
647 hotSpot = CGPointZero;
648 cursor = [[NSCursor alloc] initWithImage:image hotSpot:NSPointFromCGPoint(hotSpot)];
655 - (void) nextCursorFrame:(NSTimer*)theTimer
658 NSTimeInterval duration;
662 if (cursorFrame >= [cursorFrames count])
666 frame = [cursorFrames objectAtIndex:cursorFrame];
667 duration = [[frame objectForKey:@"duration"] doubleValue];
668 date = [[theTimer fireDate] dateByAddingTimeInterval:duration];
669 [cursorTimer setFireDate:date];
672 - (void) setCursorWithFrames:(NSArray*)frames
674 if (self.cursorFrames == frames)
677 self.cursorFrames = frames;
679 [cursorTimer invalidate];
680 self.cursorTimer = nil;
684 if ([frames count] > 1)
686 NSDictionary* frame = [frames objectAtIndex:0];
687 NSTimeInterval duration = [[frame objectForKey:@"duration"] doubleValue];
688 NSDate* date = [NSDate dateWithTimeIntervalSinceNow:duration];
689 self.cursorTimer = [[[NSTimer alloc] initWithFireDate:date
692 selector:@selector(nextCursorFrame:)
694 repeats:YES] autorelease];
695 [[NSRunLoop currentRunLoop] addTimer:cursorTimer forMode:NSRunLoopCommonModes];
702 - (void) setApplicationIconFromCGImageArray:(NSArray*)images
704 NSImage* nsimage = nil;
708 NSSize bestSize = NSZeroSize;
711 nsimage = [[[NSImage alloc] initWithSize:NSZeroSize] autorelease];
713 for (image in images)
715 CGImageRef cgimage = (CGImageRef)image;
716 NSBitmapImageRep* imageRep = [[NSBitmapImageRep alloc] initWithCGImage:cgimage];
719 NSSize size = [imageRep size];
721 [nsimage addRepresentation:imageRep];
724 if (MIN(size.width, size.height) > MIN(bestSize.width, bestSize.height))
729 if ([[nsimage representations] count] && bestSize.width && bestSize.height)
730 [nsimage setSize:bestSize];
735 self.applicationIcon = nsimage;
736 [NSApp setApplicationIconImage:nsimage];
739 - (void) handleCommandTab
741 if ([NSApp isActive])
743 NSRunningApplication* thisApp = [NSRunningApplication currentApplication];
744 NSRunningApplication* app;
745 NSRunningApplication* otherValidApp = nil;
747 if ([originalDisplayModes count])
749 CGRestorePermanentDisplayConfiguration();
750 CGReleaseAllDisplays();
751 [originalDisplayModes removeAllObjects];
754 for (app in [[NSWorkspace sharedWorkspace] runningApplications])
756 if (![app isEqual:thisApp] && !app.terminated &&
757 app.activationPolicy == NSApplicationActivationPolicyRegular)
761 // There's another visible app. Just hide ourselves and let
762 // the system activate the other app.
772 // Didn't find a visible GUI app. Try the Finder or, if that's not
773 // running, the first hidden GUI app. If even that doesn't work, we
774 // just fail to switch and remain the active app.
775 app = [[NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.finder"] lastObject];
776 if (!app) app = otherValidApp;
778 [app activateWithOptions:0];
783 * ---------- Cursor clipping methods ----------
785 * Neither Quartz nor Cocoa has an exact analog for Win32 cursor clipping.
786 * For one simple case, clipping to a 1x1 rectangle, Quartz does have an
787 * equivalent: CGAssociateMouseAndMouseCursorPosition(false). For the
788 * general case, we leverage that. We disassociate mouse movements from
789 * the cursor position and then move the cursor manually, keeping it within
790 * the clipping rectangle.
792 * Moving the cursor manually isn't enough. We need to modify the event
793 * stream so that the events have the new location, too. We need to do
794 * this at a point before the events enter Cocoa, so that Cocoa will assign
795 * the correct window to the event. So, we install a Quartz event tap to
798 * Also, there's a complication when we move the cursor. We use
799 * CGWarpMouseCursorPosition(). That doesn't generate mouse movement
800 * events, but the change of cursor position is incorporated into the
801 * deltas of the next mouse move event. When the mouse is disassociated
802 * from the cursor position, we need the deltas to only reflect actual
803 * device movement, not programmatic changes. So, the event tap cancels
804 * out the change caused by our calls to CGWarpMouseCursorPosition().
806 - (void) clipCursorLocation:(CGPoint*)location
808 if (location->x < CGRectGetMinX(cursorClipRect))
809 location->x = CGRectGetMinX(cursorClipRect);
810 if (location->y < CGRectGetMinY(cursorClipRect))
811 location->y = CGRectGetMinY(cursorClipRect);
812 if (location->x > CGRectGetMaxX(cursorClipRect) - 1)
813 location->x = CGRectGetMaxX(cursorClipRect) - 1;
814 if (location->y > CGRectGetMaxY(cursorClipRect) - 1)
815 location->y = CGRectGetMaxY(cursorClipRect) - 1;
818 - (BOOL) warpCursorTo:(CGPoint*)newLocation from:(const CGPoint*)currentLocation
823 oldLocation = *currentLocation;
825 oldLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
827 if (!CGPointEqualToPoint(oldLocation, *newLocation))
829 WarpRecord* warpRecord = [[[WarpRecord alloc] init] autorelease];
832 warpRecord.from = oldLocation;
833 warpRecord.timeBefore = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
835 /* Actually move the cursor. */
836 err = CGWarpMouseCursorPosition(*newLocation);
837 if (err != kCGErrorSuccess)
840 warpRecord.timeAfter = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
841 *newLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
843 if (!CGPointEqualToPoint(oldLocation, *newLocation))
845 warpRecord.to = *newLocation;
846 [warpRecords addObject:warpRecord];
853 - (BOOL) isMouseMoveEventType:(CGEventType)type
857 case kCGEventMouseMoved:
858 case kCGEventLeftMouseDragged:
859 case kCGEventRightMouseDragged:
860 case kCGEventOtherMouseDragged:
867 - (int) warpsFinishedByEventTime:(CGEventTimestamp)eventTime location:(CGPoint)eventLocation
869 int warpsFinished = 0;
870 for (WarpRecord* warpRecord in warpRecords)
872 if (warpRecord.timeAfter < eventTime ||
873 (warpRecord.timeBefore <= eventTime && CGPointEqualToPoint(eventLocation, warpRecord.to)))
879 return warpsFinished;
882 - (CGEventRef) eventTapWithProxy:(CGEventTapProxy)proxy
883 type:(CGEventType)type
884 event:(CGEventRef)event
886 CGEventTimestamp eventTime;
887 CGPoint eventLocation, cursorLocation;
889 if (type == kCGEventTapDisabledByUserInput)
891 if (type == kCGEventTapDisabledByTimeout)
893 CGEventTapEnable(cursorClippingEventTap, TRUE);
900 eventTime = CGEventGetTimestamp(event);
901 lastEventTapEventTime = eventTime / (double)NSEC_PER_SEC;
903 eventLocation = CGEventGetLocation(event);
905 cursorLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
907 if ([self isMouseMoveEventType:type])
909 double deltaX, deltaY;
910 int warpsFinished = [self warpsFinishedByEventTime:eventTime location:eventLocation];
913 deltaX = CGEventGetDoubleValueField(event, kCGMouseEventDeltaX);
914 deltaY = CGEventGetDoubleValueField(event, kCGMouseEventDeltaY);
916 for (i = 0; i < warpsFinished; i++)
918 WarpRecord* warpRecord = [warpRecords objectAtIndex:0];
919 deltaX -= warpRecord.to.x - warpRecord.from.x;
920 deltaY -= warpRecord.to.y - warpRecord.from.y;
921 [warpRecords removeObjectAtIndex:0];
926 CGEventSetDoubleValueField(event, kCGMouseEventDeltaX, deltaX);
927 CGEventSetDoubleValueField(event, kCGMouseEventDeltaY, deltaY);
930 synthesizedLocation.x += deltaX;
931 synthesizedLocation.y += deltaY;
934 // If the event is destined for another process, don't clip it. This may
935 // happen if the user activates Exposé or Mission Control. In that case,
936 // our app does not resign active status, so clipping is still in effect,
937 // but the cursor should not actually be clipped.
939 // In addition, the fact that mouse moves may have been delivered to a
940 // different process means we have to treat the next one we receive as
941 // absolute rather than relative.
942 if (CGEventGetIntegerValueField(event, kCGEventTargetUnixProcessID) == getpid())
943 [self clipCursorLocation:&synthesizedLocation];
945 lastSetCursorPositionTime = lastEventTapEventTime;
947 [self warpCursorTo:&synthesizedLocation from:&cursorLocation];
948 if (!CGPointEqualToPoint(eventLocation, synthesizedLocation))
949 CGEventSetLocation(event, synthesizedLocation);
954 CGEventRef WineAppEventTapCallBack(CGEventTapProxy proxy, CGEventType type,
955 CGEventRef event, void *refcon)
957 WineApplicationController* controller = refcon;
958 return [controller eventTapWithProxy:proxy type:type event:event];
961 - (BOOL) installEventTap
963 ProcessSerialNumber psn;
965 CGEventMask mask = CGEventMaskBit(kCGEventLeftMouseDown) |
966 CGEventMaskBit(kCGEventLeftMouseUp) |
967 CGEventMaskBit(kCGEventRightMouseDown) |
968 CGEventMaskBit(kCGEventRightMouseUp) |
969 CGEventMaskBit(kCGEventMouseMoved) |
970 CGEventMaskBit(kCGEventLeftMouseDragged) |
971 CGEventMaskBit(kCGEventRightMouseDragged) |
972 CGEventMaskBit(kCGEventOtherMouseDown) |
973 CGEventMaskBit(kCGEventOtherMouseUp) |
974 CGEventMaskBit(kCGEventOtherMouseDragged) |
975 CGEventMaskBit(kCGEventScrollWheel);
976 CFRunLoopSourceRef source;
978 OSErr (*pGetCurrentProcess)(ProcessSerialNumber* PSN);
980 if (cursorClippingEventTap)
983 // We need to get the Mac GetCurrentProcess() from the ApplicationServices
984 // framework with dlsym() because the Win32 function of the same name
986 appServices = dlopen("/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices", RTLD_LAZY);
990 pGetCurrentProcess = dlsym(appServices, "GetCurrentProcess");
991 if (!pGetCurrentProcess)
993 dlclose(appServices);
997 err = pGetCurrentProcess(&psn);
998 dlclose(appServices);
1002 // We create an annotated session event tap rather than a process-specific
1003 // event tap because we need to programmatically move the cursor even when
1004 // mouse moves are directed to other processes. We disable our tap when
1005 // other processes are active, but things like Exposé are handled by other
1006 // processes even when we remain active.
1007 cursorClippingEventTap = CGEventTapCreate(kCGAnnotatedSessionEventTap, kCGHeadInsertEventTap,
1008 kCGEventTapOptionDefault, mask, WineAppEventTapCallBack, self);
1009 if (!cursorClippingEventTap)
1012 CGEventTapEnable(cursorClippingEventTap, FALSE);
1014 source = CFMachPortCreateRunLoopSource(NULL, cursorClippingEventTap, 0);
1017 CFRelease(cursorClippingEventTap);
1018 cursorClippingEventTap = NULL;
1022 CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
1027 - (BOOL) setCursorPosition:(CGPoint)pos
1033 [self clipCursorLocation:&pos];
1035 synthesizedLocation = pos;
1036 ret = [self warpCursorTo:&synthesizedLocation from:NULL];
1039 // We want to discard mouse-move events that have already been
1040 // through the event tap, because it's too late to account for
1041 // the setting of the cursor position with them. However, the
1042 // events that may be queued with times after that but before
1043 // the above warp can still be used. So, use the last event
1044 // tap event time so that -sendEvent: doesn't discard them.
1045 lastSetCursorPositionTime = lastEventTapEventTime;
1050 ret = (CGWarpMouseCursorPosition(pos) == kCGErrorSuccess);
1053 lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
1055 // Annoyingly, CGWarpMouseCursorPosition() effectively disassociates
1056 // the mouse from the cursor position for 0.25 seconds. This means
1057 // that mouse movement during that interval doesn't move the cursor
1058 // and events carry a constant location (the warped-to position)
1059 // even though they have delta values. This screws us up because
1060 // the accumulated deltas we send to Wine don't match any eventual
1061 // absolute position we send (like with a button press). We can
1062 // work around this by simply forcibly reassociating the mouse and
1064 CGAssociateMouseAndMouseCursorPosition(true);
1070 WineEventQueue* queue;
1072 // Discard all pending mouse move events.
1073 [eventQueuesLock lock];
1074 for (queue in eventQueues)
1076 [queue discardEventsMatchingMask:event_mask_for_type(MOUSE_MOVED) |
1077 event_mask_for_type(MOUSE_MOVED_ABSOLUTE)
1080 [eventQueuesLock unlock];
1086 - (void) activateCursorClipping
1090 CGEventTapEnable(cursorClippingEventTap, TRUE);
1091 [self setCursorPosition:NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]])];
1095 - (void) deactivateCursorClipping
1099 CGEventTapEnable(cursorClippingEventTap, FALSE);
1100 [warpRecords removeAllObjects];
1101 lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
1105 - (BOOL) startClippingCursor:(CGRect)rect
1109 if (!cursorClippingEventTap && ![self installEventTap])
1112 err = CGAssociateMouseAndMouseCursorPosition(false);
1113 if (err != kCGErrorSuccess)
1116 clippingCursor = TRUE;
1117 cursorClipRect = rect;
1118 if ([NSApp isActive])
1119 [self activateCursorClipping];
1124 - (BOOL) stopClippingCursor
1126 CGError err = CGAssociateMouseAndMouseCursorPosition(true);
1127 if (err != kCGErrorSuccess)
1130 [self deactivateCursorClipping];
1131 clippingCursor = FALSE;
1137 // Returns TRUE if the event was handled and caller should do nothing more
1138 // with it. Returns FALSE if the caller should process it as normal and
1139 // then call -didSendEvent:.
1140 - (BOOL) handleEvent:(NSEvent*)anEvent
1142 if ([anEvent type] == NSFlagsChanged)
1143 self.lastFlagsChanged = anEvent;
1147 - (void) didSendEvent:(NSEvent*)anEvent
1149 NSEventType type = [anEvent type];
1151 if (type == NSMouseMoved || type == NSLeftMouseDragged ||
1152 type == NSRightMouseDragged || type == NSOtherMouseDragged)
1154 WineWindow* targetWindow;
1156 /* Because of the way -[NSWindow setAcceptsMouseMovedEvents:] works, the
1157 event indicates its window is the main window, even if the cursor is
1158 over a different window. Find the actual WineWindow that is under the
1159 cursor and post the event as being for that window. */
1160 if (type == NSMouseMoved)
1162 CGPoint cgpoint = CGEventGetLocation([anEvent CGEvent]);
1163 NSPoint point = [self flippedMouseLocation:NSPointFromCGPoint(cgpoint)];
1164 NSInteger windowUnderNumber;
1166 windowUnderNumber = [NSWindow windowNumberAtPoint:point
1167 belowWindowWithWindowNumber:0];
1168 targetWindow = (WineWindow*)[NSApp windowWithWindowNumber:windowUnderNumber];
1171 targetWindow = (WineWindow*)[anEvent window];
1173 if ([targetWindow isKindOfClass:[WineWindow class]])
1175 BOOL absolute = forceNextMouseMoveAbsolute || (targetWindow != lastTargetWindow);
1176 forceNextMouseMoveAbsolute = FALSE;
1178 // If we recently warped the cursor (other than in our cursor-clipping
1179 // event tap), discard mouse move events until we see an event which is
1180 // later than that time.
1181 if (lastSetCursorPositionTime)
1183 if ([anEvent timestamp] <= lastSetCursorPositionTime)
1186 lastSetCursorPositionTime = 0;
1190 [targetWindow postMouseMovedEvent:anEvent absolute:absolute];
1191 lastTargetWindow = targetWindow;
1193 else if (lastTargetWindow)
1195 [[NSCursor arrowCursor] set];
1196 [self unhideCursor];
1197 lastTargetWindow = nil;
1200 else if (type == NSLeftMouseDown || type == NSLeftMouseUp ||
1201 type == NSRightMouseDown || type == NSRightMouseUp ||
1202 type == NSOtherMouseDown || type == NSOtherMouseUp ||
1203 type == NSScrollWheel)
1205 // Since mouse button and scroll wheel events deliver absolute cursor
1206 // position, the accumulating delta from move events is invalidated.
1207 // Make sure next mouse move event starts over from an absolute baseline.
1208 forceNextMouseMoveAbsolute = TRUE;
1210 else if (type == NSKeyDown && ![anEvent isARepeat] && [anEvent keyCode] == kVK_Tab)
1212 NSUInteger modifiers = [anEvent modifierFlags];
1213 if ((modifiers & NSCommandKeyMask) &&
1214 !(modifiers & (NSControlKeyMask | NSAlternateKeyMask)))
1216 // Command-Tab and Command-Shift-Tab would normally be intercepted
1217 // by the system to switch applications. If we're seeing it, it's
1218 // presumably because we've captured the displays, preventing
1219 // normal application switching. Do it manually.
1220 [self handleCommandTab];
1225 - (void) setupObservations
1227 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
1229 [nc addObserverForName:NSWindowDidBecomeKeyNotification
1232 usingBlock:^(NSNotification *note){
1233 NSWindow* window = [note object];
1234 [keyWindows removeObjectIdenticalTo:window];
1235 [keyWindows insertObject:window atIndex:0];
1238 [nc addObserverForName:NSWindowWillCloseNotification
1240 queue:[NSOperationQueue mainQueue]
1241 usingBlock:^(NSNotification *note){
1242 NSWindow* window = [note object];
1243 [keyWindows removeObjectIdenticalTo:window];
1244 [orderedWineWindows removeObjectIdenticalTo:window];
1245 if (window == lastTargetWindow)
1246 lastTargetWindow = nil;
1249 [nc addObserver:self
1250 selector:@selector(keyboardSelectionDidChange)
1251 name:NSTextInputContextKeyboardSelectionDidChangeNotification
1254 /* The above notification isn't sent unless the NSTextInputContext
1255 class has initialized itself. Poke it. */
1256 [NSTextInputContext self];
1259 - (BOOL) inputSourceIsInputMethod
1261 if (!inputSourceIsInputMethodValid)
1263 TISInputSourceRef inputSource = TISCopyCurrentKeyboardInputSource();
1266 CFStringRef type = TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceType);
1267 inputSourceIsInputMethod = !CFEqual(type, kTISTypeKeyboardLayout);
1268 CFRelease(inputSource);
1271 inputSourceIsInputMethod = FALSE;
1272 inputSourceIsInputMethodValid = TRUE;
1275 return inputSourceIsInputMethod;
1280 * ---------- NSApplicationDelegate methods ----------
1282 - (void)applicationDidBecomeActive:(NSNotification *)notification
1284 [self activateCursorClipping];
1286 [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
1287 WineWindow* window = obj;
1288 if ([window levelWhenActive] != [window level])
1289 [window setLevel:[window levelWhenActive]];
1292 // If a Wine process terminates abruptly while it has the display captured
1293 // and switched to a different resolution, Mac OS X will uncapture the
1294 // displays and switch their resolutions back. However, the other Wine
1295 // processes won't have their notion of the desktop rect changed back.
1296 // This can lead them to refuse to draw or acknowledge clicks in certain
1297 // portions of their windows.
1299 // To solve this, we synthesize a displays-changed event whenever we're
1300 // activated. This will provoke a re-synchronization of Wine's notion of
1301 // the desktop rect with the actual state.
1302 [self sendDisplaysChanged:TRUE];
1304 // The cursor probably moved while we were inactive. Accumulated mouse
1305 // movement deltas are invalidated. Make sure the next mouse move event
1306 // starts over from an absolute baseline.
1307 forceNextMouseMoveAbsolute = TRUE;
1310 - (void)applicationDidChangeScreenParameters:(NSNotification *)notification
1312 primaryScreenHeightValid = FALSE;
1313 [self sendDisplaysChanged:FALSE];
1315 // When the display configuration changes, the cursor position may jump.
1316 // Accumulated mouse movement deltas are invalidated. Make sure the next
1317 // mouse move event starts over from an absolute baseline.
1318 forceNextMouseMoveAbsolute = TRUE;
1321 - (void)applicationDidResignActive:(NSNotification *)notification
1323 macdrv_event* event;
1324 WineEventQueue* queue;
1326 [self invalidateGotFocusEvents];
1328 event = macdrv_create_event(APP_DEACTIVATED, nil);
1330 [eventQueuesLock lock];
1331 for (queue in eventQueues)
1332 [queue postEvent:event];
1333 [eventQueuesLock unlock];
1335 macdrv_release_event(event);
1338 - (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication *)sender
1340 NSApplicationTerminateReply ret = NSTerminateNow;
1341 NSAppleEventManager* m = [NSAppleEventManager sharedAppleEventManager];
1342 NSAppleEventDescriptor* desc = [m currentAppleEvent];
1343 macdrv_event* event;
1344 WineEventQueue* queue;
1346 event = macdrv_create_event(APP_QUIT_REQUESTED, nil);
1348 switch ([[desc attributeDescriptorForKeyword:kAEQuitReason] int32Value])
1351 case kAEReallyLogOut:
1352 event->app_quit_requested.reason = QUIT_REASON_LOGOUT;
1354 case kAEShowRestartDialog:
1355 event->app_quit_requested.reason = QUIT_REASON_RESTART;
1357 case kAEShowShutdownDialog:
1358 event->app_quit_requested.reason = QUIT_REASON_SHUTDOWN;
1361 event->app_quit_requested.reason = QUIT_REASON_NONE;
1365 [eventQueuesLock lock];
1367 if ([eventQueues count])
1369 for (queue in eventQueues)
1370 [queue postEvent:event];
1371 ret = NSTerminateLater;
1374 [eventQueuesLock unlock];
1376 macdrv_release_event(event);
1381 - (void)applicationWillResignActive:(NSNotification *)notification
1383 [self deactivateCursorClipping];
1385 [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
1386 WineWindow* window = obj;
1387 NSInteger level = window.floating ? NSFloatingWindowLevel : NSNormalWindowLevel;
1388 if ([window level] > level)
1389 [window setLevel:level];
1393 /***********************************************************************
1396 * Run-loop-source perform callback. Pull request blocks from the
1397 * array of queued requests and invoke them.
1399 static void PerformRequest(void *info)
1401 WineApplicationController* controller = [WineApplicationController sharedController];
1405 __block dispatch_block_t block;
1407 dispatch_sync(controller->requestsManipQueue, ^{
1408 if ([controller->requests count])
1410 block = (dispatch_block_t)[[controller->requests objectAtIndex:0] retain];
1411 [controller->requests removeObjectAtIndex:0];
1425 /***********************************************************************
1428 * Run a block on the main thread asynchronously.
1430 void OnMainThreadAsync(dispatch_block_t block)
1432 WineApplicationController* controller = [WineApplicationController sharedController];
1434 block = [block copy];
1435 dispatch_sync(controller->requestsManipQueue, ^{
1436 [controller->requests addObject:block];
1439 CFRunLoopSourceSignal(controller->requestSource);
1440 CFRunLoopWakeUp(CFRunLoopGetMain());
1445 /***********************************************************************
1448 void LogError(const char* func, NSString* format, ...)
1451 va_start(args, format);
1452 LogErrorv(func, format, args);
1456 /***********************************************************************
1459 void LogErrorv(const char* func, NSString* format, va_list args)
1461 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
1463 NSString* message = [[NSString alloc] initWithFormat:format arguments:args];
1464 fprintf(stderr, "err:%s:%s", func, [message UTF8String]);
1470 /***********************************************************************
1471 * macdrv_window_rejected_focus
1473 * Pass focus to the next window that hasn't already rejected this same
1474 * WINDOW_GOT_FOCUS event.
1476 void macdrv_window_rejected_focus(const macdrv_event *event)
1479 [[WineApplicationController sharedController] windowRejectedFocusEvent:event];
1483 /***********************************************************************
1484 * macdrv_get_keyboard_layout
1486 * Returns the keyboard layout uchr data.
1488 CFDataRef macdrv_copy_keyboard_layout(CGEventSourceKeyboardType* keyboard_type, int* is_iso)
1490 __block CFDataRef result = NULL;
1493 TISInputSourceRef inputSource;
1495 inputSource = TISCopyCurrentKeyboardLayoutInputSource();
1498 CFDataRef uchr = TISGetInputSourceProperty(inputSource,
1499 kTISPropertyUnicodeKeyLayoutData);
1500 result = CFDataCreateCopy(NULL, uchr);
1501 CFRelease(inputSource);
1503 *keyboard_type = [WineApplicationController sharedController].keyboardType;
1504 *is_iso = (KBGetLayoutType(*keyboard_type) == kKeyboardISO);
1511 /***********************************************************************
1514 * Play the beep sound configured by the user in System Preferences.
1516 void macdrv_beep(void)
1518 OnMainThreadAsync(^{
1523 /***********************************************************************
1524 * macdrv_set_display_mode
1526 int macdrv_set_display_mode(const struct macdrv_display* display,
1527 CGDisplayModeRef display_mode)
1532 ret = [[WineApplicationController sharedController] setMode:display_mode forDisplay:display->displayID];
1538 /***********************************************************************
1543 * If name is non-NULL, it is a selector for a class method on NSCursor
1544 * identifying the cursor to set. In that case, frames is ignored. If
1545 * name is NULL, then frames is used.
1547 * frames is an array of dictionaries. Each dictionary is a frame of
1548 * an animated cursor. Under the key "image" is a CGImage for the
1549 * frame. Under the key "duration" is a CFNumber time interval, in
1550 * seconds, for how long that frame is presented before proceeding to
1551 * the next frame. Under the key "hotSpot" is a CFDictionary encoding a
1552 * CGPoint, to be decoded using CGPointMakeWithDictionaryRepresentation().
1553 * This is the hot spot, measured in pixels down and to the right of the
1554 * top-left corner of the image.
1556 * If the array has exactly 1 element, the cursor is static, not
1557 * animated. If frames is NULL or has 0 elements, the cursor is hidden.
1559 void macdrv_set_cursor(CFStringRef name, CFArrayRef frames)
1563 sel = NSSelectorFromString((NSString*)name);
1566 OnMainThreadAsync(^{
1567 WineApplicationController* controller = [WineApplicationController sharedController];
1568 NSCursor* cursor = [NSCursor performSelector:sel];
1569 [controller setCursorWithFrames:nil];
1571 [controller unhideCursor];
1576 NSArray* nsframes = (NSArray*)frames;
1577 if ([nsframes count])
1579 OnMainThreadAsync(^{
1580 [[WineApplicationController sharedController] setCursorWithFrames:nsframes];
1585 OnMainThreadAsync(^{
1586 WineApplicationController* controller = [WineApplicationController sharedController];
1587 [controller setCursorWithFrames:nil];
1588 [controller hideCursor];
1594 /***********************************************************************
1595 * macdrv_get_cursor_position
1597 * Obtains the current cursor position. Returns zero on failure,
1598 * non-zero on success.
1600 int macdrv_get_cursor_position(CGPoint *pos)
1603 NSPoint location = [NSEvent mouseLocation];
1604 location = [[WineApplicationController sharedController] flippedMouseLocation:location];
1605 *pos = NSPointToCGPoint(location);
1611 /***********************************************************************
1612 * macdrv_set_cursor_position
1614 * Sets the cursor position without generating events. Returns zero on
1615 * failure, non-zero on success.
1617 int macdrv_set_cursor_position(CGPoint pos)
1622 ret = [[WineApplicationController sharedController] setCursorPosition:pos];
1628 /***********************************************************************
1629 * macdrv_clip_cursor
1631 * Sets the cursor cursor clipping rectangle. If the rectangle is equal
1632 * to or larger than the whole desktop region, the cursor is unclipped.
1633 * Returns zero on failure, non-zero on success.
1635 int macdrv_clip_cursor(CGRect rect)
1640 WineApplicationController* controller = [WineApplicationController sharedController];
1641 BOOL clipping = FALSE;
1643 if (!CGRectIsInfinite(rect))
1645 NSRect nsrect = NSRectFromCGRect(rect);
1648 /* Convert the rectangle from top-down coords to bottom-up. */
1649 [controller flipRect:&nsrect];
1652 for (screen in [NSScreen screens])
1654 if (!NSContainsRect(nsrect, [screen frame]))
1663 ret = [controller startClippingCursor:rect];
1665 ret = [controller stopClippingCursor];
1671 /***********************************************************************
1672 * macdrv_set_application_icon
1674 * Set the application icon. The images array contains CGImages. If
1675 * there are more than one, then they represent different sizes or
1676 * color depths from the icon resource. If images is NULL or empty,
1677 * restores the default application image.
1679 void macdrv_set_application_icon(CFArrayRef images)
1681 NSArray* imageArray = (NSArray*)images;
1683 OnMainThreadAsync(^{
1684 [[WineApplicationController sharedController] setApplicationIconFromCGImageArray:imageArray];
1688 /***********************************************************************
1691 void macdrv_quit_reply(int reply)
1694 [NSApp replyToApplicationShouldTerminate:reply];
1698 /***********************************************************************
1699 * macdrv_using_input_method
1701 int macdrv_using_input_method(void)
1706 ret = [[WineApplicationController sharedController] inputSourceIsInputMethod];