blob: cd595b0c017d6e36a5d94f7c99fe0a098a52b067 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/base/accelerators/media_keys_listener.h"
#include <ApplicationServices/ApplicationServices.h>
#include <Carbon/Carbon.h>
#import <Cocoa/Cocoa.h>
#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/hidsystem/ev_keymap.h>
#include "ui/base/accelerators/accelerator.h"
namespace ui {
namespace {
// 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;
ui::KeyboardCode MediaKeyCodeToKeyboardCode(int key_code) {
switch (key_code) {
case NX_KEYTYPE_PLAY:
return ui::VKEY_MEDIA_PLAY_PAUSE;
case NX_KEYTYPE_PREVIOUS:
case NX_KEYTYPE_REWIND:
return ui::VKEY_MEDIA_PREV_TRACK;
case NX_KEYTYPE_NEXT:
case NX_KEYTYPE_FAST:
return ui::VKEY_MEDIA_NEXT_TRACK;
}
return ui::VKEY_UNKNOWN;
}
class MediaKeysListenerImpl : public MediaKeysListener {
public:
MediaKeysListenerImpl(MediaKeysListener::Delegate* delegate, Scope scope);
~MediaKeysListenerImpl() override;
// MediaKeysListener:
void StartWatchingMediaKeys() override;
void StopWatchingMediaKeys() override;
bool IsWatchingMediaKeys() const override;
private:
// Callback on media key event.
MediaKeysHandleResult OnMediaKeyEvent(int media_key_code);
// The callback for when an event tap happens.
static CGEventRef EventTapCallback(CGEventTapProxy proxy,
CGEventType type,
CGEventRef event,
void* refcon);
MediaKeysListener::Delegate* delegate_;
const Scope scope_;
// Event tap for intercepting mac media keys.
CFMachPortRef event_tap_ = nullptr;
CFRunLoopSourceRef event_tap_source_ = nullptr;
DISALLOW_COPY_AND_ASSIGN(MediaKeysListenerImpl);
};
MediaKeysListenerImpl::MediaKeysListenerImpl(
MediaKeysListener::Delegate* delegate,
Scope scope)
: delegate_(delegate), scope_(scope) {
CHECK_NE(delegate_, nullptr);
}
MediaKeysListenerImpl::~MediaKeysListenerImpl() {
if (event_tap_) {
StopWatchingMediaKeys();
}
}
void MediaKeysListenerImpl::StartWatchingMediaKeys() {
// Make sure there's no existing event tap.
if (event_tap_) {
return;
}
DCHECK_EQ(event_tap_, nullptr);
DCHECK_EQ(event_tap_source_, nullptr);
// Add an event tap to intercept the system defined media key events.
event_tap_ = CGEventTapCreate(
kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault,
CGEventMaskBit(NX_SYSDEFINED), EventTapCallback, this);
if (event_tap_ == nullptr) {
LOG(ERROR) << "Error: failed to create event tap.";
return;
}
event_tap_source_ =
CFMachPortCreateRunLoopSource(kCFAllocatorSystemDefault, event_tap_, 0);
if (event_tap_source_ == nullptr) {
LOG(ERROR) << "Error: failed to create new run loop source.";
return;
}
CFRunLoopAddSource(CFRunLoopGetCurrent(), event_tap_source_,
kCFRunLoopCommonModes);
}
void MediaKeysListenerImpl::StopWatchingMediaKeys() {
if (!event_tap_) {
return;
}
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), event_tap_source_,
kCFRunLoopCommonModes);
// Ensure both event tap and source are initialized.
DCHECK_NE(event_tap_, nullptr);
DCHECK_NE(event_tap_source_, nullptr);
// Invalidate the event tap.
CFMachPortInvalidate(event_tap_);
CFRelease(event_tap_);
event_tap_ = nullptr;
// Release the event tap source.
CFRelease(event_tap_source_);
event_tap_source_ = nullptr;
}
bool MediaKeysListenerImpl::IsWatchingMediaKeys() const {
return event_tap_ != nullptr;
}
MediaKeysListener::MediaKeysHandleResult MediaKeysListenerImpl::OnMediaKeyEvent(
int media_key_code) {
const ui::KeyboardCode key_code = MediaKeyCodeToKeyboardCode(media_key_code);
// Create an accelerator corresponding to the keyCode.
const ui::Accelerator accelerator(key_code, 0);
return delegate_->OnMediaKeysAccelerator(accelerator);
}
// Processed events should propagate if they aren't handled by any listeners.
// For events that don't matter, this handler should return as quickly as
// possible.
// Returning event causes the event to propagate to other applications.
// Returning nullptr prevents the event from propagating.
// static
CGEventRef MediaKeysListenerImpl::EventTapCallback(CGEventTapProxy proxy,
CGEventType type,
CGEventRef event,
void* refcon) {
MediaKeysListenerImpl* shortcut_listener =
static_cast<MediaKeysListenerImpl*>(refcon);
const bool is_active = [NSApp isActive];
if (shortcut_listener->scope_ == Scope::kFocused && !is_active) {
return event;
}
// Handle the timeout case by re-enabling the tap.
if (type == kCGEventTapDisabledByTimeout) {
CGEventTapEnable(shortcut_listener->event_tap_, true);
return event;
}
// Convert the CGEvent to an NSEvent for access to the data1 field.
NSEvent* ns_event = [NSEvent eventWithCGEvent:event];
if (ns_event == nil) {
return event;
}
// Ignore events that are not system defined media keys.
if (type != NX_SYSDEFINED || [ns_event type] != NSSystemDefined ||
[ns_event subtype] != kSystemDefinedEventMediaKeysSubtype) {
return event;
}
NSInteger data1 = [ns_event data1];
// Ignore media keys that aren't previous, next and play/pause.
// Magical constants are from http://weblog.rogueamoeba.com/2007/09/29/
int key_code = (data1 & 0xFFFF0000) >> 16;
if (key_code != NX_KEYTYPE_PLAY && key_code != NX_KEYTYPE_NEXT &&
key_code != NX_KEYTYPE_PREVIOUS && key_code != NX_KEYTYPE_FAST &&
key_code != NX_KEYTYPE_REWIND) {
return event;
}
int key_flags = data1 & 0x0000FFFF;
bool is_key_pressed = ((key_flags & 0xFF00) >> 8) == 0xA;
// If the key wasn't pressed (eg. was released), ignore this event.
if (!is_key_pressed)
return event;
// Now we have a media key that we care about. Send it to the caller.
auto result = shortcut_listener->OnMediaKeyEvent(key_code);
// Prevent event from proagating to other apps if handled by Chrome.
if (result == MediaKeysHandleResult::kSuppressPropagation) {
return nullptr;
}
// By default, pass the event through.
return event;
}
} // namespace
std::unique_ptr<MediaKeysListener> MediaKeysListener::Create(
MediaKeysListener::Delegate* delegate,
MediaKeysListener::Scope scope) {
return std::make_unique<MediaKeysListenerImpl>(delegate, scope);
}
} // namespace ui