2 * MACDRV Cocoa application class
4 * Copyright 2011, 2012, 2013 Ken Thomases for CodeWeavers Inc.
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
21 #import <Carbon/Carbon.h>
25 #import "cocoa_event.h"
26 #import "cocoa_window.h"
29 static NSString* const WineAppWaitQueryResponseMode = @"WineAppWaitQueryResponseMode";
35 @implementation WineApplication
37 @synthesize wineController;
39 - (void) sendEvent:(NSEvent*)anEvent
41 if (![wineController handleEvent:anEvent])
43 [super sendEvent:anEvent];
44 [wineController didSendEvent:anEvent];
48 - (void) setWineController:(WineApplicationController*)newController
50 wineController = newController;
51 [self setDelegate:wineController];
57 @interface WarpRecord : NSObject
59 CGEventTimestamp timeBefore, timeAfter;
63 @property (nonatomic) CGEventTimestamp timeBefore;
64 @property (nonatomic) CGEventTimestamp timeAfter;
65 @property (nonatomic) CGPoint from;
66 @property (nonatomic) CGPoint to;
71 @implementation WarpRecord
73 @synthesize timeBefore, timeAfter, from, to;
78 @interface WineApplicationController ()
80 @property (readwrite, copy, nonatomic) NSEvent* lastFlagsChanged;
81 @property (copy, nonatomic) NSArray* cursorFrames;
82 @property (retain, nonatomic) NSTimer* cursorTimer;
83 @property (retain, nonatomic) NSImage* applicationIcon;
84 @property (readonly, nonatomic) BOOL inputSourceIsInputMethod;
85 @property (retain, nonatomic) WineWindow* mouseCaptureWindow;
87 - (void) setupObservations;
88 - (void) applicationDidBecomeActive:(NSNotification *)notification;
90 static void PerformRequest(void *info);
95 @implementation WineApplicationController
97 @synthesize keyboardType, lastFlagsChanged;
98 @synthesize applicationIcon;
99 @synthesize cursorFrames, cursorTimer;
100 @synthesize mouseCaptureWindow;
104 if (self == [WineApplicationController class])
106 NSDictionary* defaults = [NSDictionary dictionaryWithObjectsAndKeys:
107 @"", @"NSQuotedKeystrokeBinding",
108 @"", @"NSRepeatCountBinding",
109 [NSNumber numberWithBool:NO], @"ApplePressAndHoldEnabled",
111 [[NSUserDefaults standardUserDefaults] registerDefaults:defaults];
115 + (WineApplicationController*) sharedController
117 static WineApplicationController* sharedController;
118 static dispatch_once_t once;
120 dispatch_once(&once, ^{
121 sharedController = [[self alloc] init];
124 return sharedController;
132 CFRunLoopSourceContext context = { 0 };
133 context.perform = PerformRequest;
134 requestSource = CFRunLoopSourceCreate(NULL, 0, &context);
140 CFRunLoopAddSource(CFRunLoopGetMain(), requestSource, kCFRunLoopCommonModes);
141 CFRunLoopAddSource(CFRunLoopGetMain(), requestSource, (CFStringRef)WineAppWaitQueryResponseMode);
143 requests = [[NSMutableArray alloc] init];
144 requestsManipQueue = dispatch_queue_create("org.winehq.WineAppRequestManipQueue", NULL);
146 eventQueues = [[NSMutableArray alloc] init];
147 eventQueuesLock = [[NSLock alloc] init];
149 keyWindows = [[NSMutableArray alloc] init];
151 originalDisplayModes = [[NSMutableDictionary alloc] init];
153 warpRecords = [[NSMutableArray alloc] init];
155 if (!requests || !requestsManipQueue || !eventQueues || !eventQueuesLock ||
156 !keyWindows || !originalDisplayModes || !warpRecords)
162 [self setupObservations];
164 keyboardType = LMGetKbdType();
166 if ([NSApp isActive])
167 [self applicationDidBecomeActive:nil];
174 [screenFrameCGRects release];
175 [applicationIcon release];
176 [warpRecords release];
177 [cursorTimer release];
178 [cursorFrames release];
179 [originalDisplayModes release];
180 [keyWindows release];
181 [eventQueues release];
182 [eventQueuesLock release];
183 if (requestsManipQueue) dispatch_release(requestsManipQueue);
187 CFRunLoopSourceInvalidate(requestSource);
188 CFRelease(requestSource);
193 - (void) transformProcessToForeground
195 if ([NSApp activationPolicy] != NSApplicationActivationPolicyRegular)
199 NSString* bundleName;
203 [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
204 [NSApp activateIgnoringOtherApps:YES];
206 mainMenu = [[[NSMenu alloc] init] autorelease];
208 submenu = [[[NSMenu alloc] initWithTitle:@"Wine"] autorelease];
209 bundleName = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString*)kCFBundleNameKey];
210 if ([bundleName length])
211 title = [NSString stringWithFormat:@"Quit %@", bundleName];
214 item = [submenu addItemWithTitle:title action:@selector(terminate:) keyEquivalent:@"q"];
215 [item setKeyEquivalentModifierMask:NSCommandKeyMask | NSAlternateKeyMask];
216 item = [[[NSMenuItem alloc] init] autorelease];
217 [item setTitle:@"Wine"];
218 [item setSubmenu:submenu];
219 [mainMenu addItem:item];
221 submenu = [[[NSMenu alloc] initWithTitle:@"Window"] autorelease];
222 [submenu addItemWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@""];
223 [submenu addItemWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""];
224 [submenu addItem:[NSMenuItem separatorItem]];
225 [submenu addItemWithTitle:@"Bring All to Front" action:@selector(arrangeInFront:) keyEquivalent:@""];
226 item = [[[NSMenuItem alloc] init] autorelease];
227 [item setTitle:@"Window"];
228 [item setSubmenu:submenu];
229 [mainMenu addItem:item];
231 [NSApp setMainMenu:mainMenu];
232 [NSApp setWindowsMenu:submenu];
234 [NSApp setApplicationIconImage:self.applicationIcon];
238 - (BOOL) waitUntilQueryDone:(int*)done timeout:(NSDate*)timeout processEvents:(BOOL)processEvents
240 PerformRequest(NULL);
246 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
247 NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask
249 inMode:NSDefaultRunLoopMode
252 [NSApp sendEvent:event];
256 [[NSRunLoop currentRunLoop] runMode:WineAppWaitQueryResponseMode beforeDate:timeout];
257 } while (!*done && [timeout timeIntervalSinceNow] >= 0);
262 - (BOOL) registerEventQueue:(WineEventQueue*)queue
264 [eventQueuesLock lock];
265 [eventQueues addObject:queue];
266 [eventQueuesLock unlock];
270 - (void) unregisterEventQueue:(WineEventQueue*)queue
272 [eventQueuesLock lock];
273 [eventQueues removeObjectIdenticalTo:queue];
274 [eventQueuesLock unlock];
277 - (void) computeEventTimeAdjustmentFromTicks:(unsigned long long)tickcount uptime:(uint64_t)uptime_ns
279 eventTimeAdjustment = (tickcount / 1000.0) - (uptime_ns / (double)NSEC_PER_SEC);
282 - (double) ticksForEventTime:(NSTimeInterval)eventTime
284 return (eventTime + eventTimeAdjustment) * 1000;
287 /* Invalidate old focus offers across all queues. */
288 - (void) invalidateGotFocusEvents
290 WineEventQueue* queue;
294 [eventQueuesLock lock];
295 for (queue in eventQueues)
297 [queue discardEventsMatchingMask:event_mask_for_type(WINDOW_GOT_FOCUS)
300 [eventQueuesLock unlock];
303 - (void) windowGotFocus:(WineWindow*)window
307 [self invalidateGotFocusEvents];
309 event = macdrv_create_event(WINDOW_GOT_FOCUS, window);
310 event->window_got_focus.serial = windowFocusSerial;
312 event->window_got_focus.tried_windows = [triedWindows retain];
314 event->window_got_focus.tried_windows = [[NSMutableSet alloc] init];
315 [window.queue postEvent:event];
316 macdrv_release_event(event);
319 - (void) windowRejectedFocusEvent:(const macdrv_event*)event
321 if (event->window_got_focus.serial == windowFocusSerial)
323 NSMutableArray* windows = [keyWindows mutableCopy];
324 NSNumber* windowNumber;
327 for (windowNumber in [NSWindow windowNumbersWithOptions:NSWindowNumberListAllSpaces])
329 window = (WineWindow*)[NSApp windowWithWindowNumber:[windowNumber integerValue]];
330 if ([window isKindOfClass:[WineWindow class]] && [window screen] &&
331 ![windows containsObject:window])
332 [windows addObject:window];
335 triedWindows = (NSMutableSet*)event->window_got_focus.tried_windows;
336 [triedWindows addObject:(WineWindow*)event->window];
337 for (window in windows)
339 if (![triedWindows containsObject:window] && [window canBecomeKeyWindow])
341 [window makeKeyWindow];
350 - (void) keyboardSelectionDidChange
352 TISInputSourceRef inputSource;
354 inputSourceIsInputMethodValid = FALSE;
356 inputSource = TISCopyCurrentKeyboardLayoutInputSource();
360 uchr = TISGetInputSourceProperty(inputSource,
361 kTISPropertyUnicodeKeyLayoutData);
365 WineEventQueue* queue;
367 event = macdrv_create_event(KEYBOARD_CHANGED, nil);
368 event->keyboard_changed.keyboard_type = self.keyboardType;
369 event->keyboard_changed.iso_keyboard = (KBGetLayoutType(self.keyboardType) == kKeyboardISO);
370 event->keyboard_changed.uchr = CFDataCreateCopy(NULL, uchr);
372 if (event->keyboard_changed.uchr)
374 [eventQueuesLock lock];
376 for (queue in eventQueues)
377 [queue postEvent:event];
379 [eventQueuesLock unlock];
382 macdrv_release_event(event);
385 CFRelease(inputSource);
389 - (CGFloat) primaryScreenHeight
391 if (!primaryScreenHeightValid)
393 NSArray* screens = [NSScreen screens];
394 NSUInteger count = [screens count];
401 primaryScreenHeight = NSHeight([[screens objectAtIndex:0] frame]);
402 primaryScreenHeightValid = TRUE;
404 size = count * sizeof(CGRect);
405 if (!screenFrameCGRects)
406 screenFrameCGRects = [[NSMutableData alloc] initWithLength:size];
408 [screenFrameCGRects setLength:size];
410 rect = [screenFrameCGRects mutableBytes];
411 for (screen in screens)
413 CGRect temp = NSRectToCGRect([screen frame]);
414 temp.origin.y = primaryScreenHeight - CGRectGetMaxY(temp);
419 return 1280; /* arbitrary value */
422 return primaryScreenHeight;
425 - (NSPoint) flippedMouseLocation:(NSPoint)point
427 /* This relies on the fact that Cocoa's mouse location points are
428 actually off by one (precisely because they were flipped from
429 Quartz screen coordinates using this same technique). */
430 point.y = [self primaryScreenHeight] - point.y;
434 - (void) flipRect:(NSRect*)rect
436 // We don't use -primaryScreenHeight here so there's no chance of having
437 // out-of-date cached info. This method is called infrequently enough
438 // that getting the screen height each time is not prohibitively expensive.
439 rect->origin.y = NSMaxY([[[NSScreen screens] objectAtIndex:0] frame]) - NSMaxY(*rect);
442 - (WineWindow*) frontWineWindow
444 NSNumber* windowNumber;
445 for (windowNumber in [NSWindow windowNumbersWithOptions:NSWindowNumberListAllSpaces])
447 NSWindow* window = [NSApp windowWithWindowNumber:[windowNumber integerValue]];
448 if ([window isKindOfClass:[WineWindow class]] && [window screen])
449 return (WineWindow*)window;
455 - (void) adjustWindowLevels:(BOOL)active
457 NSArray* windowNumbers = [NSWindow windowNumbersWithOptions:0];
458 NSMutableArray* wineWindows = [[NSMutableArray alloc] initWithCapacity:[windowNumbers count]];
459 NSNumber* windowNumber;
460 NSUInteger nextFloatingIndex = 0;
461 __block NSInteger maxLevel = NSIntegerMin;
462 __block NSInteger maxNonfloatingLevel = NSNormalWindowLevel;
463 __block WineWindow* prev = nil;
466 // For the most part, we rely on the window server's ordering of the windows
467 // to be authoritative. The one exception is if the "floating" property of
468 // one of the windows has been changed, it may be in the wrong level and thus
469 // in the order. This method is what's supposed to fix that up. So build
470 // a list of Wine windows sorted first by floating-ness and then by order
471 // as indicated by the window server.
472 for (windowNumber in windowNumbers)
474 window = (WineWindow*)[NSApp windowWithWindowNumber:[windowNumber integerValue]];
475 if ([window isKindOfClass:[WineWindow class]])
478 [wineWindows insertObject:window atIndex:nextFloatingIndex++];
480 [wineWindows addObject:window];
484 NSDisableScreenUpdates();
486 // Go from back to front so that all windows in front of one which is
487 // elevated for full-screen are also elevated.
488 [wineWindows enumerateObjectsWithOptions:NSEnumerationReverse
489 usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
490 WineWindow* window = (WineWindow*)obj;
491 NSInteger origLevel = [window level];
492 NSInteger newLevel = [window minimumLevelForActive:active];
494 if (newLevel < maxLevel)
499 if (!window.floating && maxNonfloatingLevel < newLevel)
500 maxNonfloatingLevel = newLevel;
502 if (newLevel != origLevel)
504 [window setLevel:newLevel];
506 // -setLevel: puts the window at the front of its new level. If
507 // we decreased the level, that's good (it was in front of that
508 // level before, so it should still be now). But if we increased
509 // the level, the window should be toward the back (but still
510 // ahead of the previous windows we did this to).
511 if (origLevel < newLevel)
514 [window orderWindow:NSWindowAbove relativeTo:[prev windowNumber]];
516 [window orderBack:nil];
523 NSEnableScreenUpdates();
525 [wineWindows release];
527 // The above took care of the visible windows on the current space. That
528 // leaves windows on other spaces, minimized windows, and windows which
529 // are not ordered in. We want to leave windows on other spaces alone
530 // so the space remains just as they left it (when viewed in Exposé or
531 // Mission Control, for example). We'll adjust the window levels again
532 // after we switch to another space, anyway. Windows which aren't
533 // ordered in will be handled when we order them in. Minimized windows
534 // on the current space should be set to the level they would have gotten
535 // if they were at the front of the windows with the same floating-ness,
536 // because that's where they'll go if/when they are unminimized. Again,
537 // for good measure we'll adjust window levels again when a window is
539 for (window in [NSApp windows])
541 if ([window isKindOfClass:[WineWindow class]] && [window isMiniaturized] &&
542 [window isOnActiveSpace])
544 NSInteger origLevel = [window level];
545 NSInteger newLevel = [window minimumLevelForActive:YES];
546 NSInteger maxLevelForType = window.floating ? maxLevel : maxNonfloatingLevel;
548 if (newLevel < maxLevelForType)
549 newLevel = maxLevelForType;
551 if (newLevel != origLevel)
552 [window setLevel:newLevel];
557 - (void) adjustWindowLevels
559 [self adjustWindowLevels:[NSApp isActive]];
562 - (void) sendDisplaysChanged:(BOOL)activating
565 WineEventQueue* queue;
567 event = macdrv_create_event(DISPLAYS_CHANGED, nil);
568 event->displays_changed.activating = activating;
570 [eventQueuesLock lock];
572 // If we're activating, then we just need one of our threads to get the
573 // event, so it can send it directly to the desktop window. Otherwise,
574 // we need all of the threads to get it because we don't know which owns
575 // the desktop window and only that one will do anything with it.
576 if (activating) event->deliver = 1;
578 for (queue in eventQueues)
579 [queue postEvent:event];
580 [eventQueuesLock unlock];
582 macdrv_release_event(event);
585 // We can compare two modes directly using CFEqual, but that may require that
586 // they are identical to a level that we don't need. In particular, when the
587 // OS switches between the integrated and discrete GPUs, the set of display
588 // modes can change in subtle ways. We're interested in whether two modes
589 // match in their most salient features, even if they aren't identical.
590 - (BOOL) mode:(CGDisplayModeRef)mode1 matchesMode:(CGDisplayModeRef)mode2
592 NSString *encoding1, *encoding2;
593 uint32_t ioflags1, ioflags2, different;
594 double refresh1, refresh2;
596 if (CGDisplayModeGetWidth(mode1) != CGDisplayModeGetWidth(mode2)) return FALSE;
597 if (CGDisplayModeGetHeight(mode1) != CGDisplayModeGetHeight(mode2)) return FALSE;
599 encoding1 = [(NSString*)CGDisplayModeCopyPixelEncoding(mode1) autorelease];
600 encoding2 = [(NSString*)CGDisplayModeCopyPixelEncoding(mode2) autorelease];
601 if (![encoding1 isEqualToString:encoding2]) return FALSE;
603 ioflags1 = CGDisplayModeGetIOFlags(mode1);
604 ioflags2 = CGDisplayModeGetIOFlags(mode2);
605 different = ioflags1 ^ ioflags2;
606 if (different & (kDisplayModeValidFlag | kDisplayModeSafeFlag | kDisplayModeStretchedFlag |
607 kDisplayModeInterlacedFlag | kDisplayModeTelevisionFlag))
610 refresh1 = CGDisplayModeGetRefreshRate(mode1);
611 if (refresh1 == 0) refresh1 = 60;
612 refresh2 = CGDisplayModeGetRefreshRate(mode2);
613 if (refresh2 == 0) refresh2 = 60;
614 if (fabs(refresh1 - refresh2) > 0.1) return FALSE;
619 - (CGDisplayModeRef)modeMatchingMode:(CGDisplayModeRef)mode forDisplay:(CGDirectDisplayID)displayID
621 CGDisplayModeRef ret = NULL;
622 NSArray *modes = [(NSArray*)CGDisplayCopyAllDisplayModes(displayID, NULL) autorelease];
623 for (id candidateModeObject in modes)
625 CGDisplayModeRef candidateMode = (CGDisplayModeRef)candidateModeObject;
626 if ([self mode:candidateMode matchesMode:mode])
635 - (BOOL) setMode:(CGDisplayModeRef)mode forDisplay:(CGDirectDisplayID)displayID
638 NSNumber* displayIDKey = [NSNumber numberWithUnsignedInt:displayID];
639 CGDisplayModeRef currentMode, originalMode;
641 currentMode = CGDisplayCopyDisplayMode(displayID);
642 if (!currentMode) // Invalid display ID
645 if ([self mode:mode matchesMode:currentMode]) // Already there!
647 CGDisplayModeRelease(currentMode);
651 mode = [self modeMatchingMode:mode forDisplay:displayID];
654 CGDisplayModeRelease(currentMode);
658 originalMode = (CGDisplayModeRef)[originalDisplayModes objectForKey:displayIDKey];
660 originalMode = currentMode;
662 if ([self mode:mode matchesMode:originalMode])
664 if ([originalDisplayModes count] == 1) // If this is the last changed display, do a blanket reset
666 CGRestorePermanentDisplayConfiguration();
667 CGReleaseAllDisplays();
668 [originalDisplayModes removeAllObjects];
671 else // ... otherwise, try to restore just the one display
673 if (CGDisplaySetDisplayMode(displayID, mode, NULL) == CGDisplayNoErr)
675 [originalDisplayModes removeObjectForKey:displayIDKey];
682 if ([originalDisplayModes count] || CGCaptureAllDisplays() == CGDisplayNoErr)
684 if (CGDisplaySetDisplayMode(displayID, mode, NULL) == CGDisplayNoErr)
686 [originalDisplayModes setObject:(id)originalMode forKey:displayIDKey];
689 else if (![originalDisplayModes count])
691 CGRestorePermanentDisplayConfiguration();
692 CGReleaseAllDisplays();
697 CGDisplayModeRelease(currentMode);
700 [self adjustWindowLevels];
705 - (BOOL) areDisplaysCaptured
707 return ([originalDisplayModes count] > 0);
719 - (void) unhideCursor
724 cursorHidden = FALSE;
730 NSDictionary* frame = [cursorFrames objectAtIndex:cursorFrame];
731 CGImageRef cgimage = (CGImageRef)[frame objectForKey:@"image"];
732 NSImage* image = [[NSImage alloc] initWithCGImage:cgimage size:NSZeroSize];
733 CFDictionaryRef hotSpotDict = (CFDictionaryRef)[frame objectForKey:@"hotSpot"];
737 if (!CGPointMakeWithDictionaryRepresentation(hotSpotDict, &hotSpot))
738 hotSpot = CGPointZero;
739 cursor = [[NSCursor alloc] initWithImage:image hotSpot:NSPointFromCGPoint(hotSpot)];
746 - (void) nextCursorFrame:(NSTimer*)theTimer
749 NSTimeInterval duration;
753 if (cursorFrame >= [cursorFrames count])
757 frame = [cursorFrames objectAtIndex:cursorFrame];
758 duration = [[frame objectForKey:@"duration"] doubleValue];
759 date = [[theTimer fireDate] dateByAddingTimeInterval:duration];
760 [cursorTimer setFireDate:date];
763 - (void) setCursorWithFrames:(NSArray*)frames
765 if (self.cursorFrames == frames)
768 self.cursorFrames = frames;
770 [cursorTimer invalidate];
771 self.cursorTimer = nil;
775 if ([frames count] > 1)
777 NSDictionary* frame = [frames objectAtIndex:0];
778 NSTimeInterval duration = [[frame objectForKey:@"duration"] doubleValue];
779 NSDate* date = [NSDate dateWithTimeIntervalSinceNow:duration];
780 self.cursorTimer = [[[NSTimer alloc] initWithFireDate:date
783 selector:@selector(nextCursorFrame:)
785 repeats:YES] autorelease];
786 [[NSRunLoop currentRunLoop] addTimer:cursorTimer forMode:NSRunLoopCommonModes];
793 - (void) setApplicationIconFromCGImageArray:(NSArray*)images
795 NSImage* nsimage = nil;
799 NSSize bestSize = NSZeroSize;
802 nsimage = [[[NSImage alloc] initWithSize:NSZeroSize] autorelease];
804 for (image in images)
806 CGImageRef cgimage = (CGImageRef)image;
807 NSBitmapImageRep* imageRep = [[NSBitmapImageRep alloc] initWithCGImage:cgimage];
810 NSSize size = [imageRep size];
812 [nsimage addRepresentation:imageRep];
815 if (MIN(size.width, size.height) > MIN(bestSize.width, bestSize.height))
820 if ([[nsimage representations] count] && bestSize.width && bestSize.height)
821 [nsimage setSize:bestSize];
826 self.applicationIcon = nsimage;
827 [NSApp setApplicationIconImage:nsimage];
830 - (void) handleCommandTab
832 if ([NSApp isActive])
834 NSRunningApplication* thisApp = [NSRunningApplication currentApplication];
835 NSRunningApplication* app;
836 NSRunningApplication* otherValidApp = nil;
838 if ([originalDisplayModes count])
840 CGRestorePermanentDisplayConfiguration();
841 CGReleaseAllDisplays();
842 [originalDisplayModes removeAllObjects];
845 for (app in [[NSWorkspace sharedWorkspace] runningApplications])
847 if (![app isEqual:thisApp] && !app.terminated &&
848 app.activationPolicy == NSApplicationActivationPolicyRegular)
852 // There's another visible app. Just hide ourselves and let
853 // the system activate the other app.
863 // Didn't find a visible GUI app. Try the Finder or, if that's not
864 // running, the first hidden GUI app. If even that doesn't work, we
865 // just fail to switch and remain the active app.
866 app = [[NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.finder"] lastObject];
867 if (!app) app = otherValidApp;
869 [app activateWithOptions:0];
874 * ---------- Cursor clipping methods ----------
876 * Neither Quartz nor Cocoa has an exact analog for Win32 cursor clipping.
877 * For one simple case, clipping to a 1x1 rectangle, Quartz does have an
878 * equivalent: CGAssociateMouseAndMouseCursorPosition(false). For the
879 * general case, we leverage that. We disassociate mouse movements from
880 * the cursor position and then move the cursor manually, keeping it within
881 * the clipping rectangle.
883 * Moving the cursor manually isn't enough. We need to modify the event
884 * stream so that the events have the new location, too. We need to do
885 * this at a point before the events enter Cocoa, so that Cocoa will assign
886 * the correct window to the event. So, we install a Quartz event tap to
889 * Also, there's a complication when we move the cursor. We use
890 * CGWarpMouseCursorPosition(). That doesn't generate mouse movement
891 * events, but the change of cursor position is incorporated into the
892 * deltas of the next mouse move event. When the mouse is disassociated
893 * from the cursor position, we need the deltas to only reflect actual
894 * device movement, not programmatic changes. So, the event tap cancels
895 * out the change caused by our calls to CGWarpMouseCursorPosition().
897 - (void) clipCursorLocation:(CGPoint*)location
899 if (location->x < CGRectGetMinX(cursorClipRect))
900 location->x = CGRectGetMinX(cursorClipRect);
901 if (location->y < CGRectGetMinY(cursorClipRect))
902 location->y = CGRectGetMinY(cursorClipRect);
903 if (location->x > CGRectGetMaxX(cursorClipRect) - 1)
904 location->x = CGRectGetMaxX(cursorClipRect) - 1;
905 if (location->y > CGRectGetMaxY(cursorClipRect) - 1)
906 location->y = CGRectGetMaxY(cursorClipRect) - 1;
909 - (BOOL) warpCursorTo:(CGPoint*)newLocation from:(const CGPoint*)currentLocation
914 oldLocation = *currentLocation;
916 oldLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
918 if (!CGPointEqualToPoint(oldLocation, *newLocation))
920 WarpRecord* warpRecord = [[[WarpRecord alloc] init] autorelease];
923 warpRecord.from = oldLocation;
924 warpRecord.timeBefore = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
926 /* Actually move the cursor. */
927 err = CGWarpMouseCursorPosition(*newLocation);
928 if (err != kCGErrorSuccess)
931 warpRecord.timeAfter = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
932 *newLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
934 if (!CGPointEqualToPoint(oldLocation, *newLocation))
936 warpRecord.to = *newLocation;
937 [warpRecords addObject:warpRecord];
944 - (BOOL) isMouseMoveEventType:(CGEventType)type
948 case kCGEventMouseMoved:
949 case kCGEventLeftMouseDragged:
950 case kCGEventRightMouseDragged:
951 case kCGEventOtherMouseDragged:
958 - (int) warpsFinishedByEventTime:(CGEventTimestamp)eventTime location:(CGPoint)eventLocation
960 int warpsFinished = 0;
961 for (WarpRecord* warpRecord in warpRecords)
963 if (warpRecord.timeAfter < eventTime ||
964 (warpRecord.timeBefore <= eventTime && CGPointEqualToPoint(eventLocation, warpRecord.to)))
970 return warpsFinished;
973 - (CGEventRef) eventTapWithProxy:(CGEventTapProxy)proxy
974 type:(CGEventType)type
975 event:(CGEventRef)event
977 CGEventTimestamp eventTime;
978 CGPoint eventLocation, cursorLocation;
980 if (type == kCGEventTapDisabledByUserInput)
982 if (type == kCGEventTapDisabledByTimeout)
984 CGEventTapEnable(cursorClippingEventTap, TRUE);
991 eventTime = CGEventGetTimestamp(event);
992 lastEventTapEventTime = eventTime / (double)NSEC_PER_SEC;
994 eventLocation = CGEventGetLocation(event);
996 cursorLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
998 if ([self isMouseMoveEventType:type])
1000 double deltaX, deltaY;
1001 int warpsFinished = [self warpsFinishedByEventTime:eventTime location:eventLocation];
1004 deltaX = CGEventGetDoubleValueField(event, kCGMouseEventDeltaX);
1005 deltaY = CGEventGetDoubleValueField(event, kCGMouseEventDeltaY);
1007 for (i = 0; i < warpsFinished; i++)
1009 WarpRecord* warpRecord = [warpRecords objectAtIndex:0];
1010 deltaX -= warpRecord.to.x - warpRecord.from.x;
1011 deltaY -= warpRecord.to.y - warpRecord.from.y;
1012 [warpRecords removeObjectAtIndex:0];
1017 CGEventSetDoubleValueField(event, kCGMouseEventDeltaX, deltaX);
1018 CGEventSetDoubleValueField(event, kCGMouseEventDeltaY, deltaY);
1021 synthesizedLocation.x += deltaX;
1022 synthesizedLocation.y += deltaY;
1025 // If the event is destined for another process, don't clip it. This may
1026 // happen if the user activates Exposé or Mission Control. In that case,
1027 // our app does not resign active status, so clipping is still in effect,
1028 // but the cursor should not actually be clipped.
1030 // In addition, the fact that mouse moves may have been delivered to a
1031 // different process means we have to treat the next one we receive as
1032 // absolute rather than relative.
1033 if (CGEventGetIntegerValueField(event, kCGEventTargetUnixProcessID) == getpid())
1034 [self clipCursorLocation:&synthesizedLocation];
1036 lastSetCursorPositionTime = lastEventTapEventTime;
1038 [self warpCursorTo:&synthesizedLocation from:&cursorLocation];
1039 if (!CGPointEqualToPoint(eventLocation, synthesizedLocation))
1040 CGEventSetLocation(event, synthesizedLocation);
1045 CGEventRef WineAppEventTapCallBack(CGEventTapProxy proxy, CGEventType type,
1046 CGEventRef event, void *refcon)
1048 WineApplicationController* controller = refcon;
1049 return [controller eventTapWithProxy:proxy type:type event:event];
1052 - (BOOL) installEventTap
1054 ProcessSerialNumber psn;
1056 CGEventMask mask = CGEventMaskBit(kCGEventLeftMouseDown) |
1057 CGEventMaskBit(kCGEventLeftMouseUp) |
1058 CGEventMaskBit(kCGEventRightMouseDown) |
1059 CGEventMaskBit(kCGEventRightMouseUp) |
1060 CGEventMaskBit(kCGEventMouseMoved) |
1061 CGEventMaskBit(kCGEventLeftMouseDragged) |
1062 CGEventMaskBit(kCGEventRightMouseDragged) |
1063 CGEventMaskBit(kCGEventOtherMouseDown) |
1064 CGEventMaskBit(kCGEventOtherMouseUp) |
1065 CGEventMaskBit(kCGEventOtherMouseDragged) |
1066 CGEventMaskBit(kCGEventScrollWheel);
1067 CFRunLoopSourceRef source;
1069 OSErr (*pGetCurrentProcess)(ProcessSerialNumber* PSN);
1071 if (cursorClippingEventTap)
1074 // We need to get the Mac GetCurrentProcess() from the ApplicationServices
1075 // framework with dlsym() because the Win32 function of the same name
1077 appServices = dlopen("/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices", RTLD_LAZY);
1081 pGetCurrentProcess = dlsym(appServices, "GetCurrentProcess");
1082 if (!pGetCurrentProcess)
1084 dlclose(appServices);
1088 err = pGetCurrentProcess(&psn);
1089 dlclose(appServices);
1093 // We create an annotated session event tap rather than a process-specific
1094 // event tap because we need to programmatically move the cursor even when
1095 // mouse moves are directed to other processes. We disable our tap when
1096 // other processes are active, but things like Exposé are handled by other
1097 // processes even when we remain active.
1098 cursorClippingEventTap = CGEventTapCreate(kCGAnnotatedSessionEventTap, kCGHeadInsertEventTap,
1099 kCGEventTapOptionDefault, mask, WineAppEventTapCallBack, self);
1100 if (!cursorClippingEventTap)
1103 CGEventTapEnable(cursorClippingEventTap, FALSE);
1105 source = CFMachPortCreateRunLoopSource(NULL, cursorClippingEventTap, 0);
1108 CFRelease(cursorClippingEventTap);
1109 cursorClippingEventTap = NULL;
1113 CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
1118 - (BOOL) setCursorPosition:(CGPoint)pos
1124 [self clipCursorLocation:&pos];
1126 ret = [self warpCursorTo:&pos from:NULL];
1127 synthesizedLocation = pos;
1130 // We want to discard mouse-move events that have already been
1131 // through the event tap, because it's too late to account for
1132 // the setting of the cursor position with them. However, the
1133 // events that may be queued with times after that but before
1134 // the above warp can still be used. So, use the last event
1135 // tap event time so that -sendEvent: doesn't discard them.
1136 lastSetCursorPositionTime = lastEventTapEventTime;
1141 ret = (CGWarpMouseCursorPosition(pos) == kCGErrorSuccess);
1144 lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
1146 // Annoyingly, CGWarpMouseCursorPosition() effectively disassociates
1147 // the mouse from the cursor position for 0.25 seconds. This means
1148 // that mouse movement during that interval doesn't move the cursor
1149 // and events carry a constant location (the warped-to position)
1150 // even though they have delta values. This screws us up because
1151 // the accumulated deltas we send to Wine don't match any eventual
1152 // absolute position we send (like with a button press). We can
1153 // work around this by simply forcibly reassociating the mouse and
1155 CGAssociateMouseAndMouseCursorPosition(true);
1161 WineEventQueue* queue;
1163 // Discard all pending mouse move events.
1164 [eventQueuesLock lock];
1165 for (queue in eventQueues)
1167 [queue discardEventsMatchingMask:event_mask_for_type(MOUSE_MOVED) |
1168 event_mask_for_type(MOUSE_MOVED_ABSOLUTE)
1170 [queue resetMouseEventPositions:pos];
1172 [eventQueuesLock unlock];
1178 - (void) activateCursorClipping
1182 CGEventTapEnable(cursorClippingEventTap, TRUE);
1183 [self setCursorPosition:NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]])];
1187 - (void) deactivateCursorClipping
1191 CGEventTapEnable(cursorClippingEventTap, FALSE);
1192 [warpRecords removeAllObjects];
1193 lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
1197 - (BOOL) startClippingCursor:(CGRect)rect
1201 if (!cursorClippingEventTap && ![self installEventTap])
1204 err = CGAssociateMouseAndMouseCursorPosition(false);
1205 if (err != kCGErrorSuccess)
1208 clippingCursor = TRUE;
1209 cursorClipRect = rect;
1210 if ([NSApp isActive])
1211 [self activateCursorClipping];
1216 - (BOOL) stopClippingCursor
1218 CGError err = CGAssociateMouseAndMouseCursorPosition(true);
1219 if (err != kCGErrorSuccess)
1222 [self deactivateCursorClipping];
1223 clippingCursor = FALSE;
1228 - (void) handleMouseMove:(NSEvent*)anEvent
1230 WineWindow* targetWindow;
1231 BOOL drag = [anEvent type] != NSMouseMoved;
1233 if (mouseCaptureWindow)
1234 targetWindow = mouseCaptureWindow;
1236 targetWindow = (WineWindow*)[anEvent window];
1239 /* Because of the way -[NSWindow setAcceptsMouseMovedEvents:] works, the
1240 event indicates its window is the main window, even if the cursor is
1241 over a different window. Find the actual WineWindow that is under the
1242 cursor and post the event as being for that window. */
1243 CGPoint cgpoint = CGEventGetLocation([anEvent CGEvent]);
1244 NSPoint point = [self flippedMouseLocation:NSPointFromCGPoint(cgpoint)];
1245 NSInteger windowUnderNumber;
1247 windowUnderNumber = [NSWindow windowNumberAtPoint:point
1248 belowWindowWithWindowNumber:0];
1249 targetWindow = (WineWindow*)[NSApp windowWithWindowNumber:windowUnderNumber];
1252 if ([targetWindow isKindOfClass:[WineWindow class]])
1254 CGPoint point = CGEventGetLocation([anEvent CGEvent]);
1255 macdrv_event* event;
1258 // If we recently warped the cursor (other than in our cursor-clipping
1259 // event tap), discard mouse move events until we see an event which is
1260 // later than that time.
1261 if (lastSetCursorPositionTime)
1263 if ([anEvent timestamp] <= lastSetCursorPositionTime)
1266 lastSetCursorPositionTime = 0;
1267 forceNextMouseMoveAbsolute = TRUE;
1270 if (forceNextMouseMoveAbsolute || targetWindow != lastTargetWindow)
1273 forceNextMouseMoveAbsolute = FALSE;
1277 // Send absolute move events if the cursor is in the interior of
1278 // its range. Only send relative moves if the cursor is pinned to
1279 // the boundaries of where it can go. We compute the position
1280 // that's one additional point in the direction of movement. If
1281 // that is outside of the clipping rect or desktop region (the
1282 // union of the screen frames), then we figure the cursor would
1283 // have moved outside if it could but it was pinned.
1284 CGPoint computedPoint = point;
1285 CGFloat deltaX = [anEvent deltaX];
1286 CGFloat deltaY = [anEvent deltaY];
1290 else if (deltaX < -0.001)
1295 else if (deltaY < -0.001)
1298 // Assume cursor is pinned for now
1300 if (!clippingCursor || CGRectContainsPoint(cursorClipRect, computedPoint))
1302 const CGRect* rects;
1303 NSUInteger count, i;
1305 // Caches screenFrameCGRects if necessary
1306 [self primaryScreenHeight];
1308 rects = [screenFrameCGRects bytes];
1309 count = [screenFrameCGRects length] / sizeof(rects[0]);
1311 for (i = 0; i < count; i++)
1313 if (CGRectContainsPoint(rects[i], computedPoint))
1325 [self clipCursorLocation:&point];
1327 event = macdrv_create_event(MOUSE_MOVED_ABSOLUTE, targetWindow);
1328 event->mouse_moved.x = point.x;
1329 event->mouse_moved.y = point.y;
1331 mouseMoveDeltaX = 0;
1332 mouseMoveDeltaY = 0;
1336 /* Add event delta to accumulated delta error */
1337 /* deltaY is already flipped */
1338 mouseMoveDeltaX += [anEvent deltaX];
1339 mouseMoveDeltaY += [anEvent deltaY];
1341 event = macdrv_create_event(MOUSE_MOVED, targetWindow);
1342 event->mouse_moved.x = mouseMoveDeltaX;
1343 event->mouse_moved.y = mouseMoveDeltaY;
1345 /* Keep the remainder after integer truncation. */
1346 mouseMoveDeltaX -= event->mouse_moved.x;
1347 mouseMoveDeltaY -= event->mouse_moved.y;
1350 if (event->type == MOUSE_MOVED_ABSOLUTE || event->mouse_moved.x || event->mouse_moved.y)
1352 event->mouse_moved.time_ms = [self ticksForEventTime:[anEvent timestamp]];
1353 event->mouse_moved.drag = drag;
1355 [targetWindow.queue postEvent:event];
1358 macdrv_release_event(event);
1360 lastTargetWindow = targetWindow;
1362 else if (lastTargetWindow)
1364 [[NSCursor arrowCursor] set];
1365 [self unhideCursor];
1366 lastTargetWindow = nil;
1370 - (void) handleMouseButton:(NSEvent*)theEvent
1374 if (mouseCaptureWindow)
1375 window = mouseCaptureWindow;
1377 window = (WineWindow*)[theEvent window];
1379 if ([window isKindOfClass:[WineWindow class]])
1381 NSEventType type = [theEvent type];
1382 BOOL pressed = (type == NSLeftMouseDown || type == NSRightMouseDown || type == NSOtherMouseDown);
1383 CGPoint pt = CGEventGetLocation([theEvent CGEvent]);
1387 [self clipCursorLocation:&pt];
1391 if (mouseCaptureWindow)
1395 // Test if the click was in the window's content area.
1396 NSPoint nspoint = [self flippedMouseLocation:NSPointFromCGPoint(pt)];
1397 NSRect contentRect = [window contentRectForFrameRect:[window frame]];
1398 process = NSPointInRect(nspoint, contentRect);
1399 if (process && [window styleMask] & NSResizableWindowMask)
1401 // Ignore clicks in the grow box (resize widget).
1402 HIPoint origin = { 0, 0 };
1403 HIThemeGrowBoxDrawInfo info = { 0 };
1407 info.kind = kHIThemeGrowBoxKindNormal;
1408 info.direction = kThemeGrowRight | kThemeGrowDown;
1409 if ([window styleMask] & NSUtilityWindowMask)
1410 info.size = kHIThemeGrowBoxSizeSmall;
1412 info.size = kHIThemeGrowBoxSizeNormal;
1414 status = HIThemeGetGrowBoxBounds(&origin, &info, &bounds);
1415 if (status == noErr)
1417 NSRect growBox = NSMakeRect(NSMaxX(contentRect) - bounds.size.width,
1418 NSMinY(contentRect),
1420 bounds.size.height);
1421 process = !NSPointInRect(nspoint, growBox);
1426 unmatchedMouseDowns |= NSEventMaskFromType(type);
1430 NSEventType downType = type - 1;
1431 NSUInteger downMask = NSEventMaskFromType(downType);
1432 process = (unmatchedMouseDowns & downMask) != 0;
1433 unmatchedMouseDowns &= ~downMask;
1438 macdrv_event* event;
1440 event = macdrv_create_event(MOUSE_BUTTON, window);
1441 event->mouse_button.button = [theEvent buttonNumber];
1442 event->mouse_button.pressed = pressed;
1443 event->mouse_button.x = pt.x;
1444 event->mouse_button.y = pt.y;
1445 event->mouse_button.time_ms = [self ticksForEventTime:[theEvent timestamp]];
1447 [window.queue postEvent:event];
1449 macdrv_release_event(event);
1453 // Since mouse button events deliver absolute cursor position, the
1454 // accumulating delta from move events is invalidated. Make sure
1455 // next mouse move event starts over from an absolute baseline.
1456 // Also, it's at least possible that the title bar widgets (e.g. close
1457 // button, etc.) could enter an internal event loop on a mouse down that
1458 // wouldn't exit until a mouse up. In that case, we'd miss any mouse
1459 // dragged events and, after that, any notion of the cursor position
1460 // computed from accumulating deltas would be wrong.
1461 forceNextMouseMoveAbsolute = TRUE;
1464 - (void) handleScrollWheel:(NSEvent*)theEvent
1468 if (mouseCaptureWindow)
1469 window = mouseCaptureWindow;
1471 window = (WineWindow*)[theEvent window];
1473 if ([window isKindOfClass:[WineWindow class]])
1475 CGEventRef cgevent = [theEvent CGEvent];
1476 CGPoint pt = CGEventGetLocation(cgevent);
1480 [self clipCursorLocation:&pt];
1482 if (mouseCaptureWindow)
1486 // Only process the event if it was in the window's content area.
1487 NSPoint nspoint = [self flippedMouseLocation:NSPointFromCGPoint(pt)];
1488 NSRect contentRect = [window contentRectForFrameRect:[window frame]];
1489 process = NSPointInRect(nspoint, contentRect);
1494 macdrv_event* event;
1496 BOOL continuous = FALSE;
1498 event = macdrv_create_event(MOUSE_SCROLL, window);
1499 event->mouse_scroll.x = pt.x;
1500 event->mouse_scroll.y = pt.y;
1501 event->mouse_scroll.time_ms = [self ticksForEventTime:[theEvent timestamp]];
1503 if (CGEventGetIntegerValueField(cgevent, kCGScrollWheelEventIsContinuous))
1507 /* Continuous scroll wheel events come from high-precision scrolling
1508 hardware like Apple's Magic Mouse, Mighty Mouse, and trackpads.
1509 For these, we can get more precise data from the CGEvent API. */
1510 /* Axis 1 is vertical, axis 2 is horizontal. */
1511 x = CGEventGetDoubleValueField(cgevent, kCGScrollWheelEventPointDeltaAxis2);
1512 y = CGEventGetDoubleValueField(cgevent, kCGScrollWheelEventPointDeltaAxis1);
1516 double pixelsPerLine = 10;
1517 CGEventSourceRef source;
1519 /* The non-continuous values are in units of "lines", not pixels. */
1520 if ((source = CGEventCreateSourceFromEvent(cgevent)))
1522 pixelsPerLine = CGEventSourceGetPixelsPerLine(source);
1526 x = pixelsPerLine * [theEvent deltaX];
1527 y = pixelsPerLine * [theEvent deltaY];
1530 /* Mac: negative is right or down, positive is left or up.
1531 Win32: negative is left or down, positive is right or up.
1532 So, negate the X scroll value to translate. */
1535 /* The x,y values so far are in pixels. Win32 expects to receive some
1536 fraction of WHEEL_DELTA == 120. By my estimation, that's roughly
1537 6 times the pixel value. */
1538 event->mouse_scroll.x_scroll = 6 * x;
1539 event->mouse_scroll.y_scroll = 6 * y;
1543 /* For non-continuous "clicky" wheels, if there was any motion, make
1544 sure there was at least WHEEL_DELTA motion. This is so, at slow
1545 speeds where the system's acceleration curve is actually reducing the
1546 scroll distance, the user is sure to get some action out of each click.
1547 For example, this is important for rotating though weapons in a
1548 first-person shooter. */
1549 if (0 < event->mouse_scroll.x_scroll && event->mouse_scroll.x_scroll < 120)
1550 event->mouse_scroll.x_scroll = 120;
1551 else if (-120 < event->mouse_scroll.x_scroll && event->mouse_scroll.x_scroll < 0)
1552 event->mouse_scroll.x_scroll = -120;
1554 if (0 < event->mouse_scroll.y_scroll && event->mouse_scroll.y_scroll < 120)
1555 event->mouse_scroll.y_scroll = 120;
1556 else if (-120 < event->mouse_scroll.y_scroll && event->mouse_scroll.y_scroll < 0)
1557 event->mouse_scroll.y_scroll = -120;
1560 if (event->mouse_scroll.x_scroll || event->mouse_scroll.y_scroll)
1561 [window.queue postEvent:event];
1563 macdrv_release_event(event);
1565 // Since scroll wheel events deliver absolute cursor position, the
1566 // accumulating delta from move events is invalidated. Make sure next
1567 // mouse move event starts over from an absolute baseline.
1568 forceNextMouseMoveAbsolute = TRUE;
1573 // Returns TRUE if the event was handled and caller should do nothing more
1574 // with it. Returns FALSE if the caller should process it as normal and
1575 // then call -didSendEvent:.
1576 - (BOOL) handleEvent:(NSEvent*)anEvent
1579 NSEventType type = [anEvent type];
1581 if (type == NSFlagsChanged)
1582 self.lastFlagsChanged = anEvent;
1583 else if (type == NSMouseMoved || type == NSLeftMouseDragged ||
1584 type == NSRightMouseDragged || type == NSOtherMouseDragged)
1586 [self handleMouseMove:anEvent];
1587 ret = mouseCaptureWindow != nil;
1589 else if (type == NSLeftMouseDown || type == NSLeftMouseUp ||
1590 type == NSRightMouseDown || type == NSRightMouseUp ||
1591 type == NSOtherMouseDown || type == NSOtherMouseUp)
1593 [self handleMouseButton:anEvent];
1594 ret = mouseCaptureWindow != nil;
1596 else if (type == NSScrollWheel)
1598 [self handleScrollWheel:anEvent];
1599 ret = mouseCaptureWindow != nil;
1605 - (void) didSendEvent:(NSEvent*)anEvent
1607 NSEventType type = [anEvent type];
1609 if (type == NSKeyDown && ![anEvent isARepeat] && [anEvent keyCode] == kVK_Tab)
1611 NSUInteger modifiers = [anEvent modifierFlags];
1612 if ((modifiers & NSCommandKeyMask) &&
1613 !(modifiers & (NSControlKeyMask | NSAlternateKeyMask)))
1615 // Command-Tab and Command-Shift-Tab would normally be intercepted
1616 // by the system to switch applications. If we're seeing it, it's
1617 // presumably because we've captured the displays, preventing
1618 // normal application switching. Do it manually.
1619 [self handleCommandTab];
1624 - (void) setupObservations
1626 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
1627 NSNotificationCenter* wsnc = [[NSWorkspace sharedWorkspace] notificationCenter];
1629 [nc addObserverForName:NSWindowDidBecomeKeyNotification
1632 usingBlock:^(NSNotification *note){
1633 NSWindow* window = [note object];
1634 [keyWindows removeObjectIdenticalTo:window];
1635 [keyWindows insertObject:window atIndex:0];
1638 [nc addObserverForName:NSWindowWillCloseNotification
1640 queue:[NSOperationQueue mainQueue]
1641 usingBlock:^(NSNotification *note){
1642 NSWindow* window = [note object];
1643 [keyWindows removeObjectIdenticalTo:window];
1644 if (window == lastTargetWindow)
1645 lastTargetWindow = nil;
1646 if (window == self.mouseCaptureWindow)
1647 self.mouseCaptureWindow = nil;
1650 [nc addObserver:self
1651 selector:@selector(keyboardSelectionDidChange)
1652 name:NSTextInputContextKeyboardSelectionDidChangeNotification
1655 /* The above notification isn't sent unless the NSTextInputContext
1656 class has initialized itself. Poke it. */
1657 [NSTextInputContext self];
1659 [wsnc addObserver:self
1660 selector:@selector(adjustWindowLevels)
1661 name:NSWorkspaceActiveSpaceDidChangeNotification
1665 - (BOOL) inputSourceIsInputMethod
1667 if (!inputSourceIsInputMethodValid)
1669 TISInputSourceRef inputSource = TISCopyCurrentKeyboardInputSource();
1672 CFStringRef type = TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceType);
1673 inputSourceIsInputMethod = !CFEqual(type, kTISTypeKeyboardLayout);
1674 CFRelease(inputSource);
1677 inputSourceIsInputMethod = FALSE;
1678 inputSourceIsInputMethodValid = TRUE;
1681 return inputSourceIsInputMethod;
1686 * ---------- NSApplicationDelegate methods ----------
1688 - (void)applicationDidBecomeActive:(NSNotification *)notification
1690 [self activateCursorClipping];
1692 [self adjustWindowLevels:YES];
1694 if (![self frontWineWindow])
1696 for (WineWindow* window in [NSApp windows])
1698 if ([window isKindOfClass:[WineWindow class]] && [window isMiniaturized])
1700 [window deminiaturize:self];
1706 // If a Wine process terminates abruptly while it has the display captured
1707 // and switched to a different resolution, Mac OS X will uncapture the
1708 // displays and switch their resolutions back. However, the other Wine
1709 // processes won't have their notion of the desktop rect changed back.
1710 // This can lead them to refuse to draw or acknowledge clicks in certain
1711 // portions of their windows.
1713 // To solve this, we synthesize a displays-changed event whenever we're
1714 // activated. This will provoke a re-synchronization of Wine's notion of
1715 // the desktop rect with the actual state.
1716 [self sendDisplaysChanged:TRUE];
1718 // The cursor probably moved while we were inactive. Accumulated mouse
1719 // movement deltas are invalidated. Make sure the next mouse move event
1720 // starts over from an absolute baseline.
1721 forceNextMouseMoveAbsolute = TRUE;
1724 - (void)applicationDidChangeScreenParameters:(NSNotification *)notification
1726 primaryScreenHeightValid = FALSE;
1727 [self sendDisplaysChanged:FALSE];
1728 [self adjustWindowLevels];
1730 // When the display configuration changes, the cursor position may jump.
1731 // Accumulated mouse movement deltas are invalidated. Make sure the next
1732 // mouse move event starts over from an absolute baseline.
1733 forceNextMouseMoveAbsolute = TRUE;
1736 - (void)applicationDidResignActive:(NSNotification *)notification
1738 macdrv_event* event;
1739 WineEventQueue* queue;
1741 [self invalidateGotFocusEvents];
1743 event = macdrv_create_event(APP_DEACTIVATED, nil);
1745 [eventQueuesLock lock];
1746 for (queue in eventQueues)
1747 [queue postEvent:event];
1748 [eventQueuesLock unlock];
1750 macdrv_release_event(event);
1753 - (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication *)sender
1755 NSApplicationTerminateReply ret = NSTerminateNow;
1756 NSAppleEventManager* m = [NSAppleEventManager sharedAppleEventManager];
1757 NSAppleEventDescriptor* desc = [m currentAppleEvent];
1758 macdrv_event* event;
1759 WineEventQueue* queue;
1761 event = macdrv_create_event(APP_QUIT_REQUESTED, nil);
1763 switch ([[desc attributeDescriptorForKeyword:kAEQuitReason] int32Value])
1766 case kAEReallyLogOut:
1767 event->app_quit_requested.reason = QUIT_REASON_LOGOUT;
1769 case kAEShowRestartDialog:
1770 event->app_quit_requested.reason = QUIT_REASON_RESTART;
1772 case kAEShowShutdownDialog:
1773 event->app_quit_requested.reason = QUIT_REASON_SHUTDOWN;
1776 event->app_quit_requested.reason = QUIT_REASON_NONE;
1780 [eventQueuesLock lock];
1782 if ([eventQueues count])
1784 for (queue in eventQueues)
1785 [queue postEvent:event];
1786 ret = NSTerminateLater;
1789 [eventQueuesLock unlock];
1791 macdrv_release_event(event);
1796 - (void)applicationWillResignActive:(NSNotification *)notification
1798 [self deactivateCursorClipping];
1800 [self adjustWindowLevels:NO];
1803 /***********************************************************************
1806 * Run-loop-source perform callback. Pull request blocks from the
1807 * array of queued requests and invoke them.
1809 static void PerformRequest(void *info)
1811 WineApplicationController* controller = [WineApplicationController sharedController];
1815 __block dispatch_block_t block;
1817 dispatch_sync(controller->requestsManipQueue, ^{
1818 if ([controller->requests count])
1820 block = (dispatch_block_t)[[controller->requests objectAtIndex:0] retain];
1821 [controller->requests removeObjectAtIndex:0];
1835 /***********************************************************************
1838 * Run a block on the main thread asynchronously.
1840 void OnMainThreadAsync(dispatch_block_t block)
1842 WineApplicationController* controller = [WineApplicationController sharedController];
1844 block = [block copy];
1845 dispatch_sync(controller->requestsManipQueue, ^{
1846 [controller->requests addObject:block];
1849 CFRunLoopSourceSignal(controller->requestSource);
1850 CFRunLoopWakeUp(CFRunLoopGetMain());
1855 /***********************************************************************
1858 void LogError(const char* func, NSString* format, ...)
1861 va_start(args, format);
1862 LogErrorv(func, format, args);
1866 /***********************************************************************
1869 void LogErrorv(const char* func, NSString* format, va_list args)
1871 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
1873 NSString* message = [[NSString alloc] initWithFormat:format arguments:args];
1874 fprintf(stderr, "err:%s:%s", func, [message UTF8String]);
1880 /***********************************************************************
1881 * macdrv_window_rejected_focus
1883 * Pass focus to the next window that hasn't already rejected this same
1884 * WINDOW_GOT_FOCUS event.
1886 void macdrv_window_rejected_focus(const macdrv_event *event)
1889 [[WineApplicationController sharedController] windowRejectedFocusEvent:event];
1893 /***********************************************************************
1894 * macdrv_get_keyboard_layout
1896 * Returns the keyboard layout uchr data.
1898 CFDataRef macdrv_copy_keyboard_layout(CGEventSourceKeyboardType* keyboard_type, int* is_iso)
1900 __block CFDataRef result = NULL;
1903 TISInputSourceRef inputSource;
1905 inputSource = TISCopyCurrentKeyboardLayoutInputSource();
1908 CFDataRef uchr = TISGetInputSourceProperty(inputSource,
1909 kTISPropertyUnicodeKeyLayoutData);
1910 result = CFDataCreateCopy(NULL, uchr);
1911 CFRelease(inputSource);
1913 *keyboard_type = [WineApplicationController sharedController].keyboardType;
1914 *is_iso = (KBGetLayoutType(*keyboard_type) == kKeyboardISO);
1921 /***********************************************************************
1924 * Play the beep sound configured by the user in System Preferences.
1926 void macdrv_beep(void)
1928 OnMainThreadAsync(^{
1933 /***********************************************************************
1934 * macdrv_set_display_mode
1936 int macdrv_set_display_mode(const struct macdrv_display* display,
1937 CGDisplayModeRef display_mode)
1942 ret = [[WineApplicationController sharedController] setMode:display_mode forDisplay:display->displayID];
1948 /***********************************************************************
1953 * If name is non-NULL, it is a selector for a class method on NSCursor
1954 * identifying the cursor to set. In that case, frames is ignored. If
1955 * name is NULL, then frames is used.
1957 * frames is an array of dictionaries. Each dictionary is a frame of
1958 * an animated cursor. Under the key "image" is a CGImage for the
1959 * frame. Under the key "duration" is a CFNumber time interval, in
1960 * seconds, for how long that frame is presented before proceeding to
1961 * the next frame. Under the key "hotSpot" is a CFDictionary encoding a
1962 * CGPoint, to be decoded using CGPointMakeWithDictionaryRepresentation().
1963 * This is the hot spot, measured in pixels down and to the right of the
1964 * top-left corner of the image.
1966 * If the array has exactly 1 element, the cursor is static, not
1967 * animated. If frames is NULL or has 0 elements, the cursor is hidden.
1969 void macdrv_set_cursor(CFStringRef name, CFArrayRef frames)
1973 sel = NSSelectorFromString((NSString*)name);
1976 OnMainThreadAsync(^{
1977 WineApplicationController* controller = [WineApplicationController sharedController];
1978 NSCursor* cursor = [NSCursor performSelector:sel];
1979 [controller setCursorWithFrames:nil];
1981 [controller unhideCursor];
1986 NSArray* nsframes = (NSArray*)frames;
1987 if ([nsframes count])
1989 OnMainThreadAsync(^{
1990 [[WineApplicationController sharedController] setCursorWithFrames:nsframes];
1995 OnMainThreadAsync(^{
1996 WineApplicationController* controller = [WineApplicationController sharedController];
1997 [controller setCursorWithFrames:nil];
1998 [controller hideCursor];
2004 /***********************************************************************
2005 * macdrv_get_cursor_position
2007 * Obtains the current cursor position. Returns zero on failure,
2008 * non-zero on success.
2010 int macdrv_get_cursor_position(CGPoint *pos)
2013 NSPoint location = [NSEvent mouseLocation];
2014 location = [[WineApplicationController sharedController] flippedMouseLocation:location];
2015 *pos = NSPointToCGPoint(location);
2021 /***********************************************************************
2022 * macdrv_set_cursor_position
2024 * Sets the cursor position without generating events. Returns zero on
2025 * failure, non-zero on success.
2027 int macdrv_set_cursor_position(CGPoint pos)
2032 ret = [[WineApplicationController sharedController] setCursorPosition:pos];
2038 /***********************************************************************
2039 * macdrv_clip_cursor
2041 * Sets the cursor cursor clipping rectangle. If the rectangle is equal
2042 * to or larger than the whole desktop region, the cursor is unclipped.
2043 * Returns zero on failure, non-zero on success.
2045 int macdrv_clip_cursor(CGRect rect)
2050 WineApplicationController* controller = [WineApplicationController sharedController];
2051 BOOL clipping = FALSE;
2053 if (!CGRectIsInfinite(rect))
2055 NSRect nsrect = NSRectFromCGRect(rect);
2058 /* Convert the rectangle from top-down coords to bottom-up. */
2059 [controller flipRect:&nsrect];
2062 for (screen in [NSScreen screens])
2064 if (!NSContainsRect(nsrect, [screen frame]))
2073 ret = [controller startClippingCursor:rect];
2075 ret = [controller stopClippingCursor];
2081 /***********************************************************************
2082 * macdrv_set_application_icon
2084 * Set the application icon. The images array contains CGImages. If
2085 * there are more than one, then they represent different sizes or
2086 * color depths from the icon resource. If images is NULL or empty,
2087 * restores the default application image.
2089 void macdrv_set_application_icon(CFArrayRef images)
2091 NSArray* imageArray = (NSArray*)images;
2093 OnMainThreadAsync(^{
2094 [[WineApplicationController sharedController] setApplicationIconFromCGImageArray:imageArray];
2098 /***********************************************************************
2101 void macdrv_quit_reply(int reply)
2104 [NSApp replyToApplicationShouldTerminate:reply];
2108 /***********************************************************************
2109 * macdrv_using_input_method
2111 int macdrv_using_input_method(void)
2116 ret = [[WineApplicationController sharedController] inputSourceIsInputMethod];
2122 /***********************************************************************
2123 * macdrv_set_mouse_capture_window
2125 void macdrv_set_mouse_capture_window(macdrv_window window)
2127 WineWindow* w = (WineWindow*)window;
2130 [[WineApplicationController sharedController] setMouseCaptureWindow:w];