2 * MACDRV Cocoa application class
4 * Copyright 2011, 2012, 2013 Ken Thomases for CodeWeavers Inc.
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
21 #import <Carbon/Carbon.h>
25 #import "cocoa_event.h"
26 #import "cocoa_window.h"
29 static NSString* const WineAppWaitQueryResponseMode = @"WineAppWaitQueryResponseMode";
35 @interface WarpRecord : NSObject
37 CGEventTimestamp timeBefore, timeAfter;
41 @property (nonatomic) CGEventTimestamp timeBefore;
42 @property (nonatomic) CGEventTimestamp timeAfter;
43 @property (nonatomic) CGPoint from;
44 @property (nonatomic) CGPoint to;
49 @implementation WarpRecord
51 @synthesize timeBefore, timeAfter, from, to;
56 @interface WineApplication ()
58 @property (readwrite, copy, nonatomic) NSEvent* lastFlagsChanged;
59 @property (copy, nonatomic) NSArray* cursorFrames;
60 @property (retain, nonatomic) NSTimer* cursorTimer;
61 @property (retain, nonatomic) NSImage* applicationIcon;
63 static void PerformRequest(void *info);
68 @implementation WineApplication
70 @synthesize keyboardType, lastFlagsChanged;
71 @synthesize orderedWineWindows, applicationIcon;
72 @synthesize cursorFrames, cursorTimer;
79 CFRunLoopSourceContext context = { 0 };
80 context.perform = PerformRequest;
81 requestSource = CFRunLoopSourceCreate(NULL, 0, &context);
87 CFRunLoopAddSource(CFRunLoopGetMain(), requestSource, kCFRunLoopCommonModes);
88 CFRunLoopAddSource(CFRunLoopGetMain(), requestSource, (CFStringRef)WineAppWaitQueryResponseMode);
90 requests = [[NSMutableArray alloc] init];
91 requestsManipQueue = dispatch_queue_create("org.winehq.WineAppRequestManipQueue", NULL);
93 eventQueues = [[NSMutableArray alloc] init];
94 eventQueuesLock = [[NSLock alloc] init];
96 keyWindows = [[NSMutableArray alloc] init];
97 orderedWineWindows = [[NSMutableArray alloc] init];
99 originalDisplayModes = [[NSMutableDictionary alloc] init];
101 warpRecords = [[NSMutableArray alloc] init];
103 if (!requests || !requestsManipQueue || !eventQueues || !eventQueuesLock ||
104 !keyWindows || !orderedWineWindows || !originalDisplayModes || !warpRecords)
115 [applicationIcon release];
116 [warpRecords release];
117 [cursorTimer release];
118 [cursorFrames release];
119 [originalDisplayModes release];
120 [orderedWineWindows release];
121 [keyWindows release];
122 [eventQueues release];
123 [eventQueuesLock release];
124 if (requestsManipQueue) dispatch_release(requestsManipQueue);
128 CFRunLoopSourceInvalidate(requestSource);
129 CFRelease(requestSource);
134 - (void) transformProcessToForeground
136 if ([self activationPolicy] != NSApplicationActivationPolicyRegular)
140 NSString* bundleName;
144 [self setActivationPolicy:NSApplicationActivationPolicyRegular];
145 [self activateIgnoringOtherApps:YES];
147 mainMenu = [[[NSMenu alloc] init] autorelease];
149 submenu = [[[NSMenu alloc] initWithTitle:@"Wine"] autorelease];
150 bundleName = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString*)kCFBundleNameKey];
151 if ([bundleName length])
152 title = [NSString stringWithFormat:@"Quit %@", bundleName];
155 item = [submenu addItemWithTitle:title action:@selector(terminate:) keyEquivalent:@"q"];
156 [item setKeyEquivalentModifierMask:NSCommandKeyMask | NSAlternateKeyMask];
157 item = [[[NSMenuItem alloc] init] autorelease];
158 [item setTitle:@"Wine"];
159 [item setSubmenu:submenu];
160 [mainMenu addItem:item];
162 submenu = [[[NSMenu alloc] initWithTitle:@"Window"] autorelease];
163 [submenu addItemWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@""];
164 [submenu addItemWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""];
165 [submenu addItem:[NSMenuItem separatorItem]];
166 [submenu addItemWithTitle:@"Bring All to Front" action:@selector(arrangeInFront:) keyEquivalent:@""];
167 item = [[[NSMenuItem alloc] init] autorelease];
168 [item setTitle:@"Window"];
169 [item setSubmenu:submenu];
170 [mainMenu addItem:item];
172 [self setMainMenu:mainMenu];
173 [self setWindowsMenu:submenu];
175 [self setApplicationIconImage:self.applicationIcon];
179 - (BOOL) waitUntilQueryDone:(int*)done timeout:(NSDate*)timeout processEvents:(BOOL)processEvents
181 PerformRequest(NULL);
187 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
188 NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask
190 inMode:NSDefaultRunLoopMode
193 [NSApp sendEvent:event];
197 [[NSRunLoop currentRunLoop] runMode:WineAppWaitQueryResponseMode beforeDate:timeout];
198 } while (!*done && [timeout timeIntervalSinceNow] >= 0);
203 - (BOOL) registerEventQueue:(WineEventQueue*)queue
205 [eventQueuesLock lock];
206 [eventQueues addObject:queue];
207 [eventQueuesLock unlock];
211 - (void) unregisterEventQueue:(WineEventQueue*)queue
213 [eventQueuesLock lock];
214 [eventQueues removeObjectIdenticalTo:queue];
215 [eventQueuesLock unlock];
218 - (void) computeEventTimeAdjustmentFromTicks:(unsigned long long)tickcount uptime:(uint64_t)uptime_ns
220 eventTimeAdjustment = (tickcount / 1000.0) - (uptime_ns / (double)NSEC_PER_SEC);
223 - (double) ticksForEventTime:(NSTimeInterval)eventTime
225 return (eventTime + eventTimeAdjustment) * 1000;
228 /* Invalidate old focus offers across all queues. */
229 - (void) invalidateGotFocusEvents
231 WineEventQueue* queue;
235 [eventQueuesLock lock];
236 for (queue in eventQueues)
238 [queue discardEventsMatchingMask:event_mask_for_type(WINDOW_GOT_FOCUS)
241 [eventQueuesLock unlock];
244 - (void) windowGotFocus:(WineWindow*)window
248 [NSApp invalidateGotFocusEvents];
250 event.type = WINDOW_GOT_FOCUS;
251 event.window = (macdrv_window)[window retain];
252 event.window_got_focus.serial = windowFocusSerial;
254 event.window_got_focus.tried_windows = [triedWindows retain];
256 event.window_got_focus.tried_windows = [[NSMutableSet alloc] init];
257 [window.queue postEvent:&event];
260 - (void) windowRejectedFocusEvent:(const macdrv_event*)event
262 if (event->window_got_focus.serial == windowFocusSerial)
264 triedWindows = (NSMutableSet*)event->window_got_focus.tried_windows;
265 [triedWindows addObject:(WineWindow*)event->window];
266 for (NSWindow* window in [keyWindows arrayByAddingObjectsFromArray:[self orderedWindows]])
268 if (![triedWindows containsObject:window] && [window canBecomeKeyWindow])
270 [window makeKeyWindow];
278 - (void) keyboardSelectionDidChange
280 TISInputSourceRef inputSource;
282 inputSource = TISCopyCurrentKeyboardLayoutInputSource();
286 uchr = TISGetInputSourceProperty(inputSource,
287 kTISPropertyUnicodeKeyLayoutData);
291 WineEventQueue* queue;
293 event.type = KEYBOARD_CHANGED;
295 event.keyboard_changed.keyboard_type = self.keyboardType;
296 event.keyboard_changed.iso_keyboard = (KBGetLayoutType(self.keyboardType) == kKeyboardISO);
297 event.keyboard_changed.uchr = CFDataCreateCopy(NULL, uchr);
299 if (event.keyboard_changed.uchr)
301 [eventQueuesLock lock];
303 for (queue in eventQueues)
305 CFRetain(event.keyboard_changed.uchr);
306 [queue postEvent:&event];
309 [eventQueuesLock unlock];
311 CFRelease(event.keyboard_changed.uchr);
315 CFRelease(inputSource);
319 - (CGFloat) primaryScreenHeight
321 if (!primaryScreenHeightValid)
323 NSArray* screens = [NSScreen screens];
326 primaryScreenHeight = NSHeight([[screens objectAtIndex:0] frame]);
327 primaryScreenHeightValid = TRUE;
330 return 1280; /* arbitrary value */
333 return primaryScreenHeight;
336 - (NSPoint) flippedMouseLocation:(NSPoint)point
338 /* This relies on the fact that Cocoa's mouse location points are
339 actually off by one (precisely because they were flipped from
340 Quartz screen coordinates using this same technique). */
341 point.y = [self primaryScreenHeight] - point.y;
345 - (void) flipRect:(NSRect*)rect
347 // We don't use -primaryScreenHeight here so there's no chance of having
348 // out-of-date cached info. This method is called infrequently enough
349 // that getting the screen height each time is not prohibitively expensive.
350 rect->origin.y = NSMaxY([[[NSScreen screens] objectAtIndex:0] frame]) - NSMaxY(*rect);
353 - (void) wineWindow:(WineWindow*)window
354 ordered:(NSWindowOrderingMode)order
355 relativeTo:(WineWindow*)otherWindow
363 [orderedWineWindows removeObjectIdenticalTo:window];
366 index = [orderedWineWindows indexOfObjectIdenticalTo:otherWindow];
367 if (index == NSNotFound)
373 for (otherWindow in orderedWineWindows)
375 if ([otherWindow levelWhenActive] <= [window levelWhenActive])
380 [orderedWineWindows insertObject:window atIndex:index];
385 [orderedWineWindows removeObjectIdenticalTo:window];
388 index = [orderedWineWindows indexOfObjectIdenticalTo:otherWindow];
389 if (index == NSNotFound)
390 index = [orderedWineWindows count];
395 for (otherWindow in orderedWineWindows)
397 if ([otherWindow levelWhenActive] < [window levelWhenActive])
402 [orderedWineWindows insertObject:window atIndex:index];
411 - (void) sendDisplaysChanged:(BOOL)activating
414 WineEventQueue* queue;
416 event.type = DISPLAYS_CHANGED;
418 event.displays_changed.activating = activating;
420 [eventQueuesLock lock];
421 for (queue in eventQueues)
422 [queue postEvent:&event];
423 [eventQueuesLock unlock];
426 // We can compare two modes directly using CFEqual, but that may require that
427 // they are identical to a level that we don't need. In particular, when the
428 // OS switches between the integrated and discrete GPUs, the set of display
429 // modes can change in subtle ways. We're interested in whether two modes
430 // match in their most salient features, even if they aren't identical.
431 - (BOOL) mode:(CGDisplayModeRef)mode1 matchesMode:(CGDisplayModeRef)mode2
433 NSString *encoding1, *encoding2;
434 uint32_t ioflags1, ioflags2, different;
435 double refresh1, refresh2;
437 if (CGDisplayModeGetWidth(mode1) != CGDisplayModeGetWidth(mode2)) return FALSE;
438 if (CGDisplayModeGetHeight(mode1) != CGDisplayModeGetHeight(mode2)) return FALSE;
440 encoding1 = [(NSString*)CGDisplayModeCopyPixelEncoding(mode1) autorelease];
441 encoding2 = [(NSString*)CGDisplayModeCopyPixelEncoding(mode2) autorelease];
442 if (![encoding1 isEqualToString:encoding2]) return FALSE;
444 ioflags1 = CGDisplayModeGetIOFlags(mode1);
445 ioflags2 = CGDisplayModeGetIOFlags(mode2);
446 different = ioflags1 ^ ioflags2;
447 if (different & (kDisplayModeValidFlag | kDisplayModeSafeFlag | kDisplayModeStretchedFlag |
448 kDisplayModeInterlacedFlag | kDisplayModeTelevisionFlag))
451 refresh1 = CGDisplayModeGetRefreshRate(mode1);
452 if (refresh1 == 0) refresh1 = 60;
453 refresh2 = CGDisplayModeGetRefreshRate(mode2);
454 if (refresh2 == 0) refresh2 = 60;
455 if (fabs(refresh1 - refresh2) > 0.1) return FALSE;
460 - (CGDisplayModeRef)modeMatchingMode:(CGDisplayModeRef)mode forDisplay:(CGDirectDisplayID)displayID
462 CGDisplayModeRef ret = NULL;
463 NSArray *modes = [(NSArray*)CGDisplayCopyAllDisplayModes(displayID, NULL) autorelease];
464 for (id candidateModeObject in modes)
466 CGDisplayModeRef candidateMode = (CGDisplayModeRef)candidateModeObject;
467 if ([self mode:candidateMode matchesMode:mode])
476 - (BOOL) setMode:(CGDisplayModeRef)mode forDisplay:(CGDirectDisplayID)displayID
479 NSNumber* displayIDKey = [NSNumber numberWithUnsignedInt:displayID];
480 CGDisplayModeRef currentMode, originalMode;
482 currentMode = CGDisplayCopyDisplayMode(displayID);
483 if (!currentMode) // Invalid display ID
486 if ([self mode:mode matchesMode:currentMode]) // Already there!
488 CGDisplayModeRelease(currentMode);
492 mode = [self modeMatchingMode:mode forDisplay:displayID];
495 CGDisplayModeRelease(currentMode);
499 originalMode = (CGDisplayModeRef)[originalDisplayModes objectForKey:displayIDKey];
501 originalMode = currentMode;
503 if ([self mode:mode matchesMode:originalMode])
505 if ([originalDisplayModes count] == 1) // If this is the last changed display, do a blanket reset
507 CGRestorePermanentDisplayConfiguration();
508 CGReleaseAllDisplays();
509 [originalDisplayModes removeAllObjects];
512 else // ... otherwise, try to restore just the one display
514 if (CGDisplaySetDisplayMode(displayID, mode, NULL) == CGDisplayNoErr)
516 [originalDisplayModes removeObjectForKey:displayIDKey];
523 if ([originalDisplayModes count] || CGCaptureAllDisplays() == CGDisplayNoErr)
525 if (CGDisplaySetDisplayMode(displayID, mode, NULL) == CGDisplayNoErr)
527 [originalDisplayModes setObject:(id)originalMode forKey:displayIDKey];
530 else if (![originalDisplayModes count])
532 CGRestorePermanentDisplayConfiguration();
533 CGReleaseAllDisplays();
538 CGDisplayModeRelease(currentMode);
542 [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
543 [(WineWindow*)obj adjustWindowLevel];
550 - (BOOL) areDisplaysCaptured
552 return ([originalDisplayModes count] > 0);
564 - (void) unhideCursor
569 cursorHidden = FALSE;
575 NSDictionary* frame = [cursorFrames objectAtIndex:cursorFrame];
576 CGImageRef cgimage = (CGImageRef)[frame objectForKey:@"image"];
577 NSImage* image = [[NSImage alloc] initWithCGImage:cgimage size:NSZeroSize];
578 CFDictionaryRef hotSpotDict = (CFDictionaryRef)[frame objectForKey:@"hotSpot"];
582 if (!CGPointMakeWithDictionaryRepresentation(hotSpotDict, &hotSpot))
583 hotSpot = CGPointZero;
584 cursor = [[NSCursor alloc] initWithImage:image hotSpot:NSPointFromCGPoint(hotSpot)];
591 - (void) nextCursorFrame:(NSTimer*)theTimer
594 NSTimeInterval duration;
598 if (cursorFrame >= [cursorFrames count])
602 frame = [cursorFrames objectAtIndex:cursorFrame];
603 duration = [[frame objectForKey:@"duration"] doubleValue];
604 date = [[theTimer fireDate] dateByAddingTimeInterval:duration];
605 [cursorTimer setFireDate:date];
608 - (void) setCursorWithFrames:(NSArray*)frames
610 if (self.cursorFrames == frames)
613 self.cursorFrames = frames;
615 [cursorTimer invalidate];
616 self.cursorTimer = nil;
620 if ([frames count] > 1)
622 NSDictionary* frame = [frames objectAtIndex:0];
623 NSTimeInterval duration = [[frame objectForKey:@"duration"] doubleValue];
624 NSDate* date = [NSDate dateWithTimeIntervalSinceNow:duration];
625 self.cursorTimer = [[[NSTimer alloc] initWithFireDate:date
628 selector:@selector(nextCursorFrame:)
630 repeats:YES] autorelease];
631 [[NSRunLoop currentRunLoop] addTimer:cursorTimer forMode:NSRunLoopCommonModes];
638 - (void) setApplicationIconFromCGImageArray:(NSArray*)images
640 NSImage* nsimage = nil;
644 NSSize bestSize = NSZeroSize;
647 nsimage = [[[NSImage alloc] initWithSize:NSZeroSize] autorelease];
649 for (image in images)
651 CGImageRef cgimage = (CGImageRef)image;
652 NSBitmapImageRep* imageRep = [[NSBitmapImageRep alloc] initWithCGImage:cgimage];
655 NSSize size = [imageRep size];
657 [nsimage addRepresentation:imageRep];
660 if (MIN(size.width, size.height) > MIN(bestSize.width, bestSize.height))
665 if ([[nsimage representations] count] && bestSize.width && bestSize.height)
666 [nsimage setSize:bestSize];
671 self.applicationIcon = nsimage;
672 [self setApplicationIconImage:nsimage];
675 - (void) handleCommandTab
679 NSRunningApplication* thisApp = [NSRunningApplication currentApplication];
680 NSRunningApplication* app;
681 NSRunningApplication* otherValidApp = nil;
683 if ([originalDisplayModes count])
685 CGRestorePermanentDisplayConfiguration();
686 CGReleaseAllDisplays();
687 [originalDisplayModes removeAllObjects];
690 for (app in [[NSWorkspace sharedWorkspace] runningApplications])
692 if (![app isEqual:thisApp] && !app.terminated &&
693 app.activationPolicy == NSApplicationActivationPolicyRegular)
697 // There's another visible app. Just hide ourselves and let
698 // the system activate the other app.
708 // Didn't find a visible GUI app. Try the Finder or, if that's not
709 // running, the first hidden GUI app. If even that doesn't work, we
710 // just fail to switch and remain the active app.
711 app = [[NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.finder"] lastObject];
712 if (!app) app = otherValidApp;
714 [app activateWithOptions:0];
719 * ---------- Cursor clipping methods ----------
721 * Neither Quartz nor Cocoa has an exact analog for Win32 cursor clipping.
722 * For one simple case, clipping to a 1x1 rectangle, Quartz does have an
723 * equivalent: CGAssociateMouseAndMouseCursorPosition(false). For the
724 * general case, we leverage that. We disassociate mouse movements from
725 * the cursor position and then move the cursor manually, keeping it within
726 * the clipping rectangle.
728 * Moving the cursor manually isn't enough. We need to modify the event
729 * stream so that the events have the new location, too. We need to do
730 * this at a point before the events enter Cocoa, so that Cocoa will assign
731 * the correct window to the event. So, we install a Quartz event tap to
734 * Also, there's a complication when we move the cursor. We use
735 * CGWarpMouseCursorPosition(). That doesn't generate mouse movement
736 * events, but the change of cursor position is incorporated into the
737 * deltas of the next mouse move event. When the mouse is disassociated
738 * from the cursor position, we need the deltas to only reflect actual
739 * device movement, not programmatic changes. So, the event tap cancels
740 * out the change caused by our calls to CGWarpMouseCursorPosition().
742 - (void) clipCursorLocation:(CGPoint*)location
744 if (location->x < CGRectGetMinX(cursorClipRect))
745 location->x = CGRectGetMinX(cursorClipRect);
746 if (location->y < CGRectGetMinY(cursorClipRect))
747 location->y = CGRectGetMinY(cursorClipRect);
748 if (location->x > CGRectGetMaxX(cursorClipRect) - 1)
749 location->x = CGRectGetMaxX(cursorClipRect) - 1;
750 if (location->y > CGRectGetMaxY(cursorClipRect) - 1)
751 location->y = CGRectGetMaxY(cursorClipRect) - 1;
754 - (BOOL) warpCursorTo:(CGPoint*)newLocation from:(const CGPoint*)currentLocation
759 oldLocation = *currentLocation;
761 oldLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
763 if (!CGPointEqualToPoint(oldLocation, *newLocation))
765 WarpRecord* warpRecord = [[[WarpRecord alloc] init] autorelease];
768 warpRecord.from = oldLocation;
769 warpRecord.timeBefore = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
771 /* Actually move the cursor. */
772 err = CGWarpMouseCursorPosition(*newLocation);
773 if (err != kCGErrorSuccess)
776 warpRecord.timeAfter = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
777 *newLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
779 if (!CGPointEqualToPoint(oldLocation, *newLocation))
781 warpRecord.to = *newLocation;
782 [warpRecords addObject:warpRecord];
789 - (BOOL) isMouseMoveEventType:(CGEventType)type
793 case kCGEventMouseMoved:
794 case kCGEventLeftMouseDragged:
795 case kCGEventRightMouseDragged:
796 case kCGEventOtherMouseDragged:
803 - (int) warpsFinishedByEventTime:(CGEventTimestamp)eventTime location:(CGPoint)eventLocation
805 int warpsFinished = 0;
806 for (WarpRecord* warpRecord in warpRecords)
808 if (warpRecord.timeAfter < eventTime ||
809 (warpRecord.timeBefore <= eventTime && CGPointEqualToPoint(eventLocation, warpRecord.to)))
815 return warpsFinished;
818 - (CGEventRef) eventTapWithProxy:(CGEventTapProxy)proxy
819 type:(CGEventType)type
820 event:(CGEventRef)event
822 CGEventTimestamp eventTime;
823 CGPoint eventLocation, cursorLocation;
825 if (type == kCGEventTapDisabledByUserInput)
827 if (type == kCGEventTapDisabledByTimeout)
829 CGEventTapEnable(cursorClippingEventTap, TRUE);
836 eventTime = CGEventGetTimestamp(event);
837 lastEventTapEventTime = eventTime / (double)NSEC_PER_SEC;
839 eventLocation = CGEventGetLocation(event);
841 cursorLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
843 if ([self isMouseMoveEventType:type])
845 double deltaX, deltaY;
846 int warpsFinished = [self warpsFinishedByEventTime:eventTime location:eventLocation];
849 deltaX = CGEventGetDoubleValueField(event, kCGMouseEventDeltaX);
850 deltaY = CGEventGetDoubleValueField(event, kCGMouseEventDeltaY);
852 for (i = 0; i < warpsFinished; i++)
854 WarpRecord* warpRecord = [warpRecords objectAtIndex:0];
855 deltaX -= warpRecord.to.x - warpRecord.from.x;
856 deltaY -= warpRecord.to.y - warpRecord.from.y;
857 [warpRecords removeObjectAtIndex:0];
862 CGEventSetDoubleValueField(event, kCGMouseEventDeltaX, deltaX);
863 CGEventSetDoubleValueField(event, kCGMouseEventDeltaY, deltaY);
866 synthesizedLocation.x += deltaX;
867 synthesizedLocation.y += deltaY;
870 // If the event is destined for another process, don't clip it. This may
871 // happen if the user activates Exposé or Mission Control. In that case,
872 // our app does not resign active status, so clipping is still in effect,
873 // but the cursor should not actually be clipped.
875 // In addition, the fact that mouse moves may have been delivered to a
876 // different process means we have to treat the next one we receive as
877 // absolute rather than relative.
878 if (CGEventGetIntegerValueField(event, kCGEventTargetUnixProcessID) == getpid())
879 [self clipCursorLocation:&synthesizedLocation];
881 lastSetCursorPositionTime = lastEventTapEventTime;
883 [self warpCursorTo:&synthesizedLocation from:&cursorLocation];
884 if (!CGPointEqualToPoint(eventLocation, synthesizedLocation))
885 CGEventSetLocation(event, synthesizedLocation);
890 CGEventRef WineAppEventTapCallBack(CGEventTapProxy proxy, CGEventType type,
891 CGEventRef event, void *refcon)
893 WineApplication* app = refcon;
894 return [app eventTapWithProxy:proxy type:type event:event];
897 - (BOOL) installEventTap
899 ProcessSerialNumber psn;
901 CGEventMask mask = CGEventMaskBit(kCGEventLeftMouseDown) |
902 CGEventMaskBit(kCGEventLeftMouseUp) |
903 CGEventMaskBit(kCGEventRightMouseDown) |
904 CGEventMaskBit(kCGEventRightMouseUp) |
905 CGEventMaskBit(kCGEventMouseMoved) |
906 CGEventMaskBit(kCGEventLeftMouseDragged) |
907 CGEventMaskBit(kCGEventRightMouseDragged) |
908 CGEventMaskBit(kCGEventOtherMouseDown) |
909 CGEventMaskBit(kCGEventOtherMouseUp) |
910 CGEventMaskBit(kCGEventOtherMouseDragged) |
911 CGEventMaskBit(kCGEventScrollWheel);
912 CFRunLoopSourceRef source;
914 OSErr (*pGetCurrentProcess)(ProcessSerialNumber* PSN);
916 if (cursorClippingEventTap)
919 // We need to get the Mac GetCurrentProcess() from the ApplicationServices
920 // framework with dlsym() because the Win32 function of the same name
922 appServices = dlopen("/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices", RTLD_LAZY);
926 pGetCurrentProcess = dlsym(appServices, "GetCurrentProcess");
927 if (!pGetCurrentProcess)
929 dlclose(appServices);
933 err = pGetCurrentProcess(&psn);
934 dlclose(appServices);
938 // We create an annotated session event tap rather than a process-specific
939 // event tap because we need to programmatically move the cursor even when
940 // mouse moves are directed to other processes. We disable our tap when
941 // other processes are active, but things like Exposé are handled by other
942 // processes even when we remain active.
943 cursorClippingEventTap = CGEventTapCreate(kCGAnnotatedSessionEventTap, kCGHeadInsertEventTap,
944 kCGEventTapOptionDefault, mask, WineAppEventTapCallBack, self);
945 if (!cursorClippingEventTap)
948 CGEventTapEnable(cursorClippingEventTap, FALSE);
950 source = CFMachPortCreateRunLoopSource(NULL, cursorClippingEventTap, 0);
953 CFRelease(cursorClippingEventTap);
954 cursorClippingEventTap = NULL;
958 CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
963 - (BOOL) setCursorPosition:(CGPoint)pos
969 [self clipCursorLocation:&pos];
971 synthesizedLocation = pos;
972 ret = [self warpCursorTo:&synthesizedLocation from:NULL];
975 // We want to discard mouse-move events that have already been
976 // through the event tap, because it's too late to account for
977 // the setting of the cursor position with them. However, the
978 // events that may be queued with times after that but before
979 // the above warp can still be used. So, use the last event
980 // tap event time so that -sendEvent: doesn't discard them.
981 lastSetCursorPositionTime = lastEventTapEventTime;
986 ret = (CGWarpMouseCursorPosition(pos) == kCGErrorSuccess);
988 lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
993 WineEventQueue* queue;
995 // Discard all pending mouse move events.
996 [eventQueuesLock lock];
997 for (queue in eventQueues)
999 [queue discardEventsMatchingMask:event_mask_for_type(MOUSE_MOVED) |
1000 event_mask_for_type(MOUSE_MOVED_ABSOLUTE)
1003 [eventQueuesLock unlock];
1009 - (void) activateCursorClipping
1013 CGEventTapEnable(cursorClippingEventTap, TRUE);
1014 [self setCursorPosition:NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]])];
1018 - (void) deactivateCursorClipping
1022 CGEventTapEnable(cursorClippingEventTap, FALSE);
1023 [warpRecords removeAllObjects];
1024 lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
1028 - (BOOL) startClippingCursor:(CGRect)rect
1032 if (!cursorClippingEventTap && ![self installEventTap])
1035 err = CGAssociateMouseAndMouseCursorPosition(false);
1036 if (err != kCGErrorSuccess)
1039 clippingCursor = TRUE;
1040 cursorClipRect = rect;
1041 if ([self isActive])
1042 [self activateCursorClipping];
1047 - (BOOL) stopClippingCursor
1049 CGError err = CGAssociateMouseAndMouseCursorPosition(true);
1050 if (err != kCGErrorSuccess)
1053 [self deactivateCursorClipping];
1054 clippingCursor = FALSE;
1061 * ---------- NSApplication method overrides ----------
1063 - (void) sendEvent:(NSEvent*)anEvent
1065 NSEventType type = [anEvent type];
1066 if (type == NSFlagsChanged)
1067 self.lastFlagsChanged = anEvent;
1069 [super sendEvent:anEvent];
1071 if (type == NSMouseMoved || type == NSLeftMouseDragged ||
1072 type == NSRightMouseDragged || type == NSOtherMouseDragged)
1074 WineWindow* targetWindow;
1076 /* Because of the way -[NSWindow setAcceptsMouseMovedEvents:] works, the
1077 event indicates its window is the main window, even if the cursor is
1078 over a different window. Find the actual WineWindow that is under the
1079 cursor and post the event as being for that window. */
1080 if (type == NSMouseMoved)
1082 CGPoint cgpoint = CGEventGetLocation([anEvent CGEvent]);
1083 NSPoint point = [self flippedMouseLocation:NSPointFromCGPoint(cgpoint)];
1084 NSInteger windowUnderNumber;
1086 windowUnderNumber = [NSWindow windowNumberAtPoint:point
1087 belowWindowWithWindowNumber:0];
1088 targetWindow = (WineWindow*)[self windowWithWindowNumber:windowUnderNumber];
1091 targetWindow = (WineWindow*)[anEvent window];
1093 if ([targetWindow isKindOfClass:[WineWindow class]])
1095 BOOL absolute = forceNextMouseMoveAbsolute || (targetWindow != lastTargetWindow);
1096 forceNextMouseMoveAbsolute = FALSE;
1098 // If we recently warped the cursor (other than in our cursor-clipping
1099 // event tap), discard mouse move events until we see an event which is
1100 // later than that time.
1101 if (lastSetCursorPositionTime)
1103 if ([anEvent timestamp] <= lastSetCursorPositionTime)
1106 lastSetCursorPositionTime = 0;
1110 [targetWindow postMouseMovedEvent:anEvent absolute:absolute];
1111 lastTargetWindow = targetWindow;
1113 else if (lastTargetWindow)
1115 [[NSCursor arrowCursor] set];
1116 [self unhideCursor];
1117 lastTargetWindow = nil;
1120 else if (type == NSLeftMouseDown || type == NSLeftMouseUp ||
1121 type == NSRightMouseDown || type == NSRightMouseUp ||
1122 type == NSOtherMouseDown || type == NSOtherMouseUp ||
1123 type == NSScrollWheel)
1125 // Since mouse button and scroll wheel events deliver absolute cursor
1126 // position, the accumulating delta from move events is invalidated.
1127 // Make sure next mouse move event starts over from an absolute baseline.
1128 forceNextMouseMoveAbsolute = TRUE;
1130 else if (type == NSKeyDown && ![anEvent isARepeat] && [anEvent keyCode] == kVK_Tab)
1132 NSUInteger modifiers = [anEvent modifierFlags];
1133 if ((modifiers & NSCommandKeyMask) &&
1134 !(modifiers & (NSControlKeyMask | NSAlternateKeyMask)))
1136 // Command-Tab and Command-Shift-Tab would normally be intercepted
1137 // by the system to switch applications. If we're seeing it, it's
1138 // presumably because we've captured the displays, preventing
1139 // normal application switching. Do it manually.
1140 [self handleCommandTab];
1147 * ---------- NSApplicationDelegate methods ----------
1149 - (void)applicationDidBecomeActive:(NSNotification *)notification
1151 [self activateCursorClipping];
1153 [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
1154 WineWindow* window = obj;
1155 if ([window levelWhenActive] != [window level])
1156 [window setLevel:[window levelWhenActive]];
1159 // If a Wine process terminates abruptly while it has the display captured
1160 // and switched to a different resolution, Mac OS X will uncapture the
1161 // displays and switch their resolutions back. However, the other Wine
1162 // processes won't have their notion of the desktop rect changed back.
1163 // This can lead them to refuse to draw or acknowledge clicks in certain
1164 // portions of their windows.
1166 // To solve this, we synthesize a displays-changed event whenever we're
1167 // activated. This will provoke a re-synchronization of Wine's notion of
1168 // the desktop rect with the actual state.
1169 [self sendDisplaysChanged:TRUE];
1171 // The cursor probably moved while we were inactive. Accumulated mouse
1172 // movement deltas are invalidated. Make sure the next mouse move event
1173 // starts over from an absolute baseline.
1174 forceNextMouseMoveAbsolute = TRUE;
1177 - (void)applicationDidChangeScreenParameters:(NSNotification *)notification
1179 primaryScreenHeightValid = FALSE;
1180 [self sendDisplaysChanged:FALSE];
1182 // When the display configuration changes, the cursor position may jump.
1183 // Accumulated mouse movement deltas are invalidated. Make sure the next
1184 // mouse move event starts over from an absolute baseline.
1185 forceNextMouseMoveAbsolute = TRUE;
1188 - (void)applicationDidResignActive:(NSNotification *)notification
1191 WineEventQueue* queue;
1193 [self invalidateGotFocusEvents];
1195 event.type = APP_DEACTIVATED;
1196 event.window = NULL;
1198 [eventQueuesLock lock];
1199 for (queue in eventQueues)
1200 [queue postEvent:&event];
1201 [eventQueuesLock unlock];
1204 - (void)applicationWillFinishLaunching:(NSNotification *)notification
1206 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
1208 [nc addObserverForName:NSWindowDidBecomeKeyNotification
1211 usingBlock:^(NSNotification *note){
1212 NSWindow* window = [note object];
1213 [keyWindows removeObjectIdenticalTo:window];
1214 [keyWindows insertObject:window atIndex:0];
1217 [nc addObserverForName:NSWindowWillCloseNotification
1219 queue:[NSOperationQueue mainQueue]
1220 usingBlock:^(NSNotification *note){
1221 NSWindow* window = [note object];
1222 [keyWindows removeObjectIdenticalTo:window];
1223 [orderedWineWindows removeObjectIdenticalTo:window];
1224 if (window == lastTargetWindow)
1225 lastTargetWindow = nil;
1228 [nc addObserver:self
1229 selector:@selector(keyboardSelectionDidChange)
1230 name:NSTextInputContextKeyboardSelectionDidChangeNotification
1233 /* The above notification isn't sent unless the NSTextInputContext
1234 class has initialized itself. Poke it. */
1235 [NSTextInputContext self];
1237 self.keyboardType = LMGetKbdType();
1240 - (void)applicationWillResignActive:(NSNotification *)notification
1242 [self deactivateCursorClipping];
1244 [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
1245 WineWindow* window = obj;
1246 NSInteger level = window.floating ? NSFloatingWindowLevel : NSNormalWindowLevel;
1247 if ([window level] > level)
1248 [window setLevel:level];
1252 /***********************************************************************
1255 * Run-loop-source perform callback. Pull request blocks from the
1256 * array of queued requests and invoke them.
1258 static void PerformRequest(void *info)
1260 WineApplication* app = (WineApplication*)NSApp;
1264 __block dispatch_block_t block;
1266 dispatch_sync(app->requestsManipQueue, ^{
1267 if ([app->requests count])
1269 block = (dispatch_block_t)[[app->requests objectAtIndex:0] retain];
1270 [app->requests removeObjectAtIndex:0];
1284 /***********************************************************************
1287 * Run a block on the main thread asynchronously.
1289 void OnMainThreadAsync(dispatch_block_t block)
1291 WineApplication* app = (WineApplication*)NSApp;
1293 block = [block copy];
1294 dispatch_sync(app->requestsManipQueue, ^{
1295 [app->requests addObject:block];
1298 CFRunLoopSourceSignal(app->requestSource);
1299 CFRunLoopWakeUp(CFRunLoopGetMain());
1304 /***********************************************************************
1307 void LogError(const char* func, NSString* format, ...)
1310 va_start(args, format);
1311 LogErrorv(func, format, args);
1315 /***********************************************************************
1318 void LogErrorv(const char* func, NSString* format, va_list args)
1320 NSString* message = [[NSString alloc] initWithFormat:format arguments:args];
1321 fprintf(stderr, "err:%s:%s", func, [message UTF8String]);
1325 /***********************************************************************
1326 * macdrv_window_rejected_focus
1328 * Pass focus to the next window that hasn't already rejected this same
1329 * WINDOW_GOT_FOCUS event.
1331 void macdrv_window_rejected_focus(const macdrv_event *event)
1334 [NSApp windowRejectedFocusEvent:event];
1338 /***********************************************************************
1339 * macdrv_get_keyboard_layout
1341 * Returns the keyboard layout uchr data.
1343 CFDataRef macdrv_copy_keyboard_layout(CGEventSourceKeyboardType* keyboard_type, int* is_iso)
1345 __block CFDataRef result = NULL;
1348 TISInputSourceRef inputSource;
1350 inputSource = TISCopyCurrentKeyboardLayoutInputSource();
1353 CFDataRef uchr = TISGetInputSourceProperty(inputSource,
1354 kTISPropertyUnicodeKeyLayoutData);
1355 result = CFDataCreateCopy(NULL, uchr);
1356 CFRelease(inputSource);
1358 *keyboard_type = ((WineApplication*)NSApp).keyboardType;
1359 *is_iso = (KBGetLayoutType(*keyboard_type) == kKeyboardISO);
1366 /***********************************************************************
1369 * Play the beep sound configured by the user in System Preferences.
1371 void macdrv_beep(void)
1373 OnMainThreadAsync(^{
1378 /***********************************************************************
1379 * macdrv_set_display_mode
1381 int macdrv_set_display_mode(const struct macdrv_display* display,
1382 CGDisplayModeRef display_mode)
1387 ret = [NSApp setMode:display_mode forDisplay:display->displayID];
1393 /***********************************************************************
1398 * If name is non-NULL, it is a selector for a class method on NSCursor
1399 * identifying the cursor to set. In that case, frames is ignored. If
1400 * name is NULL, then frames is used.
1402 * frames is an array of dictionaries. Each dictionary is a frame of
1403 * an animated cursor. Under the key "image" is a CGImage for the
1404 * frame. Under the key "duration" is a CFNumber time interval, in
1405 * seconds, for how long that frame is presented before proceeding to
1406 * the next frame. Under the key "hotSpot" is a CFDictionary encoding a
1407 * CGPoint, to be decoded using CGPointMakeWithDictionaryRepresentation().
1408 * This is the hot spot, measured in pixels down and to the right of the
1409 * top-left corner of the image.
1411 * If the array has exactly 1 element, the cursor is static, not
1412 * animated. If frames is NULL or has 0 elements, the cursor is hidden.
1414 void macdrv_set_cursor(CFStringRef name, CFArrayRef frames)
1418 sel = NSSelectorFromString((NSString*)name);
1421 OnMainThreadAsync(^{
1422 NSCursor* cursor = [NSCursor performSelector:sel];
1423 [NSApp setCursorWithFrames:nil];
1425 [NSApp unhideCursor];
1430 NSArray* nsframes = (NSArray*)frames;
1431 if ([nsframes count])
1433 OnMainThreadAsync(^{
1434 [NSApp setCursorWithFrames:nsframes];
1439 OnMainThreadAsync(^{
1440 [NSApp setCursorWithFrames:nil];
1447 /***********************************************************************
1448 * macdrv_get_cursor_position
1450 * Obtains the current cursor position. Returns zero on failure,
1451 * non-zero on success.
1453 int macdrv_get_cursor_position(CGPoint *pos)
1456 NSPoint location = [NSEvent mouseLocation];
1457 location = [NSApp flippedMouseLocation:location];
1458 *pos = NSPointToCGPoint(location);
1464 /***********************************************************************
1465 * macdrv_set_cursor_position
1467 * Sets the cursor position without generating events. Returns zero on
1468 * failure, non-zero on success.
1470 int macdrv_set_cursor_position(CGPoint pos)
1475 ret = [NSApp setCursorPosition:pos];
1481 /***********************************************************************
1482 * macdrv_clip_cursor
1484 * Sets the cursor cursor clipping rectangle. If the rectangle is equal
1485 * to or larger than the whole desktop region, the cursor is unclipped.
1486 * Returns zero on failure, non-zero on success.
1488 int macdrv_clip_cursor(CGRect rect)
1493 BOOL clipping = FALSE;
1495 if (!CGRectIsInfinite(rect))
1497 NSRect nsrect = NSRectFromCGRect(rect);
1500 /* Convert the rectangle from top-down coords to bottom-up. */
1501 [NSApp flipRect:&nsrect];
1504 for (screen in [NSScreen screens])
1506 if (!NSContainsRect(nsrect, [screen frame]))
1515 ret = [NSApp startClippingCursor:rect];
1517 ret = [NSApp stopClippingCursor];
1523 /***********************************************************************
1524 * macdrv_set_application_icon
1526 * Set the application icon. The images array contains CGImages. If
1527 * there are more than one, then they represent different sizes or
1528 * color depths from the icon resource. If images is NULL or empty,
1529 * restores the default application image.
1531 void macdrv_set_application_icon(CFArrayRef images)
1533 NSArray* imageArray = (NSArray*)images;
1535 OnMainThreadAsync(^{
1536 [NSApp setApplicationIconFromCGImageArray:imageArray];