diff options
Diffstat (limited to 'widget/cocoa/MediaHardwareKeysEventSourceMac.mm')
-rw-r--r-- | widget/cocoa/MediaHardwareKeysEventSourceMac.mm | 183 |
1 files changed, 183 insertions, 0 deletions
diff --git a/widget/cocoa/MediaHardwareKeysEventSourceMac.mm b/widget/cocoa/MediaHardwareKeysEventSourceMac.mm new file mode 100644 index 0000000000..e8347f64c7 --- /dev/null +++ b/widget/cocoa/MediaHardwareKeysEventSourceMac.mm @@ -0,0 +1,183 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MediaHardwareKeysEventSourceMac.h" + +#import <AppKit/AppKit.h> +#import <AppKit/NSEvent.h> +#import <IOKit/hidsystem/ev_keymap.h> + +#include "mozilla/dom/MediaControlUtils.h" + +using namespace mozilla::dom; + +// avoid redefined macro in unified build +#undef LOG +#define LOG(msg, ...) \ + MOZ_LOG(gMediaControlLog, LogLevel::Debug, \ + ("MediaHardwareKeysEventSourceMac=%p, " msg, this, ##__VA_ARGS__)) + +// This macro is used in static callback function, where we have to send `this` +// explicitly. +#define LOG2(msg, this, ...) \ + MOZ_LOG(gMediaControlLog, LogLevel::Debug, \ + ("MediaHardwareKeysEventSourceMac=%p, " msg, this, ##__VA_ARGS__)) + +static const char* ToMediaControlKeyStr(int aKeyCode) { + switch (aKeyCode) { + case NX_KEYTYPE_PLAY: + return "Play"; + case NX_KEYTYPE_NEXT: + return "Next"; + case NX_KEYTYPE_PREVIOUS: + return "Previous"; + case NX_KEYTYPE_FAST: + return "Fast"; + case NX_KEYTYPE_REWIND: + return "Rewind"; + default: + MOZ_ASSERT_UNREACHABLE("Invalid key code."); + return "UNKNOWN"; + } +} + +// The media keys subtype. No official docs found, but widely known. +// http://lists.apple.com/archives/cocoa-dev/2007/Aug/msg00499.html +const int kSystemDefinedEventMediaKeysSubtype = 8; + +static bool IsSupportedKeyCode(int aKeyCode) { + return aKeyCode == NX_KEYTYPE_PLAY || aKeyCode == NX_KEYTYPE_NEXT || + aKeyCode == NX_KEYTYPE_FAST || aKeyCode == NX_KEYTYPE_PREVIOUS || + aKeyCode == NX_KEYTYPE_REWIND; +} + +static MediaControlKey ToMediaControlKey(int aKeyCode) { + MOZ_ASSERT(IsSupportedKeyCode(aKeyCode)); + switch (aKeyCode) { + case NX_KEYTYPE_NEXT: + case NX_KEYTYPE_FAST: + return MediaControlKey::Nexttrack; + case NX_KEYTYPE_PREVIOUS: + case NX_KEYTYPE_REWIND: + return MediaControlKey::Previoustrack; + default: + MOZ_ASSERT(aKeyCode == NX_KEYTYPE_PLAY); + return MediaControlKey::Playpause; + } +} + +namespace mozilla { +namespace widget { + +bool MediaHardwareKeysEventSourceMac::IsOpened() const { return mEventTap && mEventTapSource; } + +bool MediaHardwareKeysEventSourceMac::Open() { + LOG("Open MediaHardwareKeysEventSourceMac"); + return StartEventTap(); +} + +void MediaHardwareKeysEventSourceMac::Close() { + LOG("Close MediaHardwareKeysEventSourceMac"); + StopEventTap(); + MediaControlKeySource::Close(); +} + +bool MediaHardwareKeysEventSourceMac::StartEventTap() { + LOG("StartEventTap"); + MOZ_ASSERT(!mEventTap); + MOZ_ASSERT(!mEventTapSource); + + // Add an event tap to intercept the system defined media key events. + mEventTap = + CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionListenOnly, + CGEventMaskBit(NX_SYSDEFINED), EventTapCallback, this); + if (!mEventTap) { + LOG("Fail to create event tap"); + return false; + } + + mEventTapSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, mEventTap, 0); + if (!mEventTapSource) { + LOG("Fail to create an event tap source."); + return false; + } + + LOG("Add an event tap source to current loop"); + CFRunLoopAddSource(CFRunLoopGetCurrent(), mEventTapSource, kCFRunLoopCommonModes); + return true; +} + +void MediaHardwareKeysEventSourceMac::StopEventTap() { + LOG("StopEventTapIfNecessary"); + if (mEventTap) { + CFMachPortInvalidate(mEventTap); + mEventTap = nullptr; + } + if (mEventTapSource) { + CFRunLoopRemoveSource(CFRunLoopGetCurrent(), mEventTapSource, kCFRunLoopCommonModes); + CFRelease(mEventTapSource); + mEventTapSource = nullptr; + } +} + +CGEventRef MediaHardwareKeysEventSourceMac::EventTapCallback(CGEventTapProxy proxy, + CGEventType type, CGEventRef event, + void* refcon) { + // Re-enable event tap when receiving disabled events. + MediaHardwareKeysEventSourceMac* source = static_cast<MediaHardwareKeysEventSourceMac*>(refcon); + if (type == kCGEventTapDisabledByUserInput || type == kCGEventTapDisabledByTimeout) { + MOZ_ASSERT(source->mEventTap); + CGEventTapEnable(source->mEventTap, true); + return event; + } + + NSEvent* nsEvent = [NSEvent eventWithCGEvent:event]; + if (nsEvent == nil) { + return event; + } + + // Ignore not system defined media keys event. + if ([nsEvent type] != NSSystemDefined || + [nsEvent subtype] != kSystemDefinedEventMediaKeysSubtype) { + return event; + } + + // Ignore media keys that aren't previous, next and play/pause. + // Magical constants are from http://weblog.rogueamoeba.com/2007/09/29/ + // - keyCode = (([event data1] & 0xFFFF0000) >> 16) + // - keyFlags = ([event data1] & 0x0000FFFF) + // - keyState = (((keyFlags & 0xFF00) >> 8)) == 0xA + // - keyRepeat = (keyFlags & 0x1); + const NSInteger data1 = [nsEvent data1]; + int keyCode = (data1 & 0xFFFF0000) >> 16; + if (keyCode != NX_KEYTYPE_PLAY && keyCode != NX_KEYTYPE_NEXT && keyCode != NX_KEYTYPE_PREVIOUS && + keyCode != NX_KEYTYPE_FAST && keyCode != NX_KEYTYPE_REWIND) { + return event; + } + + // Ignore non-key pressed event (eg. key released). + int keyFlags = data1 & 0x0000FFFF; + bool isKeyPressed = ((keyFlags & 0xFF00) >> 8) == 0xA; + if (!isKeyPressed) { + return event; + } + + // There is no listener waiting to process event. + if (source->mListeners.IsEmpty()) { + return event; + } + + if (!IsSupportedKeyCode(keyCode)) { + return event; + } + + LOG2("Get media key %s", source, ToMediaControlKeyStr(keyCode)); + for (auto iter = source->mListeners.begin(); iter != source->mListeners.end(); ++iter) { + (*iter)->OnActionPerformed(MediaControlAction(ToMediaControlKey(keyCode))); + } + return event; +} + +} // namespace widget +} // namespace mozilla |