msxml3: Embed user/password in uri used to create a moniker.
[wine] / dlls / winemac.drv / cocoa_app.m
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  */
20
21 #import <Carbon/Carbon.h>
22 #include <dlfcn.h>
23
24 #import "cocoa_app.h"
25 #import "cocoa_event.h"
26 #import "cocoa_window.h"
27
28
29 static NSString* const WineAppWaitQueryResponseMode = @"WineAppWaitQueryResponseMode";
30
31
32 int macdrv_err_on;
33
34
35 @interface WarpRecord : NSObject
36 {
37     CGEventTimestamp timeBefore, timeAfter;
38     CGPoint from, to;
39 }
40
41 @property (nonatomic) CGEventTimestamp timeBefore;
42 @property (nonatomic) CGEventTimestamp timeAfter;
43 @property (nonatomic) CGPoint from;
44 @property (nonatomic) CGPoint to;
45
46 @end
47
48
49 @implementation WarpRecord
50
51 @synthesize timeBefore, timeAfter, from, to;
52
53 @end;
54
55
56 @interface WineApplication ()
57
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;
62
63     static void PerformRequest(void *info);
64
65 @end
66
67
68 @implementation WineApplication
69
70     @synthesize keyboardType, lastFlagsChanged;
71     @synthesize orderedWineWindows, applicationIcon;
72     @synthesize cursorFrames, cursorTimer;
73
74     - (id) init
75     {
76         self = [super init];
77         if (self != nil)
78         {
79             CFRunLoopSourceContext context = { 0 };
80             context.perform = PerformRequest;
81             requestSource = CFRunLoopSourceCreate(NULL, 0, &context);
82             if (!requestSource)
83             {
84                 [self release];
85                 return nil;
86             }
87             CFRunLoopAddSource(CFRunLoopGetMain(), requestSource, kCFRunLoopCommonModes);
88             CFRunLoopAddSource(CFRunLoopGetMain(), requestSource, (CFStringRef)WineAppWaitQueryResponseMode);
89
90             requests =  [[NSMutableArray alloc] init];
91             requestsManipQueue = dispatch_queue_create("org.winehq.WineAppRequestManipQueue", NULL);
92
93             eventQueues = [[NSMutableArray alloc] init];
94             eventQueuesLock = [[NSLock alloc] init];
95
96             keyWindows = [[NSMutableArray alloc] init];
97             orderedWineWindows = [[NSMutableArray alloc] init];
98
99             originalDisplayModes = [[NSMutableDictionary alloc] init];
100
101             warpRecords = [[NSMutableArray alloc] init];
102
103             if (!requests || !requestsManipQueue || !eventQueues || !eventQueuesLock ||
104                 !keyWindows || !orderedWineWindows || !originalDisplayModes || !warpRecords)
105             {
106                 [self release];
107                 return nil;
108             }
109         }
110         return self;
111     }
112
113     - (void) dealloc
114     {
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);
125         [requests release];
126         if (requestSource)
127         {
128             CFRunLoopSourceInvalidate(requestSource);
129             CFRelease(requestSource);
130         }
131         [super dealloc];
132     }
133
134     - (void) transformProcessToForeground
135     {
136         if ([self activationPolicy] != NSApplicationActivationPolicyRegular)
137         {
138             NSMenu* mainMenu;
139             NSMenu* submenu;
140             NSString* bundleName;
141             NSString* title;
142             NSMenuItem* item;
143
144             [self setActivationPolicy:NSApplicationActivationPolicyRegular];
145             [self activateIgnoringOtherApps:YES];
146
147             mainMenu = [[[NSMenu alloc] init] autorelease];
148
149             submenu = [[[NSMenu alloc] initWithTitle:@"Wine"] autorelease];
150             bundleName = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString*)kCFBundleNameKey];
151             if ([bundleName length])
152                 title = [NSString stringWithFormat:@"Quit %@", bundleName];
153             else
154                 title = @"Quit";
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];
161
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];
171
172             [self setMainMenu:mainMenu];
173             [self setWindowsMenu:submenu];
174
175             [self setApplicationIconImage:self.applicationIcon];
176         }
177     }
178
179     - (BOOL) waitUntilQueryDone:(int*)done timeout:(NSDate*)timeout processEvents:(BOOL)processEvents
180     {
181         PerformRequest(NULL);
182
183         do
184         {
185             if (processEvents)
186             {
187                 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
188                 NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask
189                                                     untilDate:timeout
190                                                        inMode:NSDefaultRunLoopMode
191                                                       dequeue:YES];
192                 if (event)
193                     [NSApp sendEvent:event];
194                 [pool release];
195             }
196             else
197                 [[NSRunLoop currentRunLoop] runMode:WineAppWaitQueryResponseMode beforeDate:timeout];
198         } while (!*done && [timeout timeIntervalSinceNow] >= 0);
199
200         return *done;
201     }
202
203     - (BOOL) registerEventQueue:(WineEventQueue*)queue
204     {
205         [eventQueuesLock lock];
206         [eventQueues addObject:queue];
207         [eventQueuesLock unlock];
208         return TRUE;
209     }
210
211     - (void) unregisterEventQueue:(WineEventQueue*)queue
212     {
213         [eventQueuesLock lock];
214         [eventQueues removeObjectIdenticalTo:queue];
215         [eventQueuesLock unlock];
216     }
217
218     - (void) computeEventTimeAdjustmentFromTicks:(unsigned long long)tickcount uptime:(uint64_t)uptime_ns
219     {
220         eventTimeAdjustment = (tickcount / 1000.0) - (uptime_ns / (double)NSEC_PER_SEC);
221     }
222
223     - (double) ticksForEventTime:(NSTimeInterval)eventTime
224     {
225         return (eventTime + eventTimeAdjustment) * 1000;
226     }
227
228     /* Invalidate old focus offers across all queues. */
229     - (void) invalidateGotFocusEvents
230     {
231         WineEventQueue* queue;
232
233         windowFocusSerial++;
234
235         [eventQueuesLock lock];
236         for (queue in eventQueues)
237         {
238             [queue discardEventsMatchingMask:event_mask_for_type(WINDOW_GOT_FOCUS)
239                                    forWindow:nil];
240         }
241         [eventQueuesLock unlock];
242     }
243
244     - (void) windowGotFocus:(WineWindow*)window
245     {
246         macdrv_event event;
247
248         [NSApp invalidateGotFocusEvents];
249
250         event.type = WINDOW_GOT_FOCUS;
251         event.window = (macdrv_window)[window retain];
252         event.window_got_focus.serial = windowFocusSerial;
253         if (triedWindows)
254             event.window_got_focus.tried_windows = [triedWindows retain];
255         else
256             event.window_got_focus.tried_windows = [[NSMutableSet alloc] init];
257         [window.queue postEvent:&event];
258     }
259
260     - (void) windowRejectedFocusEvent:(const macdrv_event*)event
261     {
262         if (event->window_got_focus.serial == windowFocusSerial)
263         {
264             triedWindows = (NSMutableSet*)event->window_got_focus.tried_windows;
265             [triedWindows addObject:(WineWindow*)event->window];
266             for (NSWindow* window in [keyWindows arrayByAddingObjectsFromArray:[self orderedWindows]])
267             {
268                 if (![triedWindows containsObject:window] && [window canBecomeKeyWindow])
269                 {
270                     [window makeKeyWindow];
271                     break;
272                 }
273             }
274             triedWindows = nil;
275         }
276     }
277
278     - (void) keyboardSelectionDidChange
279     {
280         TISInputSourceRef inputSource;
281
282         inputSource = TISCopyCurrentKeyboardLayoutInputSource();
283         if (inputSource)
284         {
285             CFDataRef uchr;
286             uchr = TISGetInputSourceProperty(inputSource,
287                     kTISPropertyUnicodeKeyLayoutData);
288             if (uchr)
289             {
290                 macdrv_event event;
291                 WineEventQueue* queue;
292
293                 event.type = KEYBOARD_CHANGED;
294                 event.window = NULL;
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);
298
299                 if (event.keyboard_changed.uchr)
300                 {
301                     [eventQueuesLock lock];
302
303                     for (queue in eventQueues)
304                     {
305                         CFRetain(event.keyboard_changed.uchr);
306                         [queue postEvent:&event];
307                     }
308
309                     [eventQueuesLock unlock];
310
311                     CFRelease(event.keyboard_changed.uchr);
312                 }
313             }
314
315             CFRelease(inputSource);
316         }
317     }
318
319     - (CGFloat) primaryScreenHeight
320     {
321         if (!primaryScreenHeightValid)
322         {
323             NSArray* screens = [NSScreen screens];
324             if ([screens count])
325             {
326                 primaryScreenHeight = NSHeight([[screens objectAtIndex:0] frame]);
327                 primaryScreenHeightValid = TRUE;
328             }
329             else
330                 return 1280; /* arbitrary value */
331         }
332
333         return primaryScreenHeight;
334     }
335
336     - (NSPoint) flippedMouseLocation:(NSPoint)point
337     {
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;
342         return point;
343     }
344
345     - (void) flipRect:(NSRect*)rect
346     {
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);
351     }
352
353     - (void) wineWindow:(WineWindow*)window
354                 ordered:(NSWindowOrderingMode)order
355              relativeTo:(WineWindow*)otherWindow
356     {
357         NSUInteger index;
358
359         switch (order)
360         {
361             case NSWindowAbove:
362                 [window retain];
363                 [orderedWineWindows removeObjectIdenticalTo:window];
364                 if (otherWindow)
365                 {
366                     index = [orderedWineWindows indexOfObjectIdenticalTo:otherWindow];
367                     if (index == NSNotFound)
368                         index = 0;
369                 }
370                 else
371                 {
372                     index = 0;
373                     for (otherWindow in orderedWineWindows)
374                     {
375                         if ([otherWindow levelWhenActive] <= [window levelWhenActive])
376                             break;
377                         index++;
378                     }
379                 }
380                 [orderedWineWindows insertObject:window atIndex:index];
381                 [window release];
382                 break;
383             case NSWindowBelow:
384                 [window retain];
385                 [orderedWineWindows removeObjectIdenticalTo:window];
386                 if (otherWindow)
387                 {
388                     index = [orderedWineWindows indexOfObjectIdenticalTo:otherWindow];
389                     if (index == NSNotFound)
390                         index = [orderedWineWindows count];
391                 }
392                 else
393                 {
394                     index = 0;
395                     for (otherWindow in orderedWineWindows)
396                     {
397                         if ([otherWindow levelWhenActive] < [window levelWhenActive])
398                             break;
399                         index++;
400                     }
401                 }
402                 [orderedWineWindows insertObject:window atIndex:index];
403                 [window release];
404                 break;
405             case NSWindowOut:
406             default:
407                 break;
408         }
409     }
410
411     - (void) sendDisplaysChanged:(BOOL)activating
412     {
413         macdrv_event event;
414         WineEventQueue* queue;
415
416         event.type = DISPLAYS_CHANGED;
417         event.window = NULL;
418         event.displays_changed.activating = activating;
419
420         [eventQueuesLock lock];
421         for (queue in eventQueues)
422             [queue postEvent:&event];
423         [eventQueuesLock unlock];
424     }
425
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
432     {
433         NSString *encoding1, *encoding2;
434         uint32_t ioflags1, ioflags2, different;
435         double refresh1, refresh2;
436
437         if (CGDisplayModeGetWidth(mode1) != CGDisplayModeGetWidth(mode2)) return FALSE;
438         if (CGDisplayModeGetHeight(mode1) != CGDisplayModeGetHeight(mode2)) return FALSE;
439
440         encoding1 = [(NSString*)CGDisplayModeCopyPixelEncoding(mode1) autorelease];
441         encoding2 = [(NSString*)CGDisplayModeCopyPixelEncoding(mode2) autorelease];
442         if (![encoding1 isEqualToString:encoding2]) return FALSE;
443
444         ioflags1 = CGDisplayModeGetIOFlags(mode1);
445         ioflags2 = CGDisplayModeGetIOFlags(mode2);
446         different = ioflags1 ^ ioflags2;
447         if (different & (kDisplayModeValidFlag | kDisplayModeSafeFlag | kDisplayModeStretchedFlag |
448                          kDisplayModeInterlacedFlag | kDisplayModeTelevisionFlag))
449             return FALSE;
450
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;
456
457         return TRUE;
458     }
459
460     - (CGDisplayModeRef)modeMatchingMode:(CGDisplayModeRef)mode forDisplay:(CGDirectDisplayID)displayID
461     {
462         CGDisplayModeRef ret = NULL;
463         NSArray *modes = [(NSArray*)CGDisplayCopyAllDisplayModes(displayID, NULL) autorelease];
464         for (id candidateModeObject in modes)
465         {
466             CGDisplayModeRef candidateMode = (CGDisplayModeRef)candidateModeObject;
467             if ([self mode:candidateMode matchesMode:mode])
468             {
469                 ret = candidateMode;
470                 break;
471             }
472         }
473         return ret;
474     }
475
476     - (BOOL) setMode:(CGDisplayModeRef)mode forDisplay:(CGDirectDisplayID)displayID
477     {
478         BOOL ret = FALSE;
479         NSNumber* displayIDKey = [NSNumber numberWithUnsignedInt:displayID];
480         CGDisplayModeRef currentMode, originalMode;
481
482         currentMode = CGDisplayCopyDisplayMode(displayID);
483         if (!currentMode) // Invalid display ID
484             return FALSE;
485
486         if ([self mode:mode matchesMode:currentMode]) // Already there!
487         {
488             CGDisplayModeRelease(currentMode);
489             return TRUE;
490         }
491
492         mode = [self modeMatchingMode:mode forDisplay:displayID];
493         if (!mode)
494         {
495             CGDisplayModeRelease(currentMode);
496             return FALSE;
497         }
498
499         originalMode = (CGDisplayModeRef)[originalDisplayModes objectForKey:displayIDKey];
500         if (!originalMode)
501             originalMode = currentMode;
502
503         if ([self mode:mode matchesMode:originalMode])
504         {
505             if ([originalDisplayModes count] == 1) // If this is the last changed display, do a blanket reset
506             {
507                 CGRestorePermanentDisplayConfiguration();
508                 CGReleaseAllDisplays();
509                 [originalDisplayModes removeAllObjects];
510                 ret = TRUE;
511             }
512             else // ... otherwise, try to restore just the one display
513             {
514                 if (CGDisplaySetDisplayMode(displayID, mode, NULL) == CGDisplayNoErr)
515                 {
516                     [originalDisplayModes removeObjectForKey:displayIDKey];
517                     ret = TRUE;
518                 }
519             }
520         }
521         else
522         {
523             if ([originalDisplayModes count] || CGCaptureAllDisplays() == CGDisplayNoErr)
524             {
525                 if (CGDisplaySetDisplayMode(displayID, mode, NULL) == CGDisplayNoErr)
526                 {
527                     [originalDisplayModes setObject:(id)originalMode forKey:displayIDKey];
528                     ret = TRUE;
529                 }
530                 else if (![originalDisplayModes count])
531                 {
532                     CGRestorePermanentDisplayConfiguration();
533                     CGReleaseAllDisplays();
534                 }
535             }
536         }
537
538         CGDisplayModeRelease(currentMode);
539
540         if (ret)
541         {
542             [orderedWineWindows enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
543                 [(WineWindow*)obj adjustWindowLevel];
544             }];
545         }
546
547         return ret;
548     }
549
550     - (BOOL) areDisplaysCaptured
551     {
552         return ([originalDisplayModes count] > 0);
553     }
554
555     - (void) hideCursor
556     {
557         if (!cursorHidden)
558         {
559             [NSCursor hide];
560             cursorHidden = TRUE;
561         }
562     }
563
564     - (void) unhideCursor
565     {
566         if (cursorHidden)
567         {
568             [NSCursor unhide];
569             cursorHidden = FALSE;
570         }
571     }
572
573     - (void) setCursor
574     {
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"];
579         CGPoint hotSpot;
580         NSCursor* cursor;
581
582         if (!CGPointMakeWithDictionaryRepresentation(hotSpotDict, &hotSpot))
583             hotSpot = CGPointZero;
584         cursor = [[NSCursor alloc] initWithImage:image hotSpot:NSPointFromCGPoint(hotSpot)];
585         [image release];
586         [cursor set];
587         [self unhideCursor];
588         [cursor release];
589     }
590
591     - (void) nextCursorFrame:(NSTimer*)theTimer
592     {
593         NSDictionary* frame;
594         NSTimeInterval duration;
595         NSDate* date;
596
597         cursorFrame++;
598         if (cursorFrame >= [cursorFrames count])
599             cursorFrame = 0;
600         [self setCursor];
601
602         frame = [cursorFrames objectAtIndex:cursorFrame];
603         duration = [[frame objectForKey:@"duration"] doubleValue];
604         date = [[theTimer fireDate] dateByAddingTimeInterval:duration];
605         [cursorTimer setFireDate:date];
606     }
607
608     - (void) setCursorWithFrames:(NSArray*)frames
609     {
610         if (self.cursorFrames == frames)
611             return;
612
613         self.cursorFrames = frames;
614         cursorFrame = 0;
615         [cursorTimer invalidate];
616         self.cursorTimer = nil;
617
618         if ([frames count])
619         {
620             if ([frames count] > 1)
621             {
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
626                                                              interval:1000000
627                                                                target:self
628                                                              selector:@selector(nextCursorFrame:)
629                                                              userInfo:nil
630                                                               repeats:YES] autorelease];
631                 [[NSRunLoop currentRunLoop] addTimer:cursorTimer forMode:NSRunLoopCommonModes];
632             }
633
634             [self setCursor];
635         }
636     }
637
638     - (void) setApplicationIconFromCGImageArray:(NSArray*)images
639     {
640         NSImage* nsimage = nil;
641
642         if ([images count])
643         {
644             NSSize bestSize = NSZeroSize;
645             id image;
646
647             nsimage = [[[NSImage alloc] initWithSize:NSZeroSize] autorelease];
648
649             for (image in images)
650             {
651                 CGImageRef cgimage = (CGImageRef)image;
652                 NSBitmapImageRep* imageRep = [[NSBitmapImageRep alloc] initWithCGImage:cgimage];
653                 if (imageRep)
654                 {
655                     NSSize size = [imageRep size];
656
657                     [nsimage addRepresentation:imageRep];
658                     [imageRep release];
659
660                     if (MIN(size.width, size.height) > MIN(bestSize.width, bestSize.height))
661                         bestSize = size;
662                 }
663             }
664
665             if ([[nsimage representations] count] && bestSize.width && bestSize.height)
666                 [nsimage setSize:bestSize];
667             else
668                 nsimage = nil;
669         }
670
671         self.applicationIcon = nsimage;
672         [self setApplicationIconImage:nsimage];
673     }
674
675     - (void) handleCommandTab
676     {
677         if ([self isActive])
678         {
679             NSRunningApplication* thisApp = [NSRunningApplication currentApplication];
680             NSRunningApplication* app;
681             NSRunningApplication* otherValidApp = nil;
682
683             if ([originalDisplayModes count])
684             {
685                 CGRestorePermanentDisplayConfiguration();
686                 CGReleaseAllDisplays();
687                 [originalDisplayModes removeAllObjects];
688             }
689
690             for (app in [[NSWorkspace sharedWorkspace] runningApplications])
691             {
692                 if (![app isEqual:thisApp] && !app.terminated &&
693                     app.activationPolicy == NSApplicationActivationPolicyRegular)
694                 {
695                     if (!app.hidden)
696                     {
697                         // There's another visible app.  Just hide ourselves and let
698                         // the system activate the other app.
699                         [self hide:self];
700                         return;
701                     }
702
703                     if (!otherValidApp)
704                         otherValidApp = app;
705                 }
706             }
707
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;
713             [app unhide];
714             [app activateWithOptions:0];
715         }
716     }
717
718     /*
719      * ---------- Cursor clipping methods ----------
720      *
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.
727      *
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
732      * do that.
733      *
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().
741      */
742     - (void) clipCursorLocation:(CGPoint*)location
743     {
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;
752     }
753
754     - (BOOL) warpCursorTo:(CGPoint*)newLocation from:(const CGPoint*)currentLocation
755     {
756         CGPoint oldLocation;
757
758         if (currentLocation)
759             oldLocation = *currentLocation;
760         else
761             oldLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
762
763         if (!CGPointEqualToPoint(oldLocation, *newLocation))
764         {
765             WarpRecord* warpRecord = [[[WarpRecord alloc] init] autorelease];
766             CGError err;
767
768             warpRecord.from = oldLocation;
769             warpRecord.timeBefore = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
770
771             /* Actually move the cursor. */
772             err = CGWarpMouseCursorPosition(*newLocation);
773             if (err != kCGErrorSuccess)
774                 return FALSE;
775
776             warpRecord.timeAfter = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
777             *newLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
778
779             if (!CGPointEqualToPoint(oldLocation, *newLocation))
780             {
781                 warpRecord.to = *newLocation;
782                 [warpRecords addObject:warpRecord];
783             }
784         }
785
786         return TRUE;
787     }
788
789     - (BOOL) isMouseMoveEventType:(CGEventType)type
790     {
791         switch(type)
792         {
793         case kCGEventMouseMoved:
794         case kCGEventLeftMouseDragged:
795         case kCGEventRightMouseDragged:
796         case kCGEventOtherMouseDragged:
797             return TRUE;
798         }
799
800         return FALSE;
801     }
802
803     - (int) warpsFinishedByEventTime:(CGEventTimestamp)eventTime location:(CGPoint)eventLocation
804     {
805         int warpsFinished = 0;
806         for (WarpRecord* warpRecord in warpRecords)
807         {
808             if (warpRecord.timeAfter < eventTime ||
809                 (warpRecord.timeBefore <= eventTime && CGPointEqualToPoint(eventLocation, warpRecord.to)))
810                 warpsFinished++;
811             else
812                 break;
813         }
814
815         return warpsFinished;
816     }
817
818     - (CGEventRef) eventTapWithProxy:(CGEventTapProxy)proxy
819                                 type:(CGEventType)type
820                                event:(CGEventRef)event
821     {
822         CGEventTimestamp eventTime;
823         CGPoint eventLocation, cursorLocation;
824
825         if (type == kCGEventTapDisabledByUserInput)
826             return event;
827         if (type == kCGEventTapDisabledByTimeout)
828         {
829             CGEventTapEnable(cursorClippingEventTap, TRUE);
830             return event;
831         }
832
833         if (!clippingCursor)
834             return event;
835
836         eventTime = CGEventGetTimestamp(event);
837         lastEventTapEventTime = eventTime / (double)NSEC_PER_SEC;
838
839         eventLocation = CGEventGetLocation(event);
840
841         cursorLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
842
843         if ([self isMouseMoveEventType:type])
844         {
845             double deltaX, deltaY;
846             int warpsFinished = [self warpsFinishedByEventTime:eventTime location:eventLocation];
847             int i;
848
849             deltaX = CGEventGetDoubleValueField(event, kCGMouseEventDeltaX);
850             deltaY = CGEventGetDoubleValueField(event, kCGMouseEventDeltaY);
851
852             for (i = 0; i < warpsFinished; i++)
853             {
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];
858             }
859
860             if (warpsFinished)
861             {
862                 CGEventSetDoubleValueField(event, kCGMouseEventDeltaX, deltaX);
863                 CGEventSetDoubleValueField(event, kCGMouseEventDeltaY, deltaY);
864             }
865
866             synthesizedLocation.x += deltaX;
867             synthesizedLocation.y += deltaY;
868         }
869
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.
874         //
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];
880         else
881             lastSetCursorPositionTime = lastEventTapEventTime;
882
883         [self warpCursorTo:&synthesizedLocation from:&cursorLocation];
884         if (!CGPointEqualToPoint(eventLocation, synthesizedLocation))
885             CGEventSetLocation(event, synthesizedLocation);
886
887         return event;
888     }
889
890     CGEventRef WineAppEventTapCallBack(CGEventTapProxy proxy, CGEventType type,
891                                        CGEventRef event, void *refcon)
892     {
893         WineApplication* app = refcon;
894         return [app eventTapWithProxy:proxy type:type event:event];
895     }
896
897     - (BOOL) installEventTap
898     {
899         ProcessSerialNumber psn;
900         OSErr err;
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;
913         void* appServices;
914         OSErr (*pGetCurrentProcess)(ProcessSerialNumber* PSN);
915
916         if (cursorClippingEventTap)
917             return TRUE;
918
919         // We need to get the Mac GetCurrentProcess() from the ApplicationServices
920         // framework with dlsym() because the Win32 function of the same name
921         // obscures it.
922         appServices = dlopen("/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices", RTLD_LAZY);
923         if (!appServices)
924             return FALSE;
925
926         pGetCurrentProcess = dlsym(appServices, "GetCurrentProcess");
927         if (!pGetCurrentProcess)
928         {
929             dlclose(appServices);
930             return FALSE;
931         }
932
933         err = pGetCurrentProcess(&psn);
934         dlclose(appServices);
935         if (err != noErr)
936             return FALSE;
937
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)
946             return FALSE;
947
948         CGEventTapEnable(cursorClippingEventTap, FALSE);
949
950         source = CFMachPortCreateRunLoopSource(NULL, cursorClippingEventTap, 0);
951         if (!source)
952         {
953             CFRelease(cursorClippingEventTap);
954             cursorClippingEventTap = NULL;
955             return FALSE;
956         }
957
958         CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
959         CFRelease(source);
960         return TRUE;
961     }
962
963     - (BOOL) setCursorPosition:(CGPoint)pos
964     {
965         BOOL ret;
966
967         if (clippingCursor)
968         {
969             [self clipCursorLocation:&pos];
970
971             synthesizedLocation = pos;
972             ret = [self warpCursorTo:&synthesizedLocation from:NULL];
973             if (ret)
974             {
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;
982             }
983         }
984         else
985         {
986             ret = (CGWarpMouseCursorPosition(pos) == kCGErrorSuccess);
987             if (ret)
988                 lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
989         }
990
991         if (ret)
992         {
993             WineEventQueue* queue;
994
995             // Discard all pending mouse move events.
996             [eventQueuesLock lock];
997             for (queue in eventQueues)
998             {
999                 [queue discardEventsMatchingMask:event_mask_for_type(MOUSE_MOVED) |
1000                                                  event_mask_for_type(MOUSE_MOVED_ABSOLUTE)
1001                                        forWindow:nil];
1002             }
1003             [eventQueuesLock unlock];
1004         }
1005
1006         return ret;
1007     }
1008
1009     - (void) activateCursorClipping
1010     {
1011         if (clippingCursor)
1012         {
1013             CGEventTapEnable(cursorClippingEventTap, TRUE);
1014             [self setCursorPosition:NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]])];
1015         }
1016     }
1017
1018     - (void) deactivateCursorClipping
1019     {
1020         if (clippingCursor)
1021         {
1022             CGEventTapEnable(cursorClippingEventTap, FALSE);
1023             [warpRecords removeAllObjects];
1024             lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
1025         }
1026     }
1027
1028     - (BOOL) startClippingCursor:(CGRect)rect
1029     {
1030         CGError err;
1031
1032         if (!cursorClippingEventTap && ![self installEventTap])
1033             return FALSE;
1034
1035         err = CGAssociateMouseAndMouseCursorPosition(false);
1036         if (err != kCGErrorSuccess)
1037             return FALSE;
1038
1039         clippingCursor = TRUE;
1040         cursorClipRect = rect;
1041         if ([self isActive])
1042             [self activateCursorClipping];
1043
1044         return TRUE;
1045     }
1046
1047     - (BOOL) stopClippingCursor
1048     {
1049         CGError err = CGAssociateMouseAndMouseCursorPosition(true);
1050         if (err != kCGErrorSuccess)
1051             return FALSE;
1052
1053         [self deactivateCursorClipping];
1054         clippingCursor = FALSE;
1055
1056         return TRUE;
1057     }
1058
1059
1060     /*
1061      * ---------- NSApplication method overrides ----------
1062      */
1063     - (void) sendEvent:(NSEvent*)anEvent
1064     {
1065         NSEventType type = [anEvent type];
1066         if (type == NSFlagsChanged)
1067             self.lastFlagsChanged = anEvent;
1068
1069         [super sendEvent:anEvent];
1070
1071         if (type == NSMouseMoved || type == NSLeftMouseDragged ||
1072             type == NSRightMouseDragged || type == NSOtherMouseDragged)
1073         {
1074             WineWindow* targetWindow;
1075
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)
1081             {
1082                 CGPoint cgpoint = CGEventGetLocation([anEvent CGEvent]);
1083                 NSPoint point = [self flippedMouseLocation:NSPointFromCGPoint(cgpoint)];
1084                 NSInteger windowUnderNumber;
1085
1086                 windowUnderNumber = [NSWindow windowNumberAtPoint:point
1087                                       belowWindowWithWindowNumber:0];
1088                 targetWindow = (WineWindow*)[self windowWithWindowNumber:windowUnderNumber];
1089             }
1090             else
1091                 targetWindow = (WineWindow*)[anEvent window];
1092
1093             if ([targetWindow isKindOfClass:[WineWindow class]])
1094             {
1095                 BOOL absolute = forceNextMouseMoveAbsolute || (targetWindow != lastTargetWindow);
1096                 forceNextMouseMoveAbsolute = FALSE;
1097
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)
1102                 {
1103                     if ([anEvent timestamp] <= lastSetCursorPositionTime)
1104                         return;
1105
1106                     lastSetCursorPositionTime = 0;
1107                     absolute = TRUE;
1108                 }
1109
1110                 [targetWindow postMouseMovedEvent:anEvent absolute:absolute];
1111                 lastTargetWindow = targetWindow;
1112             }
1113             else if (lastTargetWindow)
1114             {
1115                 [[NSCursor arrowCursor] set];
1116                 [self unhideCursor];
1117                 lastTargetWindow = nil;
1118             }
1119         }
1120         else if (type == NSLeftMouseDown || type == NSLeftMouseUp ||
1121                  type == NSRightMouseDown || type == NSRightMouseUp ||
1122                  type == NSOtherMouseDown || type == NSOtherMouseUp ||
1123                  type == NSScrollWheel)
1124         {
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;
1129         }
1130         else if (type == NSKeyDown && ![anEvent isARepeat] && [anEvent keyCode] == kVK_Tab)
1131         {
1132             NSUInteger modifiers = [anEvent modifierFlags];
1133             if ((modifiers & NSCommandKeyMask) &&
1134                 !(modifiers & (NSControlKeyMask | NSAlternateKeyMask)))
1135             {
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];
1141             }
1142         }
1143     }
1144
1145
1146     /*
1147      * ---------- NSApplicationDelegate methods ----------
1148      */
1149     - (void)applicationDidBecomeActive:(NSNotification *)notification
1150     {
1151         [self activateCursorClipping];
1152
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]];
1157         }];
1158
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.
1165         //
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];
1170
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;
1175     }
1176
1177     - (void)applicationDidChangeScreenParameters:(NSNotification *)notification
1178     {
1179         primaryScreenHeightValid = FALSE;
1180         [self sendDisplaysChanged:FALSE];
1181
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;
1186     }
1187
1188     - (void)applicationDidResignActive:(NSNotification *)notification
1189     {
1190         macdrv_event event;
1191         WineEventQueue* queue;
1192
1193         [self invalidateGotFocusEvents];
1194
1195         event.type = APP_DEACTIVATED;
1196         event.window = NULL;
1197
1198         [eventQueuesLock lock];
1199         for (queue in eventQueues)
1200             [queue postEvent:&event];
1201         [eventQueuesLock unlock];
1202     }
1203
1204     - (void)applicationWillFinishLaunching:(NSNotification *)notification
1205     {
1206         NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
1207
1208         [nc addObserverForName:NSWindowDidBecomeKeyNotification
1209                         object:nil
1210                          queue:nil
1211                     usingBlock:^(NSNotification *note){
1212             NSWindow* window = [note object];
1213             [keyWindows removeObjectIdenticalTo:window];
1214             [keyWindows insertObject:window atIndex:0];
1215         }];
1216
1217         [nc addObserverForName:NSWindowWillCloseNotification
1218                         object:nil
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;
1226         }];
1227
1228         [nc addObserver:self
1229                selector:@selector(keyboardSelectionDidChange)
1230                    name:NSTextInputContextKeyboardSelectionDidChangeNotification
1231                  object:nil];
1232
1233         /* The above notification isn't sent unless the NSTextInputContext
1234            class has initialized itself.  Poke it. */
1235         [NSTextInputContext self];
1236
1237         self.keyboardType = LMGetKbdType();
1238     }
1239
1240     - (void)applicationWillResignActive:(NSNotification *)notification
1241     {
1242         [self deactivateCursorClipping];
1243
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];
1249         }];
1250     }
1251
1252 /***********************************************************************
1253  *              PerformRequest
1254  *
1255  * Run-loop-source perform callback.  Pull request blocks from the
1256  * array of queued requests and invoke them.
1257  */
1258 static void PerformRequest(void *info)
1259 {
1260     WineApplication* app = (WineApplication*)NSApp;
1261
1262     for (;;)
1263     {
1264         __block dispatch_block_t block;
1265
1266         dispatch_sync(app->requestsManipQueue, ^{
1267             if ([app->requests count])
1268             {
1269                 block = (dispatch_block_t)[[app->requests objectAtIndex:0] retain];
1270                 [app->requests removeObjectAtIndex:0];
1271             }
1272             else
1273                 block = nil;
1274         });
1275
1276         if (!block)
1277             break;
1278
1279         block();
1280         [block release];
1281     }
1282 }
1283
1284 /***********************************************************************
1285  *              OnMainThreadAsync
1286  *
1287  * Run a block on the main thread asynchronously.
1288  */
1289 void OnMainThreadAsync(dispatch_block_t block)
1290 {
1291     WineApplication* app = (WineApplication*)NSApp;
1292
1293     block = [block copy];
1294     dispatch_sync(app->requestsManipQueue, ^{
1295         [app->requests addObject:block];
1296     });
1297     [block release];
1298     CFRunLoopSourceSignal(app->requestSource);
1299     CFRunLoopWakeUp(CFRunLoopGetMain());
1300 }
1301
1302 @end
1303
1304 /***********************************************************************
1305  *              LogError
1306  */
1307 void LogError(const char* func, NSString* format, ...)
1308 {
1309     va_list args;
1310     va_start(args, format);
1311     LogErrorv(func, format, args);
1312     va_end(args);
1313 }
1314
1315 /***********************************************************************
1316  *              LogErrorv
1317  */
1318 void LogErrorv(const char* func, NSString* format, va_list args)
1319 {
1320     NSString* message = [[NSString alloc] initWithFormat:format arguments:args];
1321     fprintf(stderr, "err:%s:%s", func, [message UTF8String]);
1322     [message release];
1323 }
1324
1325 /***********************************************************************
1326  *              macdrv_window_rejected_focus
1327  *
1328  * Pass focus to the next window that hasn't already rejected this same
1329  * WINDOW_GOT_FOCUS event.
1330  */
1331 void macdrv_window_rejected_focus(const macdrv_event *event)
1332 {
1333     OnMainThread(^{
1334         [NSApp windowRejectedFocusEvent:event];
1335     });
1336 }
1337
1338 /***********************************************************************
1339  *              macdrv_get_keyboard_layout
1340  *
1341  * Returns the keyboard layout uchr data.
1342  */
1343 CFDataRef macdrv_copy_keyboard_layout(CGEventSourceKeyboardType* keyboard_type, int* is_iso)
1344 {
1345     __block CFDataRef result = NULL;
1346
1347     OnMainThread(^{
1348         TISInputSourceRef inputSource;
1349
1350         inputSource = TISCopyCurrentKeyboardLayoutInputSource();
1351         if (inputSource)
1352         {
1353             CFDataRef uchr = TISGetInputSourceProperty(inputSource,
1354                                 kTISPropertyUnicodeKeyLayoutData);
1355             result = CFDataCreateCopy(NULL, uchr);
1356             CFRelease(inputSource);
1357
1358             *keyboard_type = ((WineApplication*)NSApp).keyboardType;
1359             *is_iso = (KBGetLayoutType(*keyboard_type) == kKeyboardISO);
1360         }
1361     });
1362
1363     return result;
1364 }
1365
1366 /***********************************************************************
1367  *              macdrv_beep
1368  *
1369  * Play the beep sound configured by the user in System Preferences.
1370  */
1371 void macdrv_beep(void)
1372 {
1373     OnMainThreadAsync(^{
1374         NSBeep();
1375     });
1376 }
1377
1378 /***********************************************************************
1379  *              macdrv_set_display_mode
1380  */
1381 int macdrv_set_display_mode(const struct macdrv_display* display,
1382                             CGDisplayModeRef display_mode)
1383 {
1384     __block int ret;
1385
1386     OnMainThread(^{
1387         ret = [NSApp setMode:display_mode forDisplay:display->displayID];
1388     });
1389
1390     return ret;
1391 }
1392
1393 /***********************************************************************
1394  *              macdrv_set_cursor
1395  *
1396  * Set the cursor.
1397  *
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.
1401  *
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.
1410  *
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.
1413  */
1414 void macdrv_set_cursor(CFStringRef name, CFArrayRef frames)
1415 {
1416     SEL sel;
1417
1418     sel = NSSelectorFromString((NSString*)name);
1419     if (sel)
1420     {
1421         OnMainThreadAsync(^{
1422             NSCursor* cursor = [NSCursor performSelector:sel];
1423             [NSApp setCursorWithFrames:nil];
1424             [cursor set];
1425             [NSApp unhideCursor];
1426         });
1427     }
1428     else
1429     {
1430         NSArray* nsframes = (NSArray*)frames;
1431         if ([nsframes count])
1432         {
1433             OnMainThreadAsync(^{
1434                 [NSApp setCursorWithFrames:nsframes];
1435             });
1436         }
1437         else
1438         {
1439             OnMainThreadAsync(^{
1440                 [NSApp setCursorWithFrames:nil];
1441                 [NSApp hideCursor];
1442             });
1443         }
1444     }
1445 }
1446
1447 /***********************************************************************
1448  *              macdrv_get_cursor_position
1449  *
1450  * Obtains the current cursor position.  Returns zero on failure,
1451  * non-zero on success.
1452  */
1453 int macdrv_get_cursor_position(CGPoint *pos)
1454 {
1455     OnMainThread(^{
1456         NSPoint location = [NSEvent mouseLocation];
1457         location = [NSApp flippedMouseLocation:location];
1458         *pos = NSPointToCGPoint(location);
1459     });
1460
1461     return TRUE;
1462 }
1463
1464 /***********************************************************************
1465  *              macdrv_set_cursor_position
1466  *
1467  * Sets the cursor position without generating events.  Returns zero on
1468  * failure, non-zero on success.
1469  */
1470 int macdrv_set_cursor_position(CGPoint pos)
1471 {
1472     __block int ret;
1473
1474     OnMainThread(^{
1475         ret = [NSApp setCursorPosition:pos];
1476     });
1477
1478     return ret;
1479 }
1480
1481 /***********************************************************************
1482  *              macdrv_clip_cursor
1483  *
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.
1487  */
1488 int macdrv_clip_cursor(CGRect rect)
1489 {
1490     __block int ret;
1491
1492     OnMainThread(^{
1493         BOOL clipping = FALSE;
1494
1495         if (!CGRectIsInfinite(rect))
1496         {
1497             NSRect nsrect = NSRectFromCGRect(rect);
1498             NSScreen* screen;
1499
1500             /* Convert the rectangle from top-down coords to bottom-up. */
1501             [NSApp flipRect:&nsrect];
1502
1503             clipping = FALSE;
1504             for (screen in [NSScreen screens])
1505             {
1506                 if (!NSContainsRect(nsrect, [screen frame]))
1507                 {
1508                     clipping = TRUE;
1509                     break;
1510                 }
1511             }
1512         }
1513
1514         if (clipping)
1515             ret = [NSApp startClippingCursor:rect];
1516         else
1517             ret = [NSApp stopClippingCursor];
1518     });
1519
1520     return ret;
1521 }
1522
1523 /***********************************************************************
1524  *              macdrv_set_application_icon
1525  *
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.
1530  */
1531 void macdrv_set_application_icon(CFArrayRef images)
1532 {
1533     NSArray* imageArray = (NSArray*)images;
1534
1535     OnMainThreadAsync(^{
1536         [NSApp setApplicationIconFromCGImageArray:imageArray];
1537     });
1538 }