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 ret = [self warpCursorTo:&pos from:NULL];
1036 synthesizedLocation = pos;
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)
1079 [queue resetMouseEventPositions:pos];
1081 [eventQueuesLock unlock];
1087 - (void) activateCursorClipping
1091 CGEventTapEnable(cursorClippingEventTap, TRUE);
1092 [self setCursorPosition:NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]])];
1096 - (void) deactivateCursorClipping
1100 CGEventTapEnable(cursorClippingEventTap, FALSE);
1101 [warpRecords removeAllObjects];
1102 lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
1106 - (BOOL) startClippingCursor:(CGRect)rect
1110 if (!cursorClippingEventTap && ![self installEventTap])
1113 err = CGAssociateMouseAndMouseCursorPosition(false);
1114 if (err != kCGErrorSuccess)
1117 clippingCursor = TRUE;
1118 cursorClipRect = rect;
1119 if ([NSApp isActive])
1120 [self activateCursorClipping];
1125 - (BOOL) stopClippingCursor
1127 CGError err = CGAssociateMouseAndMouseCursorPosition(true);
1128 if (err != kCGErrorSuccess)
1131 [self deactivateCursorClipping];
1132 clippingCursor = FALSE;
1138 // Returns TRUE if the event was handled and caller should do nothing more
1139 // with it. Returns FALSE if the caller should process it as normal and
1140 // then call -didSendEvent:.
1141 - (BOOL) handleEvent:(NSEvent*)anEvent
1143 if ([anEvent type] == NSFlagsChanged)
1144 self.lastFlagsChanged = anEvent;
1148 - (void) didSendEvent:(NSEvent*)anEvent
1150 NSEventType type = [anEvent type];
1152 if (type == NSMouseMoved || type == NSLeftMouseDragged ||
1153 type == NSRightMouseDragged || type == NSOtherMouseDragged)
1155 WineWindow* targetWindow;
1157 /* Because of the way -[NSWindow setAcceptsMouseMovedEvents:] works, the
1158 event indicates its window is the main window, even if the cursor is
1159 over a different window. Find the actual WineWindow that is under the
1160 cursor and post the event as being for that window. */
1161 if (type == NSMouseMoved)
1163 CGPoint cgpoint = CGEventGetLocation([anEvent CGEvent]);
1164 NSPoint point = [self flippedMouseLocation:NSPointFromCGPoint(cgpoint)];
1165 NSInteger windowUnderNumber;
1167 windowUnderNumber = [NSWindow windowNumberAtPoint:point
1168 belowWindowWithWindowNumber:0];
1169 targetWindow = (WineWindow*)[NSApp windowWithWindowNumber:windowUnderNumber];
1172 targetWindow = (WineWindow*)[anEvent window];
1174 if ([targetWindow isKindOfClass:[WineWindow class]])
1176 BOOL absolute = forceNextMouseMoveAbsolute || (targetWindow != lastTargetWindow);
1177 forceNextMouseMoveAbsolute = FALSE;
1179 // If we recently warped the cursor (other than in our cursor-clipping
1180 // event tap), discard mouse move events until we see an event which is
1181 // later than that time.
1182 if (lastSetCursorPositionTime)
1184 if ([anEvent timestamp] <= lastSetCursorPositionTime)
1187 lastSetCursorPositionTime = 0;
1191 [targetWindow postMouseMovedEvent:anEvent absolute:absolute];
1192 lastTargetWindow = targetWindow;
1194 else if (lastTargetWindow)
1196 [[NSCursor arrowCursor] set];
1197 [self unhideCursor];
1198 lastTargetWindow = nil;
1201 else if (type == NSLeftMouseDown || type == NSLeftMouseUp ||
1202 type == NSRightMouseDown || type == NSRightMouseUp ||
1203 type == NSOtherMouseDown || type == NSOtherMouseUp ||
1204 type == NSScrollWheel)
1206 // Since mouse button and scroll wheel events deliver absolute cursor
1207 // position, the accumulating delta from move events is invalidated.
1208 // Make sure next mouse move event starts over from an absolute baseline.
1209 forceNextMouseMoveAbsolute = TRUE;
1211 else if (type == NSKeyDown && ![anEvent isARepeat] && [anEvent keyCode] == kVK_Tab)
1213 NSUInteger modifiers = [anEvent modifierFlags];
1214 if ((modifiers & NSCommandKeyMask) &&
1215 !(modifiers & (NSControlKeyMask | NSAlternateKeyMask)))
1217 // Command-Tab and Command-Shift-Tab would normally be intercepted
1218 // by the system to switch applications. If we're seeing it, it's
1219 // presumably because we've captured the displays, preventing
1220 // normal application switching. Do it manually.
1221 [self handleCommandTab];
1226 - (void) setupObservations
1228 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
1230 [nc addObserverForName:NSWindowDidBecomeKeyNotification
1233 usingBlock:^(NSNotification *note){
1234 NSWindow* window = [note object];
1235 [keyWindows removeObjectIdenticalTo:window];
1236 [keyWindows insertObject:window atIndex:0];
1239 [nc addObserverForName:NSWindowWillCloseNotification
1241 queue:[NSOperationQueue mainQueue]
1242 usingBlock:^(NSNotification *note){
1243 NSWindow* window = [note object];
1244 [keyWindows removeObjectIdenticalTo:window];
1245 [orderedWineWindows removeObjectIdenticalTo:window];
1246 if (window == lastTargetWindow)
1247 lastTargetWindow = nil;
1250 [nc addObserver:self
1251 selector:@selector(keyboardSelectionDidChange)
1252 name:NSTextInputContextKeyboardSelectionDidChangeNotification
1255 /* The above notification isn't sent unless the NSTextInputContext
1256 class has initialized itself. Poke it. */
1257 [NSTextInputContext self];
1260 - (BOOL) inputSourceIsInputMethod
1262 if (!inputSourceIsInputMethodValid)
1264 TISInputSourceRef inputSource = TISCopyCurrentKeyboardInputSource();
1267 CFStringRef type = TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceType);
1268 inputSourceIsInputMethod = !CFEqual(type, kTISTypeKeyboardLayout);
1269 CFRelease(inputSource);
1272 inputSourceIsInputMethod = FALSE;
1273 inputSourceIsInputMethodValid = TRUE;
1276 return inputSourceIsInputMethod;
1281 * ---------- NSApplicationDelegate methods ----------
1283 - (void)applicationDidBecomeActive:(NSNotification *)notification
1285 [self activateCursorClipping];
1287 [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
1288 WineWindow* window = obj;
1289 if ([window levelWhenActive] != [window level])
1290 [window setLevel:[window levelWhenActive]];
1293 // If a Wine process terminates abruptly while it has the display captured
1294 // and switched to a different resolution, Mac OS X will uncapture the
1295 // displays and switch their resolutions back. However, the other Wine
1296 // processes won't have their notion of the desktop rect changed back.
1297 // This can lead them to refuse to draw or acknowledge clicks in certain
1298 // portions of their windows.
1300 // To solve this, we synthesize a displays-changed event whenever we're
1301 // activated. This will provoke a re-synchronization of Wine's notion of
1302 // the desktop rect with the actual state.
1303 [self sendDisplaysChanged:TRUE];
1305 // The cursor probably moved while we were inactive. Accumulated mouse
1306 // movement deltas are invalidated. Make sure the next mouse move event
1307 // starts over from an absolute baseline.
1308 forceNextMouseMoveAbsolute = TRUE;
1311 - (void)applicationDidChangeScreenParameters:(NSNotification *)notification
1313 primaryScreenHeightValid = FALSE;
1314 [self sendDisplaysChanged:FALSE];
1316 // When the display configuration changes, the cursor position may jump.
1317 // Accumulated mouse movement deltas are invalidated. Make sure the next
1318 // mouse move event starts over from an absolute baseline.
1319 forceNextMouseMoveAbsolute = TRUE;
1322 - (void)applicationDidResignActive:(NSNotification *)notification
1324 macdrv_event* event;
1325 WineEventQueue* queue;
1327 [self invalidateGotFocusEvents];
1329 event = macdrv_create_event(APP_DEACTIVATED, nil);
1331 [eventQueuesLock lock];
1332 for (queue in eventQueues)
1333 [queue postEvent:event];
1334 [eventQueuesLock unlock];
1336 macdrv_release_event(event);
1339 - (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication *)sender
1341 NSApplicationTerminateReply ret = NSTerminateNow;
1342 NSAppleEventManager* m = [NSAppleEventManager sharedAppleEventManager];
1343 NSAppleEventDescriptor* desc = [m currentAppleEvent];
1344 macdrv_event* event;
1345 WineEventQueue* queue;
1347 event = macdrv_create_event(APP_QUIT_REQUESTED, nil);
1349 switch ([[desc attributeDescriptorForKeyword:kAEQuitReason] int32Value])
1352 case kAEReallyLogOut:
1353 event->app_quit_requested.reason = QUIT_REASON_LOGOUT;
1355 case kAEShowRestartDialog:
1356 event->app_quit_requested.reason = QUIT_REASON_RESTART;
1358 case kAEShowShutdownDialog:
1359 event->app_quit_requested.reason = QUIT_REASON_SHUTDOWN;
1362 event->app_quit_requested.reason = QUIT_REASON_NONE;
1366 [eventQueuesLock lock];
1368 if ([eventQueues count])
1370 for (queue in eventQueues)
1371 [queue postEvent:event];
1372 ret = NSTerminateLater;
1375 [eventQueuesLock unlock];
1377 macdrv_release_event(event);
1382 - (void)applicationWillResignActive:(NSNotification *)notification
1384 [self deactivateCursorClipping];
1386 [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
1387 WineWindow* window = obj;
1388 NSInteger level = window.floating ? NSFloatingWindowLevel : NSNormalWindowLevel;
1389 if ([window level] > level)
1390 [window setLevel:level];
1394 /***********************************************************************
1397 * Run-loop-source perform callback. Pull request blocks from the
1398 * array of queued requests and invoke them.
1400 static void PerformRequest(void *info)
1402 WineApplicationController* controller = [WineApplicationController sharedController];
1406 __block dispatch_block_t block;
1408 dispatch_sync(controller->requestsManipQueue, ^{
1409 if ([controller->requests count])
1411 block = (dispatch_block_t)[[controller->requests objectAtIndex:0] retain];
1412 [controller->requests removeObjectAtIndex:0];
1426 /***********************************************************************
1429 * Run a block on the main thread asynchronously.
1431 void OnMainThreadAsync(dispatch_block_t block)
1433 WineApplicationController* controller = [WineApplicationController sharedController];
1435 block = [block copy];
1436 dispatch_sync(controller->requestsManipQueue, ^{
1437 [controller->requests addObject:block];
1440 CFRunLoopSourceSignal(controller->requestSource);
1441 CFRunLoopWakeUp(CFRunLoopGetMain());
1446 /***********************************************************************
1449 void LogError(const char* func, NSString* format, ...)
1452 va_start(args, format);
1453 LogErrorv(func, format, args);
1457 /***********************************************************************
1460 void LogErrorv(const char* func, NSString* format, va_list args)
1462 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
1464 NSString* message = [[NSString alloc] initWithFormat:format arguments:args];
1465 fprintf(stderr, "err:%s:%s", func, [message UTF8String]);
1471 /***********************************************************************
1472 * macdrv_window_rejected_focus
1474 * Pass focus to the next window that hasn't already rejected this same
1475 * WINDOW_GOT_FOCUS event.
1477 void macdrv_window_rejected_focus(const macdrv_event *event)
1480 [[WineApplicationController sharedController] windowRejectedFocusEvent:event];
1484 /***********************************************************************
1485 * macdrv_get_keyboard_layout
1487 * Returns the keyboard layout uchr data.
1489 CFDataRef macdrv_copy_keyboard_layout(CGEventSourceKeyboardType* keyboard_type, int* is_iso)
1491 __block CFDataRef result = NULL;
1494 TISInputSourceRef inputSource;
1496 inputSource = TISCopyCurrentKeyboardLayoutInputSource();
1499 CFDataRef uchr = TISGetInputSourceProperty(inputSource,
1500 kTISPropertyUnicodeKeyLayoutData);
1501 result = CFDataCreateCopy(NULL, uchr);
1502 CFRelease(inputSource);
1504 *keyboard_type = [WineApplicationController sharedController].keyboardType;
1505 *is_iso = (KBGetLayoutType(*keyboard_type) == kKeyboardISO);
1512 /***********************************************************************
1515 * Play the beep sound configured by the user in System Preferences.
1517 void macdrv_beep(void)
1519 OnMainThreadAsync(^{
1524 /***********************************************************************
1525 * macdrv_set_display_mode
1527 int macdrv_set_display_mode(const struct macdrv_display* display,
1528 CGDisplayModeRef display_mode)
1533 ret = [[WineApplicationController sharedController] setMode:display_mode forDisplay:display->displayID];
1539 /***********************************************************************
1544 * If name is non-NULL, it is a selector for a class method on NSCursor
1545 * identifying the cursor to set. In that case, frames is ignored. If
1546 * name is NULL, then frames is used.
1548 * frames is an array of dictionaries. Each dictionary is a frame of
1549 * an animated cursor. Under the key "image" is a CGImage for the
1550 * frame. Under the key "duration" is a CFNumber time interval, in
1551 * seconds, for how long that frame is presented before proceeding to
1552 * the next frame. Under the key "hotSpot" is a CFDictionary encoding a
1553 * CGPoint, to be decoded using CGPointMakeWithDictionaryRepresentation().
1554 * This is the hot spot, measured in pixels down and to the right of the
1555 * top-left corner of the image.
1557 * If the array has exactly 1 element, the cursor is static, not
1558 * animated. If frames is NULL or has 0 elements, the cursor is hidden.
1560 void macdrv_set_cursor(CFStringRef name, CFArrayRef frames)
1564 sel = NSSelectorFromString((NSString*)name);
1567 OnMainThreadAsync(^{
1568 WineApplicationController* controller = [WineApplicationController sharedController];
1569 NSCursor* cursor = [NSCursor performSelector:sel];
1570 [controller setCursorWithFrames:nil];
1572 [controller unhideCursor];
1577 NSArray* nsframes = (NSArray*)frames;
1578 if ([nsframes count])
1580 OnMainThreadAsync(^{
1581 [[WineApplicationController sharedController] setCursorWithFrames:nsframes];
1586 OnMainThreadAsync(^{
1587 WineApplicationController* controller = [WineApplicationController sharedController];
1588 [controller setCursorWithFrames:nil];
1589 [controller hideCursor];
1595 /***********************************************************************
1596 * macdrv_get_cursor_position
1598 * Obtains the current cursor position. Returns zero on failure,
1599 * non-zero on success.
1601 int macdrv_get_cursor_position(CGPoint *pos)
1604 NSPoint location = [NSEvent mouseLocation];
1605 location = [[WineApplicationController sharedController] flippedMouseLocation:location];
1606 *pos = NSPointToCGPoint(location);
1612 /***********************************************************************
1613 * macdrv_set_cursor_position
1615 * Sets the cursor position without generating events. Returns zero on
1616 * failure, non-zero on success.
1618 int macdrv_set_cursor_position(CGPoint pos)
1623 ret = [[WineApplicationController sharedController] setCursorPosition:pos];
1629 /***********************************************************************
1630 * macdrv_clip_cursor
1632 * Sets the cursor cursor clipping rectangle. If the rectangle is equal
1633 * to or larger than the whole desktop region, the cursor is unclipped.
1634 * Returns zero on failure, non-zero on success.
1636 int macdrv_clip_cursor(CGRect rect)
1641 WineApplicationController* controller = [WineApplicationController sharedController];
1642 BOOL clipping = FALSE;
1644 if (!CGRectIsInfinite(rect))
1646 NSRect nsrect = NSRectFromCGRect(rect);
1649 /* Convert the rectangle from top-down coords to bottom-up. */
1650 [controller flipRect:&nsrect];
1653 for (screen in [NSScreen screens])
1655 if (!NSContainsRect(nsrect, [screen frame]))
1664 ret = [controller startClippingCursor:rect];
1666 ret = [controller stopClippingCursor];
1672 /***********************************************************************
1673 * macdrv_set_application_icon
1675 * Set the application icon. The images array contains CGImages. If
1676 * there are more than one, then they represent different sizes or
1677 * color depths from the icon resource. If images is NULL or empty,
1678 * restores the default application image.
1680 void macdrv_set_application_icon(CFArrayRef images)
1682 NSArray* imageArray = (NSArray*)images;
1684 OnMainThreadAsync(^{
1685 [[WineApplicationController sharedController] setApplicationIconFromCGImageArray:imageArray];
1689 /***********************************************************************
1692 void macdrv_quit_reply(int reply)
1695 [NSApp replyToApplicationShouldTerminate:reply];
1699 /***********************************************************************
1700 * macdrv_using_input_method
1702 int macdrv_using_input_method(void)
1707 ret = [[WineApplicationController sharedController] inputSourceIsInputMethod];