]> git.sesse.net Git - vlc/blob - modules/gui/macosx/SPMediaKeyTap.m
macosx: add option to hide effects button in control bar
[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"
15
16 @interface SPMediaKeyTap ()
17 -(BOOL)shouldInterceptMediaKeyEvents;
18 -(void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
19 -(void)startWatchingAppSwitching;
20 -(void)stopWatchingAppSwitching;
21 -(void)eventTapThread;
22 @end
23 static SPMediaKeyTap *singleton = nil;
24
25 static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData);
26 static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData);
27 static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon);
28
29
30 // Inspired by http://gist.github.com/546311
31
32 @implementation SPMediaKeyTap
33
34 #pragma mark -
35 #pragma mark Setup and teardown
36 -(id)initWithDelegate:(id)delegate;
37 {
38     _delegate = delegate;
39     [self startWatchingAppSwitching];
40     singleton = self;
41     _mediaKeyAppList = [NSMutableArray new];
42     _tapThreadRL=nil;
43     _eventPort=nil;
44     _eventPortSource=nil;
45     return self;
46 }
47 -(void)dealloc;
48 {
49     [self stopWatchingMediaKeys];
50     [self stopWatchingAppSwitching];
51     [_mediaKeyAppList release];
52     [super dealloc];
53 }
54
55 -(void)startWatchingAppSwitching;
56 {
57     // Listen to "app switched" event, so that we don't intercept media keys if we
58     // weren't the last "media key listening" app to be active
59     EventTypeSpec eventType = { kEventClassApplication, kEventAppFrontSwitched };
60     OSStatus err = InstallApplicationEventHandler(NewEventHandlerUPP(appSwitched), 1, &eventType, self, &_app_switching_ref);
61     assert(err == noErr);
62
63     eventType.eventKind = kEventAppTerminated;
64     err = InstallApplicationEventHandler(NewEventHandlerUPP(appTerminated), 1, &eventType, self, &_app_terminating_ref);
65     assert(err == noErr);
66 }
67 -(void)stopWatchingAppSwitching;
68 {
69     if(!_app_switching_ref) return;
70     RemoveEventHandler(_app_switching_ref);
71     _app_switching_ref = NULL;
72 }
73
74 -(void)startWatchingMediaKeys;{
75     // Prevent having multiple mediaKeys threads
76     [self stopWatchingMediaKeys];
77
78     [self setShouldInterceptMediaKeyEvents:YES];
79
80     // Add an event tap to intercept the system defined media key events
81     _eventPort = CGEventTapCreate(kCGSessionEventTap,
82                                   kCGHeadInsertEventTap,
83                                   kCGEventTapOptionDefault,
84                                   CGEventMaskBit(NX_SYSDEFINED),
85                                   tapEventCallback,
86                                   self);
87     assert(_eventPort != NULL);
88
89     _eventPortSource = CFMachPortCreateRunLoopSource(kCFAllocatorSystemDefault, _eventPort, 0);
90     assert(_eventPortSource != NULL);
91
92     // Let's do this in a separate thread so that a slow app doesn't lag the event tap
93     [NSThread detachNewThreadSelector:@selector(eventTapThread) toTarget:self withObject:nil];
94 }
95 -(void)stopWatchingMediaKeys;
96 {
97     // TODO<nevyn>: Shut down thread, remove event tap port and source
98
99     if(_tapThreadRL){
100         CFRunLoopStop(_tapThreadRL);
101         _tapThreadRL=nil;
102     }
103
104     if(_eventPort){
105         CFMachPortInvalidate(_eventPort);
106         CFRelease(_eventPort);
107         _eventPort=nil;
108     }
109
110     if(_eventPortSource){
111         CFRelease(_eventPortSource);
112         _eventPortSource=nil;
113     }
114 }
115
116 #pragma mark -
117 #pragma mark Accessors
118
119 +(BOOL)usesGlobalMediaKeyTap
120 {
121 #ifdef _DEBUG
122     // breaking in gdb with a key tap inserted sometimes locks up all mouse and keyboard input forever, forcing reboot
123     return NO;
124 #else
125     // XXX(nevyn): MediaKey event tap doesn't work on 10.4, feel free to figure out why if you have the energy.
126     return
127         ![[NSUserDefaults standardUserDefaults] boolForKey:kIgnoreMediaKeysDefaultsKey]
128         && floor(NSAppKitVersionNumber) >= 949/*NSAppKitVersionNumber10_5*/;
129 #endif
130 }
131
132 + (NSArray*)defaultMediaKeyUserBundleIdentifiers;
133 {
134     return [NSArray arrayWithObjects:
135         [[NSBundle mainBundle] bundleIdentifier], // your app
136         @"com.spotify.client",
137         @"com.apple.iTunes",
138         @"com.apple.QuickTimePlayerX",
139         @"com.apple.quicktimeplayer",
140         @"com.apple.iWork.Keynote",
141         @"com.apple.iPhoto",
142         @"org.videolan.vlc",
143         @"com.apple.Aperture",
144         @"com.plexsquared.Plex",
145         @"com.soundcloud.desktop",
146         @"org.niltsh.MPlayerX",
147         @"com.ilabs.PandorasHelper",
148         @"com.mahasoftware.pandabar",
149         @"com.bitcartel.pandorajam",
150         @"org.clementine-player.clementine",
151         @"fm.last.Last.fm",
152         @"com.beatport.BeatportPro",
153         @"com.Timenut.SongKey",
154         @"com.macromedia.fireworks", // the tap messes up their mouse input
155         nil
156     ];
157 }
158
159
160 -(BOOL)shouldInterceptMediaKeyEvents;
161 {
162     BOOL shouldIntercept = NO;
163     @synchronized(self) {
164         shouldIntercept = _shouldInterceptMediaKeyEvents;
165     }
166     return shouldIntercept;
167 }
168
169 -(void)pauseTapOnTapThread:(BOOL)yeahno;
170 {
171     CGEventTapEnable(self->_eventPort, yeahno);
172 }
173 -(void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
174 {
175     BOOL oldSetting;
176     @synchronized(self) {
177         oldSetting = _shouldInterceptMediaKeyEvents;
178         _shouldInterceptMediaKeyEvents = newSetting;
179     }
180     if(_tapThreadRL && oldSetting != newSetting) {
181         id grab = [self grab];
182         [grab pauseTapOnTapThread:newSetting];
183         NSTimer *timer = [NSTimer timerWithTimeInterval:0 invocation:[grab invocation] repeats:NO];
184         CFRunLoopAddTimer(_tapThreadRL, (CFRunLoopTimerRef)timer, kCFRunLoopCommonModes);
185     }
186 }
187
188 #pragma mark
189 #pragma mark -
190 #pragma mark Event tap callbacks
191
192 // Note: method called on background thread
193
194 static CGEventRef tapEventCallback2(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
195 {
196     SPMediaKeyTap *self = refcon;
197
198     if(type == kCGEventTapDisabledByTimeout) {
199         NSLog(@"Media key event tap was disabled by timeout");
200         CGEventTapEnable(self->_eventPort, TRUE);
201         return event;
202     } else if(type == kCGEventTapDisabledByUserInput) {
203         // Was disabled manually by -[pauseTapOnTapThread]
204         return event;
205     }
206     NSEvent *nsEvent = nil;
207     @try {
208         nsEvent = [NSEvent eventWithCGEvent:event];
209     }
210     @catch (NSException * e) {
211         NSLog(@"Strange CGEventType: %d: %@", type, e);
212         assert(0);
213         return event;
214     }
215
216     if (type != NX_SYSDEFINED || [nsEvent subtype] != SPSystemDefinedEventMediaKeys)
217         return event;
218
219     int keyCode = (([nsEvent data1] & 0xFFFF0000) >> 16);
220     if (keyCode != NX_KEYTYPE_PLAY && keyCode != NX_KEYTYPE_FAST && keyCode != NX_KEYTYPE_REWIND && keyCode != NX_KEYTYPE_PREVIOUS && keyCode != NX_KEYTYPE_NEXT)
221         return event;
222
223     if (![self shouldInterceptMediaKeyEvents])
224         return event;
225
226     [nsEvent retain]; // matched in handleAndReleaseMediaKeyEvent:
227     [self performSelectorOnMainThread:@selector(handleAndReleaseMediaKeyEvent:) withObject:nsEvent waitUntilDone:NO];
228
229     return NULL;
230 }
231
232 static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
233 {
234     NSAutoreleasePool *pool = [NSAutoreleasePool new];
235     CGEventRef ret = tapEventCallback2(proxy, type, event, refcon);
236     [pool drain];
237     return ret;
238 }
239
240
241 // event will have been retained in the other thread
242 -(void)handleAndReleaseMediaKeyEvent:(NSEvent *)event {
243     [event autorelease];
244
245     [_delegate mediaKeyTap:self receivedMediaKeyEvent:event];
246 }
247
248
249 -(void)eventTapThread;
250 {
251     _tapThreadRL = CFRunLoopGetCurrent();
252     CFRunLoopAddSource(_tapThreadRL, _eventPortSource, kCFRunLoopCommonModes);
253     CFRunLoopRun();
254 }
255
256 #pragma mark Task switching callbacks
257
258 NSString *kMediaKeyUsingBundleIdentifiersDefaultsKey = @"SPApplicationsNeedingMediaKeys";
259 NSString *kIgnoreMediaKeysDefaultsKey = @"SPIgnoreMediaKeys";
260
261
262
263 -(void)mediaKeyAppListChanged;
264 {
265     if([_mediaKeyAppList count] == 0) return;
266
267     /*NSLog(@"--");
268     int i = 0;
269     for (NSValue *psnv in _mediaKeyAppList) {
270         ProcessSerialNumber psn; [psnv getValue:&psn];
271         NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
272             &psn,
273             kProcessDictionaryIncludeAllInformationMask
274         ) autorelease];
275         NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
276         NSLog(@"%d: %@", i++, bundleIdentifier);
277     }*/
278
279     ProcessSerialNumber mySerial, topSerial;
280     GetCurrentProcess(&mySerial);
281     [[_mediaKeyAppList objectAtIndex:0] getValue:&topSerial];
282
283     Boolean same;
284     OSErr err = SameProcess(&mySerial, &topSerial, &same);
285     [self setShouldInterceptMediaKeyEvents:(err == noErr && same)];
286 }
287 -(void)appIsNowFrontmost:(ProcessSerialNumber)psn;
288 {
289     NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
290
291     NSDictionary *processInfo = [(id)ProcessInformationCopyDictionary(
292         &psn,
293         kProcessDictionaryIncludeAllInformationMask
294     ) autorelease];
295     NSString *bundleIdentifier = [processInfo objectForKey:(id)kCFBundleIdentifierKey];
296
297     NSArray *whitelistIdentifiers = [[NSUserDefaults standardUserDefaults] arrayForKey:kMediaKeyUsingBundleIdentifiersDefaultsKey];
298     if(![whitelistIdentifiers containsObject:bundleIdentifier]) return;
299
300     [_mediaKeyAppList removeObject:psnv];
301     [_mediaKeyAppList insertObject:psnv atIndex:0];
302     [self mediaKeyAppListChanged];
303 }
304 -(void)appTerminated:(ProcessSerialNumber)psn;
305 {
306     NSValue *psnv = [NSValue valueWithBytes:&psn objCType:@encode(ProcessSerialNumber)];
307     [_mediaKeyAppList removeObject:psnv];
308     [self mediaKeyAppListChanged];
309 }
310
311 static pascal OSStatus appSwitched (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
312 {
313     SPMediaKeyTap *self = (id)userData;
314
315     ProcessSerialNumber newSerial;
316     GetFrontProcess(&newSerial);
317
318     [self appIsNowFrontmost:newSerial];
319
320     return CallNextEventHandler(nextHandler, evt);
321 }
322
323 static pascal OSStatus appTerminated (EventHandlerCallRef nextHandler, EventRef evt, void* userData)
324 {
325     SPMediaKeyTap *self = (id)userData;
326
327     ProcessSerialNumber deadPSN;
328
329     GetEventParameter(
330         evt,
331         kEventParamProcessID,
332         typeProcessSerialNumber,
333         NULL,
334         sizeof(deadPSN),
335         NULL,
336         &deadPSN
337     );
338
339     [self appTerminated:deadPSN];
340     return CallNextEventHandler(nextHandler, evt);
341 }
342
343 @end