2
Copyright (c) 2011, Joachim Bengtsson
5
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
7
* Neither the name of the organization nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
9
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
12
// Copyright (c) 2010 Spotify AB
13
#import "SPMediaKeyTap.h"
14
#import "SPInvocationGrabbing.h" // https://gist.github.com/511181
16
@interface SPMediaKeyTap ()
17
-(BOOL)shouldInterceptMediaKeyEvents;
18
-(void)startWatchingAppSwitching;
19
-(void)stopWatchingAppSwitching;
20
-(void)eventTapThread;
22
static SPMediaKeyTap *singleton = nil;
24
static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData);
25
static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData);
26
static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon);
29
// Inspired by http://gist.github.com/546311
31
@implementation SPMediaKeyTap
34
#pragma mark Setup and teardown
35
-(id)initWithDelegate:(id)delegate;
38
[self startWatchingAppSwitching];
40
_mediaKeyAppList = [NSMutableArray new];
45
[self stopWatchingMediaKeys];
46
[self stopWatchingAppSwitching];
47
[_mediaKeyAppList release];
51
-(void)startWatchingAppSwitching;
53
// Listen to "app switched" event, so that we don't intercept media keys if we
54
// weren't the last "media key listening" app to be active
55
EventTypeSpec eventType = { kEventClassApplication, kEventAppFrontSwitched };
56
OSStatus err = InstallApplicationEventHandler(NewEventHandlerUPP(appSwitched), 1, &eventType, self, &_app_switching_ref);
59
eventType.eventKind = kEventAppTerminated;
60
err = InstallApplicationEventHandler(NewEventHandlerUPP(appTerminated), 1, &eventType, self, &_app_terminating_ref);
63
-(void)stopWatchingAppSwitching;
65
if(!_app_switching_ref) return;
66
RemoveEventHandler(_app_switching_ref);
67
_app_switching_ref = NULL;
70
-(void)startWatchingMediaKeys;{
71
[self setShouldInterceptMediaKeyEvents:YES];
73
// Add an event tap to intercept the system defined media key events
74
_eventPort = CGEventTapCreate(kCGSessionEventTap,
75
kCGHeadInsertEventTap,
76
kCGEventTapOptionDefault,
77
CGEventMaskBit(NX_SYSDEFINED),
80
assert(_eventPort != NULL);
82
_eventPortSource = CFMachPortCreateRunLoopSource(kCFAllocatorSystemDefault, _eventPort, 0);
83
assert(_eventPortSource != NULL);
85
// Let's do this in a separate thread so that a slow app doesn't lag the event tap
86
[NSThread detachNewThreadSelector:@selector(eventTapThread) toTarget:self withObject:nil];
88
-(void)stopWatchingMediaKeys;
90
// TODO<nevyn>: Shut down thread, remove event tap port and source
94
#pragma mark Accessors
96
+(BOOL)usesGlobalMediaKeyTap
102
// XXX(nevyn): MediaKey event tap doesn't work on 10.4, feel free to figure out why if you have the energy.
103
return floor(NSAppKitVersionNumber) >= 949/*NSAppKitVersionNumber10_5*/;
107
+ (NSArray*)defaultMediaKeyUserBundleIdentifiers;
109
return [NSArray arrayWithObjects:
110
@"com.spotify.client",
112
@"com.apple.QuickTimePlayerX",
113
@"com.apple.quicktimeplayer",
114
@"com.apple.iWork.Keynote",
117
@"com.apple.Aperture",
118
@"com.plexsquared.Plex",
119
@"com.soundcloud.desktop",
120
@"com.macromedia.fireworks", // the tap messes up their mouse input
126
-(BOOL)shouldInterceptMediaKeyEvents;
128
BOOL shouldIntercept = NO;
129
@synchronized(self) {
130
shouldIntercept = _shouldInterceptMediaKeyEvents;
132
return shouldIntercept;
135
-(void)pauseTapOnTapThread:(BOOL)yeahno;
137
CGEventTapEnable(self->_eventPort, yeahno);
139
-(void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
142
@synchronized(self) {
143
oldSetting = _shouldInterceptMediaKeyEvents;
144
_shouldInterceptMediaKeyEvents = newSetting;
146
if(_tapThreadRL && oldSetting != newSetting) {
147
id grab = [self grab];
148
[grab pauseTapOnTapThread:newSetting];
149
NSTimer *timer = [NSTimer timerWithTimeInterval:0 invocation:[grab invocation] repeats:NO];
150
CFRunLoopAddTimer(_tapThreadRL, (CFRunLoopTimerRef)timer, kCFRunLoopCommonModes);
156
#pragma mark Event tap callbacks
158
// Note: method called on background thread
160
static CGEventRef tapEventCallback2(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
162
SPMediaKeyTap *self = refcon;
164
if(type == kCGEventTapDisabledByTimeout) {
165
NSLog(@"Media key event tap was disabled by timeout");
166
CGEventTapEnable(self->_eventPort, TRUE);
168
} else if(type == kCGEventTapDisabledByUserInput) {
169
// Was disabled manually by -[pauseTapOnTapThread]
172
NSEvent *nsEvent = nil;
174
nsEvent = [NSEvent eventWithCGEvent:event];
176
@catch (NSException * e) {
177
NSLog(@"Strange CGEventType: %d: %@", type, e);
182
if (type != NX_SYSDEFINED || [nsEvent subtype] != SPSystemDefinedEventMediaKeys)
185
int keyCode = (([nsEvent data1] & 0xFFFF0000) >> 16);
186
if (keyCode != NX_KEYTYPE_PLAY && keyCode != NX_KEYTYPE_FAST && keyCode != NX_KEYTYPE_REWIND)
189
if (![self shouldInterceptMediaKeyEvents])
192
[nsEvent retain]; // matched in handleAndReleaseMediaKeyEvent:
193
[self performSelectorOnMainThread:@selector(handleAndReleaseMediaKeyEvent:) withObject:nsEvent waitUntilDone:NO];
198
static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
200
NSAutoreleasePool *pool = [NSAutoreleasePool new];
201
CGEventRef ret = tapEventCallback2(proxy, type, event, refcon);
207
// event will have been retained in the other thread
208
-(void)handleAndReleaseMediaKeyEvent:(NSEvent *)event {
211
[_delegate mediaKeyTap:self receivedMediaKeyEvent:event];
215
-(void)eventTapThread;
217
_tapThreadRL = CFRunLoopGetCurrent();
218
CFRunLoopAddSource(_tapThreadRL, _eventPortSource, kCFRunLoopCommonModes);
222
#pragma mark Task switching callbacks
224
NSString *kMediaKeyUsingBundleIdentifiersDefaultsKey = @"SPApplicationsNeedingMediaKeys";
227
-(void)mediaKeyAppListChanged;
229
if([_mediaKeyAppList count] == 0) return;
233
for (NSValue *psnv in _mediaKeyAppList) {
234
ProcessSerialNumber psn; [psnv getValue:&psn];
235
NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
237
kProcessDictionaryIncludeAllInformationMask
239
NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
240
NSLog(@"%d: %@", i++, bundleIdentifier);
243
ProcessSerialNumber mySerial, topSerial;
244
GetCurrentProcess(&mySerial);
245
[[_mediaKeyAppList objectAtIndex:0] getValue:&topSerial];
248
OSErr err = SameProcess(&mySerial, &topSerial, &same);
249
[self setShouldInterceptMediaKeyEvents:(err == noErr && same)];
252
-(void)appIsNowFrontmost:(ProcessSerialNumber)psn;
254
NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
256
NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
258
kProcessDictionaryIncludeAllInformationMask
260
NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
262
NSArray *whitelistIdentifiers = [[NSUserDefaults standardUserDefaults] arrayForKey:kMediaKeyUsingBundleIdentifiersDefaultsKey];
263
if(![whitelistIdentifiers containsObject:bundleIdentifier]) return;
265
[_mediaKeyAppList removeObject:psnv];
266
[_mediaKeyAppList insertObject:psnv atIndex:0];
267
[self mediaKeyAppListChanged];
269
-(void)appTerminated:(ProcessSerialNumber)psn;
271
NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
272
[_mediaKeyAppList removeObject:psnv];
273
[self mediaKeyAppListChanged];
276
static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
278
SPMediaKeyTap *self = (id)userData;
280
ProcessSerialNumber newSerial;
281
GetFrontProcess(&newSerial);
283
[self appIsNowFrontmost:newSerial];
285
return CallNextEventHandler(nextHandler, evt);
288
static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
290
SPMediaKeyTap *self = (id)userData;
292
ProcessSerialNumber deadPSN;
296
kEventParamProcessID,
297
typeProcessSerialNumber,
305
[self appTerminated:deadPSN];
306
return CallNextEventHandler(nextHandler, evt);