]> git.sesse.net Git - vlc/blob - modules/gui/macosx/SPMediaKeyTap.m
ASF: help stupid compiler
[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