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);
1052 lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
1057 WineEventQueue* queue;
1059 // Discard all pending mouse move events.
1060 [eventQueuesLock lock];
1061 for (queue in eventQueues)
1063 [queue discardEventsMatchingMask:event_mask_for_type(MOUSE_MOVED) |
1064 event_mask_for_type(MOUSE_MOVED_ABSOLUTE)
1067 [eventQueuesLock unlock];
1073 - (void) activateCursorClipping
1077 CGEventTapEnable(cursorClippingEventTap, TRUE);
1078 [self setCursorPosition:NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]])];
1082 - (void) deactivateCursorClipping
1086 CGEventTapEnable(cursorClippingEventTap, FALSE);
1087 [warpRecords removeAllObjects];
1088 lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
1092 - (BOOL) startClippingCursor:(CGRect)rect
1096 if (!cursorClippingEventTap && ![self installEventTap])
1099 err = CGAssociateMouseAndMouseCursorPosition(false);
1100 if (err != kCGErrorSuccess)
1103 clippingCursor = TRUE;
1104 cursorClipRect = rect;
1105 if ([NSApp isActive])
1106 [self activateCursorClipping];
1111 - (BOOL) stopClippingCursor
1113 CGError err = CGAssociateMouseAndMouseCursorPosition(true);
1114 if (err != kCGErrorSuccess)
1117 [self deactivateCursorClipping];
1118 clippingCursor = FALSE;
1124 // Returns TRUE if the event was handled and caller should do nothing more
1125 // with it. Returns FALSE if the caller should process it as normal and
1126 // then call -didSendEvent:.
1127 - (BOOL) handleEvent:(NSEvent*)anEvent
1129 if ([anEvent type] == NSFlagsChanged)
1130 self.lastFlagsChanged = anEvent;
1134 - (void) didSendEvent:(NSEvent*)anEvent
1136 NSEventType type = [anEvent type];
1138 if (type == NSMouseMoved || type == NSLeftMouseDragged ||
1139 type == NSRightMouseDragged || type == NSOtherMouseDragged)
1141 WineWindow* targetWindow;
1143 /* Because of the way -[NSWindow setAcceptsMouseMovedEvents:] works, the
1144 event indicates its window is the main window, even if the cursor is
1145 over a different window. Find the actual WineWindow that is under the
1146 cursor and post the event as being for that window. */
1147 if (type == NSMouseMoved)
1149 CGPoint cgpoint = CGEventGetLocation([anEvent CGEvent]);
1150 NSPoint point = [self flippedMouseLocation:NSPointFromCGPoint(cgpoint)];
1151 NSInteger windowUnderNumber;
1153 windowUnderNumber = [NSWindow windowNumberAtPoint:point
1154 belowWindowWithWindowNumber:0];
1155 targetWindow = (WineWindow*)[NSApp windowWithWindowNumber:windowUnderNumber];
1158 targetWindow = (WineWindow*)[anEvent window];
1160 if ([targetWindow isKindOfClass:[WineWindow class]])
1162 BOOL absolute = forceNextMouseMoveAbsolute || (targetWindow != lastTargetWindow);
1163 forceNextMouseMoveAbsolute = FALSE;
1165 // If we recently warped the cursor (other than in our cursor-clipping
1166 // event tap), discard mouse move events until we see an event which is
1167 // later than that time.
1168 if (lastSetCursorPositionTime)
1170 if ([anEvent timestamp] <= lastSetCursorPositionTime)
1173 lastSetCursorPositionTime = 0;
1177 [targetWindow postMouseMovedEvent:anEvent absolute:absolute];
1178 lastTargetWindow = targetWindow;
1180 else if (lastTargetWindow)
1182 [[NSCursor arrowCursor] set];
1183 [self unhideCursor];
1184 lastTargetWindow = nil;
1187 else if (type == NSLeftMouseDown || type == NSLeftMouseUp ||
1188 type == NSRightMouseDown || type == NSRightMouseUp ||
1189 type == NSOtherMouseDown || type == NSOtherMouseUp ||
1190 type == NSScrollWheel)
1192 // Since mouse button and scroll wheel events deliver absolute cursor
1193 // position, the accumulating delta from move events is invalidated.
1194 // Make sure next mouse move event starts over from an absolute baseline.
1195 forceNextMouseMoveAbsolute = TRUE;
1197 else if (type == NSKeyDown && ![anEvent isARepeat] && [anEvent keyCode] == kVK_Tab)
1199 NSUInteger modifiers = [anEvent modifierFlags];
1200 if ((modifiers & NSCommandKeyMask) &&
1201 !(modifiers & (NSControlKeyMask | NSAlternateKeyMask)))
1203 // Command-Tab and Command-Shift-Tab would normally be intercepted
1204 // by the system to switch applications. If we're seeing it, it's
1205 // presumably because we've captured the displays, preventing
1206 // normal application switching. Do it manually.
1207 [self handleCommandTab];
1212 - (void) setupObservations
1214 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
1216 [nc addObserverForName:NSWindowDidBecomeKeyNotification
1219 usingBlock:^(NSNotification *note){
1220 NSWindow* window = [note object];
1221 [keyWindows removeObjectIdenticalTo:window];
1222 [keyWindows insertObject:window atIndex:0];
1225 [nc addObserverForName:NSWindowWillCloseNotification
1227 queue:[NSOperationQueue mainQueue]
1228 usingBlock:^(NSNotification *note){
1229 NSWindow* window = [note object];
1230 [keyWindows removeObjectIdenticalTo:window];
1231 [orderedWineWindows removeObjectIdenticalTo:window];
1232 if (window == lastTargetWindow)
1233 lastTargetWindow = nil;
1236 [nc addObserver:self
1237 selector:@selector(keyboardSelectionDidChange)
1238 name:NSTextInputContextKeyboardSelectionDidChangeNotification
1241 /* The above notification isn't sent unless the NSTextInputContext
1242 class has initialized itself. Poke it. */
1243 [NSTextInputContext self];
1246 - (BOOL) inputSourceIsInputMethod
1248 if (!inputSourceIsInputMethodValid)
1250 TISInputSourceRef inputSource = TISCopyCurrentKeyboardInputSource();
1253 CFStringRef type = TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceType);
1254 inputSourceIsInputMethod = !CFEqual(type, kTISTypeKeyboardLayout);
1255 CFRelease(inputSource);
1258 inputSourceIsInputMethod = FALSE;
1259 inputSourceIsInputMethodValid = TRUE;
1262 return inputSourceIsInputMethod;
1267 * ---------- NSApplicationDelegate methods ----------
1269 - (void)applicationDidBecomeActive:(NSNotification *)notification
1271 [self activateCursorClipping];
1273 [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
1274 WineWindow* window = obj;
1275 if ([window levelWhenActive] != [window level])
1276 [window setLevel:[window levelWhenActive]];
1279 // If a Wine process terminates abruptly while it has the display captured
1280 // and switched to a different resolution, Mac OS X will uncapture the
1281 // displays and switch their resolutions back. However, the other Wine
1282 // processes won't have their notion of the desktop rect changed back.
1283 // This can lead them to refuse to draw or acknowledge clicks in certain
1284 // portions of their windows.
1286 // To solve this, we synthesize a displays-changed event whenever we're
1287 // activated. This will provoke a re-synchronization of Wine's notion of
1288 // the desktop rect with the actual state.
1289 [self sendDisplaysChanged:TRUE];
1291 // The cursor probably moved while we were inactive. Accumulated mouse
1292 // movement deltas are invalidated. Make sure the next mouse move event
1293 // starts over from an absolute baseline.
1294 forceNextMouseMoveAbsolute = TRUE;
1297 - (void)applicationDidChangeScreenParameters:(NSNotification *)notification
1299 primaryScreenHeightValid = FALSE;
1300 [self sendDisplaysChanged:FALSE];
1302 // When the display configuration changes, the cursor position may jump.
1303 // Accumulated mouse movement deltas are invalidated. Make sure the next
1304 // mouse move event starts over from an absolute baseline.
1305 forceNextMouseMoveAbsolute = TRUE;
1308 - (void)applicationDidResignActive:(NSNotification *)notification
1310 macdrv_event* event;
1311 WineEventQueue* queue;
1313 [self invalidateGotFocusEvents];
1315 event = macdrv_create_event(APP_DEACTIVATED, nil);
1317 [eventQueuesLock lock];
1318 for (queue in eventQueues)
1319 [queue postEvent:event];
1320 [eventQueuesLock unlock];
1322 macdrv_release_event(event);
1325 - (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication *)sender
1327 NSApplicationTerminateReply ret = NSTerminateNow;
1328 NSAppleEventManager* m = [NSAppleEventManager sharedAppleEventManager];
1329 NSAppleEventDescriptor* desc = [m currentAppleEvent];
1330 macdrv_event* event;
1331 WineEventQueue* queue;
1333 event = macdrv_create_event(APP_QUIT_REQUESTED, nil);
1335 switch ([[desc attributeDescriptorForKeyword:kAEQuitReason] int32Value])
1338 case kAEReallyLogOut:
1339 event->app_quit_requested.reason = QUIT_REASON_LOGOUT;
1341 case kAEShowRestartDialog:
1342 event->app_quit_requested.reason = QUIT_REASON_RESTART;
1344 case kAEShowShutdownDialog:
1345 event->app_quit_requested.reason = QUIT_REASON_SHUTDOWN;
1348 event->app_quit_requested.reason = QUIT_REASON_NONE;
1352 [eventQueuesLock lock];
1354 if ([eventQueues count])
1356 for (queue in eventQueues)
1357 [queue postEvent:event];
1358 ret = NSTerminateLater;
1361 [eventQueuesLock unlock];
1363 macdrv_release_event(event);
1368 - (void)applicationWillResignActive:(NSNotification *)notification
1370 [self deactivateCursorClipping];
1372 [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
1373 WineWindow* window = obj;
1374 NSInteger level = window.floating ? NSFloatingWindowLevel : NSNormalWindowLevel;
1375 if ([window level] > level)
1376 [window setLevel:level];
1380 /***********************************************************************
1383 * Run-loop-source perform callback. Pull request blocks from the
1384 * array of queued requests and invoke them.
1386 static void PerformRequest(void *info)
1388 WineApplicationController* controller = [WineApplicationController sharedController];
1392 __block dispatch_block_t block;
1394 dispatch_sync(controller->requestsManipQueue, ^{
1395 if ([controller->requests count])
1397 block = (dispatch_block_t)[[controller->requests objectAtIndex:0] retain];
1398 [controller->requests removeObjectAtIndex:0];
1412 /***********************************************************************
1415 * Run a block on the main thread asynchronously.
1417 void OnMainThreadAsync(dispatch_block_t block)
1419 WineApplicationController* controller = [WineApplicationController sharedController];
1421 block = [block copy];
1422 dispatch_sync(controller->requestsManipQueue, ^{
1423 [controller->requests addObject:block];
1426 CFRunLoopSourceSignal(controller->requestSource);
1427 CFRunLoopWakeUp(CFRunLoopGetMain());
1432 /***********************************************************************
1435 void LogError(const char* func, NSString* format, ...)
1438 va_start(args, format);
1439 LogErrorv(func, format, args);
1443 /***********************************************************************
1446 void LogErrorv(const char* func, NSString* format, va_list args)
1448 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
1450 NSString* message = [[NSString alloc] initWithFormat:format arguments:args];
1451 fprintf(stderr, "err:%s:%s", func, [message UTF8String]);
1457 /***********************************************************************
1458 * macdrv_window_rejected_focus
1460 * Pass focus to the next window that hasn't already rejected this same
1461 * WINDOW_GOT_FOCUS event.
1463 void macdrv_window_rejected_focus(const macdrv_event *event)
1466 [[WineApplicationController sharedController] windowRejectedFocusEvent:event];
1470 /***********************************************************************
1471 * macdrv_get_keyboard_layout
1473 * Returns the keyboard layout uchr data.
1475 CFDataRef macdrv_copy_keyboard_layout(CGEventSourceKeyboardType* keyboard_type, int* is_iso)
1477 __block CFDataRef result = NULL;
1480 TISInputSourceRef inputSource;
1482 inputSource = TISCopyCurrentKeyboardLayoutInputSource();
1485 CFDataRef uchr = TISGetInputSourceProperty(inputSource,
1486 kTISPropertyUnicodeKeyLayoutData);
1487 result = CFDataCreateCopy(NULL, uchr);
1488 CFRelease(inputSource);
1490 *keyboard_type = [WineApplicationController sharedController].keyboardType;
1491 *is_iso = (KBGetLayoutType(*keyboard_type) == kKeyboardISO);
1498 /***********************************************************************
1501 * Play the beep sound configured by the user in System Preferences.
1503 void macdrv_beep(void)
1505 OnMainThreadAsync(^{
1510 /***********************************************************************
1511 * macdrv_set_display_mode
1513 int macdrv_set_display_mode(const struct macdrv_display* display,
1514 CGDisplayModeRef display_mode)
1519 ret = [[WineApplicationController sharedController] setMode:display_mode forDisplay:display->displayID];
1525 /***********************************************************************
1530 * If name is non-NULL, it is a selector for a class method on NSCursor
1531 * identifying the cursor to set. In that case, frames is ignored. If
1532 * name is NULL, then frames is used.
1534 * frames is an array of dictionaries. Each dictionary is a frame of
1535 * an animated cursor. Under the key "image" is a CGImage for the
1536 * frame. Under the key "duration" is a CFNumber time interval, in
1537 * seconds, for how long that frame is presented before proceeding to
1538 * the next frame. Under the key "hotSpot" is a CFDictionary encoding a
1539 * CGPoint, to be decoded using CGPointMakeWithDictionaryRepresentation().
1540 * This is the hot spot, measured in pixels down and to the right of the
1541 * top-left corner of the image.
1543 * If the array has exactly 1 element, the cursor is static, not
1544 * animated. If frames is NULL or has 0 elements, the cursor is hidden.
1546 void macdrv_set_cursor(CFStringRef name, CFArrayRef frames)
1550 sel = NSSelectorFromString((NSString*)name);
1553 OnMainThreadAsync(^{
1554 WineApplicationController* controller = [WineApplicationController sharedController];
1555 NSCursor* cursor = [NSCursor performSelector:sel];
1556 [controller setCursorWithFrames:nil];
1558 [controller unhideCursor];
1563 NSArray* nsframes = (NSArray*)frames;
1564 if ([nsframes count])
1566 OnMainThreadAsync(^{
1567 [[WineApplicationController sharedController] setCursorWithFrames:nsframes];
1572 OnMainThreadAsync(^{
1573 WineApplicationController* controller = [WineApplicationController sharedController];
1574 [controller setCursorWithFrames:nil];
1575 [controller hideCursor];
1581 /***********************************************************************
1582 * macdrv_get_cursor_position
1584 * Obtains the current cursor position. Returns zero on failure,
1585 * non-zero on success.
1587 int macdrv_get_cursor_position(CGPoint *pos)
1590 NSPoint location = [NSEvent mouseLocation];
1591 location = [[WineApplicationController sharedController] flippedMouseLocation:location];
1592 *pos = NSPointToCGPoint(location);
1598 /***********************************************************************
1599 * macdrv_set_cursor_position
1601 * Sets the cursor position without generating events. Returns zero on
1602 * failure, non-zero on success.
1604 int macdrv_set_cursor_position(CGPoint pos)
1609 ret = [[WineApplicationController sharedController] setCursorPosition:pos];
1615 /***********************************************************************
1616 * macdrv_clip_cursor
1618 * Sets the cursor cursor clipping rectangle. If the rectangle is equal
1619 * to or larger than the whole desktop region, the cursor is unclipped.
1620 * Returns zero on failure, non-zero on success.
1622 int macdrv_clip_cursor(CGRect rect)
1627 WineApplicationController* controller = [WineApplicationController sharedController];
1628 BOOL clipping = FALSE;
1630 if (!CGRectIsInfinite(rect))
1632 NSRect nsrect = NSRectFromCGRect(rect);
1635 /* Convert the rectangle from top-down coords to bottom-up. */
1636 [controller flipRect:&nsrect];
1639 for (screen in [NSScreen screens])
1641 if (!NSContainsRect(nsrect, [screen frame]))
1650 ret = [controller startClippingCursor:rect];
1652 ret = [controller stopClippingCursor];
1658 /***********************************************************************
1659 * macdrv_set_application_icon
1661 * Set the application icon. The images array contains CGImages. If
1662 * there are more than one, then they represent different sizes or
1663 * color depths from the icon resource. If images is NULL or empty,
1664 * restores the default application image.
1666 void macdrv_set_application_icon(CFArrayRef images)
1668 NSArray* imageArray = (NSArray*)images;
1670 OnMainThreadAsync(^{
1671 [[WineApplicationController sharedController] setApplicationIconFromCGImageArray:imageArray];
1675 /***********************************************************************
1678 void macdrv_quit_reply(int reply)
1681 [NSApp replyToApplicationShouldTerminate:reply];
1685 /***********************************************************************
1686 * macdrv_using_input_method
1688 int macdrv_using_input_method(void)
1693 ret = [[WineApplicationController sharedController] inputSourceIsInputMethod];