winemac: Allow for processing Cocoa events while waiting for query results.
[wine.git] / dlls / winemac.drv / cocoa_app.m
blob7ee009dc6f43f4dca6777d48403a541f21d0220b
1 /*
2  * MACDRV Cocoa application class
3  *
4  * Copyright 2011, 2012, 2013 Ken Thomases for CodeWeavers Inc.
5  *
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.
10  *
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.
15  *
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
19  */
21 #import <Carbon/Carbon.h>
22 #include <dlfcn.h>
24 #import "cocoa_app.h"
25 #import "cocoa_event.h"
26 #import "cocoa_window.h"
29 static NSString* const WineAppWaitQueryResponseMode = @"WineAppWaitQueryResponseMode";
32 int macdrv_err_on;
35 @interface WarpRecord : NSObject
37     CGEventTimestamp timeBefore, timeAfter;
38     CGPoint from, to;
41 @property (nonatomic) CGEventTimestamp timeBefore;
42 @property (nonatomic) CGEventTimestamp timeAfter;
43 @property (nonatomic) CGPoint from;
44 @property (nonatomic) CGPoint to;
46 @end
49 @implementation WarpRecord
51 @synthesize timeBefore, timeAfter, from, to;
53 @end;
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);
64 @end
67 @implementation WineApplication
69     @synthesize keyboardType, lastFlagsChanged;
70     @synthesize orderedWineWindows;
71     @synthesize cursorFrames, cursorTimer;
73     - (id) init
74     {
75         self = [super init];
76         if (self != nil)
77         {
78             CFRunLoopSourceContext context = { 0 };
79             context.perform = PerformRequest;
80             requestSource = CFRunLoopSourceCreate(NULL, 0, &context);
81             if (!requestSource)
82             {
83                 [self release];
84                 return nil;
85             }
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)
104             {
105                 [self release];
106                 return nil;
107             }
108         }
109         return self;
110     }
112     - (void) dealloc
113     {
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);
123         [requests release];
124         if (requestSource)
125         {
126             CFRunLoopSourceInvalidate(requestSource);
127             CFRelease(requestSource);
128         }
129         [super dealloc];
130     }
132     - (void) transformProcessToForeground
133     {
134         if ([self activationPolicy] != NSApplicationActivationPolicyRegular)
135         {
136             NSMenu* mainMenu;
137             NSMenu* submenu;
138             NSString* bundleName;
139             NSString* title;
140             NSMenuItem* item;
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];
151             else
152                 title = @"Quit";
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];
172         }
173     }
175     - (BOOL) waitUntilQueryDone:(int*)done timeout:(NSDate*)timeout processEvents:(BOOL)processEvents
176     {
177         PerformRequest(NULL);
179         do
180         {
181             if (processEvents)
182             {
183                 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
184                 NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask
185                                                     untilDate:timeout
186                                                        inMode:NSDefaultRunLoopMode
187                                                       dequeue:YES];
188                 if (event)
189                     [NSApp sendEvent:event];
190                 [pool release];
191             }
192             else
193                 [[NSRunLoop currentRunLoop] runMode:WineAppWaitQueryResponseMode beforeDate:timeout];
194         } while (!*done && [timeout timeIntervalSinceNow] >= 0);
196         return *done;
197     }
199     - (BOOL) registerEventQueue:(WineEventQueue*)queue
200     {
201         [eventQueuesLock lock];
202         [eventQueues addObject:queue];
203         [eventQueuesLock unlock];
204         return TRUE;
205     }
207     - (void) unregisterEventQueue:(WineEventQueue*)queue
208     {
209         [eventQueuesLock lock];
210         [eventQueues removeObjectIdenticalTo:queue];
211         [eventQueuesLock unlock];
212     }
214     - (void) computeEventTimeAdjustmentFromTicks:(unsigned long long)tickcount uptime:(uint64_t)uptime_ns
215     {
216         eventTimeAdjustment = (tickcount / 1000.0) - (uptime_ns / (double)NSEC_PER_SEC);
217     }
219     - (double) ticksForEventTime:(NSTimeInterval)eventTime
220     {
221         return (eventTime + eventTimeAdjustment) * 1000;
222     }
224     /* Invalidate old focus offers across all queues. */
225     - (void) invalidateGotFocusEvents
226     {
227         WineEventQueue* queue;
229         windowFocusSerial++;
231         [eventQueuesLock lock];
232         for (queue in eventQueues)
233         {
234             [queue discardEventsMatchingMask:event_mask_for_type(WINDOW_GOT_FOCUS)
235                                    forWindow:nil];
236         }
237         [eventQueuesLock unlock];
238     }
240     - (void) windowGotFocus:(WineWindow*)window
241     {
242         macdrv_event event;
244         [NSApp invalidateGotFocusEvents];
246         event.type = WINDOW_GOT_FOCUS;
247         event.window = (macdrv_window)[window retain];
248         event.window_got_focus.serial = windowFocusSerial;
249         if (triedWindows)
250             event.window_got_focus.tried_windows = [triedWindows retain];
251         else
252             event.window_got_focus.tried_windows = [[NSMutableSet alloc] init];
253         [window.queue postEvent:&event];
254     }
256     - (void) windowRejectedFocusEvent:(const macdrv_event*)event
257     {
258         if (event->window_got_focus.serial == windowFocusSerial)
259         {
260             triedWindows = (NSMutableSet*)event->window_got_focus.tried_windows;
261             [triedWindows addObject:(WineWindow*)event->window];
262             for (NSWindow* window in [keyWindows arrayByAddingObjectsFromArray:[self orderedWindows]])
263             {
264                 if (![triedWindows containsObject:window] && [window canBecomeKeyWindow])
265                 {
266                     [window makeKeyWindow];
267                     break;
268                 }
269             }
270             triedWindows = nil;
271         }
272     }
274     - (void) keyboardSelectionDidChange
275     {
276         TISInputSourceRef inputSource;
278         inputSource = TISCopyCurrentKeyboardLayoutInputSource();
279         if (inputSource)
280         {
281             CFDataRef uchr;
282             uchr = TISGetInputSourceProperty(inputSource,
283                     kTISPropertyUnicodeKeyLayoutData);
284             if (uchr)
285             {
286                 macdrv_event event;
287                 WineEventQueue* queue;
289                 event.type = KEYBOARD_CHANGED;
290                 event.window = NULL;
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)
296                 {
297                     [eventQueuesLock lock];
299                     for (queue in eventQueues)
300                     {
301                         CFRetain(event.keyboard_changed.uchr);
302                         [queue postEvent:&event];
303                     }
305                     [eventQueuesLock unlock];
307                     CFRelease(event.keyboard_changed.uchr);
308                 }
309             }
311             CFRelease(inputSource);
312         }
313     }
315     - (CGFloat) primaryScreenHeight
316     {
317         if (!primaryScreenHeightValid)
318         {
319             NSArray* screens = [NSScreen screens];
320             if ([screens count])
321             {
322                 primaryScreenHeight = NSHeight([[screens objectAtIndex:0] frame]);
323                 primaryScreenHeightValid = TRUE;
324             }
325             else
326                 return 1280; /* arbitrary value */
327         }
329         return primaryScreenHeight;
330     }
332     - (NSPoint) flippedMouseLocation:(NSPoint)point
333     {
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;
338         return point;
339     }
341     - (void) flipRect:(NSRect*)rect
342     {
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);
347     }
349     - (void) wineWindow:(WineWindow*)window
350                 ordered:(NSWindowOrderingMode)order
351              relativeTo:(WineWindow*)otherWindow
352     {
353         NSUInteger index;
355         switch (order)
356         {
357             case NSWindowAbove:
358                 [window retain];
359                 [orderedWineWindows removeObjectIdenticalTo:window];
360                 if (otherWindow)
361                 {
362                     index = [orderedWineWindows indexOfObjectIdenticalTo:otherWindow];
363                     if (index == NSNotFound)
364                         index = 0;
365                 }
366                 else
367                 {
368                     index = 0;
369                     for (otherWindow in orderedWineWindows)
370                     {
371                         if ([otherWindow levelWhenActive] <= [window levelWhenActive])
372                             break;
373                         index++;
374                     }
375                 }
376                 [orderedWineWindows insertObject:window atIndex:index];
377                 [window release];
378                 break;
379             case NSWindowBelow:
380                 [window retain];
381                 [orderedWineWindows removeObjectIdenticalTo:window];
382                 if (otherWindow)
383                 {
384                     index = [orderedWineWindows indexOfObjectIdenticalTo:otherWindow];
385                     if (index == NSNotFound)
386                         index = [orderedWineWindows count];
387                 }
388                 else
389                 {
390                     index = 0;
391                     for (otherWindow in orderedWineWindows)
392                     {
393                         if ([otherWindow levelWhenActive] < [window levelWhenActive])
394                             break;
395                         index++;
396                     }
397                 }
398                 [orderedWineWindows insertObject:window atIndex:index];
399                 [window release];
400                 break;
401             case NSWindowOut:
402             default:
403                 break;
404         }
405     }
407     - (void) sendDisplaysChanged:(BOOL)activating
408     {
409         macdrv_event event;
410         WineEventQueue* queue;
412         event.type = DISPLAYS_CHANGED;
413         event.window = NULL;
414         event.displays_changed.activating = activating;
416         [eventQueuesLock lock];
417         for (queue in eventQueues)
418             [queue postEvent:&event];
419         [eventQueuesLock unlock];
420     }
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
428     {
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))
445             return FALSE;
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;
453         return TRUE;
454     }
456     - (CGDisplayModeRef)modeMatchingMode:(CGDisplayModeRef)mode forDisplay:(CGDirectDisplayID)displayID
457     {
458         CGDisplayModeRef ret = NULL;
459         NSArray *modes = [(NSArray*)CGDisplayCopyAllDisplayModes(displayID, NULL) autorelease];
460         for (id candidateModeObject in modes)
461         {
462             CGDisplayModeRef candidateMode = (CGDisplayModeRef)candidateModeObject;
463             if ([self mode:candidateMode matchesMode:mode])
464             {
465                 ret = candidateMode;
466                 break;
467             }
468         }
469         return ret;
470     }
472     - (BOOL) setMode:(CGDisplayModeRef)mode forDisplay:(CGDirectDisplayID)displayID
473     {
474         BOOL ret = FALSE;
475         NSNumber* displayIDKey = [NSNumber numberWithUnsignedInt:displayID];
476         CGDisplayModeRef currentMode, originalMode;
478         currentMode = CGDisplayCopyDisplayMode(displayID);
479         if (!currentMode) // Invalid display ID
480             return FALSE;
482         if ([self mode:mode matchesMode:currentMode]) // Already there!
483         {
484             CGDisplayModeRelease(currentMode);
485             return TRUE;
486         }
488         mode = [self modeMatchingMode:mode forDisplay:displayID];
489         if (!mode)
490         {
491             CGDisplayModeRelease(currentMode);
492             return FALSE;
493         }
495         originalMode = (CGDisplayModeRef)[originalDisplayModes objectForKey:displayIDKey];
496         if (!originalMode)
497             originalMode = currentMode;
499         if ([self mode:mode matchesMode:originalMode])
500         {
501             if ([originalDisplayModes count] == 1) // If this is the last changed display, do a blanket reset
502             {
503                 CGRestorePermanentDisplayConfiguration();
504                 CGReleaseAllDisplays();
505                 [originalDisplayModes removeAllObjects];
506                 ret = TRUE;
507             }
508             else // ... otherwise, try to restore just the one display
509             {
510                 if (CGDisplaySetDisplayMode(displayID, mode, NULL) == CGDisplayNoErr)
511                 {
512                     [originalDisplayModes removeObjectForKey:displayIDKey];
513                     ret = TRUE;
514                 }
515             }
516         }
517         else
518         {
519             if ([originalDisplayModes count] || CGCaptureAllDisplays() == CGDisplayNoErr)
520             {
521                 if (CGDisplaySetDisplayMode(displayID, mode, NULL) == CGDisplayNoErr)
522                 {
523                     [originalDisplayModes setObject:(id)originalMode forKey:displayIDKey];
524                     ret = TRUE;
525                 }
526                 else if (![originalDisplayModes count])
527                 {
528                     CGRestorePermanentDisplayConfiguration();
529                     CGReleaseAllDisplays();
530                 }
531             }
532         }
534         CGDisplayModeRelease(currentMode);
536         if (ret)
537         {
538             [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
539                 [(WineWindow*)obj adjustWindowLevel];
540             }];
541         }
543         return ret;
544     }
546     - (BOOL) areDisplaysCaptured
547     {
548         return ([originalDisplayModes count] > 0);
549     }
551     - (void) hideCursor
552     {
553         if (!cursorHidden)
554         {
555             [NSCursor hide];
556             cursorHidden = TRUE;
557         }
558     }
560     - (void) unhideCursor
561     {
562         if (cursorHidden)
563         {
564             [NSCursor unhide];
565             cursorHidden = FALSE;
566         }
567     }
569     - (void) setCursor
570     {
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"];
575         CGPoint hotSpot;
576         NSCursor* cursor;
578         if (!CGPointMakeWithDictionaryRepresentation(hotSpotDict, &hotSpot))
579             hotSpot = CGPointZero;
580         cursor = [[NSCursor alloc] initWithImage:image hotSpot:NSPointFromCGPoint(hotSpot)];
581         [image release];
582         [cursor set];
583         [self unhideCursor];
584         [cursor release];
585     }
587     - (void) nextCursorFrame:(NSTimer*)theTimer
588     {
589         NSDictionary* frame;
590         NSTimeInterval duration;
591         NSDate* date;
593         cursorFrame++;
594         if (cursorFrame >= [cursorFrames count])
595             cursorFrame = 0;
596         [self setCursor];
598         frame = [cursorFrames objectAtIndex:cursorFrame];
599         duration = [[frame objectForKey:@"duration"] doubleValue];
600         date = [[theTimer fireDate] dateByAddingTimeInterval:duration];
601         [cursorTimer setFireDate:date];
602     }
604     - (void) setCursorWithFrames:(NSArray*)frames
605     {
606         if (self.cursorFrames == frames)
607             return;
609         self.cursorFrames = frames;
610         cursorFrame = 0;
611         [cursorTimer invalidate];
612         self.cursorTimer = nil;
614         if ([frames count])
615         {
616             if ([frames count] > 1)
617             {
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
622                                                              interval:1000000
623                                                                target:self
624                                                              selector:@selector(nextCursorFrame:)
625                                                              userInfo:nil
626                                                               repeats:YES] autorelease];
627                 [[NSRunLoop currentRunLoop] addTimer:cursorTimer forMode:NSRunLoopCommonModes];
628             }
630             [self setCursor];
631         }
632     }
634     /*
635      * ---------- Cursor clipping methods ----------
636      *
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.
643      *
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
648      * do that.
649      *
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().
657      */
658     - (void) clipCursorLocation:(CGPoint*)location
659     {
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;
668     }
670     - (BOOL) warpCursorTo:(CGPoint*)newLocation from:(const CGPoint*)currentLocation
671     {
672         CGPoint oldLocation;
674         if (currentLocation)
675             oldLocation = *currentLocation;
676         else
677             oldLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
679         if (!CGPointEqualToPoint(oldLocation, *newLocation))
680         {
681             WarpRecord* warpRecord = [[[WarpRecord alloc] init] autorelease];
682             CGError err;
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)
690                 return FALSE;
692             warpRecord.timeAfter = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
693             *newLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
695             if (!CGPointEqualToPoint(oldLocation, *newLocation))
696             {
697                 warpRecord.to = *newLocation;
698                 [warpRecords addObject:warpRecord];
699             }
700         }
702         return TRUE;
703     }
705     - (BOOL) isMouseMoveEventType:(CGEventType)type
706     {
707         switch(type)
708         {
709         case kCGEventMouseMoved:
710         case kCGEventLeftMouseDragged:
711         case kCGEventRightMouseDragged:
712         case kCGEventOtherMouseDragged:
713             return TRUE;
714         }
716         return FALSE;
717     }
719     - (int) warpsFinishedByEventTime:(CGEventTimestamp)eventTime location:(CGPoint)eventLocation
720     {
721         int warpsFinished = 0;
722         for (WarpRecord* warpRecord in warpRecords)
723         {
724             if (warpRecord.timeAfter < eventTime ||
725                 (warpRecord.timeBefore <= eventTime && CGPointEqualToPoint(eventLocation, warpRecord.to)))
726                 warpsFinished++;
727             else
728                 break;
729         }
731         return warpsFinished;
732     }
734     - (CGEventRef) eventTapWithProxy:(CGEventTapProxy)proxy
735                                 type:(CGEventType)type
736                                event:(CGEventRef)event
737     {
738         CGEventTimestamp eventTime;
739         CGPoint eventLocation, cursorLocation;
741         if (type == kCGEventTapDisabledByUserInput)
742             return event;
743         if (type == kCGEventTapDisabledByTimeout)
744         {
745             CGEventTapEnable(cursorClippingEventTap, TRUE);
746             return event;
747         }
749         if (!clippingCursor)
750             return event;
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])
760         {
761             double deltaX, deltaY;
762             int warpsFinished = [self warpsFinishedByEventTime:eventTime location:eventLocation];
763             int i;
765             deltaX = CGEventGetDoubleValueField(event, kCGMouseEventDeltaX);
766             deltaY = CGEventGetDoubleValueField(event, kCGMouseEventDeltaY);
768             for (i = 0; i < warpsFinished; i++)
769             {
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];
774             }
776             if (warpsFinished)
777             {
778                 CGEventSetDoubleValueField(event, kCGMouseEventDeltaX, deltaX);
779                 CGEventSetDoubleValueField(event, kCGMouseEventDeltaY, deltaY);
780             }
782             synthesizedLocation.x += deltaX;
783             synthesizedLocation.y += deltaY;
784         }
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.
790         //
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];
796         else
797             lastSetCursorPositionTime = lastEventTapEventTime;
799         [self warpCursorTo:&synthesizedLocation from:&cursorLocation];
800         if (!CGPointEqualToPoint(eventLocation, synthesizedLocation))
801             CGEventSetLocation(event, synthesizedLocation);
803         return event;
804     }
806     CGEventRef WineAppEventTapCallBack(CGEventTapProxy proxy, CGEventType type,
807                                        CGEventRef event, void *refcon)
808     {
809         WineApplication* app = refcon;
810         return [app eventTapWithProxy:proxy type:type event:event];
811     }
813     - (BOOL) installEventTap
814     {
815         ProcessSerialNumber psn;
816         OSErr err;
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;
829         void* appServices;
830         OSErr (*pGetCurrentProcess)(ProcessSerialNumber* PSN);
832         if (cursorClippingEventTap)
833             return TRUE;
835         // We need to get the Mac GetCurrentProcess() from the ApplicationServices
836         // framework with dlsym() because the Win32 function of the same name
837         // obscures it.
838         appServices = dlopen("/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices", RTLD_LAZY);
839         if (!appServices)
840             return FALSE;
842         pGetCurrentProcess = dlsym(appServices, "GetCurrentProcess");
843         if (!pGetCurrentProcess)
844         {
845             dlclose(appServices);
846             return FALSE;
847         }
849         err = pGetCurrentProcess(&psn);
850         dlclose(appServices);
851         if (err != noErr)
852             return FALSE;
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)
862             return FALSE;
864         CGEventTapEnable(cursorClippingEventTap, FALSE);
866         source = CFMachPortCreateRunLoopSource(NULL, cursorClippingEventTap, 0);
867         if (!source)
868         {
869             CFRelease(cursorClippingEventTap);
870             cursorClippingEventTap = NULL;
871             return FALSE;
872         }
874         CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
875         CFRelease(source);
876         return TRUE;
877     }
879     - (BOOL) setCursorPosition:(CGPoint)pos
880     {
881         BOOL ret;
883         if (clippingCursor)
884         {
885             [self clipCursorLocation:&pos];
887             synthesizedLocation = pos;
888             ret = [self warpCursorTo:&synthesizedLocation from:NULL];
889             if (ret)
890             {
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;
898             }
899         }
900         else
901         {
902             ret = (CGWarpMouseCursorPosition(pos) == kCGErrorSuccess);
903             if (ret)
904                 lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
905         }
907         if (ret)
908         {
909             WineEventQueue* queue;
911             // Discard all pending mouse move events.
912             [eventQueuesLock lock];
913             for (queue in eventQueues)
914             {
915                 [queue discardEventsMatchingMask:event_mask_for_type(MOUSE_MOVED) |
916                                                  event_mask_for_type(MOUSE_MOVED_ABSOLUTE)
917                                        forWindow:nil];
918             }
919             [eventQueuesLock unlock];
920         }
922         return ret;
923     }
925     - (void) activateCursorClipping
926     {
927         if (clippingCursor)
928         {
929             CGEventTapEnable(cursorClippingEventTap, TRUE);
930             [self setCursorPosition:NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]])];
931         }
932     }
934     - (void) deactivateCursorClipping
935     {
936         if (clippingCursor)
937         {
938             CGEventTapEnable(cursorClippingEventTap, FALSE);
939             [warpRecords removeAllObjects];
940             lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
941         }
942     }
944     - (BOOL) startClippingCursor:(CGRect)rect
945     {
946         CGError err;
948         if (!cursorClippingEventTap && ![self installEventTap])
949             return FALSE;
951         err = CGAssociateMouseAndMouseCursorPosition(false);
952         if (err != kCGErrorSuccess)
953             return FALSE;
955         clippingCursor = TRUE;
956         cursorClipRect = rect;
957         if ([self isActive])
958             [self activateCursorClipping];
960         return TRUE;
961     }
963     - (BOOL) stopClippingCursor
964     {
965         CGError err = CGAssociateMouseAndMouseCursorPosition(true);
966         if (err != kCGErrorSuccess)
967             return FALSE;
969         [self deactivateCursorClipping];
970         clippingCursor = FALSE;
972         return TRUE;
973     }
976     /*
977      * ---------- NSApplication method overrides ----------
978      */
979     - (void) sendEvent:(NSEvent*)anEvent
980     {
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)
989         {
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)
997             {
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];
1005             }
1006             else
1007                 targetWindow = (WineWindow*)[anEvent window];
1009             if ([targetWindow isKindOfClass:[WineWindow class]])
1010             {
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)
1018                 {
1019                     if ([anEvent timestamp] <= lastSetCursorPositionTime)
1020                         return;
1022                     lastSetCursorPositionTime = 0;
1023                     absolute = TRUE;
1024                 }
1026                 [targetWindow postMouseMovedEvent:anEvent absolute:absolute];
1027                 lastTargetWindow = targetWindow;
1028             }
1029             else if (lastTargetWindow)
1030             {
1031                 [[NSCursor arrowCursor] set];
1032                 [self unhideCursor];
1033                 lastTargetWindow = nil;
1034             }
1035         }
1036         else if (type == NSLeftMouseDown || type == NSLeftMouseUp ||
1037                  type == NSRightMouseDown || type == NSRightMouseUp ||
1038                  type == NSOtherMouseDown || type == NSOtherMouseUp ||
1039                  type == NSScrollWheel)
1040         {
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;
1045         }
1046     }
1049     /*
1050      * ---------- NSApplicationDelegate methods ----------
1051      */
1052     - (void)applicationDidBecomeActive:(NSNotification *)notification
1053     {
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]];
1060         }];
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.
1068         //
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;
1078     }
1080     - (void)applicationDidChangeScreenParameters:(NSNotification *)notification
1081     {
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;
1089     }
1091     - (void)applicationDidResignActive:(NSNotification *)notification
1092     {
1093         macdrv_event event;
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];
1105     }
1107     - (void)applicationWillFinishLaunching:(NSNotification *)notification
1108     {
1109         NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
1111         [nc addObserverForName:NSWindowDidBecomeKeyNotification
1112                         object:nil
1113                          queue:nil
1114                     usingBlock:^(NSNotification *note){
1115             NSWindow* window = [note object];
1116             [keyWindows removeObjectIdenticalTo:window];
1117             [keyWindows insertObject:window atIndex:0];
1118         }];
1120         [nc addObserverForName:NSWindowWillCloseNotification
1121                         object:nil
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;
1129         }];
1131         [nc addObserver:self
1132                selector:@selector(keyboardSelectionDidChange)
1133                    name:NSTextInputContextKeyboardSelectionDidChangeNotification
1134                  object:nil];
1136         /* The above notification isn't sent unless the NSTextInputContext
1137            class has initialized itself.  Poke it. */
1138         [NSTextInputContext self];
1140         self.keyboardType = LMGetKbdType();
1141     }
1143     - (void)applicationWillResignActive:(NSNotification *)notification
1144     {
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];
1152         }];
1153     }
1155 /***********************************************************************
1156  *              PerformRequest
1158  * Run-loop-source perform callback.  Pull request blocks from the
1159  * array of queued requests and invoke them.
1160  */
1161 static void PerformRequest(void *info)
1163     WineApplication* app = (WineApplication*)NSApp;
1165     for (;;)
1166     {
1167         __block dispatch_block_t block;
1169         dispatch_sync(app->requestsManipQueue, ^{
1170             if ([app->requests count])
1171             {
1172                 block = (dispatch_block_t)[[app->requests objectAtIndex:0] retain];
1173                 [app->requests removeObjectAtIndex:0];
1174             }
1175             else
1176                 block = nil;
1177         });
1179         if (!block)
1180             break;
1182         block();
1183         [block release];
1184     }
1187 /***********************************************************************
1188  *              OnMainThreadAsync
1190  * Run a block on the main thread asynchronously.
1191  */
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];
1199     });
1200     [block release];
1201     CFRunLoopSourceSignal(app->requestSource);
1202     CFRunLoopWakeUp(CFRunLoopGetMain());
1205 @end
1207 /***********************************************************************
1208  *              LogError
1209  */
1210 void LogError(const char* func, NSString* format, ...)
1212     va_list args;
1213     va_start(args, format);
1214     LogErrorv(func, format, args);
1215     va_end(args);
1218 /***********************************************************************
1219  *              LogErrorv
1220  */
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]);
1225     [message release];
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.
1233  */
1234 void macdrv_window_rejected_focus(const macdrv_event *event)
1236     OnMainThread(^{
1237         [NSApp windowRejectedFocusEvent:event];
1238     });
1241 /***********************************************************************
1242  *              macdrv_get_keyboard_layout
1244  * Returns the keyboard layout uchr data.
1245  */
1246 CFDataRef macdrv_copy_keyboard_layout(CGEventSourceKeyboardType* keyboard_type, int* is_iso)
1248     __block CFDataRef result = NULL;
1250     OnMainThread(^{
1251         TISInputSourceRef inputSource;
1253         inputSource = TISCopyCurrentKeyboardLayoutInputSource();
1254         if (inputSource)
1255         {
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);
1263         }
1264     });
1266     return result;
1269 /***********************************************************************
1270  *              macdrv_beep
1272  * Play the beep sound configured by the user in System Preferences.
1273  */
1274 void macdrv_beep(void)
1276     OnMainThreadAsync(^{
1277         NSBeep();
1278     });
1281 /***********************************************************************
1282  *              macdrv_set_display_mode
1283  */
1284 int macdrv_set_display_mode(const struct macdrv_display* display,
1285                             CGDisplayModeRef display_mode)
1287     __block int ret;
1289     OnMainThread(^{
1290         ret = [NSApp setMode:display_mode forDisplay:display->displayID];
1291     });
1293     return ret;
1296 /***********************************************************************
1297  *              macdrv_set_cursor
1299  * Set the cursor.
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.
1316  */
1317 void macdrv_set_cursor(CFStringRef name, CFArrayRef frames)
1319     SEL sel;
1321     sel = NSSelectorFromString((NSString*)name);
1322     if (sel)
1323     {
1324         OnMainThreadAsync(^{
1325             NSCursor* cursor = [NSCursor performSelector:sel];
1326             [NSApp setCursorWithFrames:nil];
1327             [cursor set];
1328             [NSApp unhideCursor];
1329         });
1330     }
1331     else
1332     {
1333         NSArray* nsframes = (NSArray*)frames;
1334         if ([nsframes count])
1335         {
1336             OnMainThreadAsync(^{
1337                 [NSApp setCursorWithFrames:nsframes];
1338             });
1339         }
1340         else
1341         {
1342             OnMainThreadAsync(^{
1343                 [NSApp setCursorWithFrames:nil];
1344                 [NSApp hideCursor];
1345             });
1346         }
1347     }
1350 /***********************************************************************
1351  *              macdrv_get_cursor_position
1353  * Obtains the current cursor position.  Returns zero on failure,
1354  * non-zero on success.
1355  */
1356 int macdrv_get_cursor_position(CGPoint *pos)
1358     OnMainThread(^{
1359         NSPoint location = [NSEvent mouseLocation];
1360         location = [NSApp flippedMouseLocation:location];
1361         *pos = NSPointToCGPoint(location);
1362     });
1364     return TRUE;
1367 /***********************************************************************
1368  *              macdrv_set_cursor_position
1370  * Sets the cursor position without generating events.  Returns zero on
1371  * failure, non-zero on success.
1372  */
1373 int macdrv_set_cursor_position(CGPoint pos)
1375     __block int ret;
1377     OnMainThread(^{
1378         ret = [NSApp setCursorPosition:pos];
1379     });
1381     return ret;
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.
1390  */
1391 int macdrv_clip_cursor(CGRect rect)
1393     __block int ret;
1395     OnMainThread(^{
1396         BOOL clipping = FALSE;
1398         if (!CGRectIsInfinite(rect))
1399         {
1400             NSRect nsrect = NSRectFromCGRect(rect);
1401             NSScreen* screen;
1403             /* Convert the rectangle from top-down coords to bottom-up. */
1404             [NSApp flipRect:&nsrect];
1406             clipping = FALSE;
1407             for (screen in [NSScreen screens])
1408             {
1409                 if (!NSContainsRect(nsrect, [screen frame]))
1410                 {
1411                     clipping = TRUE;
1412                     break;
1413                 }
1414             }
1415         }
1417         if (clipping)
1418             ret = [NSApp startClippingCursor:rect];
1419         else
1420             ret = [NSApp stopClippingCursor];
1421     });
1423     return ret;