362 lines
11 KiB
Objective-C
362 lines
11 KiB
Objective-C
/*
|
|
Copyright (c) 2011, Joachim Bengtsson
|
|
All rights reserved.
|
|
|
|
Redistribution and use in source and binary forms, with or without modification,
|
|
are permitted provided that the following conditions are met:
|
|
|
|
* 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.
|
|
|
|
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.
|
|
*/
|
|
|
|
// Copyright (c) 2010 Spotify AB
|
|
#import "SPMediaKeyTap.h"
|
|
|
|
// Define to enable app list debug output
|
|
// #define DEBUG_SPMEDIAKEY_APPLIST 1
|
|
|
|
NSString *kIgnoreMediaKeysDefaultsKey = @"SPIgnoreMediaKeys";
|
|
|
|
@interface SPMediaKeyTap () {
|
|
CFMachPortRef _eventPort;
|
|
CFRunLoopSourceRef _eventPortSource;
|
|
CFRunLoopRef _tapThreadRL;
|
|
NSThread *_tapThread;
|
|
BOOL _shouldInterceptMediaKeyEvents;
|
|
id _delegate;
|
|
// The app that is frontmost in this list owns media keys
|
|
NSMutableArray<NSRunningApplication *> *_mediaKeyAppList;
|
|
}
|
|
|
|
- (BOOL)shouldInterceptMediaKeyEvents;
|
|
- (void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
|
|
- (void)startWatchingAppSwitching;
|
|
- (void)stopWatchingAppSwitching;
|
|
- (void)eventTapThread;
|
|
@end
|
|
|
|
static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon);
|
|
|
|
|
|
// Inspired by http://gist.github.com/546311
|
|
|
|
@implementation SPMediaKeyTap
|
|
|
|
#pragma mark -
|
|
#pragma mark Setup and teardown
|
|
|
|
- (id)initWithDelegate:(id)delegate
|
|
{
|
|
self = [super init];
|
|
if (self) {
|
|
_delegate = delegate;
|
|
[self startWatchingAppSwitching];
|
|
_mediaKeyAppList = [NSMutableArray new];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[self stopWatchingMediaKeys];
|
|
[self stopWatchingAppSwitching];
|
|
[super dealloc];
|
|
}
|
|
|
|
- (void)startWatchingAppSwitching
|
|
{
|
|
// Listen to "app switched" event, so that we don't intercept media keys if we
|
|
// weren't the last "media key listening" app to be active
|
|
|
|
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self
|
|
selector:@selector(frontmostAppChanged:)
|
|
name:NSWorkspaceDidActivateApplicationNotification
|
|
object:nil];
|
|
|
|
|
|
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self
|
|
selector:@selector(appTerminated:)
|
|
name:NSWorkspaceDidTerminateApplicationNotification
|
|
object:nil];
|
|
}
|
|
|
|
- (void)stopWatchingAppSwitching
|
|
{
|
|
[[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
|
|
}
|
|
|
|
- (BOOL)startWatchingMediaKeys
|
|
{
|
|
// Prevent having multiple mediaKeys threads
|
|
[self stopWatchingMediaKeys];
|
|
|
|
[self setShouldInterceptMediaKeyEvents:YES];
|
|
|
|
// Add an event tap to intercept the system defined media key events
|
|
_eventPort = CGEventTapCreate(kCGSessionEventTap,
|
|
kCGHeadInsertEventTap,
|
|
kCGEventTapOptionDefault,
|
|
CGEventMaskBit(NX_SYSDEFINED),
|
|
tapEventCallback,
|
|
(__bridge void * __nullable)(self));
|
|
|
|
// Can be NULL if the app has no accessibility access permission
|
|
if (_eventPort == NULL)
|
|
return NO;
|
|
|
|
_eventPortSource = CFMachPortCreateRunLoopSource(kCFAllocatorSystemDefault, _eventPort, 0);
|
|
assert(_eventPortSource != NULL);
|
|
|
|
if (_eventPortSource == NULL)
|
|
return NO;
|
|
|
|
// Let's do this in a separate thread so that a slow app doesn't lag the event tap
|
|
_tapThread = [[NSThread alloc] initWithTarget:self
|
|
selector:@selector(eventTapThread)
|
|
object:nil];
|
|
[_tapThread start];
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (void)stopWatchingMediaKeys
|
|
{
|
|
// Shut down tap thread
|
|
if(_tapThreadRL){
|
|
CFRunLoopStop(_tapThreadRL);
|
|
_tapThreadRL = nil;
|
|
}
|
|
|
|
// Remove tap port
|
|
if(_eventPort){
|
|
CFMachPortInvalidate(_eventPort);
|
|
CFRelease(_eventPort);
|
|
_eventPort = nil;
|
|
}
|
|
|
|
// Remove tap source
|
|
if(_eventPortSource){
|
|
CFRelease(_eventPortSource);
|
|
_eventPortSource = nil;
|
|
}
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Accessors
|
|
|
|
+ (BOOL)usesGlobalMediaKeyTap
|
|
{
|
|
#ifdef _DEBUG
|
|
// breaking in gdb with a key tap inserted sometimes locks up all mouse and keyboard input forever, forcing reboot
|
|
return NO;
|
|
#else
|
|
// XXX(nevyn): MediaKey event tap doesn't work on 10.4, feel free to figure out why if you have the energy.
|
|
return
|
|
![[NSUserDefaults standardUserDefaults] boolForKey:kIgnoreMediaKeysDefaultsKey]
|
|
&& floor(NSAppKitVersionNumber) >= 949/*NSAppKitVersionNumber10_5*/;
|
|
#endif
|
|
}
|
|
|
|
+ (NSArray*)mediaKeyUserBundleIdentifiers
|
|
{
|
|
return [NSArray arrayWithObjects:
|
|
[[NSBundle mainBundle] bundleIdentifier], // your app
|
|
@"com.spotify.client",
|
|
@"com.apple.iTunes",
|
|
@"com.apple.QuickTimePlayerX",
|
|
@"com.apple.quicktimeplayer",
|
|
@"com.apple.iWork.Keynote",
|
|
@"com.apple.iPhoto",
|
|
@"org.videolan.vlc",
|
|
@"com.apple.Aperture",
|
|
@"com.plexsquared.Plex",
|
|
@"com.soundcloud.desktop",
|
|
@"org.niltsh.MPlayerX",
|
|
@"com.ilabs.PandorasHelper",
|
|
@"com.mahasoftware.pandabar",
|
|
@"com.bitcartel.pandorajam",
|
|
@"org.clementine-player.clementine",
|
|
@"fm.last.Last.fm",
|
|
@"fm.last.Scrobbler",
|
|
@"com.beatport.BeatportPro",
|
|
@"com.Timenut.SongKey",
|
|
@"com.macromedia.fireworks", // the tap messes up their mouse input
|
|
@"at.justp.Theremin",
|
|
@"ru.ya.themblsha.YandexMusic",
|
|
@"com.jriver.MediaCenter18",
|
|
@"com.jriver.MediaCenter19",
|
|
@"com.jriver.MediaCenter20",
|
|
@"co.rackit.mate",
|
|
@"com.ttitt.b-music",
|
|
@"com.beardedspice.BeardedSpice",
|
|
@"com.plug.Plug",
|
|
@"com.plug.Plug2",
|
|
@"com.netease.163music",
|
|
@"org.quodlibet.quodlibet",
|
|
nil
|
|
];
|
|
}
|
|
|
|
|
|
- (BOOL)shouldInterceptMediaKeyEvents
|
|
{
|
|
BOOL shouldIntercept = NO;
|
|
@synchronized(self) {
|
|
shouldIntercept = _shouldInterceptMediaKeyEvents;
|
|
}
|
|
return shouldIntercept;
|
|
}
|
|
|
|
- (void)pauseTapOnTapThread:(NSNumber *)yeahno
|
|
{
|
|
CGEventTapEnable(self->_eventPort, [yeahno boolValue]);
|
|
}
|
|
|
|
- (void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting
|
|
{
|
|
BOOL oldSetting;
|
|
@synchronized(self) {
|
|
oldSetting = _shouldInterceptMediaKeyEvents;
|
|
_shouldInterceptMediaKeyEvents = newSetting;
|
|
}
|
|
if(_tapThreadRL && oldSetting != newSetting) {
|
|
[self performSelector:@selector(pauseTapOnTapThread:)
|
|
onThread:_tapThread
|
|
withObject:@(newSetting)
|
|
waitUntilDone:NO];
|
|
|
|
}
|
|
}
|
|
|
|
|
|
#pragma mark -
|
|
#pragma mark Event tap callbacks
|
|
|
|
// Note: method called on background thread
|
|
|
|
static CGEventRef tapEventCallback2(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
|
|
{
|
|
#pragma unused(proxy)
|
|
SPMediaKeyTap *self = (__bridge SPMediaKeyTap *)refcon;
|
|
|
|
if(type == kCGEventTapDisabledByTimeout) {
|
|
NSLog(@"Media key event tap was disabled by timeout");
|
|
CGEventTapEnable(self->_eventPort, TRUE);
|
|
return event;
|
|
} else if(type == kCGEventTapDisabledByUserInput) {
|
|
// Was disabled manually by -[pauseTapOnTapThread]
|
|
return event;
|
|
}
|
|
NSEvent *nsEvent = nil;
|
|
@try {
|
|
nsEvent = [NSEvent eventWithCGEvent:event];
|
|
}
|
|
@catch (NSException * e) {
|
|
NSLog(@"Strange CGEventType: %d: %@", type, e);
|
|
assert(0);
|
|
return event;
|
|
}
|
|
|
|
if (type != NX_SYSDEFINED || [nsEvent subtype] != SPSystemDefinedEventMediaKeys)
|
|
return event;
|
|
|
|
int keyCode = (([nsEvent data1] & 0xFFFF0000) >> 16);
|
|
if (keyCode != NX_KEYTYPE_PLAY && keyCode != NX_KEYTYPE_FAST && keyCode != NX_KEYTYPE_REWIND && keyCode != NX_KEYTYPE_PREVIOUS && keyCode != NX_KEYTYPE_NEXT)
|
|
return event;
|
|
|
|
if (![self shouldInterceptMediaKeyEvents])
|
|
return event;
|
|
|
|
[self performSelectorOnMainThread:@selector(handleAndReleaseMediaKeyEvent:) withObject:nsEvent waitUntilDone:NO];
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
|
|
{
|
|
@autoreleasepool {
|
|
CGEventRef ret = tapEventCallback2(proxy, type, event, refcon);
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
- (void)handleAndReleaseMediaKeyEvent:(NSEvent *)event
|
|
{
|
|
[_delegate mediaKeyTap:self receivedMediaKeyEvent:event];
|
|
}
|
|
|
|
- (void)eventTapThread
|
|
{
|
|
_tapThreadRL = CFRunLoopGetCurrent();
|
|
CFRunLoopAddSource(_tapThreadRL, _eventPortSource, kCFRunLoopCommonModes);
|
|
CFRunLoopRun();
|
|
}
|
|
|
|
|
|
#pragma mark -
|
|
#pragma mark Task switching callbacks
|
|
|
|
- (void)mediaKeyAppListChanged
|
|
{
|
|
#ifdef DEBUG_SPMEDIAKEY_APPLIST
|
|
[self debugPrintAppList];
|
|
#endif
|
|
|
|
if([_mediaKeyAppList count] == 0)
|
|
return;
|
|
|
|
NSRunningApplication *thisApp = [NSRunningApplication currentApplication];
|
|
NSRunningApplication *otherApp = [_mediaKeyAppList firstObject];
|
|
|
|
BOOL isCurrent = [thisApp isEqual:otherApp];
|
|
|
|
[self setShouldInterceptMediaKeyEvents:isCurrent];
|
|
}
|
|
|
|
- (void)frontmostAppChanged:(NSNotification *)notification
|
|
{
|
|
NSRunningApplication *app = [notification.userInfo objectForKey:NSWorkspaceApplicationKey];
|
|
if (app.bundleIdentifier == nil)
|
|
return;
|
|
|
|
if (![[SPMediaKeyTap mediaKeyUserBundleIdentifiers] containsObject:app.bundleIdentifier])
|
|
return;
|
|
|
|
[_mediaKeyAppList removeObject:app];
|
|
[_mediaKeyAppList insertObject:app atIndex:0];
|
|
[self mediaKeyAppListChanged];
|
|
}
|
|
|
|
- (void)appTerminated:(NSNotification *)notification
|
|
{
|
|
NSRunningApplication *app = [notification.userInfo objectForKey:NSWorkspaceApplicationKey];
|
|
[_mediaKeyAppList removeObject:app];
|
|
|
|
[self mediaKeyAppListChanged];
|
|
}
|
|
|
|
#ifdef DEBUG_SPMEDIAKEY_APPLIST
|
|
- (void)debugPrintAppList
|
|
{
|
|
NSMutableString *list = [NSMutableString stringWithCapacity:255];
|
|
for (NSRunningApplication *app in _mediaKeyAppList) {
|
|
[list appendFormat:@" - %@\n", app.bundleIdentifier];
|
|
}
|
|
NSLog(@"List: \n%@", list);
|
|
}
|
|
#endif
|
|
|
|
@end
|