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];
209 submenu = [[[NSMenu alloc] initWithTitle:@"Wine"] autorelease];
210 bundleName = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString*)kCFBundleNameKey];
212 if ([bundleName length])
213 title = [NSString stringWithFormat:@"Hide %@", bundleName];
216 item = [submenu addItemWithTitle:title action:@selector(hide:) keyEquivalent:@""];
218 item = [submenu addItemWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h"];
219 [item setKeyEquivalentModifierMask:NSCommandKeyMask | NSAlternateKeyMask];
221 item = [submenu addItemWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@""];
223 [submenu addItem:[NSMenuItem separatorItem]];
225 if ([bundleName length])
226 title = [NSString stringWithFormat:@"Quit %@", bundleName];
229 item = [submenu addItemWithTitle:title action:@selector(terminate:) keyEquivalent:@"q"];
230 [item setKeyEquivalentModifierMask:NSCommandKeyMask | NSAlternateKeyMask];
231 item = [[[NSMenuItem alloc] init] autorelease];
232 [item setTitle:@"Wine"];
233 [item setSubmenu:submenu];
234 [mainMenu addItem:item];
237 submenu = [[[NSMenu alloc] initWithTitle:@"Window"] autorelease];
238 [submenu addItemWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@""];
239 [submenu addItemWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""];
240 [submenu addItem:[NSMenuItem separatorItem]];
241 [submenu addItemWithTitle:@"Bring All to Front" action:@selector(arrangeInFront:) keyEquivalent:@""];
242 item = [[[NSMenuItem alloc] init] autorelease];
243 [item setTitle:@"Window"];
244 [item setSubmenu:submenu];
245 [mainMenu addItem:item];
247 [NSApp setMainMenu:mainMenu];
248 [NSApp setWindowsMenu:submenu];
250 [NSApp setApplicationIconImage:self.applicationIcon];
254 - (BOOL) waitUntilQueryDone:(int*)done timeout:(NSDate*)timeout processEvents:(BOOL)processEvents
256 PerformRequest(NULL);
262 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
263 NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask
265 inMode:NSDefaultRunLoopMode
268 [NSApp sendEvent:event];
272 [[NSRunLoop currentRunLoop] runMode:WineAppWaitQueryResponseMode beforeDate:timeout];
273 } while (!*done && [timeout timeIntervalSinceNow] >= 0);
278 - (BOOL) registerEventQueue:(WineEventQueue*)queue
280 [eventQueuesLock lock];
281 [eventQueues addObject:queue];
282 [eventQueuesLock unlock];
286 - (void) unregisterEventQueue:(WineEventQueue*)queue
288 [eventQueuesLock lock];
289 [eventQueues removeObjectIdenticalTo:queue];
290 [eventQueuesLock unlock];
293 - (void) computeEventTimeAdjustmentFromTicks:(unsigned long long)tickcount uptime:(uint64_t)uptime_ns
295 eventTimeAdjustment = (tickcount / 1000.0) - (uptime_ns / (double)NSEC_PER_SEC);
298 - (double) ticksForEventTime:(NSTimeInterval)eventTime
300 return (eventTime + eventTimeAdjustment) * 1000;
303 /* Invalidate old focus offers across all queues. */
304 - (void) invalidateGotFocusEvents
306 WineEventQueue* queue;
310 [eventQueuesLock lock];
311 for (queue in eventQueues)
313 [queue discardEventsMatchingMask:event_mask_for_type(WINDOW_GOT_FOCUS)
316 [eventQueuesLock unlock];
319 - (void) windowGotFocus:(WineWindow*)window
323 [self invalidateGotFocusEvents];
325 event = macdrv_create_event(WINDOW_GOT_FOCUS, window);
326 event->window_got_focus.serial = windowFocusSerial;
328 event->window_got_focus.tried_windows = [triedWindows retain];
330 event->window_got_focus.tried_windows = [[NSMutableSet alloc] init];
331 [window.queue postEvent:event];
332 macdrv_release_event(event);
335 - (void) windowRejectedFocusEvent:(const macdrv_event*)event
337 if (event->window_got_focus.serial == windowFocusSerial)
339 NSMutableArray* windows = [keyWindows mutableCopy];
340 NSNumber* windowNumber;
343 for (windowNumber in [NSWindow windowNumbersWithOptions:NSWindowNumberListAllSpaces])
345 window = (WineWindow*)[NSApp windowWithWindowNumber:[windowNumber integerValue]];
346 if ([window isKindOfClass:[WineWindow class]] && [window screen] &&
347 ![windows containsObject:window])
348 [windows addObject:window];
351 triedWindows = (NSMutableSet*)event->window_got_focus.tried_windows;
352 [triedWindows addObject:(WineWindow*)event->window];
353 for (window in windows)
355 if (![triedWindows containsObject:window] && [window canBecomeKeyWindow])
357 [window makeKeyWindow];
366 - (void) keyboardSelectionDidChange
368 TISInputSourceRef inputSource;
370 inputSourceIsInputMethodValid = FALSE;
372 inputSource = TISCopyCurrentKeyboardLayoutInputSource();
376 uchr = TISGetInputSourceProperty(inputSource,
377 kTISPropertyUnicodeKeyLayoutData);
381 WineEventQueue* queue;
383 event = macdrv_create_event(KEYBOARD_CHANGED, nil);
384 event->keyboard_changed.keyboard_type = self.keyboardType;
385 event->keyboard_changed.iso_keyboard = (KBGetLayoutType(self.keyboardType) == kKeyboardISO);
386 event->keyboard_changed.uchr = CFDataCreateCopy(NULL, uchr);
388 if (event->keyboard_changed.uchr)
390 [eventQueuesLock lock];
392 for (queue in eventQueues)
393 [queue postEvent:event];
395 [eventQueuesLock unlock];
398 macdrv_release_event(event);
401 CFRelease(inputSource);
405 - (CGFloat) primaryScreenHeight
407 if (!primaryScreenHeightValid)
409 NSArray* screens = [NSScreen screens];
410 NSUInteger count = [screens count];
417 primaryScreenHeight = NSHeight([[screens objectAtIndex:0] frame]);
418 primaryScreenHeightValid = TRUE;
420 size = count * sizeof(CGRect);
421 if (!screenFrameCGRects)
422 screenFrameCGRects = [[NSMutableData alloc] initWithLength:size];
424 [screenFrameCGRects setLength:size];
426 rect = [screenFrameCGRects mutableBytes];
427 for (screen in screens)
429 CGRect temp = NSRectToCGRect([screen frame]);
430 temp.origin.y = primaryScreenHeight - CGRectGetMaxY(temp);
435 return 1280; /* arbitrary value */
438 return primaryScreenHeight;
441 - (NSPoint) flippedMouseLocation:(NSPoint)point
443 /* This relies on the fact that Cocoa's mouse location points are
444 actually off by one (precisely because they were flipped from
445 Quartz screen coordinates using this same technique). */
446 point.y = [self primaryScreenHeight] - point.y;
450 - (void) flipRect:(NSRect*)rect
452 // We don't use -primaryScreenHeight here so there's no chance of having
453 // out-of-date cached info. This method is called infrequently enough
454 // that getting the screen height each time is not prohibitively expensive.
455 rect->origin.y = NSMaxY([[[NSScreen screens] objectAtIndex:0] frame]) - NSMaxY(*rect);
458 - (WineWindow*) frontWineWindow
460 NSNumber* windowNumber;
461 for (windowNumber in [NSWindow windowNumbersWithOptions:NSWindowNumberListAllSpaces])
463 NSWindow* window = [NSApp windowWithWindowNumber:[windowNumber integerValue]];
464 if ([window isKindOfClass:[WineWindow class]] && [window screen])
465 return (WineWindow*)window;
471 - (void) adjustWindowLevels:(BOOL)active
473 NSArray* windowNumbers = [NSWindow windowNumbersWithOptions:0];
474 NSMutableArray* wineWindows = [[NSMutableArray alloc] initWithCapacity:[windowNumbers count]];
475 NSNumber* windowNumber;
476 NSUInteger nextFloatingIndex = 0;
477 __block NSInteger maxLevel = NSIntegerMin;
478 __block NSInteger maxNonfloatingLevel = NSNormalWindowLevel;
479 __block WineWindow* prev = nil;
482 // For the most part, we rely on the window server's ordering of the windows
483 // to be authoritative. The one exception is if the "floating" property of
484 // one of the windows has been changed, it may be in the wrong level and thus
485 // in the order. This method is what's supposed to fix that up. So build
486 // a list of Wine windows sorted first by floating-ness and then by order
487 // as indicated by the window server.
488 for (windowNumber in windowNumbers)
490 window = (WineWindow*)[NSApp windowWithWindowNumber:[windowNumber integerValue]];
491 if ([window isKindOfClass:[WineWindow class]])
494 [wineWindows insertObject:window atIndex:nextFloatingIndex++];
496 [wineWindows addObject:window];
500 NSDisableScreenUpdates();
502 // Go from back to front so that all windows in front of one which is
503 // elevated for full-screen are also elevated.
504 [wineWindows enumerateObjectsWithOptions:NSEnumerationReverse
505 usingBlock:^(id obj, NSUInteger idx, BOOL *stop){
506 WineWindow* window = (WineWindow*)obj;
507 NSInteger origLevel = [window level];
508 NSInteger newLevel = [window minimumLevelForActive:active];
510 if (newLevel < maxLevel)
515 if (!window.floating && maxNonfloatingLevel < newLevel)
516 maxNonfloatingLevel = newLevel;
518 if (newLevel != origLevel)
520 [window setLevel:newLevel];
522 // -setLevel: puts the window at the front of its new level. If
523 // we decreased the level, that's good (it was in front of that
524 // level before, so it should still be now). But if we increased
525 // the level, the window should be toward the back (but still
526 // ahead of the previous windows we did this to).
527 if (origLevel < newLevel)
530 [window orderWindow:NSWindowAbove relativeTo:[prev windowNumber]];
532 [window orderBack:nil];
539 NSEnableScreenUpdates();
541 [wineWindows release];
543 // The above took care of the visible windows on the current space. That
544 // leaves windows on other spaces, minimized windows, and windows which
545 // are not ordered in. We want to leave windows on other spaces alone
546 // so the space remains just as they left it (when viewed in Exposé or
547 // Mission Control, for example). We'll adjust the window levels again
548 // after we switch to another space, anyway. Windows which aren't
549 // ordered in will be handled when we order them in. Minimized windows
550 // on the current space should be set to the level they would have gotten
551 // if they were at the front of the windows with the same floating-ness,
552 // because that's where they'll go if/when they are unminimized. Again,
553 // for good measure we'll adjust window levels again when a window is
555 for (window in [NSApp windows])
557 if ([window isKindOfClass:[WineWindow class]] && [window isMiniaturized] &&
558 [window isOnActiveSpace])
560 NSInteger origLevel = [window level];
561 NSInteger newLevel = [window minimumLevelForActive:YES];
562 NSInteger maxLevelForType = window.floating ? maxLevel : maxNonfloatingLevel;
564 if (newLevel < maxLevelForType)
565 newLevel = maxLevelForType;
567 if (newLevel != origLevel)
568 [window setLevel:newLevel];
573 - (void) adjustWindowLevels
575 [self adjustWindowLevels:[NSApp isActive]];
578 - (void) updateFullscreenWindows
580 if (capture_displays_for_fullscreen && [NSApp isActive])
582 BOOL anyFullscreen = FALSE;
583 NSNumber* windowNumber;
584 for (windowNumber in [NSWindow windowNumbersWithOptions:0])
586 WineWindow* window = (WineWindow*)[NSApp windowWithWindowNumber:[windowNumber integerValue]];
587 if ([window isKindOfClass:[WineWindow class]] && window.fullscreen)
589 anyFullscreen = TRUE;
596 if ([self areDisplaysCaptured] || CGCaptureAllDisplays() == CGDisplayNoErr)
597 displaysCapturedForFullscreen = TRUE;
599 else if (displaysCapturedForFullscreen)
601 if ([originalDisplayModes count] || CGReleaseAllDisplays() == CGDisplayNoErr)
602 displaysCapturedForFullscreen = FALSE;
607 - (void) activeSpaceDidChange
609 [self updateFullscreenWindows];
610 [self adjustWindowLevels];
613 - (void) sendDisplaysChanged:(BOOL)activating
616 WineEventQueue* queue;
618 event = macdrv_create_event(DISPLAYS_CHANGED, nil);
619 event->displays_changed.activating = activating;
621 [eventQueuesLock lock];
623 // If we're activating, then we just need one of our threads to get the
624 // event, so it can send it directly to the desktop window. Otherwise,
625 // we need all of the threads to get it because we don't know which owns
626 // the desktop window and only that one will do anything with it.
627 if (activating) event->deliver = 1;
629 for (queue in eventQueues)
630 [queue postEvent:event];
631 [eventQueuesLock unlock];
633 macdrv_release_event(event);
636 // We can compare two modes directly using CFEqual, but that may require that
637 // they are identical to a level that we don't need. In particular, when the
638 // OS switches between the integrated and discrete GPUs, the set of display
639 // modes can change in subtle ways. We're interested in whether two modes
640 // match in their most salient features, even if they aren't identical.
641 - (BOOL) mode:(CGDisplayModeRef)mode1 matchesMode:(CGDisplayModeRef)mode2
643 NSString *encoding1, *encoding2;
644 uint32_t ioflags1, ioflags2, different;
645 double refresh1, refresh2;
647 if (CGDisplayModeGetWidth(mode1) != CGDisplayModeGetWidth(mode2)) return FALSE;
648 if (CGDisplayModeGetHeight(mode1) != CGDisplayModeGetHeight(mode2)) return FALSE;
650 encoding1 = [(NSString*)CGDisplayModeCopyPixelEncoding(mode1) autorelease];
651 encoding2 = [(NSString*)CGDisplayModeCopyPixelEncoding(mode2) autorelease];
652 if (![encoding1 isEqualToString:encoding2]) return FALSE;
654 ioflags1 = CGDisplayModeGetIOFlags(mode1);
655 ioflags2 = CGDisplayModeGetIOFlags(mode2);
656 different = ioflags1 ^ ioflags2;
657 if (different & (kDisplayModeValidFlag | kDisplayModeSafeFlag | kDisplayModeStretchedFlag |
658 kDisplayModeInterlacedFlag | kDisplayModeTelevisionFlag))
661 refresh1 = CGDisplayModeGetRefreshRate(mode1);
662 if (refresh1 == 0) refresh1 = 60;
663 refresh2 = CGDisplayModeGetRefreshRate(mode2);
664 if (refresh2 == 0) refresh2 = 60;
665 if (fabs(refresh1 - refresh2) > 0.1) return FALSE;
670 - (CGDisplayModeRef)modeMatchingMode:(CGDisplayModeRef)mode forDisplay:(CGDirectDisplayID)displayID
672 CGDisplayModeRef ret = NULL;
673 NSArray *modes = [(NSArray*)CGDisplayCopyAllDisplayModes(displayID, NULL) autorelease];
674 for (id candidateModeObject in modes)
676 CGDisplayModeRef candidateMode = (CGDisplayModeRef)candidateModeObject;
677 if ([self mode:candidateMode matchesMode:mode])
686 - (BOOL) setMode:(CGDisplayModeRef)mode forDisplay:(CGDirectDisplayID)displayID
689 NSNumber* displayIDKey = [NSNumber numberWithUnsignedInt:displayID];
690 CGDisplayModeRef currentMode, originalMode;
692 currentMode = CGDisplayCopyDisplayMode(displayID);
693 if (!currentMode) // Invalid display ID
696 if ([self mode:mode matchesMode:currentMode]) // Already there!
698 CGDisplayModeRelease(currentMode);
702 mode = [self modeMatchingMode:mode forDisplay:displayID];
705 CGDisplayModeRelease(currentMode);
709 originalMode = (CGDisplayModeRef)[originalDisplayModes objectForKey:displayIDKey];
711 originalMode = currentMode;
713 if ([self mode:mode matchesMode:originalMode])
715 if ([originalDisplayModes count] == 1) // If this is the last changed display, do a blanket reset
717 CGRestorePermanentDisplayConfiguration();
718 if (!displaysCapturedForFullscreen)
719 CGReleaseAllDisplays();
720 [originalDisplayModes removeAllObjects];
723 else // ... otherwise, try to restore just the one display
725 if (CGDisplaySetDisplayMode(displayID, mode, NULL) == CGDisplayNoErr)
727 [originalDisplayModes removeObjectForKey:displayIDKey];
734 if ([originalDisplayModes count] || displaysCapturedForFullscreen ||
735 CGCaptureAllDisplays() == CGDisplayNoErr)
737 if (CGDisplaySetDisplayMode(displayID, mode, NULL) == CGDisplayNoErr)
739 [originalDisplayModes setObject:(id)originalMode forKey:displayIDKey];
742 else if (![originalDisplayModes count])
744 CGRestorePermanentDisplayConfiguration();
745 if (!displaysCapturedForFullscreen)
746 CGReleaseAllDisplays();
751 CGDisplayModeRelease(currentMode);
754 [self adjustWindowLevels];
759 - (BOOL) areDisplaysCaptured
761 return ([originalDisplayModes count] > 0 || displaysCapturedForFullscreen);
773 - (void) unhideCursor
778 cursorHidden = FALSE;
784 NSDictionary* frame = [cursorFrames objectAtIndex:cursorFrame];
785 CGImageRef cgimage = (CGImageRef)[frame objectForKey:@"image"];
786 NSImage* image = [[NSImage alloc] initWithCGImage:cgimage size:NSZeroSize];
787 CFDictionaryRef hotSpotDict = (CFDictionaryRef)[frame objectForKey:@"hotSpot"];
791 if (!CGPointMakeWithDictionaryRepresentation(hotSpotDict, &hotSpot))
792 hotSpot = CGPointZero;
793 cursor = [[NSCursor alloc] initWithImage:image hotSpot:NSPointFromCGPoint(hotSpot)];
800 - (void) nextCursorFrame:(NSTimer*)theTimer
803 NSTimeInterval duration;
807 if (cursorFrame >= [cursorFrames count])
811 frame = [cursorFrames objectAtIndex:cursorFrame];
812 duration = [[frame objectForKey:@"duration"] doubleValue];
813 date = [[theTimer fireDate] dateByAddingTimeInterval:duration];
814 [cursorTimer setFireDate:date];
817 - (void) setCursorWithFrames:(NSArray*)frames
819 if (self.cursorFrames == frames)
822 self.cursorFrames = frames;
824 [cursorTimer invalidate];
825 self.cursorTimer = nil;
829 if ([frames count] > 1)
831 NSDictionary* frame = [frames objectAtIndex:0];
832 NSTimeInterval duration = [[frame objectForKey:@"duration"] doubleValue];
833 NSDate* date = [NSDate dateWithTimeIntervalSinceNow:duration];
834 self.cursorTimer = [[[NSTimer alloc] initWithFireDate:date
837 selector:@selector(nextCursorFrame:)
839 repeats:YES] autorelease];
840 [[NSRunLoop currentRunLoop] addTimer:cursorTimer forMode:NSRunLoopCommonModes];
847 - (void) setApplicationIconFromCGImageArray:(NSArray*)images
849 NSImage* nsimage = nil;
853 NSSize bestSize = NSZeroSize;
856 nsimage = [[[NSImage alloc] initWithSize:NSZeroSize] autorelease];
858 for (image in images)
860 CGImageRef cgimage = (CGImageRef)image;
861 NSBitmapImageRep* imageRep = [[NSBitmapImageRep alloc] initWithCGImage:cgimage];
864 NSSize size = [imageRep size];
866 [nsimage addRepresentation:imageRep];
869 if (MIN(size.width, size.height) > MIN(bestSize.width, bestSize.height))
874 if ([[nsimage representations] count] && bestSize.width && bestSize.height)
875 [nsimage setSize:bestSize];
880 self.applicationIcon = nsimage;
881 [NSApp setApplicationIconImage:nsimage];
884 - (void) handleCommandTab
886 if ([NSApp isActive])
888 NSRunningApplication* thisApp = [NSRunningApplication currentApplication];
889 NSRunningApplication* app;
890 NSRunningApplication* otherValidApp = nil;
892 if ([originalDisplayModes count] || displaysCapturedForFullscreen)
894 CGRestorePermanentDisplayConfiguration();
895 CGReleaseAllDisplays();
896 [originalDisplayModes removeAllObjects];
897 displaysCapturedForFullscreen = FALSE;
900 for (app in [[NSWorkspace sharedWorkspace] runningApplications])
902 if (![app isEqual:thisApp] && !app.terminated &&
903 app.activationPolicy == NSApplicationActivationPolicyRegular)
907 // There's another visible app. Just hide ourselves and let
908 // the system activate the other app.
918 // Didn't find a visible GUI app. Try the Finder or, if that's not
919 // running, the first hidden GUI app. If even that doesn't work, we
920 // just fail to switch and remain the active app.
921 app = [[NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.finder"] lastObject];
922 if (!app) app = otherValidApp;
924 [app activateWithOptions:0];
929 * ---------- Cursor clipping methods ----------
931 * Neither Quartz nor Cocoa has an exact analog for Win32 cursor clipping.
932 * For one simple case, clipping to a 1x1 rectangle, Quartz does have an
933 * equivalent: CGAssociateMouseAndMouseCursorPosition(false). For the
934 * general case, we leverage that. We disassociate mouse movements from
935 * the cursor position and then move the cursor manually, keeping it within
936 * the clipping rectangle.
938 * Moving the cursor manually isn't enough. We need to modify the event
939 * stream so that the events have the new location, too. We need to do
940 * this at a point before the events enter Cocoa, so that Cocoa will assign
941 * the correct window to the event. So, we install a Quartz event tap to
944 * Also, there's a complication when we move the cursor. We use
945 * CGWarpMouseCursorPosition(). That doesn't generate mouse movement
946 * events, but the change of cursor position is incorporated into the
947 * deltas of the next mouse move event. When the mouse is disassociated
948 * from the cursor position, we need the deltas to only reflect actual
949 * device movement, not programmatic changes. So, the event tap cancels
950 * out the change caused by our calls to CGWarpMouseCursorPosition().
952 - (void) clipCursorLocation:(CGPoint*)location
954 if (location->x < CGRectGetMinX(cursorClipRect))
955 location->x = CGRectGetMinX(cursorClipRect);
956 if (location->y < CGRectGetMinY(cursorClipRect))
957 location->y = CGRectGetMinY(cursorClipRect);
958 if (location->x > CGRectGetMaxX(cursorClipRect) - 1)
959 location->x = CGRectGetMaxX(cursorClipRect) - 1;
960 if (location->y > CGRectGetMaxY(cursorClipRect) - 1)
961 location->y = CGRectGetMaxY(cursorClipRect) - 1;
964 - (BOOL) warpCursorTo:(CGPoint*)newLocation from:(const CGPoint*)currentLocation
969 oldLocation = *currentLocation;
971 oldLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
973 if (!CGPointEqualToPoint(oldLocation, *newLocation))
975 WarpRecord* warpRecord = [[[WarpRecord alloc] init] autorelease];
978 warpRecord.from = oldLocation;
979 warpRecord.timeBefore = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
981 /* Actually move the cursor. */
982 err = CGWarpMouseCursorPosition(*newLocation);
983 if (err != kCGErrorSuccess)
986 warpRecord.timeAfter = [[NSProcessInfo processInfo] systemUptime] * NSEC_PER_SEC;
987 *newLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
989 if (!CGPointEqualToPoint(oldLocation, *newLocation))
991 warpRecord.to = *newLocation;
992 [warpRecords addObject:warpRecord];
999 - (BOOL) isMouseMoveEventType:(CGEventType)type
1003 case kCGEventMouseMoved:
1004 case kCGEventLeftMouseDragged:
1005 case kCGEventRightMouseDragged:
1006 case kCGEventOtherMouseDragged:
1013 - (int) warpsFinishedByEventTime:(CGEventTimestamp)eventTime location:(CGPoint)eventLocation
1015 int warpsFinished = 0;
1016 for (WarpRecord* warpRecord in warpRecords)
1018 if (warpRecord.timeAfter < eventTime ||
1019 (warpRecord.timeBefore <= eventTime && CGPointEqualToPoint(eventLocation, warpRecord.to)))
1025 return warpsFinished;
1028 - (CGEventRef) eventTapWithProxy:(CGEventTapProxy)proxy
1029 type:(CGEventType)type
1030 event:(CGEventRef)event
1032 CGEventTimestamp eventTime;
1033 CGPoint eventLocation, cursorLocation;
1035 if (type == kCGEventTapDisabledByUserInput)
1037 if (type == kCGEventTapDisabledByTimeout)
1039 CGEventTapEnable(cursorClippingEventTap, TRUE);
1043 if (!clippingCursor)
1046 eventTime = CGEventGetTimestamp(event);
1047 lastEventTapEventTime = eventTime / (double)NSEC_PER_SEC;
1049 eventLocation = CGEventGetLocation(event);
1051 cursorLocation = NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]]);
1053 if ([self isMouseMoveEventType:type])
1055 double deltaX, deltaY;
1056 int warpsFinished = [self warpsFinishedByEventTime:eventTime location:eventLocation];
1059 deltaX = CGEventGetDoubleValueField(event, kCGMouseEventDeltaX);
1060 deltaY = CGEventGetDoubleValueField(event, kCGMouseEventDeltaY);
1062 for (i = 0; i < warpsFinished; i++)
1064 WarpRecord* warpRecord = [warpRecords objectAtIndex:0];
1065 deltaX -= warpRecord.to.x - warpRecord.from.x;
1066 deltaY -= warpRecord.to.y - warpRecord.from.y;
1067 [warpRecords removeObjectAtIndex:0];
1072 CGEventSetDoubleValueField(event, kCGMouseEventDeltaX, deltaX);
1073 CGEventSetDoubleValueField(event, kCGMouseEventDeltaY, deltaY);
1076 synthesizedLocation.x += deltaX;
1077 synthesizedLocation.y += deltaY;
1080 // If the event is destined for another process, don't clip it. This may
1081 // happen if the user activates Exposé or Mission Control. In that case,
1082 // our app does not resign active status, so clipping is still in effect,
1083 // but the cursor should not actually be clipped.
1085 // In addition, the fact that mouse moves may have been delivered to a
1086 // different process means we have to treat the next one we receive as
1087 // absolute rather than relative.
1088 if (CGEventGetIntegerValueField(event, kCGEventTargetUnixProcessID) == getpid())
1089 [self clipCursorLocation:&synthesizedLocation];
1091 lastSetCursorPositionTime = lastEventTapEventTime;
1093 [self warpCursorTo:&synthesizedLocation from:&cursorLocation];
1094 if (!CGPointEqualToPoint(eventLocation, synthesizedLocation))
1095 CGEventSetLocation(event, synthesizedLocation);
1100 CGEventRef WineAppEventTapCallBack(CGEventTapProxy proxy, CGEventType type,
1101 CGEventRef event, void *refcon)
1103 WineApplicationController* controller = refcon;
1104 return [controller eventTapWithProxy:proxy type:type event:event];
1107 - (BOOL) installEventTap
1109 ProcessSerialNumber psn;
1111 CGEventMask mask = CGEventMaskBit(kCGEventLeftMouseDown) |
1112 CGEventMaskBit(kCGEventLeftMouseUp) |
1113 CGEventMaskBit(kCGEventRightMouseDown) |
1114 CGEventMaskBit(kCGEventRightMouseUp) |
1115 CGEventMaskBit(kCGEventMouseMoved) |
1116 CGEventMaskBit(kCGEventLeftMouseDragged) |
1117 CGEventMaskBit(kCGEventRightMouseDragged) |
1118 CGEventMaskBit(kCGEventOtherMouseDown) |
1119 CGEventMaskBit(kCGEventOtherMouseUp) |
1120 CGEventMaskBit(kCGEventOtherMouseDragged) |
1121 CGEventMaskBit(kCGEventScrollWheel);
1122 CFRunLoopSourceRef source;
1124 OSErr (*pGetCurrentProcess)(ProcessSerialNumber* PSN);
1126 if (cursorClippingEventTap)
1129 // We need to get the Mac GetCurrentProcess() from the ApplicationServices
1130 // framework with dlsym() because the Win32 function of the same name
1132 appServices = dlopen("/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices", RTLD_LAZY);
1136 pGetCurrentProcess = dlsym(appServices, "GetCurrentProcess");
1137 if (!pGetCurrentProcess)
1139 dlclose(appServices);
1143 err = pGetCurrentProcess(&psn);
1144 dlclose(appServices);
1148 // We create an annotated session event tap rather than a process-specific
1149 // event tap because we need to programmatically move the cursor even when
1150 // mouse moves are directed to other processes. We disable our tap when
1151 // other processes are active, but things like Exposé are handled by other
1152 // processes even when we remain active.
1153 cursorClippingEventTap = CGEventTapCreate(kCGAnnotatedSessionEventTap, kCGHeadInsertEventTap,
1154 kCGEventTapOptionDefault, mask, WineAppEventTapCallBack, self);
1155 if (!cursorClippingEventTap)
1158 CGEventTapEnable(cursorClippingEventTap, FALSE);
1160 source = CFMachPortCreateRunLoopSource(NULL, cursorClippingEventTap, 0);
1163 CFRelease(cursorClippingEventTap);
1164 cursorClippingEventTap = NULL;
1168 CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
1173 - (BOOL) setCursorPosition:(CGPoint)pos
1179 [self clipCursorLocation:&pos];
1181 ret = [self warpCursorTo:&pos from:NULL];
1182 synthesizedLocation = pos;
1185 // We want to discard mouse-move events that have already been
1186 // through the event tap, because it's too late to account for
1187 // the setting of the cursor position with them. However, the
1188 // events that may be queued with times after that but before
1189 // the above warp can still be used. So, use the last event
1190 // tap event time so that -sendEvent: doesn't discard them.
1191 lastSetCursorPositionTime = lastEventTapEventTime;
1196 ret = (CGWarpMouseCursorPosition(pos) == kCGErrorSuccess);
1199 lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
1201 // Annoyingly, CGWarpMouseCursorPosition() effectively disassociates
1202 // the mouse from the cursor position for 0.25 seconds. This means
1203 // that mouse movement during that interval doesn't move the cursor
1204 // and events carry a constant location (the warped-to position)
1205 // even though they have delta values. This screws us up because
1206 // the accumulated deltas we send to Wine don't match any eventual
1207 // absolute position we send (like with a button press). We can
1208 // work around this by simply forcibly reassociating the mouse and
1210 CGAssociateMouseAndMouseCursorPosition(true);
1216 WineEventQueue* queue;
1218 // Discard all pending mouse move events.
1219 [eventQueuesLock lock];
1220 for (queue in eventQueues)
1222 [queue discardEventsMatchingMask:event_mask_for_type(MOUSE_MOVED) |
1223 event_mask_for_type(MOUSE_MOVED_ABSOLUTE)
1225 [queue resetMouseEventPositions:pos];
1227 [eventQueuesLock unlock];
1233 - (void) activateCursorClipping
1237 CGEventTapEnable(cursorClippingEventTap, TRUE);
1238 [self setCursorPosition:NSPointToCGPoint([self flippedMouseLocation:[NSEvent mouseLocation]])];
1242 - (void) deactivateCursorClipping
1246 CGEventTapEnable(cursorClippingEventTap, FALSE);
1247 [warpRecords removeAllObjects];
1248 lastSetCursorPositionTime = [[NSProcessInfo processInfo] systemUptime];
1252 - (BOOL) startClippingCursor:(CGRect)rect
1256 if (!cursorClippingEventTap && ![self installEventTap])
1259 err = CGAssociateMouseAndMouseCursorPosition(false);
1260 if (err != kCGErrorSuccess)
1263 clippingCursor = TRUE;
1264 cursorClipRect = rect;
1265 if ([NSApp isActive])
1266 [self activateCursorClipping];
1271 - (BOOL) stopClippingCursor
1273 CGError err = CGAssociateMouseAndMouseCursorPosition(true);
1274 if (err != kCGErrorSuccess)
1277 [self deactivateCursorClipping];
1278 clippingCursor = FALSE;
1283 - (BOOL) isKeyPressed:(uint16_t)keyCode
1285 int bits = sizeof(pressedKeyCodes[0]) * 8;
1286 int index = keyCode / bits;
1287 uint32_t mask = 1 << (keyCode % bits);
1288 return (pressedKeyCodes[index] & mask) != 0;
1291 - (void) noteKey:(uint16_t)keyCode pressed:(BOOL)pressed
1293 int bits = sizeof(pressedKeyCodes[0]) * 8;
1294 int index = keyCode / bits;
1295 uint32_t mask = 1 << (keyCode % bits);
1297 pressedKeyCodes[index] |= mask;
1299 pressedKeyCodes[index] &= ~mask;
1302 - (void) handleMouseMove:(NSEvent*)anEvent
1304 WineWindow* targetWindow;
1305 BOOL drag = [anEvent type] != NSMouseMoved;
1307 if (mouseCaptureWindow)
1308 targetWindow = mouseCaptureWindow;
1310 targetWindow = (WineWindow*)[anEvent window];
1313 /* Because of the way -[NSWindow setAcceptsMouseMovedEvents:] works, the
1314 event indicates its window is the main window, even if the cursor is
1315 over a different window. Find the actual WineWindow that is under the
1316 cursor and post the event as being for that window. */
1317 CGPoint cgpoint = CGEventGetLocation([anEvent CGEvent]);
1318 NSPoint point = [self flippedMouseLocation:NSPointFromCGPoint(cgpoint)];
1319 NSInteger windowUnderNumber;
1321 windowUnderNumber = [NSWindow windowNumberAtPoint:point
1322 belowWindowWithWindowNumber:0];
1323 targetWindow = (WineWindow*)[NSApp windowWithWindowNumber:windowUnderNumber];
1326 if ([targetWindow isKindOfClass:[WineWindow class]])
1328 CGPoint point = CGEventGetLocation([anEvent CGEvent]);
1329 macdrv_event* event;
1332 // If we recently warped the cursor (other than in our cursor-clipping
1333 // event tap), discard mouse move events until we see an event which is
1334 // later than that time.
1335 if (lastSetCursorPositionTime)
1337 if ([anEvent timestamp] <= lastSetCursorPositionTime)
1340 lastSetCursorPositionTime = 0;
1341 forceNextMouseMoveAbsolute = TRUE;
1344 if (forceNextMouseMoveAbsolute || targetWindow != lastTargetWindow)
1347 forceNextMouseMoveAbsolute = FALSE;
1351 // Send absolute move events if the cursor is in the interior of
1352 // its range. Only send relative moves if the cursor is pinned to
1353 // the boundaries of where it can go. We compute the position
1354 // that's one additional point in the direction of movement. If
1355 // that is outside of the clipping rect or desktop region (the
1356 // union of the screen frames), then we figure the cursor would
1357 // have moved outside if it could but it was pinned.
1358 CGPoint computedPoint = point;
1359 CGFloat deltaX = [anEvent deltaX];
1360 CGFloat deltaY = [anEvent deltaY];
1364 else if (deltaX < -0.001)
1369 else if (deltaY < -0.001)
1372 // Assume cursor is pinned for now
1374 if (!clippingCursor || CGRectContainsPoint(cursorClipRect, computedPoint))
1376 const CGRect* rects;
1377 NSUInteger count, i;
1379 // Caches screenFrameCGRects if necessary
1380 [self primaryScreenHeight];
1382 rects = [screenFrameCGRects bytes];
1383 count = [screenFrameCGRects length] / sizeof(rects[0]);
1385 for (i = 0; i < count; i++)
1387 if (CGRectContainsPoint(rects[i], computedPoint))
1399 [self clipCursorLocation:&point];
1401 event = macdrv_create_event(MOUSE_MOVED_ABSOLUTE, targetWindow);
1402 event->mouse_moved.x = point.x;
1403 event->mouse_moved.y = point.y;
1405 mouseMoveDeltaX = 0;
1406 mouseMoveDeltaY = 0;
1410 /* Add event delta to accumulated delta error */
1411 /* deltaY is already flipped */
1412 mouseMoveDeltaX += [anEvent deltaX];
1413 mouseMoveDeltaY += [anEvent deltaY];
1415 event = macdrv_create_event(MOUSE_MOVED, targetWindow);
1416 event->mouse_moved.x = mouseMoveDeltaX;
1417 event->mouse_moved.y = mouseMoveDeltaY;
1419 /* Keep the remainder after integer truncation. */
1420 mouseMoveDeltaX -= event->mouse_moved.x;
1421 mouseMoveDeltaY -= event->mouse_moved.y;
1424 if (event->type == MOUSE_MOVED_ABSOLUTE || event->mouse_moved.x || event->mouse_moved.y)
1426 event->mouse_moved.time_ms = [self ticksForEventTime:[anEvent timestamp]];
1427 event->mouse_moved.drag = drag;
1429 [targetWindow.queue postEvent:event];
1432 macdrv_release_event(event);
1434 lastTargetWindow = targetWindow;
1436 else if (lastTargetWindow)
1438 [[NSCursor arrowCursor] set];
1439 [self unhideCursor];
1440 lastTargetWindow = nil;
1444 - (void) handleMouseButton:(NSEvent*)theEvent
1446 WineWindow* window = (WineWindow*)[theEvent window];
1447 NSEventType type = [theEvent type];
1448 BOOL broughtWindowForward = FALSE;
1450 if ([window isKindOfClass:[WineWindow class]] &&
1451 !window.disabled && !window.noActivate &&
1452 type == NSLeftMouseDown &&
1453 (([theEvent modifierFlags] & (NSShiftKeyMask | NSControlKeyMask| NSAlternateKeyMask | NSCommandKeyMask)) != NSCommandKeyMask))
1455 NSWindowButton windowButton;
1457 broughtWindowForward = TRUE;
1459 /* Any left-click on our window anyplace other than the close or
1460 minimize buttons will bring it forward. */
1461 for (windowButton = NSWindowCloseButton;
1462 windowButton <= NSWindowMiniaturizeButton;
1465 NSButton* button = [window standardWindowButton:windowButton];
1468 NSPoint point = [button convertPoint:[theEvent locationInWindow] fromView:nil];
1469 if ([button mouse:point inRect:[button bounds]])
1471 broughtWindowForward = FALSE;
1477 if (broughtWindowForward)
1479 // Clicking on a child window does not normally reorder it with
1480 // respect to its siblings, but we want it to. We have to do it
1482 NSWindow* parent = [window parentWindow];
1483 [parent removeChildWindow:window];
1484 [parent addChildWindow:window ordered:NSWindowAbove];
1488 if (mouseCaptureWindow)
1489 window = mouseCaptureWindow;
1491 if ([window isKindOfClass:[WineWindow class]])
1493 BOOL pressed = (type == NSLeftMouseDown || type == NSRightMouseDown || type == NSOtherMouseDown);
1494 CGPoint pt = CGEventGetLocation([theEvent CGEvent]);
1498 [self clipCursorLocation:&pt];
1502 if (mouseCaptureWindow)
1506 // Test if the click was in the window's content area.
1507 NSPoint nspoint = [self flippedMouseLocation:NSPointFromCGPoint(pt)];
1508 NSRect contentRect = [window contentRectForFrameRect:[window frame]];
1509 process = NSPointInRect(nspoint, contentRect);
1510 if (process && [window styleMask] & NSResizableWindowMask)
1512 // Ignore clicks in the grow box (resize widget).
1513 HIPoint origin = { 0, 0 };
1514 HIThemeGrowBoxDrawInfo info = { 0 };
1518 info.kind = kHIThemeGrowBoxKindNormal;
1519 info.direction = kThemeGrowRight | kThemeGrowDown;
1520 if ([window styleMask] & NSUtilityWindowMask)
1521 info.size = kHIThemeGrowBoxSizeSmall;
1523 info.size = kHIThemeGrowBoxSizeNormal;
1525 status = HIThemeGetGrowBoxBounds(&origin, &info, &bounds);
1526 if (status == noErr)
1528 NSRect growBox = NSMakeRect(NSMaxX(contentRect) - bounds.size.width,
1529 NSMinY(contentRect),
1531 bounds.size.height);
1532 process = !NSPointInRect(nspoint, growBox);
1537 unmatchedMouseDowns |= NSEventMaskFromType(type);
1541 NSEventType downType = type - 1;
1542 NSUInteger downMask = NSEventMaskFromType(downType);
1543 process = (unmatchedMouseDowns & downMask) != 0;
1544 unmatchedMouseDowns &= ~downMask;
1549 macdrv_event* event;
1551 event = macdrv_create_event(MOUSE_BUTTON, window);
1552 event->mouse_button.button = [theEvent buttonNumber];
1553 event->mouse_button.pressed = pressed;
1554 event->mouse_button.x = pt.x;
1555 event->mouse_button.y = pt.y;
1556 event->mouse_button.time_ms = [self ticksForEventTime:[theEvent timestamp]];
1558 [window.queue postEvent:event];
1560 macdrv_release_event(event);
1562 else if (broughtWindowForward && ![window isKeyWindow])
1563 [self windowGotFocus:window];
1566 // Since mouse button events deliver absolute cursor position, the
1567 // accumulating delta from move events is invalidated. Make sure
1568 // next mouse move event starts over from an absolute baseline.
1569 // Also, it's at least possible that the title bar widgets (e.g. close
1570 // button, etc.) could enter an internal event loop on a mouse down that
1571 // wouldn't exit until a mouse up. In that case, we'd miss any mouse
1572 // dragged events and, after that, any notion of the cursor position
1573 // computed from accumulating deltas would be wrong.
1574 forceNextMouseMoveAbsolute = TRUE;
1577 - (void) handleScrollWheel:(NSEvent*)theEvent
1581 if (mouseCaptureWindow)
1582 window = mouseCaptureWindow;
1584 window = (WineWindow*)[theEvent window];
1586 if ([window isKindOfClass:[WineWindow class]])
1588 CGEventRef cgevent = [theEvent CGEvent];
1589 CGPoint pt = CGEventGetLocation(cgevent);
1593 [self clipCursorLocation:&pt];
1595 if (mouseCaptureWindow)
1599 // Only process the event if it was in the window's content area.
1600 NSPoint nspoint = [self flippedMouseLocation:NSPointFromCGPoint(pt)];
1601 NSRect contentRect = [window contentRectForFrameRect:[window frame]];
1602 process = NSPointInRect(nspoint, contentRect);
1607 macdrv_event* event;
1609 BOOL continuous = FALSE;
1611 event = macdrv_create_event(MOUSE_SCROLL, window);
1612 event->mouse_scroll.x = pt.x;
1613 event->mouse_scroll.y = pt.y;
1614 event->mouse_scroll.time_ms = [self ticksForEventTime:[theEvent timestamp]];
1616 if (CGEventGetIntegerValueField(cgevent, kCGScrollWheelEventIsContinuous))
1620 /* Continuous scroll wheel events come from high-precision scrolling
1621 hardware like Apple's Magic Mouse, Mighty Mouse, and trackpads.
1622 For these, we can get more precise data from the CGEvent API. */
1623 /* Axis 1 is vertical, axis 2 is horizontal. */
1624 x = CGEventGetDoubleValueField(cgevent, kCGScrollWheelEventPointDeltaAxis2);
1625 y = CGEventGetDoubleValueField(cgevent, kCGScrollWheelEventPointDeltaAxis1);
1629 double pixelsPerLine = 10;
1630 CGEventSourceRef source;
1632 /* The non-continuous values are in units of "lines", not pixels. */
1633 if ((source = CGEventCreateSourceFromEvent(cgevent)))
1635 pixelsPerLine = CGEventSourceGetPixelsPerLine(source);
1639 x = pixelsPerLine * [theEvent deltaX];
1640 y = pixelsPerLine * [theEvent deltaY];
1643 /* Mac: negative is right or down, positive is left or up.
1644 Win32: negative is left or down, positive is right or up.
1645 So, negate the X scroll value to translate. */
1648 /* The x,y values so far are in pixels. Win32 expects to receive some
1649 fraction of WHEEL_DELTA == 120. By my estimation, that's roughly
1650 6 times the pixel value. */
1651 event->mouse_scroll.x_scroll = 6 * x;
1652 event->mouse_scroll.y_scroll = 6 * y;
1656 /* For non-continuous "clicky" wheels, if there was any motion, make
1657 sure there was at least WHEEL_DELTA motion. This is so, at slow
1658 speeds where the system's acceleration curve is actually reducing the
1659 scroll distance, the user is sure to get some action out of each click.
1660 For example, this is important for rotating though weapons in a
1661 first-person shooter. */
1662 if (0 < event->mouse_scroll.x_scroll && event->mouse_scroll.x_scroll < 120)
1663 event->mouse_scroll.x_scroll = 120;
1664 else if (-120 < event->mouse_scroll.x_scroll && event->mouse_scroll.x_scroll < 0)
1665 event->mouse_scroll.x_scroll = -120;
1667 if (0 < event->mouse_scroll.y_scroll && event->mouse_scroll.y_scroll < 120)
1668 event->mouse_scroll.y_scroll = 120;
1669 else if (-120 < event->mouse_scroll.y_scroll && event->mouse_scroll.y_scroll < 0)
1670 event->mouse_scroll.y_scroll = -120;
1673 if (event->mouse_scroll.x_scroll || event->mouse_scroll.y_scroll)
1674 [window.queue postEvent:event];
1676 macdrv_release_event(event);
1678 // Since scroll wheel events deliver absolute cursor position, the
1679 // accumulating delta from move events is invalidated. Make sure next
1680 // mouse move event starts over from an absolute baseline.
1681 forceNextMouseMoveAbsolute = TRUE;
1686 // Returns TRUE if the event was handled and caller should do nothing more
1687 // with it. Returns FALSE if the caller should process it as normal and
1688 // then call -didSendEvent:.
1689 - (BOOL) handleEvent:(NSEvent*)anEvent
1692 NSEventType type = [anEvent type];
1694 if (type == NSFlagsChanged)
1695 self.lastFlagsChanged = anEvent;
1696 else if (type == NSMouseMoved || type == NSLeftMouseDragged ||
1697 type == NSRightMouseDragged || type == NSOtherMouseDragged)
1699 [self handleMouseMove:anEvent];
1700 ret = mouseCaptureWindow != nil;
1702 else if (type == NSLeftMouseDown || type == NSLeftMouseUp ||
1703 type == NSRightMouseDown || type == NSRightMouseUp ||
1704 type == NSOtherMouseDown || type == NSOtherMouseUp)
1706 [self handleMouseButton:anEvent];
1707 ret = mouseCaptureWindow != nil;
1709 else if (type == NSScrollWheel)
1711 [self handleScrollWheel:anEvent];
1712 ret = mouseCaptureWindow != nil;
1714 else if (type == NSKeyUp)
1716 uint16_t keyCode = [anEvent keyCode];
1717 if ([self isKeyPressed:keyCode])
1719 WineWindow* window = (WineWindow*)[anEvent window];
1720 [self noteKey:keyCode pressed:FALSE];
1721 if ([window isKindOfClass:[WineWindow class]])
1722 [window postKeyEvent:anEvent];
1729 - (void) didSendEvent:(NSEvent*)anEvent
1731 NSEventType type = [anEvent type];
1733 if (type == NSKeyDown && ![anEvent isARepeat] && [anEvent keyCode] == kVK_Tab)
1735 NSUInteger modifiers = [anEvent modifierFlags];
1736 if ((modifiers & NSCommandKeyMask) &&
1737 !(modifiers & (NSControlKeyMask | NSAlternateKeyMask)))
1739 // Command-Tab and Command-Shift-Tab would normally be intercepted
1740 // by the system to switch applications. If we're seeing it, it's
1741 // presumably because we've captured the displays, preventing
1742 // normal application switching. Do it manually.
1743 [self handleCommandTab];
1748 - (void) setupObservations
1750 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
1751 NSNotificationCenter* wsnc = [[NSWorkspace sharedWorkspace] notificationCenter];
1752 NSDistributedNotificationCenter* dnc = [NSDistributedNotificationCenter defaultCenter];
1754 [nc addObserverForName:NSWindowDidBecomeKeyNotification
1757 usingBlock:^(NSNotification *note){
1758 NSWindow* window = [note object];
1759 [keyWindows removeObjectIdenticalTo:window];
1760 [keyWindows insertObject:window atIndex:0];
1763 [nc addObserverForName:NSWindowWillCloseNotification
1765 queue:[NSOperationQueue mainQueue]
1766 usingBlock:^(NSNotification *note){
1767 NSWindow* window = [note object];
1768 [keyWindows removeObjectIdenticalTo:window];
1769 if (window == lastTargetWindow)
1770 lastTargetWindow = nil;
1771 if (window == self.mouseCaptureWindow)
1772 self.mouseCaptureWindow = nil;
1773 if ([window isKindOfClass:[WineWindow class]] && [(WineWindow*)window isFullscreen])
1775 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0), dispatch_get_main_queue(), ^{
1776 [self updateFullscreenWindows];
1781 [nc addObserver:self
1782 selector:@selector(keyboardSelectionDidChange)
1783 name:NSTextInputContextKeyboardSelectionDidChangeNotification
1786 /* The above notification isn't sent unless the NSTextInputContext
1787 class has initialized itself. Poke it. */
1788 [NSTextInputContext self];
1790 [wsnc addObserver:self
1791 selector:@selector(activeSpaceDidChange)
1792 name:NSWorkspaceActiveSpaceDidChangeNotification
1795 [nc addObserver:self
1796 selector:@selector(releaseMouseCapture)
1797 name:NSMenuDidBeginTrackingNotification
1800 [dnc addObserver:self
1801 selector:@selector(releaseMouseCapture)
1802 name:@"com.apple.HIToolbox.beginMenuTrackingNotification"
1804 suspensionBehavior:NSNotificationSuspensionBehaviorDrop];
1807 - (BOOL) inputSourceIsInputMethod
1809 if (!inputSourceIsInputMethodValid)
1811 TISInputSourceRef inputSource = TISCopyCurrentKeyboardInputSource();
1814 CFStringRef type = TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceType);
1815 inputSourceIsInputMethod = !CFEqual(type, kTISTypeKeyboardLayout);
1816 CFRelease(inputSource);
1819 inputSourceIsInputMethod = FALSE;
1820 inputSourceIsInputMethodValid = TRUE;
1823 return inputSourceIsInputMethod;
1826 - (void) releaseMouseCapture
1828 // This might be invoked on a background thread by the distributed
1829 // notification center. Shunt it to the main thread.
1830 if (![NSThread isMainThread])
1832 dispatch_async(dispatch_get_main_queue(), ^{ [self releaseMouseCapture]; });
1836 if (mouseCaptureWindow)
1838 macdrv_event* event;
1840 event = macdrv_create_event(RELEASE_CAPTURE, mouseCaptureWindow);
1841 [mouseCaptureWindow.queue postEvent:event];
1842 macdrv_release_event(event);
1848 * ---------- NSApplicationDelegate methods ----------
1850 - (void)applicationDidBecomeActive:(NSNotification *)notification
1852 [self activateCursorClipping];
1854 [self updateFullscreenWindows];
1855 [self adjustWindowLevels:YES];
1857 if (beenActive && ![self frontWineWindow])
1859 for (WineWindow* window in [NSApp windows])
1861 if ([window isKindOfClass:[WineWindow class]] && [window isMiniaturized])
1863 [window deminiaturize:self];
1870 // If a Wine process terminates abruptly while it has the display captured
1871 // and switched to a different resolution, Mac OS X will uncapture the
1872 // displays and switch their resolutions back. However, the other Wine
1873 // processes won't have their notion of the desktop rect changed back.
1874 // This can lead them to refuse to draw or acknowledge clicks in certain
1875 // portions of their windows.
1877 // To solve this, we synthesize a displays-changed event whenever we're
1878 // activated. This will provoke a re-synchronization of Wine's notion of
1879 // the desktop rect with the actual state.
1880 [self sendDisplaysChanged:TRUE];
1882 // The cursor probably moved while we were inactive. Accumulated mouse
1883 // movement deltas are invalidated. Make sure the next mouse move event
1884 // starts over from an absolute baseline.
1885 forceNextMouseMoveAbsolute = TRUE;
1888 - (void)applicationDidChangeScreenParameters:(NSNotification *)notification
1890 primaryScreenHeightValid = FALSE;
1891 [self sendDisplaysChanged:FALSE];
1892 [self adjustWindowLevels];
1894 // When the display configuration changes, the cursor position may jump.
1895 // Accumulated mouse movement deltas are invalidated. Make sure the next
1896 // mouse move event starts over from an absolute baseline.
1897 forceNextMouseMoveAbsolute = TRUE;
1900 - (void)applicationDidResignActive:(NSNotification *)notification
1902 macdrv_event* event;
1903 WineEventQueue* queue;
1905 [self invalidateGotFocusEvents];
1907 event = macdrv_create_event(APP_DEACTIVATED, nil);
1909 [eventQueuesLock lock];
1910 for (queue in eventQueues)
1911 [queue postEvent:event];
1912 [eventQueuesLock unlock];
1914 macdrv_release_event(event);
1916 [self releaseMouseCapture];
1919 - (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication *)sender
1921 NSApplicationTerminateReply ret = NSTerminateNow;
1922 NSAppleEventManager* m = [NSAppleEventManager sharedAppleEventManager];
1923 NSAppleEventDescriptor* desc = [m currentAppleEvent];
1924 macdrv_event* event;
1925 WineEventQueue* queue;
1927 event = macdrv_create_event(APP_QUIT_REQUESTED, nil);
1929 switch ([[desc attributeDescriptorForKeyword:kAEQuitReason] int32Value])
1932 case kAEReallyLogOut:
1933 event->app_quit_requested.reason = QUIT_REASON_LOGOUT;
1935 case kAEShowRestartDialog:
1936 event->app_quit_requested.reason = QUIT_REASON_RESTART;
1938 case kAEShowShutdownDialog:
1939 event->app_quit_requested.reason = QUIT_REASON_SHUTDOWN;
1942 event->app_quit_requested.reason = QUIT_REASON_NONE;
1946 [eventQueuesLock lock];
1948 if ([eventQueues count])
1950 for (queue in eventQueues)
1951 [queue postEvent:event];
1952 ret = NSTerminateLater;
1955 [eventQueuesLock unlock];
1957 macdrv_release_event(event);
1962 - (void)applicationWillResignActive:(NSNotification *)notification
1964 [self deactivateCursorClipping];
1966 [self adjustWindowLevels:NO];
1969 /***********************************************************************
1972 * Run-loop-source perform callback. Pull request blocks from the
1973 * array of queued requests and invoke them.
1975 static void PerformRequest(void *info)
1977 WineApplicationController* controller = [WineApplicationController sharedController];
1981 __block dispatch_block_t block;
1983 dispatch_sync(controller->requestsManipQueue, ^{
1984 if ([controller->requests count])
1986 block = (dispatch_block_t)[[controller->requests objectAtIndex:0] retain];
1987 [controller->requests removeObjectAtIndex:0];
2001 /***********************************************************************
2004 * Run a block on the main thread asynchronously.
2006 void OnMainThreadAsync(dispatch_block_t block)
2008 WineApplicationController* controller = [WineApplicationController sharedController];
2010 block = [block copy];
2011 dispatch_sync(controller->requestsManipQueue, ^{
2012 [controller->requests addObject:block];
2015 CFRunLoopSourceSignal(controller->requestSource);
2016 CFRunLoopWakeUp(CFRunLoopGetMain());
2021 /***********************************************************************
2024 void LogError(const char* func, NSString* format, ...)
2027 va_start(args, format);
2028 LogErrorv(func, format, args);
2032 /***********************************************************************
2035 void LogErrorv(const char* func, NSString* format, va_list args)
2037 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
2039 NSString* message = [[NSString alloc] initWithFormat:format arguments:args];
2040 fprintf(stderr, "err:%s:%s", func, [message UTF8String]);
2046 /***********************************************************************
2047 * macdrv_window_rejected_focus
2049 * Pass focus to the next window that hasn't already rejected this same
2050 * WINDOW_GOT_FOCUS event.
2052 void macdrv_window_rejected_focus(const macdrv_event *event)
2055 [[WineApplicationController sharedController] windowRejectedFocusEvent:event];
2059 /***********************************************************************
2060 * macdrv_get_keyboard_layout
2062 * Returns the keyboard layout uchr data.
2064 CFDataRef macdrv_copy_keyboard_layout(CGEventSourceKeyboardType* keyboard_type, int* is_iso)
2066 __block CFDataRef result = NULL;
2069 TISInputSourceRef inputSource;
2071 inputSource = TISCopyCurrentKeyboardLayoutInputSource();
2074 CFDataRef uchr = TISGetInputSourceProperty(inputSource,
2075 kTISPropertyUnicodeKeyLayoutData);
2076 result = CFDataCreateCopy(NULL, uchr);
2077 CFRelease(inputSource);
2079 *keyboard_type = [WineApplicationController sharedController].keyboardType;
2080 *is_iso = (KBGetLayoutType(*keyboard_type) == kKeyboardISO);
2087 /***********************************************************************
2090 * Play the beep sound configured by the user in System Preferences.
2092 void macdrv_beep(void)
2094 OnMainThreadAsync(^{
2099 /***********************************************************************
2100 * macdrv_set_display_mode
2102 int macdrv_set_display_mode(const struct macdrv_display* display,
2103 CGDisplayModeRef display_mode)
2108 ret = [[WineApplicationController sharedController] setMode:display_mode forDisplay:display->displayID];
2114 /***********************************************************************
2119 * If name is non-NULL, it is a selector for a class method on NSCursor
2120 * identifying the cursor to set. In that case, frames is ignored. If
2121 * name is NULL, then frames is used.
2123 * frames is an array of dictionaries. Each dictionary is a frame of
2124 * an animated cursor. Under the key "image" is a CGImage for the
2125 * frame. Under the key "duration" is a CFNumber time interval, in
2126 * seconds, for how long that frame is presented before proceeding to
2127 * the next frame. Under the key "hotSpot" is a CFDictionary encoding a
2128 * CGPoint, to be decoded using CGPointMakeWithDictionaryRepresentation().
2129 * This is the hot spot, measured in pixels down and to the right of the
2130 * top-left corner of the image.
2132 * If the array has exactly 1 element, the cursor is static, not
2133 * animated. If frames is NULL or has 0 elements, the cursor is hidden.
2135 void macdrv_set_cursor(CFStringRef name, CFArrayRef frames)
2139 sel = NSSelectorFromString((NSString*)name);
2142 OnMainThreadAsync(^{
2143 WineApplicationController* controller = [WineApplicationController sharedController];
2144 NSCursor* cursor = [NSCursor performSelector:sel];
2145 [controller setCursorWithFrames:nil];
2147 [controller unhideCursor];
2152 NSArray* nsframes = (NSArray*)frames;
2153 if ([nsframes count])
2155 OnMainThreadAsync(^{
2156 [[WineApplicationController sharedController] setCursorWithFrames:nsframes];
2161 OnMainThreadAsync(^{
2162 WineApplicationController* controller = [WineApplicationController sharedController];
2163 [controller setCursorWithFrames:nil];
2164 [controller hideCursor];
2170 /***********************************************************************
2171 * macdrv_get_cursor_position
2173 * Obtains the current cursor position. Returns zero on failure,
2174 * non-zero on success.
2176 int macdrv_get_cursor_position(CGPoint *pos)
2179 NSPoint location = [NSEvent mouseLocation];
2180 location = [[WineApplicationController sharedController] flippedMouseLocation:location];
2181 *pos = NSPointToCGPoint(location);
2187 /***********************************************************************
2188 * macdrv_set_cursor_position
2190 * Sets the cursor position without generating events. Returns zero on
2191 * failure, non-zero on success.
2193 int macdrv_set_cursor_position(CGPoint pos)
2198 ret = [[WineApplicationController sharedController] setCursorPosition:pos];
2204 /***********************************************************************
2205 * macdrv_clip_cursor
2207 * Sets the cursor cursor clipping rectangle. If the rectangle is equal
2208 * to or larger than the whole desktop region, the cursor is unclipped.
2209 * Returns zero on failure, non-zero on success.
2211 int macdrv_clip_cursor(CGRect rect)
2216 WineApplicationController* controller = [WineApplicationController sharedController];
2217 BOOL clipping = FALSE;
2219 if (!CGRectIsInfinite(rect))
2221 NSRect nsrect = NSRectFromCGRect(rect);
2224 /* Convert the rectangle from top-down coords to bottom-up. */
2225 [controller flipRect:&nsrect];
2228 for (screen in [NSScreen screens])
2230 if (!NSContainsRect(nsrect, [screen frame]))
2239 ret = [controller startClippingCursor:rect];
2241 ret = [controller stopClippingCursor];
2247 /***********************************************************************
2248 * macdrv_set_application_icon
2250 * Set the application icon. The images array contains CGImages. If
2251 * there are more than one, then they represent different sizes or
2252 * color depths from the icon resource. If images is NULL or empty,
2253 * restores the default application image.
2255 void macdrv_set_application_icon(CFArrayRef images)
2257 NSArray* imageArray = (NSArray*)images;
2259 OnMainThreadAsync(^{
2260 [[WineApplicationController sharedController] setApplicationIconFromCGImageArray:imageArray];
2264 /***********************************************************************
2267 void macdrv_quit_reply(int reply)
2270 [NSApp replyToApplicationShouldTerminate:reply];
2274 /***********************************************************************
2275 * macdrv_using_input_method
2277 int macdrv_using_input_method(void)
2282 ret = [[WineApplicationController sharedController] inputSourceIsInputMethod];
2288 /***********************************************************************
2289 * macdrv_set_mouse_capture_window
2291 void macdrv_set_mouse_capture_window(macdrv_window window)
2293 WineWindow* w = (WineWindow*)window;
2296 [[WineApplicationController sharedController] setMouseCaptureWindow:w];