diff --git a/ThirdParty/SPMediaKeyTap/SPInvocationGrabbing/NSObject+SPInvocationGrabbing.h b/ThirdParty/SPMediaKeyTap/SPInvocationGrabbing/NSObject+SPInvocationGrabbing.h new file mode 100644 index 000000000..d30233daf --- /dev/null +++ b/ThirdParty/SPMediaKeyTap/SPInvocationGrabbing/NSObject+SPInvocationGrabbing.h @@ -0,0 +1,30 @@ +#import + +@interface SPInvocationGrabber : NSObject { + id _object; + NSInvocation *_invocation; + int frameCount; + char **frameStrings; + BOOL backgroundAfterForward; + BOOL onMainAfterForward; + BOOL waitUntilDone; +} +-(id)initWithObject:(id)obj; +-(id)initWithObject:(id)obj stacktraceSaving:(BOOL)saveStack; +@property (readonly, retain, nonatomic) id object; +@property (readonly, retain, nonatomic) NSInvocation *invocation; +@property BOOL backgroundAfterForward; +@property BOOL onMainAfterForward; +@property BOOL waitUntilDone; +-(void)invoke; // will release object and invocation +-(void)printBacktrace; +-(void)saveBacktrace; +@end + +@interface NSObject (SPInvocationGrabbing) +-(id)grab; +-(id)invokeAfter:(NSTimeInterval)delta; +-(id)nextRunloop; +-(id)inBackground; +-(id)onMainAsync:(BOOL)async; +@end diff --git a/ThirdParty/SPMediaKeyTap/SPInvocationGrabbing/NSObject+SPInvocationGrabbing.m b/ThirdParty/SPMediaKeyTap/SPInvocationGrabbing/NSObject+SPInvocationGrabbing.m new file mode 100644 index 000000000..ce3b9f48c --- /dev/null +++ b/ThirdParty/SPMediaKeyTap/SPInvocationGrabbing/NSObject+SPInvocationGrabbing.m @@ -0,0 +1,127 @@ +#import "NSObject+SPInvocationGrabbing.h" +#import + +#pragma mark Invocation grabbing +@interface SPInvocationGrabber () +@property (readwrite, retain, nonatomic) id object; +@property (readwrite, retain, nonatomic) NSInvocation *invocation; + +@end + +@implementation SPInvocationGrabber +- (id)initWithObject:(id)obj; +{ + return [self initWithObject:obj stacktraceSaving:YES]; +} + +-(id)initWithObject:(id)obj stacktraceSaving:(BOOL)saveStack; +{ + self.object = obj; + + if(saveStack) + [self saveBacktrace]; + + return self; +} +-(void)dealloc; +{ + free(frameStrings); + self.object = nil; + self.invocation = nil; + [super dealloc]; +} +@synthesize invocation = _invocation, object = _object; + +@synthesize backgroundAfterForward, onMainAfterForward, waitUntilDone; +- (void)runInBackground; +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + @try { + [self invoke]; + } + @finally { + [pool drain]; + } +} + + +- (void)forwardInvocation:(NSInvocation *)anInvocation { + [anInvocation retainArguments]; + anInvocation.target = _object; + self.invocation = anInvocation; + + if(backgroundAfterForward) + [NSThread detachNewThreadSelector:@selector(runInBackground) toTarget:self withObject:nil]; + else if(onMainAfterForward) + [self performSelectorOnMainThread:@selector(invoke) withObject:nil waitUntilDone:waitUntilDone]; +} +- (NSMethodSignature *)methodSignatureForSelector:(SEL)inSelector { + NSMethodSignature *signature = [super methodSignatureForSelector:inSelector]; + if (signature == NULL) + signature = [_object methodSignatureForSelector:inSelector]; + + return signature; +} + +- (void)invoke; +{ + + @try { + [_invocation invoke]; + } + @catch (NSException * e) { + NSLog(@"SPInvocationGrabber's target raised %@:\n\t%@\nInvocation was originally scheduled at:", e.name, e); + [self printBacktrace]; + printf("\n"); + [e raise]; + } + + self.invocation = nil; + self.object = nil; +} + +-(void)saveBacktrace; +{ + void *backtraceFrames[128]; + frameCount = backtrace(&backtraceFrames[0], 128); + frameStrings = backtrace_symbols(&backtraceFrames[0], frameCount); +} +-(void)printBacktrace; +{ + for(int x = 3; x < frameCount; x++) { + if(frameStrings[x] == NULL) { break; } + printf("%s\n", frameStrings[x]); + } +} +@end + +@implementation NSObject (SPInvocationGrabbing) +-(id)grab; +{ + return [[[SPInvocationGrabber alloc] initWithObject:self] autorelease]; +} +-(id)invokeAfter:(NSTimeInterval)delta; +{ + id grabber = [self grab]; + [NSTimer scheduledTimerWithTimeInterval:delta target:grabber selector:@selector(invoke) userInfo:nil repeats:NO]; + return grabber; +} +- (id)nextRunloop; +{ + return [self invokeAfter:0]; +} +-(id)inBackground; +{ + SPInvocationGrabber *grabber = [self grab]; + grabber.backgroundAfterForward = YES; + return grabber; +} +-(id)onMainAsync:(BOOL)async; +{ + SPInvocationGrabber *grabber = [self grab]; + grabber.onMainAfterForward = YES; + grabber.waitUntilDone = !async; + return grabber; +} + +@end diff --git a/ThirdParty/SPMediaKeyTap/SPMediaKeyTap.h b/ThirdParty/SPMediaKeyTap/SPMediaKeyTap.h new file mode 100644 index 000000000..aa974d238 --- /dev/null +++ b/ThirdParty/SPMediaKeyTap/SPMediaKeyTap.h @@ -0,0 +1,43 @@ +#include +#import +#import + +// http://overooped.com/post/2593597587/mediakeys + +#define SPSystemDefinedEventMediaKeys 8 + +@interface SPMediaKeyTap : NSObject { + EventHandlerRef _app_switching_ref; + EventHandlerRef _app_terminating_ref; + CFMachPortRef _eventPort; + CFRunLoopSourceRef _eventPortSource; + CFRunLoopRef _tapThreadRL; + BOOL _shouldInterceptMediaKeyEvents; + id _delegate; + // The app that is frontmost in this list owns media keys + NSMutableArray *_mediaKeyAppList; +} ++ (NSArray*)defaultMediaKeyUserBundleIdentifiers; + +-(id)initWithDelegate:(id)delegate; + ++(BOOL)usesGlobalMediaKeyTap; +-(void)startWatchingMediaKeys; +-(void)stopWatchingMediaKeys; +-(void)handleAndReleaseMediaKeyEvent:(NSEvent *)event; +@end + +@interface NSObject (SPMediaKeyTapDelegate) +-(void)mediaKeyTap:(SPMediaKeyTap*)keyTap receivedMediaKeyEvent:(NSEvent*)event; +@end + +#ifdef __cplusplus +extern "C" { +#endif + +extern NSString *kMediaKeyUsingBundleIdentifiersDefaultsKey; +extern NSString *kIgnoreMediaKeysDefaultsKey; + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/ThirdParty/SPMediaKeyTap/SPMediaKeyTap.m b/ThirdParty/SPMediaKeyTap/SPMediaKeyTap.m new file mode 100644 index 000000000..e22912d1e --- /dev/null +++ b/ThirdParty/SPMediaKeyTap/SPMediaKeyTap.m @@ -0,0 +1,335 @@ +// Copyright (c) 2010 Spotify AB +#import "SPMediaKeyTap.h" +#import "SPInvocationGrabbing/NSObject+SPInvocationGrabbing.h" // https://gist.github.com/511181, in submodule + +@interface SPMediaKeyTap () +-(BOOL)shouldInterceptMediaKeyEvents; +-(void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting; +-(void)startWatchingAppSwitching; +-(void)stopWatchingAppSwitching; +-(void)eventTapThread; +@end +static SPMediaKeyTap *singleton = nil; + +static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData); +static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData); +static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon); + + +// Inspired by http://gist.github.com/546311 + +@implementation SPMediaKeyTap + +#pragma mark - +#pragma mark Setup and teardown +-(id)initWithDelegate:(id)delegate; +{ + _delegate = delegate; + [self startWatchingAppSwitching]; + singleton = self; + _mediaKeyAppList = [NSMutableArray new]; + _tapThreadRL=nil; + _eventPort=nil; + _eventPortSource=nil; + return self; +} +-(void)dealloc; +{ + [self stopWatchingMediaKeys]; + [self stopWatchingAppSwitching]; + [_mediaKeyAppList release]; + [super dealloc]; +} + +-(void)startWatchingAppSwitching; +{ + // Listen to "app switched" event, so that we don't intercept media keys if we + // weren't the last "media key listening" app to be active + EventTypeSpec eventType = { kEventClassApplication, kEventAppFrontSwitched }; + OSStatus err = InstallApplicationEventHandler(NewEventHandlerUPP(appSwitched), 1, &eventType, self, &_app_switching_ref); + assert(err == noErr); + + eventType.eventKind = kEventAppTerminated; + err = InstallApplicationEventHandler(NewEventHandlerUPP(appTerminated), 1, &eventType, self, &_app_terminating_ref); + assert(err == noErr); +} +-(void)stopWatchingAppSwitching; +{ + if(!_app_switching_ref) return; + RemoveEventHandler(_app_switching_ref); + _app_switching_ref = NULL; +} + +-(void)startWatchingMediaKeys;{ + // Prevent having multiple mediaKeys threads + [self stopWatchingMediaKeys]; + + [self setShouldInterceptMediaKeyEvents:YES]; + + // Add an event tap to intercept the system defined media key events + _eventPort = CGEventTapCreate(kCGSessionEventTap, + kCGHeadInsertEventTap, + kCGEventTapOptionDefault, + CGEventMaskBit(NX_SYSDEFINED), + tapEventCallback, + self); + assert(_eventPort != NULL); + + _eventPortSource = CFMachPortCreateRunLoopSource(kCFAllocatorSystemDefault, _eventPort, 0); + assert(_eventPortSource != NULL); + + // Let's do this in a separate thread so that a slow app doesn't lag the event tap + [NSThread detachNewThreadSelector:@selector(eventTapThread) toTarget:self withObject:nil]; +} +-(void)stopWatchingMediaKeys; +{ + // TODO: Shut down thread, remove event tap port and source + + if(_tapThreadRL){ + CFRunLoopStop(_tapThreadRL); + _tapThreadRL=nil; + } + + if(_eventPort){ + CFMachPortInvalidate(_eventPort); + CFRelease(_eventPort); + _eventPort=nil; + } + + if(_eventPortSource){ + CFRelease(_eventPortSource); + _eventPortSource=nil; + } +} + +#pragma mark - +#pragma mark Accessors + ++(BOOL)usesGlobalMediaKeyTap +{ +#ifdef _DEBUG + // breaking in gdb with a key tap inserted sometimes locks up all mouse and keyboard input forever, forcing reboot + return NO; +#else + // XXX(nevyn): MediaKey event tap doesn't work on 10.4, feel free to figure out why if you have the energy. + return + ![[NSUserDefaults standardUserDefaults] boolForKey:kIgnoreMediaKeysDefaultsKey] + && floor(NSAppKitVersionNumber) >= 949/*NSAppKitVersionNumber10_5*/; +#endif +} + ++ (NSArray*)defaultMediaKeyUserBundleIdentifiers; +{ + return [NSArray arrayWithObjects: + [[NSBundle mainBundle] bundleIdentifier], // your app + @"com.spotify.client", + @"com.apple.iTunes", + @"com.apple.QuickTimePlayerX", + @"com.apple.quicktimeplayer", + @"com.apple.iWork.Keynote", + @"com.apple.iPhoto", + @"org.videolan.vlc", + @"com.apple.Aperture", + @"com.plexsquared.Plex", + @"com.soundcloud.desktop", + @"org.niltsh.MPlayerX", + @"com.ilabs.PandorasHelper", + @"com.mahasoftware.pandabar", + @"com.bitcartel.pandorajam", + @"org.clementine-player.clementine", + @"fm.last.Last.fm", + @"fm.last.Scrobbler", + @"com.beatport.BeatportPro", + @"com.Timenut.SongKey", + @"com.macromedia.fireworks", // the tap messes up their mouse input + nil + ]; +} + + +-(BOOL)shouldInterceptMediaKeyEvents; +{ + BOOL shouldIntercept = NO; + @synchronized(self) { + shouldIntercept = _shouldInterceptMediaKeyEvents; + } + return shouldIntercept; +} + +-(void)pauseTapOnTapThread:(BOOL)yeahno; +{ + CGEventTapEnable(self->_eventPort, yeahno); +} +-(void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting; +{ + BOOL oldSetting; + @synchronized(self) { + oldSetting = _shouldInterceptMediaKeyEvents; + _shouldInterceptMediaKeyEvents = newSetting; + } + if(_tapThreadRL && oldSetting != newSetting) { + id grab = [self grab]; + [grab pauseTapOnTapThread:newSetting]; + NSTimer *timer = [NSTimer timerWithTimeInterval:0 invocation:[grab invocation] repeats:NO]; + CFRunLoopAddTimer(_tapThreadRL, (CFRunLoopTimerRef)timer, kCFRunLoopCommonModes); + } +} + +#pragma mark +#pragma mark - +#pragma mark Event tap callbacks + +// Note: method called on background thread + +static CGEventRef tapEventCallback2(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) +{ + SPMediaKeyTap *self = refcon; + + if(type == kCGEventTapDisabledByTimeout) { + NSLog(@"Media key event tap was disabled by timeout"); + CGEventTapEnable(self->_eventPort, TRUE); + return event; + } else if(type == kCGEventTapDisabledByUserInput) { + // Was disabled manually by -[pauseTapOnTapThread] + return event; + } + NSEvent *nsEvent = nil; + @try { + nsEvent = [NSEvent eventWithCGEvent:event]; + } + @catch (NSException * e) { + NSLog(@"Strange CGEventType: %d: %@", type, e); + assert(0); + return event; + } + + if (type != NX_SYSDEFINED || [nsEvent subtype] != SPSystemDefinedEventMediaKeys) + return event; + + int keyCode = (([nsEvent data1] & 0xFFFF0000) >> 16); + if (keyCode != NX_KEYTYPE_PLAY && keyCode != NX_KEYTYPE_FAST && keyCode != NX_KEYTYPE_REWIND && keyCode != NX_KEYTYPE_PREVIOUS && keyCode != NX_KEYTYPE_NEXT) + return event; + + if (![self shouldInterceptMediaKeyEvents]) + return event; + + [nsEvent retain]; // matched in handleAndReleaseMediaKeyEvent: + [self performSelectorOnMainThread:@selector(handleAndReleaseMediaKeyEvent:) withObject:nsEvent waitUntilDone:NO]; + + return NULL; +} + +static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) +{ + NSAutoreleasePool *pool = [NSAutoreleasePool new]; + CGEventRef ret = tapEventCallback2(proxy, type, event, refcon); + [pool drain]; + return ret; +} + + +// event will have been retained in the other thread +-(void)handleAndReleaseMediaKeyEvent:(NSEvent *)event { + [event autorelease]; + + [_delegate mediaKeyTap:self receivedMediaKeyEvent:event]; +} + + +-(void)eventTapThread; +{ + _tapThreadRL = CFRunLoopGetCurrent(); + CFRunLoopAddSource(_tapThreadRL, _eventPortSource, kCFRunLoopCommonModes); + CFRunLoopRun(); +} + +#pragma mark Task switching callbacks + +NSString *kMediaKeyUsingBundleIdentifiersDefaultsKey = @"SPApplicationsNeedingMediaKeys"; +NSString *kIgnoreMediaKeysDefaultsKey = @"SPIgnoreMediaKeys"; + + + +-(void)mediaKeyAppListChanged; +{ + if([_mediaKeyAppList count] == 0) return; + + /*NSLog(@"--"); + int i = 0; + for (NSValue *psnv in _mediaKeyAppList) { + ProcessSerialNumber psn; [psnv getValue:&psn]; + NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary( + &psn, + kProcessDictionaryIncludeAllInformationMask + ) autorelease]; + NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey]; + NSLog(@"%d: %@", i++, bundleIdentifier); + }*/ + + ProcessSerialNumber mySerial, topSerial; + GetCurrentProcess(&mySerial); + [[_mediaKeyAppList objectAtIndex:0] getValue:&topSerial]; + + Boolean same; + OSErr err = SameProcess(&mySerial, &topSerial, &same); + [self setShouldInterceptMediaKeyEvents:(err == noErr && same)]; + +} +-(void)appIsNowFrontmost:(ProcessSerialNumber)psn; +{ + NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)]; + + NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary( + &psn, + kProcessDictionaryIncludeAllInformationMask + ) autorelease]; + NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey]; + + NSArray *whitelistIdentifiers = [[NSUserDefaults standardUserDefaults] arrayForKey:kMediaKeyUsingBundleIdentifiersDefaultsKey]; + if(![whitelistIdentifiers containsObject:bundleIdentifier]) return; + + [_mediaKeyAppList removeObject:psnv]; + [_mediaKeyAppList insertObject:psnv atIndex:0]; + [self mediaKeyAppListChanged]; +} +-(void)appTerminated:(ProcessSerialNumber)psn; +{ + NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)]; + [_mediaKeyAppList removeObject:psnv]; + [self mediaKeyAppListChanged]; +} + +static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData) +{ + SPMediaKeyTap *self = (id)userData; + + ProcessSerialNumber newSerial; + GetFrontProcess(&newSerial); + + [self appIsNowFrontmost:newSerial]; + + return CallNextEventHandler(nextHandler, evt); +} + +static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData) +{ + SPMediaKeyTap *self = (id)userData; + + ProcessSerialNumber deadPSN; + + GetEventParameter( + evt, + kEventParamProcessID, + typeProcessSerialNumber, + NULL, + sizeof(deadPSN), + NULL, + &deadPSN + ); + + + [self appTerminated:deadPSN]; + return CallNextEventHandler(nextHandler, evt); +} + +@end