]> git.sesse.net Git - vlc/blob - modules/gui/macosx/SPMediaKeyTap.m
macosx: removed tabs and fixed whitespacing errors
[vlc] / modules / gui / macosx / SPMediaKeyTap.m
1 /*
2  Copyright (c) 2011, Joachim Bengtsson
3  All rights reserved.
4  
5  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
6  
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.
8  
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.
10 */
11
12 // Copyright (c) 2010 Spotify AB
13 #import "SPMediaKeyTap.h"
14 #import "SPInvocationGrabbing.h" // https://gist.github.com/511181
15
16 @interface SPMediaKeyTap ()
17 -(BOOL)shouldInterceptMediaKeyEvents;
18 -(void)startWatchingAppSwitching;
19 -(void)stopWatchingAppSwitching;
20 -(void)eventTapThread;
21 @end
22 static SPMediaKeyTap *singleton = nil;
23
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);
27
28
29 // Inspired by http://gist.github.com/546311
30
31 @implementation SPMediaKeyTap
32
33 #pragma mark -
34 #pragma mark Setup and teardown
35 -(id)initWithDelegate:(id)delegate;
36 {
37     _delegate = delegate;
38     [self startWatchingAppSwitching];
39     singleton = self;
40     _mediaKeyAppList = [NSMutableArray new];
41     return self;
42 }
43 -(void)dealloc;
44 {
45     [self stopWatchingMediaKeys];
46     [self stopWatchingAppSwitching];
47     [_mediaKeyAppList release];
48     [super dealloc];
49 }
50
51 -(void)startWatchingAppSwitching;
52 {
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);
57     assert(err == noErr);
58
59     eventType.eventKind = kEventAppTerminated;
60     err = InstallApplicationEventHandler(NewEventHandlerUPP(appTerminated), 1, &eventType, self, &_app_terminating_ref);
61     assert(err == noErr);
62 }
63 -(void)stopWatchingAppSwitching;
64 {
65     if(!_app_switching_ref) return;
66     RemoveEventHandler(_app_switching_ref);
67     _app_switching_ref = NULL;
68 }
69
70 -(void)startWatchingMediaKeys;{
71     [self setShouldInterceptMediaKeyEvents:YES];
72
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),
78                                   tapEventCallback,
79                                   self);
80     assert(_eventPort != NULL);
81
82     _eventPortSource = CFMachPortCreateRunLoopSource(kCFAllocatorSystemDefault, _eventPort, 0);
83     assert(_eventPortSource != NULL);
84
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];
87 }
88 -(void)stopWatchingMediaKeys;
89 {
90     // TODO<nevyn>: Shut down thread, remove event tap port and source
91 }
92
93 #pragma mark -
94 #pragma mark Accessors
95
96 +(BOOL)usesGlobalMediaKeyTap
97 {
98     return YES;
99 #ifdef _DEBUG
100     return NO;
101 #else
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*/;
104 #endif
105 }
106
107 + (NSArray*)defaultMediaKeyUserBundleIdentifiers;
108 {
109     return [NSArray arrayWithObjects:
110         @"com.spotify.client",
111         @"com.apple.iTunes",
112         @"com.apple.QuickTimePlayerX",
113         @"com.apple.quicktimeplayer",
114         @"com.apple.iWork.Keynote",
115         @"com.apple.iPhoto",
116         @"org.videolan.vlc",
117         @"com.apple.Aperture",
118         @"com.plexsquared.Plex",
119         @"com.soundcloud.desktop",
120         @"com.macromedia.fireworks", // the tap messes up their mouse input
121         nil
122     ];
123 }
124
125
126 -(BOOL)shouldInterceptMediaKeyEvents;
127 {
128     BOOL shouldIntercept = NO;
129     @synchronized(self) {
130         shouldIntercept = _shouldInterceptMediaKeyEvents;
131     }
132     return shouldIntercept;
133 }
134
135 -(void)pauseTapOnTapThread:(BOOL)yeahno;
136 {
137     CGEventTapEnable(self->_eventPort, yeahno);
138 }
139 -(void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
140 {
141     BOOL oldSetting;
142     @synchronized(self) {
143         oldSetting = _shouldInterceptMediaKeyEvents;
144         _shouldInterceptMediaKeyEvents = newSetting;
145     }
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);
151     }
152 }
153
154 #pragma mark 
155 #pragma mark -
156 #pragma mark Event tap callbacks
157
158 // Note: method called on background thread
159
160 static CGEventRef tapEventCallback2(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
161 {
162     SPMediaKeyTap *self = refcon;
163
164     if(type == kCGEventTapDisabledByTimeout) {
165         NSLog(@"Media key event tap was disabled by timeout");
166         CGEventTapEnable(self->_eventPort, TRUE);
167         return event;
168     } else if(type == kCGEventTapDisabledByUserInput) {
169         // Was disabled manually by -[pauseTapOnTapThread]
170         return event;
171     }
172     NSEvent *nsEvent = nil;
173     @try {
174         nsEvent = [NSEvent eventWithCGEvent:event];
175     }
176     @catch (NSException * e) {
177         NSLog(@"Strange CGEventType: %d: %@", type, e);
178         assert(0);
179         return event;
180     }
181
182     if (type != NX_SYSDEFINED || [nsEvent subtype] != SPSystemDefinedEventMediaKeys)
183         return event;
184
185     int keyCode = (([nsEvent data1] & 0xFFFF0000) >> 16);
186     if (keyCode != NX_KEYTYPE_PLAY && keyCode != NX_KEYTYPE_FAST && keyCode != NX_KEYTYPE_REWIND)
187         return event;
188
189     if (![self shouldInterceptMediaKeyEvents])
190         return event;
191
192     [nsEvent retain]; // matched in handleAndReleaseMediaKeyEvent:
193     [self performSelectorOnMainThread:@selector(handleAndReleaseMediaKeyEvent:) withObject:nsEvent waitUntilDone:NO];
194
195     return NULL;
196 }
197
198 static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
199 {
200     NSAutoreleasePool *pool = [NSAutoreleasePool new];
201     CGEventRef ret = tapEventCallback2(proxy, type, event, refcon);
202     [pool drain];
203     return ret;
204 }
205
206
207 // event will have been retained in the other thread
208 -(void)handleAndReleaseMediaKeyEvent:(NSEvent *)event {
209     [event autorelease];
210
211     [_delegate mediaKeyTap:self receivedMediaKeyEvent:event];
212 }
213
214
215 -(void)eventTapThread;
216 {
217     _tapThreadRL = CFRunLoopGetCurrent();
218     CFRunLoopAddSource(_tapThreadRL, _eventPortSource, kCFRunLoopCommonModes);
219     CFRunLoopRun();
220 }
221
222 #pragma mark Task switching callbacks
223
224 NSString *kMediaKeyUsingBundleIdentifiersDefaultsKey = @"SPApplicationsNeedingMediaKeys";
225
226
227 -(void)mediaKeyAppListChanged;
228 {
229     if([_mediaKeyAppList count] == 0) return;
230
231     /*NSLog(@"--");
232     int i = 0;
233     for (NSValue *psnv in _mediaKeyAppList) {
234         ProcessSerialNumber psn; [psnv getValue:&psn];
235         NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
236             &psn,
237             kProcessDictionaryIncludeAllInformationMask
238         ) autorelease];
239         NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
240         NSLog(@"%d: %@", i++, bundleIdentifier);
241     }*/
242
243     ProcessSerialNumber mySerial, topSerial;
244     GetCurrentProcess(&mySerial);
245     [[_mediaKeyAppList objectAtIndex:0] getValue:&topSerial];
246
247     Boolean same;
248     OSErr err = SameProcess(&mySerial, &topSerial, &same);
249     [self setShouldInterceptMediaKeyEvents:(err == noErr && same)];
250
251 }
252 -(void)appIsNowFrontmost:(ProcessSerialNumber)psn;
253 {
254     NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
255
256     NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
257         &psn,
258         kProcessDictionaryIncludeAllInformationMask
259     ) autorelease];
260     NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
261
262     NSArray *whitelistIdentifiers = [[NSUserDefaults standardUserDefaults] arrayForKey:kMediaKeyUsingBundleIdentifiersDefaultsKey];
263     if(![whitelistIdentifiers containsObject:bundleIdentifier]) return;
264
265     [_mediaKeyAppList removeObject:psnv];
266     [_mediaKeyAppList insertObject:psnv atIndex:0];
267     [self mediaKeyAppListChanged];
268 }
269 -(void)appTerminated:(ProcessSerialNumber)psn;
270 {
271     NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
272     [_mediaKeyAppList removeObject:psnv];
273     [self mediaKeyAppListChanged];
274 }
275
276 static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
277 {
278     SPMediaKeyTap *self = (id)userData;
279
280     ProcessSerialNumber newSerial;
281     GetFrontProcess(&newSerial);
282
283     [self appIsNowFrontmost:newSerial];
284
285     return CallNextEventHandler(nextHandler, evt);
286 }
287
288 static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
289 {
290     SPMediaKeyTap *self = (id)userData;
291
292     ProcessSerialNumber deadPSN;
293
294     GetEventParameter(
295         evt,
296         kEventParamProcessID,
297         typeProcessSerialNumber,
298         NULL,
299         sizeof(deadPSN),
300         NULL,
301         &deadPSN
302     );
303
304
305     [self appTerminated:deadPSN];
306     return CallNextEventHandler(nextHandler, evt);
307 }
308
309 @end