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;
62 static void PerformRequest(void *info);
67 @implementation WineApplication
69 @synthesize keyboardType, lastFlagsChanged;
70 @synthesize orderedWineWindows;
71 @synthesize cursorFrames, cursorTimer;
78 CFRunLoopSourceContext context = { 0 };
79 context.perform = PerformRequest;
80 requestSource = CFRunLoopSourceCreate(NULL, 0, &context);
86 CFRunLoopAddSource(CFRunLoopGetMain(), requestSource, kCFRunLoopCommonModes);
87 CFRunLoopAddSource(CFRunLoopGetMain(), requestSource, (CFStringRef)WineAppWaitQueryResponseMode);
89 requests = [[NSMutableArray alloc] init];
90 requestsManipQueue = dispatch_queue_create("org.winehq.WineAppRequestManipQueue", NULL);
92 eventQueues = [[NSMutableArray alloc] init];
93 eventQueuesLock = [[NSLock alloc] init];
95 keyWindows = [[NSMutableArray alloc] init];
96 orderedWineWindows = [[NSMutableArray alloc] init];
98 originalDisplayModes = [[NSMutableDictionary alloc] init];
100 warpRecords = [[NSMutableArray alloc] init];
102 if (!requests || !requestsManipQueue || !eventQueues || !eventQueuesLock ||
103 !keyWindows || !orderedWineWindows || !originalDisplayModes || !warpRecords)
114 [warpRecords release];
115 [cursorTimer release];
116 [cursorFrames release];
117 [originalDisplayModes release];
118 [orderedWineWindows release];
119 [keyWindows release];
120 [eventQueues release];
121 [eventQueuesLock release];
122 if (requestsManipQueue) dispatch_release(requestsManipQueue);
126 CFRunLoopSourceInvalidate(requestSource);
127 CFRelease(requestSource);
132 - (void) transformProcessToForeground
134 if ([self activationPolicy] != NSApplicationActivationPolicyRegular)
138 NSString* bundleName;
142 [self setActivationPolicy:NSApplicationActivationPolicyRegular];
143 [self activateIgnoringOtherApps:YES];
145 mainMenu = [[[NSMenu alloc] init] autorelease];
147 submenu = [[[NSMenu alloc] initWithTitle:@"Wine"] autorelease];
148 bundleName = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString*)kCFBundleNameKey];
149 if ([bundleName length])
150 title = [NSString stringWithFormat:@"Quit %@", bundleName];
153 item = [submenu addItemWithTitle:title action:@selector(terminate:) keyEquivalent:@"q"];
154 [item setKeyEquivalentModifierMask:NSCommandKeyMask | NSAlternateKeyMask];
155 item = [[[NSMenuItem alloc] init] autorelease];
156 [item setTitle:@"Wine"];
157 [item setSubmenu:submenu];
158 [mainMenu addItem:item];
160 submenu = [[[NSMenu alloc] initWithTitle:@"Window"] autorelease];
161 [submenu addItemWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@""];
162 [submenu addItemWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""];
163 [submenu addItem:[NSMenuItem separatorItem]];
164 [submenu addItemWithTitle:@"Bring All to Front" action:@selector(arrangeInFront:) keyEquivalent:@""];
165 item = [[[NSMenuItem alloc] init] autorelease];
166 [item setTitle:@"Window"];
167 [item setSubmenu:submenu];
168 [mainMenu addItem:item];
170 [self setMainMenu:mainMenu];
171 [self setWindowsMenu:submenu];
175 - (BOOL) waitUntilQueryDone:(int*)done timeout:(NSDate*)timeout processEvents:(BOOL)processEvents
177 PerformRequest(NULL);
183 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
184 NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask
186 inMode:NSDefaultRunLoopMode
189 [NSApp sendEvent:event];
193 [[NSRunLoop currentRunLoop] runMode:WineAppWaitQueryResponseMode beforeDate:timeout];
194 } while (!*done && [timeout timeIntervalSinceNow] >= 0);
199 - (BOOL) registerEventQueue:(WineEventQueue*)queue
201 [eventQueuesLock lock];
202 [eventQueues addObject:queue];
203 [eventQueuesLock unlock];
207 - (void) unregisterEventQueue:(WineEventQueue*)queue
209 [eventQueuesLock lock];
210 [eventQueues removeObjectIdenticalTo:queue];
211 [eventQueuesLock unlock];
214 - (void) computeEventTimeAdjustmentFromTicks:(unsigned long long)tickcount uptime:(uint64_t)uptime_ns
216 eventTimeAdjustment = (tickcount / 1000.0) - (uptime_ns / (double)NSEC_PER_SEC);
219 - (double) ticksForEventTime:(NSTimeInterval)eventTime
221 return (eventTime + eventTimeAdjustment) * 1000;
224 /* Invalidate old focus offers across all queues. */
225 - (void) invalidateGotFocusEvents
227 WineEventQueue* queue;
231 [eventQueuesLock lock];
232 for (queue in eventQueues)
234 [queue discardEventsMatchingMask:event_mask_for_type(WINDOW_GOT_FOCUS)
237 [eventQueuesLock unlock];
240 - (void) windowGotFocus:(WineWindow*)window
244 [NSApp invalidateGotFocusEvents];
246 event.type = WINDOW_GOT_FOCUS;
247 event.window = (macdrv_window)[window retain];
248 event.window_got_focus.serial = windowFocusSerial;
250 event.window_got_focus.tried_windows = [triedWindows retain];
252 event.window_got_focus.tried_windows = [[NSMutableSet alloc] init];
253 [window.queue postEvent:&event];
256 - (void) windowRejectedFocusEvent:(const macdrv_event*)event
258 if (event->window_got_focus.serial == windowFocusSerial)
260 triedWindows = (NSMutableSet*)event->window_got_focus.tried_windows;
261 [triedWindows addObject:(WineWindow*)event->window];
262 for (NSWindow* window in [keyWindows arrayByAddingObjectsFromArray:[self orderedWindows]])
264 if (![triedWindows containsObject:window] && [window canBecomeKeyWindow])
266 [window makeKeyWindow];
274 - (void) keyboardSelectionDidChange
276 TISInputSourceRef inputSource;
278 inputSource = TISCopyCurrentKeyboardLayoutInputSource();
282 uchr = TISGetInputSourceProperty(inputSource,
283 kTISPropertyUnicodeKeyLayoutData);
287 WineEventQueue* queue;
289 event.type = KEYBOARD_CHANGED;
291 event.keyboard_changed.keyboard_type = self.keyboardType;
292 event.keyboard_changed.iso_keyboard = (KBGetLayoutType(self.keyboardType) == kKeyboardISO);
293 event.keyboard_changed.uchr = CFDataCreateCopy(NULL, uchr);
295 if (event.keyboard_changed.uchr)
297 [eventQueuesLock lock];
299 for (queue in eventQueues)
301 CFRetain(event.keyboard_changed.uchr);
302 [queue postEvent:&event];
305 [eventQueuesLock unlock];
307 CFRelease(event.keyboard_changed.uchr);
311 CFRelease(inputSource);
315 - (CGFloat) primaryScreenHeight
317 if (!primaryScreenHeightValid)
319 NSArray* screens = [NSScreen screens];
322 primaryScreenHeight = NSHeight([[screens objectAtIndex:0] frame]);
323 primaryScreenHeightValid = TRUE;
326 return 1280; /* arbitrary value */
329 return primaryScreenHeight;
332 - (NSPoint) flippedMouseLocation:(NSPoint)point
334 /* This relies on the fact that Cocoa's mouse location points are
335 actually off by one (precisely because they were flipped from
336 Quartz screen coordinates using this same technique). */
337 point.y = [self primaryScreenHeight] - point.y;
341 - (void) flipRect:(NSRect*)rect
343 // We don't use -primaryScreenHeight here so there's no chance of having
344 // out-of-date cached info. This method is called infrequently enough
345 // that getting the screen height each time is not prohibitively expensive.
346 rect->origin.y = NSMaxY([[[NSScreen screens] objectAtIndex:0] frame]) - NSMaxY(*rect);
349 - (void) wineWindow:(WineWindow*)window
350 ordered:(NSWindowOrderingMode)order
351 relativeTo:(WineWindow*)otherWindow
359 [orderedWineWindows removeObjectIdenticalTo:window];
362 index = [orderedWineWindows indexOfObjectIdenticalTo:otherWindow];
363 if (index == NSNotFound)
369 for (otherWindow in orderedWineWindows)
371 if ([otherWindow levelWhenActive] <= [window levelWhenActive])
376 [orderedWineWindows insertObject:window atIndex:index];
381 [orderedWineWindows removeObjectIdenticalTo:window];
384 index = [orderedWineWindows indexOfObjectIdenticalTo:otherWindow];
385 if (index == NSNotFound)
386 index = [orderedWineWindows count];
391 for (otherWindow in orderedWineWindows)
393 if ([otherWindow levelWhenActive] < [window levelWhenActive])
398 [orderedWineWindows insertObject:window atIndex:index];
407 - (void) sendDisplaysChanged:(BOOL)activating
410 WineEventQueue* queue;
412 event.type = DISPLAYS_CHANGED;
414 event.displays_changed.activating = activating;
416 [eventQueuesLock lock];
417 for (queue in eventQueues)
418 [queue postEvent:&event];
419 [eventQueuesLock unlock];
422 // We can compare two modes directly using CFEqual, but that may require that
423 // they are identical to a level that we don't need. In particular, when the
424 // OS switches between the integrated and discrete GPUs, the set of display
425 // modes can change in subtle ways. We're interested in whether two modes
426 // match in their most salient features, even if they aren't identical.
427 - (BOOL) mode:(CGDisplayModeRef)mode1 matchesMode:(CGDisplayModeRef)mode2
429 NSString *encoding1, *encoding2;
430 uint32_t ioflags1, ioflags2, different;
431 double refresh1, refresh2;
433 if (CGDisplayModeGetWidth(mode1) != CGDisplayModeGetWidth(mode2)) return FALSE;
434 if (CGDisplayModeGetHeight(mode1) != CGDisplayModeGetHeight(mode2)) return FALSE;
436 encoding1 = [(NSString*)CGDisplayModeCopyPixelEncoding(mode1) autorelease];
437 encoding2 = [(NSString*)CGDisplayModeCopyPixelEncoding(mode2) autorelease];
438 if (![encoding1 isEqualToString:encoding2]) return FALSE;
440 ioflags1 = CGDisplayModeGetIOFlags(mode1);
441 ioflags2 = CGDisplayModeGetIOFlags(mode2);
442 different = ioflags1 ^ ioflags2;
443 if (different & (kDisplayModeValidFlag | kDisplayModeSafeFlag | kDisplayModeStretchedFlag |
444 kDisplayModeInterlacedFlag | kDisplayModeTelevisionFlag))
447 refresh1 = CGDisplayModeGetRefreshRate(mode1);
448 if (refresh1 == 0) refresh1 = 60;
449 refresh2 = CGDisplayModeGetRefreshRate(mode2);
450 if (refresh2 == 0) refresh2 = 60;
451 if (fabs(refresh1 - refresh2) > 0.1) return FALSE;
456 - (CGDisplayModeRef)modeMatchingMode:(CGDisplayModeRef)mode forDisplay:(CGDirectDisplayID)displayID
458 CGDisplayModeRef ret = NULL;
459 NSArray *modes = [(NSArray*)CGDisplayCopyAllDisplayModes(displayID, NULL) autorelease];
460 for (id candidateModeObject in modes)
462 CGDisplayModeRef candidateMode = (CGDisplayModeRef)candidateModeObject;
463 if ([self mode:candidateMode matchesMode:mode])
472 - (BOOL) setMode:(CGDisplayModeRef)mode forDisplay:(CGDirectDisplayID)displayID
475 NSNumber* displayIDKey = [NSNumber numberWithUnsignedInt:displayID];
476 CGDisplayModeRef currentMode, originalMode;
478 currentMode = CGDisplayCopyDisplayMode(displayID);
479 if (!currentMode) // Invalid display ID
482 if ([self mode:mode matchesMode:currentMode]) // Already there!
484 CGDisplayModeRelease(currentMode);
488 mode = [self modeMatchingMode:mode forDisplay:displayID];
491 CGDisplayModeRelease(currentMode);
495 originalMode = (CGDisplayModeRef)[originalDisplayModes objectForKey:displayIDKey];
497 originalMode = currentMode;
499 if ([self mode:mode matchesMode:originalMode])
501 if ([originalDisplayModes count] == 1) // If this is the last changed display, do a blanket reset
503 CGRestorePermanentDisplayConfiguration();
504 CGReleaseAllDisplays();
505 [originalDisplayModes removeAllObjects];
508 else // ... otherwise, try to restore just the one display
510 if (CGDisplaySetDisplayMode(displayID, mode, NULL) == CGDisplayNoErr)
512 [originalDisplayModes removeObjectForKey:displayIDKey];
519 if ([originalDisplayModes count] || CGCaptureAllDisplays() == CGDisplayNoErr)
521 if (CGDisplaySetDisplayMode(displayID, mode, NULL) == CGDisplayNoErr)
523 [originalDisplayModes setObject:(id)originalMode forKey:displayIDKey];
526 else if (![originalDisplayModes count])
528 CGRestorePermanentDisplayConfiguration();
529 CGReleaseAllDisplays();
534 CGDisplayModeRelease(currentMode);
538 [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
539 [(WineWindow*)obj adjustWindowLevel];
546 - (BOOL) areDisplaysCaptured
548 return ([originalDisplayModes count] > 0);
560 - (void) unhideCursor
565 cursorHidden = FALSE;
571 NSDictionary* frame = [cursorFrames objectAtIndex:cursorFrame];
572 CGImageRef cgimage = (CGImageRef)[frame objectForKey:@"image"];
573 NSImage* image = [[NSImage alloc] initWithCGImage:cgimage size:NSZeroSize];
574 CFDictionaryRef hotSpotDict = (CFDictionaryRef)[frame objectForKey:@"hotSpot"];
578 if (!CGPointMakeWithDictionaryRepresentation(hotSpotDict, &hotSpot))
579 hotSpot = CGPointZero;
580 cursor = [[NSCursor alloc] initWithImage:image hotSpot:NSPointFromCGPoint(hotSpot)];
587 - (void) nextCursorFrame:(NSTimer*)theTimer
590 NSTimeInterval duration;
594 if (cursorFrame >= [cursorFrames count])
598 frame = [cursorFrames objectAtIndex:cursorFrame];
599 duration = [[frame objectForKey:@"duration"] doubleValue];
600 date = [[theTimer fireDate] dateByAddingTimeInterval:duration];
601 [cursorTimer setFireDate:date];
604 - (void) setCursorWithFrames:(NSArray*)frames
606 if (self.cursorFrames == frames)
609 self.cursorFrames = frames;
611 [cursorTimer invalidate];
612 self.cursorTimer = nil;
616 if ([frames count] > 1)
618 NSDictionary* frame = [frames objectAtIndex:0];
619 NSTimeInterval duration = [[frame objectForKey:@"duration"] doubleValue];
620 NSDate* date = [NSDate dateWithTimeIntervalSinceNow:duration];
621 self.cursorTimer = [[[NSTimer alloc] initWithFireDate:date
624 selector:@selector(nextCursorFrame:)
626 repeats:YES] autorelease];
627 [[NSRunLoop currentRunLoop] addTimer:cursorTimer forMode:NSRunLoopCommonModes];
635 * ---------- Cursor clipping methods ----------
637 * Neither Quartz nor Cocoa has an exact analog for Win32 cursor clipping.
638 * For one simple case, clipping to a 1x1 rectangle, Quartz does have an
639 * equivalent: CGAssociateMouseAndMouseCursorPosition(false). For the
640 * general case, we leverage that. We disassociate mouse movements from
641 * the cursor position and then move the cursor manually, keeping it within
642 * the clipping rectangle.
644 * Moving the cursor manually isn't enough. We need to modify the event
645 * stream so that the events have the new location, too. We need to do
646 * this at a point before the events enter Cocoa, so that Cocoa will assign
647 * the correct window to the event. So, we install a Quartz event tap to
650 * Also, there's a complication when we move the cursor. We use
651 * CGWarpMouseCursorPosition(). That doesn't generate mouse movement
652 * events, but the change of cursor position is incorporated into the
653 * deltas of the next mouse move event. When the mouse is disassociated
654 * from the cursor position, we need the deltas to only reflect actual
655 * device movement, not programmatic changes. So, the event tap cancels
656 * out the change caused by our calls to CGWarpMouseCursorPosition().
658 - (void) clipCursorLocation:(CGPoint*)location
660 if (location->x < CGRectGetMinX(cursorClipRect))
661 location->x = CGRectGetMinX(cursorClipRect);
662 if (location->y < CGRectGetMinY(cursorClipRect))
663 location->y = CGRectGetMinY(cursorClipRect);
664 if (location->x > CGRectGetMaxX(cursorClipRect) - 1)
665 location->x = CGRectGetMaxX(cursorClipRect) - 1;
666 if (location->y > CGRectGetMaxY(cursorClipRect) - 1)
667 location->y = CGRectGetMaxY(cursorClipRect) - 1;
670 - (BOOL) warpCursorTo:(CGPoint*)newLocation from:(const CGPoint*)currentLocation
675 oldLocation = *currentLocation;
677 oldLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
679 if (!CGPointEqualToPoint(oldLocation, *newLocation))
681 WarpRecord* warpRecord = [[[WarpRecord alloc] init] autorelease];
684 warpRecord.from = oldLocation;
685 warpRecord.timeBefore = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
687 /* Actually move the cursor. */
688 err = CGWarpMouseCursorPosition(*newLocation);
689 if (err != kCGErrorSuccess)
692 warpRecord.timeAfter = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
693 *newLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
695 if (!CGPointEqualToPoint(oldLocation, *newLocation))
697 warpRecord.to = *newLocation;
698 [warpRecords addObject:warpRecord];
705 - (BOOL) isMouseMoveEventType:(CGEventType)type
709 case kCGEventMouseMoved:
710 case kCGEventLeftMouseDragged:
711 case kCGEventRightMouseDragged:
712 case kCGEventOtherMouseDragged:
719 - (int) warpsFinishedByEventTime:(CGEventTimestamp)eventTime location:(CGPoint)eventLocation
721 int warpsFinished = 0;
722 for (WarpRecord* warpRecord in warpRecords)
724 if (warpRecord.timeAfter < eventTime ||
725 (warpRecord.timeBefore <= eventTime && CGPointEqualToPoint(eventLocation, warpRecord.to)))
731 return warpsFinished;
734 - (CGEventRef) eventTapWithProxy:(CGEventTapProxy)proxy
735 type:(CGEventType)type
736 event:(CGEventRef)event
738 CGEventTimestamp eventTime;
739 CGPoint eventLocation, cursorLocation;
741 if (type == kCGEventTapDisabledByUserInput)
743 if (type == kCGEventTapDisabledByTimeout)
745 CGEventTapEnable(cursorClippingEventTap, TRUE);
752 eventTime = CGEventGetTimestamp(event);
753 lastEventTapEventTime = eventTime / (double)NSEC_PER_SEC;
755 eventLocation = CGEventGetLocation(event);
757 cursorLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
759 if ([self isMouseMoveEventType:type])
761 double deltaX, deltaY;
762 int warpsFinished = [self warpsFinishedByEventTime:eventTime location:eventLocation];
765 deltaX = CGEventGetDoubleValueField(event, kCGMouseEventDeltaX);
766 deltaY = CGEventGetDoubleValueField(event, kCGMouseEventDeltaY);
768 for (i = 0; i < warpsFinished; i++)
770 WarpRecord* warpRecord = [warpRecords objectAtIndex:0];
771 deltaX -= warpRecord.to.x - warpRecord.from.x;
772 deltaY -= warpRecord.to.y - warpRecord.from.y;
773 [warpRecords removeObjectAtIndex:0];
778 CGEventSetDoubleValueField(event, kCGMouseEventDeltaX, deltaX);
779 CGEventSetDoubleValueField(event, kCGMouseEventDeltaY, deltaY);
782 synthesizedLocation.x += deltaX;
783 synthesizedLocation.y += deltaY;
786 // If the event is destined for another process, don't clip it. This may
787 // happen if the user activates Exposé or Mission Control. In that case,
788 // our app does not resign active status, so clipping is still in effect,
789 // but the cursor should not actually be clipped.
791 // In addition, the fact that mouse moves may have been delivered to a
792 // different process means we have to treat the next one we receive as
793 // absolute rather than relative.
794 if (CGEventGetIntegerValueField(event, kCGEventTargetUnixProcessID) == getpid())
795 [self clipCursorLocation:&synthesizedLocation];
797 lastSetCursorPositionTime = lastEventTapEventTime;
799 [self warpCursorTo:&synthesizedLocation from:&cursorLocation];
800 if (!CGPointEqualToPoint(eventLocation, synthesizedLocation))
801 CGEventSetLocation(event, synthesizedLocation);
806 CGEventRef WineAppEventTapCallBack(CGEventTapProxy proxy, CGEventType type,
807 CGEventRef event, void *refcon)
809 WineApplication* app = refcon;
810 return [app eventTapWithProxy:proxy type:type event:event];
813 - (BOOL) installEventTap
815 ProcessSerialNumber psn;
817 CGEventMask mask = CGEventMaskBit(kCGEventLeftMouseDown) |
818 CGEventMaskBit(kCGEventLeftMouseUp) |
819 CGEventMaskBit(kCGEventRightMouseDown) |
820 CGEventMaskBit(kCGEventRightMouseUp) |
821 CGEventMaskBit(kCGEventMouseMoved) |
822 CGEventMaskBit(kCGEventLeftMouseDragged) |
823 CGEventMaskBit(kCGEventRightMouseDragged) |
824 CGEventMaskBit(kCGEventOtherMouseDown) |
825 CGEventMaskBit(kCGEventOtherMouseUp) |
826 CGEventMaskBit(kCGEventOtherMouseDragged) |
827 CGEventMaskBit(kCGEventScrollWheel);
828 CFRunLoopSourceRef source;
830 OSErr (*pGetCurrentProcess)(ProcessSerialNumber* PSN);
832 if (cursorClippingEventTap)
835 // We need to get the Mac GetCurrentProcess() from the ApplicationServices
836 // framework with dlsym() because the Win32 function of the same name
838 appServices = dlopen("/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices", RTLD_LAZY);
842 pGetCurrentProcess = dlsym(appServices, "GetCurrentProcess");
843 if (!pGetCurrentProcess)
845 dlclose(appServices);
849 err = pGetCurrentProcess(&psn);
850 dlclose(appServices);
854 // We create an annotated session event tap rather than a process-specific
855 // event tap because we need to programmatically move the cursor even when
856 // mouse moves are directed to other processes. We disable our tap when
857 // other processes are active, but things like Exposé are handled by other
858 // processes even when we remain active.
859 cursorClippingEventTap = CGEventTapCreate(kCGAnnotatedSessionEventTap, kCGHeadInsertEventTap,
860 kCGEventTapOptionDefault, mask, WineAppEventTapCallBack, self);
861 if (!cursorClippingEventTap)
864 CGEventTapEnable(cursorClippingEventTap, FALSE);
866 source = CFMachPortCreateRunLoopSource(NULL, cursorClippingEventTap, 0);
869 CFRelease(cursorClippingEventTap);
870 cursorClippingEventTap = NULL;
874 CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
879 - (BOOL) setCursorPosition:(CGPoint)pos
885 [self clipCursorLocation:&pos];
887 synthesizedLocation = pos;
888 ret = [self warpCursorTo:&synthesizedLocation from:NULL];
891 // We want to discard mouse-move events that have already been
892 // through the event tap, because it's too late to account for
893 // the setting of the cursor position with them. However, the
894 // events that may be queued with times after that but before
895 // the above warp can still be used. So, use the last event
896 // tap event time so that -sendEvent: doesn't discard them.
897 lastSetCursorPositionTime = lastEventTapEventTime;
902 ret = (CGWarpMouseCursorPosition(pos) == kCGErrorSuccess);
904 lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
909 WineEventQueue* queue;
911 // Discard all pending mouse move events.
912 [eventQueuesLock lock];
913 for (queue in eventQueues)
915 [queue discardEventsMatchingMask:event_mask_for_type(MOUSE_MOVED) |
916 event_mask_for_type(MOUSE_MOVED_ABSOLUTE)
919 [eventQueuesLock unlock];
925 - (void) activateCursorClipping
929 CGEventTapEnable(cursorClippingEventTap, TRUE);
930 [self setCursorPosition:NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]])];
934 - (void) deactivateCursorClipping
938 CGEventTapEnable(cursorClippingEventTap, FALSE);
939 [warpRecords removeAllObjects];
940 lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
944 - (BOOL) startClippingCursor:(CGRect)rect
948 if (!cursorClippingEventTap && ![self installEventTap])
951 err = CGAssociateMouseAndMouseCursorPosition(false);
952 if (err != kCGErrorSuccess)
955 clippingCursor = TRUE;
956 cursorClipRect = rect;
958 [self activateCursorClipping];
963 - (BOOL) stopClippingCursor
965 CGError err = CGAssociateMouseAndMouseCursorPosition(true);
966 if (err != kCGErrorSuccess)
969 [self deactivateCursorClipping];
970 clippingCursor = FALSE;
977 * ---------- NSApplication method overrides ----------
979 - (void) sendEvent:(NSEvent*)anEvent
981 NSEventType type = [anEvent type];
982 if (type == NSFlagsChanged)
983 self.lastFlagsChanged = anEvent;
985 [super sendEvent:anEvent];
987 if (type == NSMouseMoved || type == NSLeftMouseDragged ||
988 type == NSRightMouseDragged || type == NSOtherMouseDragged)
990 WineWindow* targetWindow;
992 /* Because of the way -[NSWindow setAcceptsMouseMovedEvents:] works, the
993 event indicates its window is the main window, even if the cursor is
994 over a different window. Find the actual WineWindow that is under the
995 cursor and post the event as being for that window. */
996 if (type == NSMouseMoved)
998 CGPoint cgpoint = CGEventGetLocation([anEvent CGEvent]);
999 NSPoint point = [self flippedMouseLocation:NSPointFromCGPoint(cgpoint)];
1000 NSInteger windowUnderNumber;
1002 windowUnderNumber = [NSWindow windowNumberAtPoint:point
1003 belowWindowWithWindowNumber:0];
1004 targetWindow = (WineWindow*)[self windowWithWindowNumber:windowUnderNumber];
1007 targetWindow = (WineWindow*)[anEvent window];
1009 if ([targetWindow isKindOfClass:[WineWindow class]])
1011 BOOL absolute = forceNextMouseMoveAbsolute || (targetWindow != lastTargetWindow);
1012 forceNextMouseMoveAbsolute = FALSE;
1014 // If we recently warped the cursor (other than in our cursor-clipping
1015 // event tap), discard mouse move events until we see an event which is
1016 // later than that time.
1017 if (lastSetCursorPositionTime)
1019 if ([anEvent timestamp] <= lastSetCursorPositionTime)
1022 lastSetCursorPositionTime = 0;
1026 [targetWindow postMouseMovedEvent:anEvent absolute:absolute];
1027 lastTargetWindow = targetWindow;
1029 else if (lastTargetWindow)
1031 [[NSCursor arrowCursor] set];
1032 [self unhideCursor];
1033 lastTargetWindow = nil;
1036 else if (type == NSLeftMouseDown || type == NSLeftMouseUp ||
1037 type == NSRightMouseDown || type == NSRightMouseUp ||
1038 type == NSOtherMouseDown || type == NSOtherMouseUp ||
1039 type == NSScrollWheel)
1041 // Since mouse button and scroll wheel events deliver absolute cursor
1042 // position, the accumulating delta from move events is invalidated.
1043 // Make sure next mouse move event starts over from an absolute baseline.
1044 forceNextMouseMoveAbsolute = TRUE;
1050 * ---------- NSApplicationDelegate methods ----------
1052 - (void)applicationDidBecomeActive:(NSNotification *)notification
1054 [self activateCursorClipping];
1056 [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
1057 WineWindow* window = obj;
1058 if ([window levelWhenActive] != [window level])
1059 [window setLevel:[window levelWhenActive]];
1062 // If a Wine process terminates abruptly while it has the display captured
1063 // and switched to a different resolution, Mac OS X will uncapture the
1064 // displays and switch their resolutions back. However, the other Wine
1065 // processes won't have their notion of the desktop rect changed back.
1066 // This can lead them to refuse to draw or acknowledge clicks in certain
1067 // portions of their windows.
1069 // To solve this, we synthesize a displays-changed event whenever we're
1070 // activated. This will provoke a re-synchronization of Wine's notion of
1071 // the desktop rect with the actual state.
1072 [self sendDisplaysChanged:TRUE];
1074 // The cursor probably moved while we were inactive. Accumulated mouse
1075 // movement deltas are invalidated. Make sure the next mouse move event
1076 // starts over from an absolute baseline.
1077 forceNextMouseMoveAbsolute = TRUE;
1080 - (void)applicationDidChangeScreenParameters:(NSNotification *)notification
1082 primaryScreenHeightValid = FALSE;
1083 [self sendDisplaysChanged:FALSE];
1085 // When the display configuration changes, the cursor position may jump.
1086 // Accumulated mouse movement deltas are invalidated. Make sure the next
1087 // mouse move event starts over from an absolute baseline.
1088 forceNextMouseMoveAbsolute = TRUE;
1091 - (void)applicationDidResignActive:(NSNotification *)notification
1094 WineEventQueue* queue;
1096 [self invalidateGotFocusEvents];
1098 event.type = APP_DEACTIVATED;
1099 event.window = NULL;
1101 [eventQueuesLock lock];
1102 for (queue in eventQueues)
1103 [queue postEvent:&event];
1104 [eventQueuesLock unlock];
1107 - (void)applicationWillFinishLaunching:(NSNotification *)notification
1109 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
1111 [nc addObserverForName:NSWindowDidBecomeKeyNotification
1114 usingBlock:^(NSNotification *note){
1115 NSWindow* window = [note object];
1116 [keyWindows removeObjectIdenticalTo:window];
1117 [keyWindows insertObject:window atIndex:0];
1120 [nc addObserverForName:NSWindowWillCloseNotification
1122 queue:[NSOperationQueue mainQueue]
1123 usingBlock:^(NSNotification *note){
1124 NSWindow* window = [note object];
1125 [keyWindows removeObjectIdenticalTo:window];
1126 [orderedWineWindows removeObjectIdenticalTo:window];
1127 if (window == lastTargetWindow)
1128 lastTargetWindow = nil;
1131 [nc addObserver:self
1132 selector:@selector(keyboardSelectionDidChange)
1133 name:NSTextInputContextKeyboardSelectionDidChangeNotification
1136 /* The above notification isn't sent unless the NSTextInputContext
1137 class has initialized itself. Poke it. */
1138 [NSTextInputContext self];
1140 self.keyboardType = LMGetKbdType();
1143 - (void)applicationWillResignActive:(NSNotification *)notification
1145 [self deactivateCursorClipping];
1147 [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
1148 WineWindow* window = obj;
1149 NSInteger level = window.floating ? NSFloatingWindowLevel : NSNormalWindowLevel;
1150 if ([window level] > level)
1151 [window setLevel:level];
1155 /***********************************************************************
1158 * Run-loop-source perform callback. Pull request blocks from the
1159 * array of queued requests and invoke them.
1161 static void PerformRequest(void *info)
1163 WineApplication* app = (WineApplication*)NSApp;
1167 __block dispatch_block_t block;
1169 dispatch_sync(app->requestsManipQueue, ^{
1170 if ([app->requests count])
1172 block = (dispatch_block_t)[[app->requests objectAtIndex:0] retain];
1173 [app->requests removeObjectAtIndex:0];
1187 /***********************************************************************
1190 * Run a block on the main thread asynchronously.
1192 void OnMainThreadAsync(dispatch_block_t block)
1194 WineApplication* app = (WineApplication*)NSApp;
1196 block = [block copy];
1197 dispatch_sync(app->requestsManipQueue, ^{
1198 [app->requests addObject:block];
1201 CFRunLoopSourceSignal(app->requestSource);
1202 CFRunLoopWakeUp(CFRunLoopGetMain());
1207 /***********************************************************************
1210 void LogError(const char* func, NSString* format, ...)
1213 va_start(args, format);
1214 LogErrorv(func, format, args);
1218 /***********************************************************************
1221 void LogErrorv(const char* func, NSString* format, va_list args)
1223 NSString* message = [[NSString alloc] initWithFormat:format arguments:args];
1224 fprintf(stderr, "err:%s:%s", func, [message UTF8String]);
1228 /***********************************************************************
1229 * macdrv_window_rejected_focus
1231 * Pass focus to the next window that hasn't already rejected this same
1232 * WINDOW_GOT_FOCUS event.
1234 void macdrv_window_rejected_focus(const macdrv_event *event)
1237 [NSApp windowRejectedFocusEvent:event];
1241 /***********************************************************************
1242 * macdrv_get_keyboard_layout
1244 * Returns the keyboard layout uchr data.
1246 CFDataRef macdrv_copy_keyboard_layout(CGEventSourceKeyboardType* keyboard_type, int* is_iso)
1248 __block CFDataRef result = NULL;
1251 TISInputSourceRef inputSource;
1253 inputSource = TISCopyCurrentKeyboardLayoutInputSource();
1256 CFDataRef uchr = TISGetInputSourceProperty(inputSource,
1257 kTISPropertyUnicodeKeyLayoutData);
1258 result = CFDataCreateCopy(NULL, uchr);
1259 CFRelease(inputSource);
1261 *keyboard_type = ((WineApplication*)NSApp).keyboardType;
1262 *is_iso = (KBGetLayoutType(*keyboard_type) == kKeyboardISO);
1269 /***********************************************************************
1272 * Play the beep sound configured by the user in System Preferences.
1274 void macdrv_beep(void)
1276 OnMainThreadAsync(^{
1281 /***********************************************************************
1282 * macdrv_set_display_mode
1284 int macdrv_set_display_mode(const struct macdrv_display* display,
1285 CGDisplayModeRef display_mode)
1290 ret = [NSApp setMode:display_mode forDisplay:display->displayID];
1296 /***********************************************************************
1301 * If name is non-NULL, it is a selector for a class method on NSCursor
1302 * identifying the cursor to set. In that case, frames is ignored. If
1303 * name is NULL, then frames is used.
1305 * frames is an array of dictionaries. Each dictionary is a frame of
1306 * an animated cursor. Under the key "image" is a CGImage for the
1307 * frame. Under the key "duration" is a CFNumber time interval, in
1308 * seconds, for how long that frame is presented before proceeding to
1309 * the next frame. Under the key "hotSpot" is a CFDictionary encoding a
1310 * CGPoint, to be decoded using CGPointMakeWithDictionaryRepresentation().
1311 * This is the hot spot, measured in pixels down and to the right of the
1312 * top-left corner of the image.
1314 * If the array has exactly 1 element, the cursor is static, not
1315 * animated. If frames is NULL or has 0 elements, the cursor is hidden.
1317 void macdrv_set_cursor(CFStringRef name, CFArrayRef frames)
1321 sel = NSSelectorFromString((NSString*)name);
1324 OnMainThreadAsync(^{
1325 NSCursor* cursor = [NSCursor performSelector:sel];
1326 [NSApp setCursorWithFrames:nil];
1328 [NSApp unhideCursor];
1333 NSArray* nsframes = (NSArray*)frames;
1334 if ([nsframes count])
1336 OnMainThreadAsync(^{
1337 [NSApp setCursorWithFrames:nsframes];
1342 OnMainThreadAsync(^{
1343 [NSApp setCursorWithFrames:nil];
1350 /***********************************************************************
1351 * macdrv_get_cursor_position
1353 * Obtains the current cursor position. Returns zero on failure,
1354 * non-zero on success.
1356 int macdrv_get_cursor_position(CGPoint *pos)
1359 NSPoint location = [NSEvent mouseLocation];
1360 location = [NSApp flippedMouseLocation:location];
1361 *pos = NSPointToCGPoint(location);
1367 /***********************************************************************
1368 * macdrv_set_cursor_position
1370 * Sets the cursor position without generating events. Returns zero on
1371 * failure, non-zero on success.
1373 int macdrv_set_cursor_position(CGPoint pos)
1378 ret = [NSApp setCursorPosition:pos];
1384 /***********************************************************************
1385 * macdrv_clip_cursor
1387 * Sets the cursor cursor clipping rectangle. If the rectangle is equal
1388 * to or larger than the whole desktop region, the cursor is unclipped.
1389 * Returns zero on failure, non-zero on success.
1391 int macdrv_clip_cursor(CGRect rect)
1396 BOOL clipping = FALSE;
1398 if (!CGRectIsInfinite(rect))
1400 NSRect nsrect = NSRectFromCGRect(rect);
1403 /* Convert the rectangle from top-down coords to bottom-up. */
1404 [NSApp flipRect:&nsrect];
1407 for (screen in [NSScreen screens])
1409 if (!NSContainsRect(nsrect, [screen frame]))
1418 ret = [NSApp startClippingCursor:rect];
1420 ret = [NSApp stopClippingCursor];