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;
85 - (void) setupObservations;
86 - (void) applicationDidBecomeActive:(NSNotification *)notification;
88 static void PerformRequest(void *info);
93 @implementation WineApplicationController
95 @synthesize keyboardType, lastFlagsChanged;
96 @synthesize orderedWineWindows, applicationIcon;
97 @synthesize cursorFrames, cursorTimer;
99 + (WineApplicationController*) sharedController
101 static WineApplicationController* sharedController;
102 static dispatch_once_t once;
104 dispatch_once(&once, ^{
105 sharedController = [[self alloc] init];
108 return sharedController;
116 CFRunLoopSourceContext context = { 0 };
117 context.perform = PerformRequest;
118 requestSource = CFRunLoopSourceCreate(NULL, 0, &context);
124 CFRunLoopAddSource(CFRunLoopGetMain(), requestSource, kCFRunLoopCommonModes);
125 CFRunLoopAddSource(CFRunLoopGetMain(), requestSource, (CFStringRef)WineAppWaitQueryResponseMode);
127 requests = [[NSMutableArray alloc] init];
128 requestsManipQueue = dispatch_queue_create("org.winehq.WineAppRequestManipQueue", NULL);
130 eventQueues = [[NSMutableArray alloc] init];
131 eventQueuesLock = [[NSLock alloc] init];
133 keyWindows = [[NSMutableArray alloc] init];
134 orderedWineWindows = [[NSMutableArray alloc] init];
136 originalDisplayModes = [[NSMutableDictionary alloc] init];
138 warpRecords = [[NSMutableArray alloc] init];
140 if (!requests || !requestsManipQueue || !eventQueues || !eventQueuesLock ||
141 !keyWindows || !orderedWineWindows || !originalDisplayModes || !warpRecords)
147 [self setupObservations];
149 keyboardType = LMGetKbdType();
151 if ([NSApp isActive])
152 [self applicationDidBecomeActive:nil];
159 [applicationIcon release];
160 [warpRecords release];
161 [cursorTimer release];
162 [cursorFrames release];
163 [originalDisplayModes release];
164 [orderedWineWindows release];
165 [keyWindows release];
166 [eventQueues release];
167 [eventQueuesLock release];
168 if (requestsManipQueue) dispatch_release(requestsManipQueue);
172 CFRunLoopSourceInvalidate(requestSource);
173 CFRelease(requestSource);
178 - (void) transformProcessToForeground
180 if ([NSApp activationPolicy] != NSApplicationActivationPolicyRegular)
184 NSString* bundleName;
188 [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
189 [NSApp activateIgnoringOtherApps:YES];
191 mainMenu = [[[NSMenu alloc] init] autorelease];
193 submenu = [[[NSMenu alloc] initWithTitle:@"Wine"] autorelease];
194 bundleName = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString*)kCFBundleNameKey];
195 if ([bundleName length])
196 title = [NSString stringWithFormat:@"Quit %@", bundleName];
199 item = [submenu addItemWithTitle:title action:@selector(terminate:) keyEquivalent:@"q"];
200 [item setKeyEquivalentModifierMask:NSCommandKeyMask | NSAlternateKeyMask];
201 item = [[[NSMenuItem alloc] init] autorelease];
202 [item setTitle:@"Wine"];
203 [item setSubmenu:submenu];
204 [mainMenu addItem:item];
206 submenu = [[[NSMenu alloc] initWithTitle:@"Window"] autorelease];
207 [submenu addItemWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@""];
208 [submenu addItemWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""];
209 [submenu addItem:[NSMenuItem separatorItem]];
210 [submenu addItemWithTitle:@"Bring All to Front" action:@selector(arrangeInFront:) keyEquivalent:@""];
211 item = [[[NSMenuItem alloc] init] autorelease];
212 [item setTitle:@"Window"];
213 [item setSubmenu:submenu];
214 [mainMenu addItem:item];
216 [NSApp setMainMenu:mainMenu];
217 [NSApp setWindowsMenu:submenu];
219 [NSApp setApplicationIconImage:self.applicationIcon];
223 - (BOOL) waitUntilQueryDone:(int*)done timeout:(NSDate*)timeout processEvents:(BOOL)processEvents
225 PerformRequest(NULL);
231 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
232 NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask
234 inMode:NSDefaultRunLoopMode
237 [NSApp sendEvent:event];
241 [[NSRunLoop currentRunLoop] runMode:WineAppWaitQueryResponseMode beforeDate:timeout];
242 } while (!*done && [timeout timeIntervalSinceNow] >= 0);
247 - (BOOL) registerEventQueue:(WineEventQueue*)queue
249 [eventQueuesLock lock];
250 [eventQueues addObject:queue];
251 [eventQueuesLock unlock];
255 - (void) unregisterEventQueue:(WineEventQueue*)queue
257 [eventQueuesLock lock];
258 [eventQueues removeObjectIdenticalTo:queue];
259 [eventQueuesLock unlock];
262 - (void) computeEventTimeAdjustmentFromTicks:(unsigned long long)tickcount uptime:(uint64_t)uptime_ns
264 eventTimeAdjustment = (tickcount / 1000.0) - (uptime_ns / (double)NSEC_PER_SEC);
267 - (double) ticksForEventTime:(NSTimeInterval)eventTime
269 return (eventTime + eventTimeAdjustment) * 1000;
272 /* Invalidate old focus offers across all queues. */
273 - (void) invalidateGotFocusEvents
275 WineEventQueue* queue;
279 [eventQueuesLock lock];
280 for (queue in eventQueues)
282 [queue discardEventsMatchingMask:event_mask_for_type(WINDOW_GOT_FOCUS)
285 [eventQueuesLock unlock];
288 - (void) windowGotFocus:(WineWindow*)window
292 [self invalidateGotFocusEvents];
294 event = macdrv_create_event(WINDOW_GOT_FOCUS, window);
295 event->window_got_focus.serial = windowFocusSerial;
297 event->window_got_focus.tried_windows = [triedWindows retain];
299 event->window_got_focus.tried_windows = [[NSMutableSet alloc] init];
300 [window.queue postEvent:event];
301 macdrv_release_event(event);
304 - (void) windowRejectedFocusEvent:(const macdrv_event*)event
306 if (event->window_got_focus.serial == windowFocusSerial)
308 triedWindows = (NSMutableSet*)event->window_got_focus.tried_windows;
309 [triedWindows addObject:(WineWindow*)event->window];
310 for (NSWindow* window in [keyWindows arrayByAddingObjectsFromArray:[self orderedWineWindows]])
312 if (![triedWindows containsObject:window] && [window canBecomeKeyWindow])
314 [window makeKeyWindow];
322 - (void) keyboardSelectionDidChange
324 TISInputSourceRef inputSource;
326 inputSource = TISCopyCurrentKeyboardLayoutInputSource();
330 uchr = TISGetInputSourceProperty(inputSource,
331 kTISPropertyUnicodeKeyLayoutData);
335 WineEventQueue* queue;
337 event = macdrv_create_event(KEYBOARD_CHANGED, nil);
338 event->keyboard_changed.keyboard_type = self.keyboardType;
339 event->keyboard_changed.iso_keyboard = (KBGetLayoutType(self.keyboardType) == kKeyboardISO);
340 event->keyboard_changed.uchr = CFDataCreateCopy(NULL, uchr);
342 if (event->keyboard_changed.uchr)
344 [eventQueuesLock lock];
346 for (queue in eventQueues)
347 [queue postEvent:event];
349 [eventQueuesLock unlock];
352 macdrv_release_event(event);
355 CFRelease(inputSource);
359 - (CGFloat) primaryScreenHeight
361 if (!primaryScreenHeightValid)
363 NSArray* screens = [NSScreen screens];
366 primaryScreenHeight = NSHeight([[screens objectAtIndex:0] frame]);
367 primaryScreenHeightValid = TRUE;
370 return 1280; /* arbitrary value */
373 return primaryScreenHeight;
376 - (NSPoint) flippedMouseLocation:(NSPoint)point
378 /* This relies on the fact that Cocoa's mouse location points are
379 actually off by one (precisely because they were flipped from
380 Quartz screen coordinates using this same technique). */
381 point.y = [self primaryScreenHeight] - point.y;
385 - (void) flipRect:(NSRect*)rect
387 // We don't use -primaryScreenHeight here so there's no chance of having
388 // out-of-date cached info. This method is called infrequently enough
389 // that getting the screen height each time is not prohibitively expensive.
390 rect->origin.y = NSMaxY([[[NSScreen screens] objectAtIndex:0] frame]) - NSMaxY(*rect);
393 - (void) wineWindow:(WineWindow*)window
394 ordered:(NSWindowOrderingMode)order
395 relativeTo:(WineWindow*)otherWindow
403 [orderedWineWindows removeObjectIdenticalTo:window];
406 index = [orderedWineWindows indexOfObjectIdenticalTo:otherWindow];
407 if (index == NSNotFound)
413 for (otherWindow in orderedWineWindows)
415 if ([otherWindow levelWhenActive] <= [window levelWhenActive])
420 [orderedWineWindows insertObject:window atIndex:index];
425 [orderedWineWindows removeObjectIdenticalTo:window];
428 index = [orderedWineWindows indexOfObjectIdenticalTo:otherWindow];
429 if (index == NSNotFound)
430 index = [orderedWineWindows count];
435 for (otherWindow in orderedWineWindows)
437 if ([otherWindow levelWhenActive] < [window levelWhenActive])
442 [orderedWineWindows insertObject:window atIndex:index];
451 - (void) sendDisplaysChanged:(BOOL)activating
454 WineEventQueue* queue;
456 event = macdrv_create_event(DISPLAYS_CHANGED, nil);
457 event->displays_changed.activating = activating;
459 [eventQueuesLock lock];
461 // If we're activating, then we just need one of our threads to get the
462 // event, so it can send it directly to the desktop window. Otherwise,
463 // we need all of the threads to get it because we don't know which owns
464 // the desktop window and only that one will do anything with it.
465 if (activating) event->deliver = 1;
467 for (queue in eventQueues)
468 [queue postEvent:event];
469 [eventQueuesLock unlock];
471 macdrv_release_event(event);
474 // We can compare two modes directly using CFEqual, but that may require that
475 // they are identical to a level that we don't need. In particular, when the
476 // OS switches between the integrated and discrete GPUs, the set of display
477 // modes can change in subtle ways. We're interested in whether two modes
478 // match in their most salient features, even if they aren't identical.
479 - (BOOL) mode:(CGDisplayModeRef)mode1 matchesMode:(CGDisplayModeRef)mode2
481 NSString *encoding1, *encoding2;
482 uint32_t ioflags1, ioflags2, different;
483 double refresh1, refresh2;
485 if (CGDisplayModeGetWidth(mode1) != CGDisplayModeGetWidth(mode2)) return FALSE;
486 if (CGDisplayModeGetHeight(mode1) != CGDisplayModeGetHeight(mode2)) return FALSE;
488 encoding1 = [(NSString*)CGDisplayModeCopyPixelEncoding(mode1) autorelease];
489 encoding2 = [(NSString*)CGDisplayModeCopyPixelEncoding(mode2) autorelease];
490 if (![encoding1 isEqualToString:encoding2]) return FALSE;
492 ioflags1 = CGDisplayModeGetIOFlags(mode1);
493 ioflags2 = CGDisplayModeGetIOFlags(mode2);
494 different = ioflags1 ^ ioflags2;
495 if (different & (kDisplayModeValidFlag | kDisplayModeSafeFlag | kDisplayModeStretchedFlag |
496 kDisplayModeInterlacedFlag | kDisplayModeTelevisionFlag))
499 refresh1 = CGDisplayModeGetRefreshRate(mode1);
500 if (refresh1 == 0) refresh1 = 60;
501 refresh2 = CGDisplayModeGetRefreshRate(mode2);
502 if (refresh2 == 0) refresh2 = 60;
503 if (fabs(refresh1 - refresh2) > 0.1) return FALSE;
508 - (CGDisplayModeRef)modeMatchingMode:(CGDisplayModeRef)mode forDisplay:(CGDirectDisplayID)displayID
510 CGDisplayModeRef ret = NULL;
511 NSArray *modes = [(NSArray*)CGDisplayCopyAllDisplayModes(displayID, NULL) autorelease];
512 for (id candidateModeObject in modes)
514 CGDisplayModeRef candidateMode = (CGDisplayModeRef)candidateModeObject;
515 if ([self mode:candidateMode matchesMode:mode])
524 - (BOOL) setMode:(CGDisplayModeRef)mode forDisplay:(CGDirectDisplayID)displayID
527 NSNumber* displayIDKey = [NSNumber numberWithUnsignedInt:displayID];
528 CGDisplayModeRef currentMode, originalMode;
530 currentMode = CGDisplayCopyDisplayMode(displayID);
531 if (!currentMode) // Invalid display ID
534 if ([self mode:mode matchesMode:currentMode]) // Already there!
536 CGDisplayModeRelease(currentMode);
540 mode = [self modeMatchingMode:mode forDisplay:displayID];
543 CGDisplayModeRelease(currentMode);
547 originalMode = (CGDisplayModeRef)[originalDisplayModes objectForKey:displayIDKey];
549 originalMode = currentMode;
551 if ([self mode:mode matchesMode:originalMode])
553 if ([originalDisplayModes count] == 1) // If this is the last changed display, do a blanket reset
555 CGRestorePermanentDisplayConfiguration();
556 CGReleaseAllDisplays();
557 [originalDisplayModes removeAllObjects];
560 else // ... otherwise, try to restore just the one display
562 if (CGDisplaySetDisplayMode(displayID, mode, NULL) == CGDisplayNoErr)
564 [originalDisplayModes removeObjectForKey:displayIDKey];
571 if ([originalDisplayModes count] || CGCaptureAllDisplays() == CGDisplayNoErr)
573 if (CGDisplaySetDisplayMode(displayID, mode, NULL) == CGDisplayNoErr)
575 [originalDisplayModes setObject:(id)originalMode forKey:displayIDKey];
578 else if (![originalDisplayModes count])
580 CGRestorePermanentDisplayConfiguration();
581 CGReleaseAllDisplays();
586 CGDisplayModeRelease(currentMode);
590 [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
591 [(WineWindow*)obj adjustWindowLevel];
598 - (BOOL) areDisplaysCaptured
600 return ([originalDisplayModes count] > 0);
612 - (void) unhideCursor
617 cursorHidden = FALSE;
623 NSDictionary* frame = [cursorFrames objectAtIndex:cursorFrame];
624 CGImageRef cgimage = (CGImageRef)[frame objectForKey:@"image"];
625 NSImage* image = [[NSImage alloc] initWithCGImage:cgimage size:NSZeroSize];
626 CFDictionaryRef hotSpotDict = (CFDictionaryRef)[frame objectForKey:@"hotSpot"];
630 if (!CGPointMakeWithDictionaryRepresentation(hotSpotDict, &hotSpot))
631 hotSpot = CGPointZero;
632 cursor = [[NSCursor alloc] initWithImage:image hotSpot:NSPointFromCGPoint(hotSpot)];
639 - (void) nextCursorFrame:(NSTimer*)theTimer
642 NSTimeInterval duration;
646 if (cursorFrame >= [cursorFrames count])
650 frame = [cursorFrames objectAtIndex:cursorFrame];
651 duration = [[frame objectForKey:@"duration"] doubleValue];
652 date = [[theTimer fireDate] dateByAddingTimeInterval:duration];
653 [cursorTimer setFireDate:date];
656 - (void) setCursorWithFrames:(NSArray*)frames
658 if (self.cursorFrames == frames)
661 self.cursorFrames = frames;
663 [cursorTimer invalidate];
664 self.cursorTimer = nil;
668 if ([frames count] > 1)
670 NSDictionary* frame = [frames objectAtIndex:0];
671 NSTimeInterval duration = [[frame objectForKey:@"duration"] doubleValue];
672 NSDate* date = [NSDate dateWithTimeIntervalSinceNow:duration];
673 self.cursorTimer = [[[NSTimer alloc] initWithFireDate:date
676 selector:@selector(nextCursorFrame:)
678 repeats:YES] autorelease];
679 [[NSRunLoop currentRunLoop] addTimer:cursorTimer forMode:NSRunLoopCommonModes];
686 - (void) setApplicationIconFromCGImageArray:(NSArray*)images
688 NSImage* nsimage = nil;
692 NSSize bestSize = NSZeroSize;
695 nsimage = [[[NSImage alloc] initWithSize:NSZeroSize] autorelease];
697 for (image in images)
699 CGImageRef cgimage = (CGImageRef)image;
700 NSBitmapImageRep* imageRep = [[NSBitmapImageRep alloc] initWithCGImage:cgimage];
703 NSSize size = [imageRep size];
705 [nsimage addRepresentation:imageRep];
708 if (MIN(size.width, size.height) > MIN(bestSize.width, bestSize.height))
713 if ([[nsimage representations] count] && bestSize.width && bestSize.height)
714 [nsimage setSize:bestSize];
719 self.applicationIcon = nsimage;
720 [NSApp setApplicationIconImage:nsimage];
723 - (void) handleCommandTab
725 if ([NSApp isActive])
727 NSRunningApplication* thisApp = [NSRunningApplication currentApplication];
728 NSRunningApplication* app;
729 NSRunningApplication* otherValidApp = nil;
731 if ([originalDisplayModes count])
733 CGRestorePermanentDisplayConfiguration();
734 CGReleaseAllDisplays();
735 [originalDisplayModes removeAllObjects];
738 for (app in [[NSWorkspace sharedWorkspace] runningApplications])
740 if (![app isEqual:thisApp] && !app.terminated &&
741 app.activationPolicy == NSApplicationActivationPolicyRegular)
745 // There's another visible app. Just hide ourselves and let
746 // the system activate the other app.
756 // Didn't find a visible GUI app. Try the Finder or, if that's not
757 // running, the first hidden GUI app. If even that doesn't work, we
758 // just fail to switch and remain the active app.
759 app = [[NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.finder"] lastObject];
760 if (!app) app = otherValidApp;
762 [app activateWithOptions:0];
767 * ---------- Cursor clipping methods ----------
769 * Neither Quartz nor Cocoa has an exact analog for Win32 cursor clipping.
770 * For one simple case, clipping to a 1x1 rectangle, Quartz does have an
771 * equivalent: CGAssociateMouseAndMouseCursorPosition(false). For the
772 * general case, we leverage that. We disassociate mouse movements from
773 * the cursor position and then move the cursor manually, keeping it within
774 * the clipping rectangle.
776 * Moving the cursor manually isn't enough. We need to modify the event
777 * stream so that the events have the new location, too. We need to do
778 * this at a point before the events enter Cocoa, so that Cocoa will assign
779 * the correct window to the event. So, we install a Quartz event tap to
782 * Also, there's a complication when we move the cursor. We use
783 * CGWarpMouseCursorPosition(). That doesn't generate mouse movement
784 * events, but the change of cursor position is incorporated into the
785 * deltas of the next mouse move event. When the mouse is disassociated
786 * from the cursor position, we need the deltas to only reflect actual
787 * device movement, not programmatic changes. So, the event tap cancels
788 * out the change caused by our calls to CGWarpMouseCursorPosition().
790 - (void) clipCursorLocation:(CGPoint*)location
792 if (location->x < CGRectGetMinX(cursorClipRect))
793 location->x = CGRectGetMinX(cursorClipRect);
794 if (location->y < CGRectGetMinY(cursorClipRect))
795 location->y = CGRectGetMinY(cursorClipRect);
796 if (location->x > CGRectGetMaxX(cursorClipRect) - 1)
797 location->x = CGRectGetMaxX(cursorClipRect) - 1;
798 if (location->y > CGRectGetMaxY(cursorClipRect) - 1)
799 location->y = CGRectGetMaxY(cursorClipRect) - 1;
802 - (BOOL) warpCursorTo:(CGPoint*)newLocation from:(const CGPoint*)currentLocation
807 oldLocation = *currentLocation;
809 oldLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
811 if (!CGPointEqualToPoint(oldLocation, *newLocation))
813 WarpRecord* warpRecord = [[[WarpRecord alloc] init] autorelease];
816 warpRecord.from = oldLocation;
817 warpRecord.timeBefore = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
819 /* Actually move the cursor. */
820 err = CGWarpMouseCursorPosition(*newLocation);
821 if (err != kCGErrorSuccess)
824 warpRecord.timeAfter = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
825 *newLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
827 if (!CGPointEqualToPoint(oldLocation, *newLocation))
829 warpRecord.to = *newLocation;
830 [warpRecords addObject:warpRecord];
837 - (BOOL) isMouseMoveEventType:(CGEventType)type
841 case kCGEventMouseMoved:
842 case kCGEventLeftMouseDragged:
843 case kCGEventRightMouseDragged:
844 case kCGEventOtherMouseDragged:
851 - (int) warpsFinishedByEventTime:(CGEventTimestamp)eventTime location:(CGPoint)eventLocation
853 int warpsFinished = 0;
854 for (WarpRecord* warpRecord in warpRecords)
856 if (warpRecord.timeAfter < eventTime ||
857 (warpRecord.timeBefore <= eventTime && CGPointEqualToPoint(eventLocation, warpRecord.to)))
863 return warpsFinished;
866 - (CGEventRef) eventTapWithProxy:(CGEventTapProxy)proxy
867 type:(CGEventType)type
868 event:(CGEventRef)event
870 CGEventTimestamp eventTime;
871 CGPoint eventLocation, cursorLocation;
873 if (type == kCGEventTapDisabledByUserInput)
875 if (type == kCGEventTapDisabledByTimeout)
877 CGEventTapEnable(cursorClippingEventTap, TRUE);
884 eventTime = CGEventGetTimestamp(event);
885 lastEventTapEventTime = eventTime / (double)NSEC_PER_SEC;
887 eventLocation = CGEventGetLocation(event);
889 cursorLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
891 if ([self isMouseMoveEventType:type])
893 double deltaX, deltaY;
894 int warpsFinished = [self warpsFinishedByEventTime:eventTime location:eventLocation];
897 deltaX = CGEventGetDoubleValueField(event, kCGMouseEventDeltaX);
898 deltaY = CGEventGetDoubleValueField(event, kCGMouseEventDeltaY);
900 for (i = 0; i < warpsFinished; i++)
902 WarpRecord* warpRecord = [warpRecords objectAtIndex:0];
903 deltaX -= warpRecord.to.x - warpRecord.from.x;
904 deltaY -= warpRecord.to.y - warpRecord.from.y;
905 [warpRecords removeObjectAtIndex:0];
910 CGEventSetDoubleValueField(event, kCGMouseEventDeltaX, deltaX);
911 CGEventSetDoubleValueField(event, kCGMouseEventDeltaY, deltaY);
914 synthesizedLocation.x += deltaX;
915 synthesizedLocation.y += deltaY;
918 // If the event is destined for another process, don't clip it. This may
919 // happen if the user activates Exposé or Mission Control. In that case,
920 // our app does not resign active status, so clipping is still in effect,
921 // but the cursor should not actually be clipped.
923 // In addition, the fact that mouse moves may have been delivered to a
924 // different process means we have to treat the next one we receive as
925 // absolute rather than relative.
926 if (CGEventGetIntegerValueField(event, kCGEventTargetUnixProcessID) == getpid())
927 [self clipCursorLocation:&synthesizedLocation];
929 lastSetCursorPositionTime = lastEventTapEventTime;
931 [self warpCursorTo:&synthesizedLocation from:&cursorLocation];
932 if (!CGPointEqualToPoint(eventLocation, synthesizedLocation))
933 CGEventSetLocation(event, synthesizedLocation);
938 CGEventRef WineAppEventTapCallBack(CGEventTapProxy proxy, CGEventType type,
939 CGEventRef event, void *refcon)
941 WineApplicationController* controller = refcon;
942 return [controller eventTapWithProxy:proxy type:type event:event];
945 - (BOOL) installEventTap
947 ProcessSerialNumber psn;
949 CGEventMask mask = CGEventMaskBit(kCGEventLeftMouseDown) |
950 CGEventMaskBit(kCGEventLeftMouseUp) |
951 CGEventMaskBit(kCGEventRightMouseDown) |
952 CGEventMaskBit(kCGEventRightMouseUp) |
953 CGEventMaskBit(kCGEventMouseMoved) |
954 CGEventMaskBit(kCGEventLeftMouseDragged) |
955 CGEventMaskBit(kCGEventRightMouseDragged) |
956 CGEventMaskBit(kCGEventOtherMouseDown) |
957 CGEventMaskBit(kCGEventOtherMouseUp) |
958 CGEventMaskBit(kCGEventOtherMouseDragged) |
959 CGEventMaskBit(kCGEventScrollWheel);
960 CFRunLoopSourceRef source;
962 OSErr (*pGetCurrentProcess)(ProcessSerialNumber* PSN);
964 if (cursorClippingEventTap)
967 // We need to get the Mac GetCurrentProcess() from the ApplicationServices
968 // framework with dlsym() because the Win32 function of the same name
970 appServices = dlopen("/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices", RTLD_LAZY);
974 pGetCurrentProcess = dlsym(appServices, "GetCurrentProcess");
975 if (!pGetCurrentProcess)
977 dlclose(appServices);
981 err = pGetCurrentProcess(&psn);
982 dlclose(appServices);
986 // We create an annotated session event tap rather than a process-specific
987 // event tap because we need to programmatically move the cursor even when
988 // mouse moves are directed to other processes. We disable our tap when
989 // other processes are active, but things like Exposé are handled by other
990 // processes even when we remain active.
991 cursorClippingEventTap = CGEventTapCreate(kCGAnnotatedSessionEventTap, kCGHeadInsertEventTap,
992 kCGEventTapOptionDefault, mask, WineAppEventTapCallBack, self);
993 if (!cursorClippingEventTap)
996 CGEventTapEnable(cursorClippingEventTap, FALSE);
998 source = CFMachPortCreateRunLoopSource(NULL, cursorClippingEventTap, 0);
1001 CFRelease(cursorClippingEventTap);
1002 cursorClippingEventTap = NULL;
1006 CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
1011 - (BOOL) setCursorPosition:(CGPoint)pos
1017 [self clipCursorLocation:&pos];
1019 synthesizedLocation = pos;
1020 ret = [self warpCursorTo:&synthesizedLocation from:NULL];
1023 // We want to discard mouse-move events that have already been
1024 // through the event tap, because it's too late to account for
1025 // the setting of the cursor position with them. However, the
1026 // events that may be queued with times after that but before
1027 // the above warp can still be used. So, use the last event
1028 // tap event time so that -sendEvent: doesn't discard them.
1029 lastSetCursorPositionTime = lastEventTapEventTime;
1034 ret = (CGWarpMouseCursorPosition(pos) == kCGErrorSuccess);
1036 lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
1041 WineEventQueue* queue;
1043 // Discard all pending mouse move events.
1044 [eventQueuesLock lock];
1045 for (queue in eventQueues)
1047 [queue discardEventsMatchingMask:event_mask_for_type(MOUSE_MOVED) |
1048 event_mask_for_type(MOUSE_MOVED_ABSOLUTE)
1051 [eventQueuesLock unlock];
1057 - (void) activateCursorClipping
1061 CGEventTapEnable(cursorClippingEventTap, TRUE);
1062 [self setCursorPosition:NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]])];
1066 - (void) deactivateCursorClipping
1070 CGEventTapEnable(cursorClippingEventTap, FALSE);
1071 [warpRecords removeAllObjects];
1072 lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
1076 - (BOOL) startClippingCursor:(CGRect)rect
1080 if (!cursorClippingEventTap && ![self installEventTap])
1083 err = CGAssociateMouseAndMouseCursorPosition(false);
1084 if (err != kCGErrorSuccess)
1087 clippingCursor = TRUE;
1088 cursorClipRect = rect;
1089 if ([NSApp isActive])
1090 [self activateCursorClipping];
1095 - (BOOL) stopClippingCursor
1097 CGError err = CGAssociateMouseAndMouseCursorPosition(true);
1098 if (err != kCGErrorSuccess)
1101 [self deactivateCursorClipping];
1102 clippingCursor = FALSE;
1108 // Returns TRUE if the event was handled and caller should do nothing more
1109 // with it. Returns FALSE if the caller should process it as normal and
1110 // then call -didSendEvent:.
1111 - (BOOL) handleEvent:(NSEvent*)anEvent
1113 if ([anEvent type] == NSFlagsChanged)
1114 self.lastFlagsChanged = anEvent;
1118 - (void) didSendEvent:(NSEvent*)anEvent
1120 NSEventType type = [anEvent type];
1122 if (type == NSMouseMoved || type == NSLeftMouseDragged ||
1123 type == NSRightMouseDragged || type == NSOtherMouseDragged)
1125 WineWindow* targetWindow;
1127 /* Because of the way -[NSWindow setAcceptsMouseMovedEvents:] works, the
1128 event indicates its window is the main window, even if the cursor is
1129 over a different window. Find the actual WineWindow that is under the
1130 cursor and post the event as being for that window. */
1131 if (type == NSMouseMoved)
1133 CGPoint cgpoint = CGEventGetLocation([anEvent CGEvent]);
1134 NSPoint point = [self flippedMouseLocation:NSPointFromCGPoint(cgpoint)];
1135 NSInteger windowUnderNumber;
1137 windowUnderNumber = [NSWindow windowNumberAtPoint:point
1138 belowWindowWithWindowNumber:0];
1139 targetWindow = (WineWindow*)[NSApp windowWithWindowNumber:windowUnderNumber];
1142 targetWindow = (WineWindow*)[anEvent window];
1144 if ([targetWindow isKindOfClass:[WineWindow class]])
1146 BOOL absolute = forceNextMouseMoveAbsolute || (targetWindow != lastTargetWindow);
1147 forceNextMouseMoveAbsolute = FALSE;
1149 // If we recently warped the cursor (other than in our cursor-clipping
1150 // event tap), discard mouse move events until we see an event which is
1151 // later than that time.
1152 if (lastSetCursorPositionTime)
1154 if ([anEvent timestamp] <= lastSetCursorPositionTime)
1157 lastSetCursorPositionTime = 0;
1161 [targetWindow postMouseMovedEvent:anEvent absolute:absolute];
1162 lastTargetWindow = targetWindow;
1164 else if (lastTargetWindow)
1166 [[NSCursor arrowCursor] set];
1167 [self unhideCursor];
1168 lastTargetWindow = nil;
1171 else if (type == NSLeftMouseDown || type == NSLeftMouseUp ||
1172 type == NSRightMouseDown || type == NSRightMouseUp ||
1173 type == NSOtherMouseDown || type == NSOtherMouseUp ||
1174 type == NSScrollWheel)
1176 // Since mouse button and scroll wheel events deliver absolute cursor
1177 // position, the accumulating delta from move events is invalidated.
1178 // Make sure next mouse move event starts over from an absolute baseline.
1179 forceNextMouseMoveAbsolute = TRUE;
1181 else if (type == NSKeyDown && ![anEvent isARepeat] && [anEvent keyCode] == kVK_Tab)
1183 NSUInteger modifiers = [anEvent modifierFlags];
1184 if ((modifiers & NSCommandKeyMask) &&
1185 !(modifiers & (NSControlKeyMask | NSAlternateKeyMask)))
1187 // Command-Tab and Command-Shift-Tab would normally be intercepted
1188 // by the system to switch applications. If we're seeing it, it's
1189 // presumably because we've captured the displays, preventing
1190 // normal application switching. Do it manually.
1191 [self handleCommandTab];
1196 - (void) setupObservations
1198 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
1200 [nc addObserverForName:NSWindowDidBecomeKeyNotification
1203 usingBlock:^(NSNotification *note){
1204 NSWindow* window = [note object];
1205 [keyWindows removeObjectIdenticalTo:window];
1206 [keyWindows insertObject:window atIndex:0];
1209 [nc addObserverForName:NSWindowWillCloseNotification
1211 queue:[NSOperationQueue mainQueue]
1212 usingBlock:^(NSNotification *note){
1213 NSWindow* window = [note object];
1214 [keyWindows removeObjectIdenticalTo:window];
1215 [orderedWineWindows removeObjectIdenticalTo:window];
1216 if (window == lastTargetWindow)
1217 lastTargetWindow = nil;
1220 [nc addObserver:self
1221 selector:@selector(keyboardSelectionDidChange)
1222 name:NSTextInputContextKeyboardSelectionDidChangeNotification
1225 /* The above notification isn't sent unless the NSTextInputContext
1226 class has initialized itself. Poke it. */
1227 [NSTextInputContext self];
1232 * ---------- NSApplicationDelegate methods ----------
1234 - (void)applicationDidBecomeActive:(NSNotification *)notification
1236 [self activateCursorClipping];
1238 [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
1239 WineWindow* window = obj;
1240 if ([window levelWhenActive] != [window level])
1241 [window setLevel:[window levelWhenActive]];
1244 // If a Wine process terminates abruptly while it has the display captured
1245 // and switched to a different resolution, Mac OS X will uncapture the
1246 // displays and switch their resolutions back. However, the other Wine
1247 // processes won't have their notion of the desktop rect changed back.
1248 // This can lead them to refuse to draw or acknowledge clicks in certain
1249 // portions of their windows.
1251 // To solve this, we synthesize a displays-changed event whenever we're
1252 // activated. This will provoke a re-synchronization of Wine's notion of
1253 // the desktop rect with the actual state.
1254 [self sendDisplaysChanged:TRUE];
1256 // The cursor probably moved while we were inactive. Accumulated mouse
1257 // movement deltas are invalidated. Make sure the next mouse move event
1258 // starts over from an absolute baseline.
1259 forceNextMouseMoveAbsolute = TRUE;
1262 - (void)applicationDidChangeScreenParameters:(NSNotification *)notification
1264 primaryScreenHeightValid = FALSE;
1265 [self sendDisplaysChanged:FALSE];
1267 // When the display configuration changes, the cursor position may jump.
1268 // Accumulated mouse movement deltas are invalidated. Make sure the next
1269 // mouse move event starts over from an absolute baseline.
1270 forceNextMouseMoveAbsolute = TRUE;
1273 - (void)applicationDidResignActive:(NSNotification *)notification
1275 macdrv_event* event;
1276 WineEventQueue* queue;
1278 [self invalidateGotFocusEvents];
1280 event = macdrv_create_event(APP_DEACTIVATED, nil);
1282 [eventQueuesLock lock];
1283 for (queue in eventQueues)
1284 [queue postEvent:event];
1285 [eventQueuesLock unlock];
1287 macdrv_release_event(event);
1290 - (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication *)sender
1292 NSApplicationTerminateReply ret = NSTerminateNow;
1293 NSAppleEventManager* m = [NSAppleEventManager sharedAppleEventManager];
1294 NSAppleEventDescriptor* desc = [m currentAppleEvent];
1295 macdrv_event* event;
1296 WineEventQueue* queue;
1298 event = macdrv_create_event(APP_QUIT_REQUESTED, nil);
1300 switch ([[desc attributeDescriptorForKeyword:kAEQuitReason] int32Value])
1303 case kAEReallyLogOut:
1304 event->app_quit_requested.reason = QUIT_REASON_LOGOUT;
1306 case kAEShowRestartDialog:
1307 event->app_quit_requested.reason = QUIT_REASON_RESTART;
1309 case kAEShowShutdownDialog:
1310 event->app_quit_requested.reason = QUIT_REASON_SHUTDOWN;
1313 event->app_quit_requested.reason = QUIT_REASON_NONE;
1317 [eventQueuesLock lock];
1319 if ([eventQueues count])
1321 for (queue in eventQueues)
1322 [queue postEvent:event];
1323 ret = NSTerminateLater;
1326 [eventQueuesLock unlock];
1328 macdrv_release_event(event);
1333 - (void)applicationWillResignActive:(NSNotification *)notification
1335 [self deactivateCursorClipping];
1337 [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
1338 WineWindow* window = obj;
1339 NSInteger level = window.floating ? NSFloatingWindowLevel : NSNormalWindowLevel;
1340 if ([window level] > level)
1341 [window setLevel:level];
1345 /***********************************************************************
1348 * Run-loop-source perform callback. Pull request blocks from the
1349 * array of queued requests and invoke them.
1351 static void PerformRequest(void *info)
1353 WineApplicationController* controller = [WineApplicationController sharedController];
1357 __block dispatch_block_t block;
1359 dispatch_sync(controller->requestsManipQueue, ^{
1360 if ([controller->requests count])
1362 block = (dispatch_block_t)[[controller->requests objectAtIndex:0] retain];
1363 [controller->requests removeObjectAtIndex:0];
1377 /***********************************************************************
1380 * Run a block on the main thread asynchronously.
1382 void OnMainThreadAsync(dispatch_block_t block)
1384 WineApplicationController* controller = [WineApplicationController sharedController];
1386 block = [block copy];
1387 dispatch_sync(controller->requestsManipQueue, ^{
1388 [controller->requests addObject:block];
1391 CFRunLoopSourceSignal(controller->requestSource);
1392 CFRunLoopWakeUp(CFRunLoopGetMain());
1397 /***********************************************************************
1400 void LogError(const char* func, NSString* format, ...)
1403 va_start(args, format);
1404 LogErrorv(func, format, args);
1408 /***********************************************************************
1411 void LogErrorv(const char* func, NSString* format, va_list args)
1413 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
1415 NSString* message = [[NSString alloc] initWithFormat:format arguments:args];
1416 fprintf(stderr, "err:%s:%s", func, [message UTF8String]);
1422 /***********************************************************************
1423 * macdrv_window_rejected_focus
1425 * Pass focus to the next window that hasn't already rejected this same
1426 * WINDOW_GOT_FOCUS event.
1428 void macdrv_window_rejected_focus(const macdrv_event *event)
1431 [[WineApplicationController sharedController] windowRejectedFocusEvent:event];
1435 /***********************************************************************
1436 * macdrv_get_keyboard_layout
1438 * Returns the keyboard layout uchr data.
1440 CFDataRef macdrv_copy_keyboard_layout(CGEventSourceKeyboardType* keyboard_type, int* is_iso)
1442 __block CFDataRef result = NULL;
1445 TISInputSourceRef inputSource;
1447 inputSource = TISCopyCurrentKeyboardLayoutInputSource();
1450 CFDataRef uchr = TISGetInputSourceProperty(inputSource,
1451 kTISPropertyUnicodeKeyLayoutData);
1452 result = CFDataCreateCopy(NULL, uchr);
1453 CFRelease(inputSource);
1455 *keyboard_type = [WineApplicationController sharedController].keyboardType;
1456 *is_iso = (KBGetLayoutType(*keyboard_type) == kKeyboardISO);
1463 /***********************************************************************
1466 * Play the beep sound configured by the user in System Preferences.
1468 void macdrv_beep(void)
1470 OnMainThreadAsync(^{
1475 /***********************************************************************
1476 * macdrv_set_display_mode
1478 int macdrv_set_display_mode(const struct macdrv_display* display,
1479 CGDisplayModeRef display_mode)
1484 ret = [[WineApplicationController sharedController] setMode:display_mode forDisplay:display->displayID];
1490 /***********************************************************************
1495 * If name is non-NULL, it is a selector for a class method on NSCursor
1496 * identifying the cursor to set. In that case, frames is ignored. If
1497 * name is NULL, then frames is used.
1499 * frames is an array of dictionaries. Each dictionary is a frame of
1500 * an animated cursor. Under the key "image" is a CGImage for the
1501 * frame. Under the key "duration" is a CFNumber time interval, in
1502 * seconds, for how long that frame is presented before proceeding to
1503 * the next frame. Under the key "hotSpot" is a CFDictionary encoding a
1504 * CGPoint, to be decoded using CGPointMakeWithDictionaryRepresentation().
1505 * This is the hot spot, measured in pixels down and to the right of the
1506 * top-left corner of the image.
1508 * If the array has exactly 1 element, the cursor is static, not
1509 * animated. If frames is NULL or has 0 elements, the cursor is hidden.
1511 void macdrv_set_cursor(CFStringRef name, CFArrayRef frames)
1515 sel = NSSelectorFromString((NSString*)name);
1518 OnMainThreadAsync(^{
1519 WineApplicationController* controller = [WineApplicationController sharedController];
1520 NSCursor* cursor = [NSCursor performSelector:sel];
1521 [controller setCursorWithFrames:nil];
1523 [controller unhideCursor];
1528 NSArray* nsframes = (NSArray*)frames;
1529 if ([nsframes count])
1531 OnMainThreadAsync(^{
1532 [[WineApplicationController sharedController] setCursorWithFrames:nsframes];
1537 OnMainThreadAsync(^{
1538 WineApplicationController* controller = [WineApplicationController sharedController];
1539 [controller setCursorWithFrames:nil];
1540 [controller hideCursor];
1546 /***********************************************************************
1547 * macdrv_get_cursor_position
1549 * Obtains the current cursor position. Returns zero on failure,
1550 * non-zero on success.
1552 int macdrv_get_cursor_position(CGPoint *pos)
1555 NSPoint location = [NSEvent mouseLocation];
1556 location = [[WineApplicationController sharedController] flippedMouseLocation:location];
1557 *pos = NSPointToCGPoint(location);
1563 /***********************************************************************
1564 * macdrv_set_cursor_position
1566 * Sets the cursor position without generating events. Returns zero on
1567 * failure, non-zero on success.
1569 int macdrv_set_cursor_position(CGPoint pos)
1574 ret = [[WineApplicationController sharedController] setCursorPosition:pos];
1580 /***********************************************************************
1581 * macdrv_clip_cursor
1583 * Sets the cursor cursor clipping rectangle. If the rectangle is equal
1584 * to or larger than the whole desktop region, the cursor is unclipped.
1585 * Returns zero on failure, non-zero on success.
1587 int macdrv_clip_cursor(CGRect rect)
1592 WineApplicationController* controller = [WineApplicationController sharedController];
1593 BOOL clipping = FALSE;
1595 if (!CGRectIsInfinite(rect))
1597 NSRect nsrect = NSRectFromCGRect(rect);
1600 /* Convert the rectangle from top-down coords to bottom-up. */
1601 [controller flipRect:&nsrect];
1604 for (screen in [NSScreen screens])
1606 if (!NSContainsRect(nsrect, [screen frame]))
1615 ret = [controller startClippingCursor:rect];
1617 ret = [controller stopClippingCursor];
1623 /***********************************************************************
1624 * macdrv_set_application_icon
1626 * Set the application icon. The images array contains CGImages. If
1627 * there are more than one, then they represent different sizes or
1628 * color depths from the icon resource. If images is NULL or empty,
1629 * restores the default application image.
1631 void macdrv_set_application_icon(CFArrayRef images)
1633 NSArray* imageArray = (NSArray*)images;
1635 OnMainThreadAsync(^{
1636 [[WineApplicationController sharedController] setApplicationIconFromCGImageArray:imageArray];
1640 /***********************************************************************
1643 void macdrv_quit_reply(int reply)
1646 [NSApp replyToApplicationShouldTerminate:reply];