34
34
#pragma mark Setup and teardown
35
35
-(id)initWithDelegate:(id)delegate;
38
[self startWatchingAppSwitching];
40
_mediaKeyAppList = [NSMutableArray new];
38
[self startWatchingAppSwitching];
40
_mediaKeyAppList = [NSMutableArray new];
45
[self stopWatchingMediaKeys];
46
[self stopWatchingAppSwitching];
47
[_mediaKeyAppList release];
45
[self stopWatchingMediaKeys];
46
[self stopWatchingAppSwitching];
47
[_mediaKeyAppList release];
51
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 };
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
56
OSStatus err = InstallApplicationEventHandler(NewEventHandlerUPP(appSwitched), 1, &eventType, self, &_app_switching_ref);
59
eventType.eventKind = kEventAppTerminated;
59
eventType.eventKind = kEventAppTerminated;
60
60
err = InstallApplicationEventHandler(NewEventHandlerUPP(appTerminated), 1, &eventType, self, &_app_terminating_ref);
63
63
-(void)stopWatchingAppSwitching;
65
if(!_app_switching_ref) return;
66
RemoveEventHandler(_app_switching_ref);
67
_app_switching_ref = NULL;
65
if(!_app_switching_ref) return;
66
RemoveEventHandler(_app_switching_ref);
67
_app_switching_ref = NULL;
70
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);
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
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];
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
88
-(void)stopWatchingMediaKeys;
90
// TODO<nevyn>: Shut down thread, remove event tap port and source
90
// TODO<nevyn>: Shut down thread, remove event tap port and source
96
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*/;
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
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
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
126
-(BOOL)shouldInterceptMediaKeyEvents;
128
BOOL shouldIntercept = NO;
129
@synchronized(self) {
130
shouldIntercept = _shouldInterceptMediaKeyEvents;
132
return shouldIntercept;
128
BOOL shouldIntercept = NO;
129
@synchronized(self) {
130
shouldIntercept = _shouldInterceptMediaKeyEvents;
132
return shouldIntercept;
135
135
-(void)pauseTapOnTapThread:(BOOL)yeahno;
137
CGEventTapEnable(self->_eventPort, yeahno);
137
CGEventTapEnable(self->_eventPort, yeahno);
139
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);
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);
160
160
static CGEventRef tapEventCallback2(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
162
SPMediaKeyTap *self = refcon;
162
SPMediaKeyTap *self = refcon;
164
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];
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
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);
200
NSAutoreleasePool *pool = [NSAutoreleasePool new];
201
CGEventRef ret = tapEventCallback2(proxy, type, event, refcon);
207
207
// event will have been retained in the other thread
208
208
-(void)handleAndReleaseMediaKeyEvent:(NSEvent *)event {
211
[_delegate mediaKeyTap:self receivedMediaKeyEvent:event];
211
[_delegate mediaKeyTap:self receivedMediaKeyEvent:event];
215
215
-(void)eventTapThread;
217
_tapThreadRL = CFRunLoopGetCurrent();
218
CFRunLoopAddSource(_tapThreadRL, _eventPortSource, kCFRunLoopCommonModes);
217
_tapThreadRL = CFRunLoopGetCurrent();
218
CFRunLoopAddSource(_tapThreadRL, _eventPortSource, kCFRunLoopCommonModes);
222
222
#pragma mark Task switching callbacks
227
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);
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
243
ProcessSerialNumber mySerial, topSerial;
244
GetCurrentProcess(&mySerial);
245
[[_mediaKeyAppList objectAtIndex:0] getValue:&topSerial];
244
GetCurrentProcess(&mySerial);
245
[[_mediaKeyAppList objectAtIndex:0] getValue:&topSerial];
248
OSErr err = SameProcess(&mySerial, &topSerial, &same);
249
[self setShouldInterceptMediaKeyEvents:(err == noErr && same)];
248
OSErr err = SameProcess(&mySerial, &topSerial, &same);
249
[self setShouldInterceptMediaKeyEvents:(err == noErr && same)];
252
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];
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
269
-(void)appTerminated:(ProcessSerialNumber)psn;
271
NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
272
[_mediaKeyAppList removeObject:psnv];
273
[self mediaKeyAppListChanged];
271
NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
272
[_mediaKeyAppList removeObject:psnv];
273
[self mediaKeyAppListChanged];
276
276
static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
278
SPMediaKeyTap *self = (id)userData;
278
SPMediaKeyTap *self = (id)userData;
280
280
ProcessSerialNumber newSerial;
281
281
GetFrontProcess(&newSerial);
283
[self appIsNowFrontmost:newSerial];
283
[self appIsNowFrontmost:newSerial];
285
285
return CallNextEventHandler(nextHandler, evt);
288
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];
290
SPMediaKeyTap *self = (id)userData;
292
ProcessSerialNumber deadPSN;
296
kEventParamProcessID,
297
typeProcessSerialNumber,
305
[self appTerminated:deadPSN];
306
306
return CallNextEventHandler(nextHandler, evt);