mac: Implement global keyboard shortcut API.
This commit is contained in:
parent
476f545a67
commit
4b3bd9c3cc
9 changed files with 922 additions and 0 deletions
7
atom.gyp
7
atom.gyp
|
@ -24,6 +24,7 @@
|
|||
'atom/browser/api/lib/menu-item.coffee',
|
||||
'atom/browser/api/lib/power-monitor.coffee',
|
||||
'atom/browser/api/lib/protocol.coffee',
|
||||
'atom/browser/api/lib/shortcut.coffee',
|
||||
'atom/browser/api/lib/tray.coffee',
|
||||
'atom/browser/api/lib/web-contents.coffee',
|
||||
'atom/browser/lib/init.coffee',
|
||||
|
@ -62,6 +63,8 @@
|
|||
'atom/browser/api/atom_api_power_monitor.h',
|
||||
'atom/browser/api/atom_api_protocol.cc',
|
||||
'atom/browser/api/atom_api_protocol.h',
|
||||
'atom/browser/api/atom_api_shortcut.cc',
|
||||
'atom/browser/api/atom_api_shortcut.h',
|
||||
'atom/browser/api/atom_api_tray.cc',
|
||||
'atom/browser/api/atom_api_tray.h',
|
||||
'atom/browser/api/atom_api_web_contents.cc',
|
||||
|
@ -231,6 +234,10 @@
|
|||
'chromium_src/chrome/browser/ui/views/frame/global_menu_bar_registrar_x11.h',
|
||||
'chromium_src/chrome/browser/ui/views/status_icons/status_tray_state_changer_win.cc',
|
||||
'chromium_src/chrome/browser/ui/views/status_icons/status_tray_state_changer_win.h',
|
||||
'chromium_src/chrome/browser/ui/shortcut/global_shortcut_listener.cc',
|
||||
'chromium_src/chrome/browser/ui/shortcut/global_shortcut_listener.h',
|
||||
'chromium_src/chrome/browser/ui/shortcut/global_shortcut_listener_mac.mm',
|
||||
'chromium_src/chrome/browser/ui/shortcut/global_shortcut_listener_mac.h',
|
||||
'<@(native_mate_files)',
|
||||
],
|
||||
'framework_sources': [
|
||||
|
|
107
atom/browser/api/atom_api_shortcut.cc
Normal file
107
atom/browser/api/atom_api_shortcut.cc
Normal file
|
@ -0,0 +1,107 @@
|
|||
// Copyright (c) 2014 GitHub, Inc. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "atom/browser/api/atom_api_shortcut.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "atom/browser/ui/accelerator_util.h"
|
||||
#include "base/values.h"
|
||||
#include "native_mate/constructor.h"
|
||||
#include "native_mate/dictionary.h"
|
||||
|
||||
#include "atom/common/node_includes.h"
|
||||
|
||||
namespace atom {
|
||||
|
||||
namespace api {
|
||||
|
||||
Shortcut::Shortcut(const std::string& key) {
|
||||
is_key_valid_ = accelerator_util::StringToAccelerator(key, &accelerator_);
|
||||
}
|
||||
|
||||
Shortcut::~Shortcut() {
|
||||
Unregister();
|
||||
}
|
||||
|
||||
// static
|
||||
mate::Wrappable* Shortcut::Create(const std::string& key) {
|
||||
return new Shortcut(key);
|
||||
}
|
||||
|
||||
void Shortcut::OnActive() {
|
||||
Emit("active");
|
||||
}
|
||||
|
||||
void Shortcut::OnFailed(const std::string& error_msg) {
|
||||
base::ListValue args;
|
||||
args.AppendString(error_msg);
|
||||
Emit("failed", args);
|
||||
}
|
||||
|
||||
void Shortcut::SetKey(const std::string& key) {
|
||||
// We need to unregister the previous key before set new key eachtime.
|
||||
Unregister();
|
||||
is_key_valid_ = accelerator_util::StringToAccelerator(key, &accelerator_);
|
||||
}
|
||||
|
||||
void Shortcut::OnKeyPressed(const ui::Accelerator& accelerator) {
|
||||
if (accelerator != accelerator_) {
|
||||
// This should never occur, because if it does, GlobalShortcutListener
|
||||
// notifes us with wrong accelerator.
|
||||
NOTREACHED();
|
||||
return;
|
||||
}
|
||||
|
||||
OnActive();
|
||||
}
|
||||
|
||||
void Shortcut::Register() {
|
||||
if (!is_key_valid_) {
|
||||
OnFailed("Shortcut is invalid.");
|
||||
return;
|
||||
}
|
||||
GlobalShortcutListener::GetInstance()->RegisterAccelerator(
|
||||
accelerator_, this);
|
||||
}
|
||||
|
||||
void Shortcut::Unregister() {
|
||||
GlobalShortcutListener::GetInstance()->UnregisterAccelerator(accelerator_, this);
|
||||
}
|
||||
|
||||
bool Shortcut::IsRegistered() {
|
||||
return GlobalShortcutListener::GetInstance()->IsAcceleratorRegistered(accelerator_);
|
||||
}
|
||||
|
||||
// static
|
||||
void Shortcut::BuildPrototype(v8::Isolate* isolate,
|
||||
v8::Handle<v8::ObjectTemplate> prototype) {
|
||||
mate::ObjectTemplateBuilder(isolate, prototype)
|
||||
.SetMethod("setKey", &Shortcut::SetKey)
|
||||
.SetMethod("register", &Shortcut::Register)
|
||||
.SetMethod("unregister", &Shortcut::Unregister)
|
||||
.SetMethod("isRegistered", &Shortcut::IsRegistered);
|
||||
}
|
||||
|
||||
} // namespace api
|
||||
|
||||
} // namespace atom
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
void Initialize(v8::Handle<v8::Object> exports, v8::Handle<v8::Value> unused,
|
||||
v8::Handle<v8::Context> context, void* priv) {
|
||||
using atom::api::Shortcut;
|
||||
v8::Isolate* isolate = context->GetIsolate();
|
||||
v8::Handle<v8::Function> constructor = mate::CreateConstructor<Shortcut>(
|
||||
isolate, "Shortcut", base::Bind(&Shortcut::Create));
|
||||
mate::Dictionary dict(isolate, exports);
|
||||
dict.Set("Shortcut", static_cast<v8::Handle<v8::Value>>(constructor));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NODE_MODULE_CONTEXT_AWARE_BUILTIN(atom_browser_shortcut, Initialize)
|
67
atom/browser/api/atom_api_shortcut.h
Normal file
67
atom/browser/api/atom_api_shortcut.h
Normal file
|
@ -0,0 +1,67 @@
|
|||
// Copyright (c) 2014 GitHub, Inc. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef ATOM_BROWSER_API_ATOM_API_SHORTCUT_H_
|
||||
#define ATOM_BROWSER_API_ATOM_API_SHORTCUT_H_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "atom/browser/api/event_emitter.h"
|
||||
#include "chrome/browser/ui/shortcut/global_shortcut_listener.h"
|
||||
#include "ui/base/accelerators/accelerator.h"
|
||||
|
||||
namespace mate {
|
||||
|
||||
class Dictionary;
|
||||
|
||||
}
|
||||
|
||||
namespace atom {
|
||||
|
||||
class Shortcut;
|
||||
|
||||
namespace api {
|
||||
|
||||
class Menu;
|
||||
|
||||
class Shortcut : public mate::EventEmitter,
|
||||
public GlobalShortcutListener::Observer {
|
||||
public:
|
||||
static mate::Wrappable* Create(const std::string& key);
|
||||
|
||||
static void BuildPrototype(v8::Isolate* isolate,
|
||||
v8::Handle<v8::ObjectTemplate> prototype);
|
||||
|
||||
protected:
|
||||
explicit Shortcut(const std::string& key);
|
||||
virtual ~Shortcut();
|
||||
|
||||
const ui::Accelerator& GetAccelerator() const {
|
||||
return accelerator_;
|
||||
}
|
||||
|
||||
void OnActive() ;
|
||||
void OnFailed(const std::string& error_msg) ;
|
||||
|
||||
// GlobalShortcutListener::Observer implementation.
|
||||
virtual void OnKeyPressed(const ui::Accelerator& accelerator) OVERRIDE;
|
||||
|
||||
void SetKey(const std::string& key);
|
||||
void Register();
|
||||
void Unregister();
|
||||
bool IsRegistered();
|
||||
|
||||
private:
|
||||
bool is_key_valid_;
|
||||
ui::Accelerator accelerator_;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(Shortcut);
|
||||
};
|
||||
|
||||
} // namespace api
|
||||
|
||||
} // namespace atom
|
||||
|
||||
#endif // ATOM_BROWSER_API_ATOM_API_SHORTCUT_H_
|
7
atom/browser/api/lib/shortcut.coffee
Normal file
7
atom/browser/api/lib/shortcut.coffee
Normal file
|
@ -0,0 +1,7 @@
|
|||
EventEmitter = require('events').EventEmitter
|
||||
bindings = process.atomBinding 'shortcut'
|
||||
|
||||
Shortcut = bindings.Shortcut
|
||||
Shortcut::__proto__ = EventEmitter.prototype
|
||||
|
||||
module.exports = Shortcut
|
|
@ -65,6 +65,7 @@ REFERENCE_MODULE(atom_browser_dialog);
|
|||
REFERENCE_MODULE(atom_browser_menu);
|
||||
REFERENCE_MODULE(atom_browser_power_monitor);
|
||||
REFERENCE_MODULE(atom_browser_protocol);
|
||||
REFERENCE_MODULE(atom_browser_shortcut);
|
||||
REFERENCE_MODULE(atom_browser_tray);
|
||||
REFERENCE_MODULE(atom_browser_window);
|
||||
REFERENCE_MODULE(atom_common_clipboard);
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
// Copyright (c) 2014 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 "base/logging.h"
|
||||
#include "chrome/browser/ui/shortcut/global_shortcut_listener.h"
|
||||
#include "content/public/browser/browser_thread.h"
|
||||
#include "ui/base/accelerators/accelerator.h"
|
||||
|
||||
using content::BrowserThread;
|
||||
|
||||
namespace atom {
|
||||
|
||||
namespace api {
|
||||
|
||||
GlobalShortcutListener::GlobalShortcutListener()
|
||||
: shortcut_handling_suspended_(false) {
|
||||
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
|
||||
}
|
||||
|
||||
GlobalShortcutListener::~GlobalShortcutListener() {
|
||||
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
|
||||
}
|
||||
|
||||
bool GlobalShortcutListener::RegisterAccelerator(
|
||||
const ui::Accelerator& accelerator, Observer* observer) {
|
||||
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
|
||||
if (IsShortcutHandlingSuspended())
|
||||
return false;
|
||||
|
||||
AcceleratorMap::const_iterator it = accelerator_map_.find(accelerator);
|
||||
if (it != accelerator_map_.end()) {
|
||||
// The accelerator has been registered.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!RegisterAcceleratorImpl(accelerator)) {
|
||||
// If the platform-specific registration fails, mostly likely the shortcut
|
||||
// has been registered by other native applications.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (accelerator_map_.empty())
|
||||
StartListening();
|
||||
|
||||
accelerator_map_[accelerator] = observer;
|
||||
return true;
|
||||
}
|
||||
|
||||
void GlobalShortcutListener::UnregisterAccelerator(
|
||||
const ui::Accelerator& accelerator, Observer* observer) {
|
||||
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
|
||||
if (IsShortcutHandlingSuspended())
|
||||
return;
|
||||
|
||||
AcceleratorMap::iterator it = accelerator_map_.find(accelerator);
|
||||
if (it == accelerator_map_.end())
|
||||
return;
|
||||
// The caller should call this function with the right observer.
|
||||
DCHECK(it->second == observer);
|
||||
|
||||
UnregisterAcceleratorImpl(accelerator);
|
||||
accelerator_map_.erase(it);
|
||||
if (accelerator_map_.empty())
|
||||
StopListening();
|
||||
}
|
||||
|
||||
void GlobalShortcutListener::UnregisterAccelerators(Observer* observer) {
|
||||
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
|
||||
if (IsShortcutHandlingSuspended())
|
||||
return;
|
||||
|
||||
AcceleratorMap::iterator it = accelerator_map_.begin();
|
||||
while (it != accelerator_map_.end()) {
|
||||
if (it->second == observer) {
|
||||
AcceleratorMap::iterator to_remove = it++;
|
||||
UnregisterAccelerator(to_remove->first, observer);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GlobalShortcutListener::SetShortcutHandlingSuspended(bool suspended) {
|
||||
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
|
||||
if (shortcut_handling_suspended_ == suspended)
|
||||
return;
|
||||
|
||||
shortcut_handling_suspended_ = suspended;
|
||||
for (AcceleratorMap::iterator it = accelerator_map_.begin();
|
||||
it != accelerator_map_.end();
|
||||
++it) {
|
||||
// On Linux, when shortcut handling is suspended we cannot simply early
|
||||
// return in NotifyKeyPressed (similar to what we do for non-global
|
||||
// shortcuts) because we'd eat the keyboard event thereby preventing the
|
||||
// user from setting the shortcut. Therefore we must unregister while
|
||||
// handling is suspended and register when handling resumes.
|
||||
if (shortcut_handling_suspended_)
|
||||
UnregisterAcceleratorImpl(it->first);
|
||||
else
|
||||
RegisterAcceleratorImpl(it->first);
|
||||
}
|
||||
}
|
||||
|
||||
bool GlobalShortcutListener::IsShortcutHandlingSuspended() const {
|
||||
return shortcut_handling_suspended_;
|
||||
}
|
||||
|
||||
void GlobalShortcutListener::NotifyKeyPressed(
|
||||
const ui::Accelerator& accelerator) {
|
||||
AcceleratorMap::iterator iter = accelerator_map_.find(accelerator);
|
||||
if (iter == accelerator_map_.end()) {
|
||||
// This should never occur, because if it does, we have failed to unregister
|
||||
// or failed to clean up the map after unregistering the shortcut.
|
||||
NOTREACHED();
|
||||
return; // No-one is listening to this key.
|
||||
}
|
||||
|
||||
iter->second->OnKeyPressed(accelerator);
|
||||
}
|
||||
|
||||
} // namespace api
|
||||
|
||||
} // namespace atom
|
|
@ -0,0 +1,105 @@
|
|||
// Copyright (c) 2014 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.
|
||||
|
||||
#ifndef CHROME_BROWSER_UI_SHORTCUT_GLOBAL_SHORTCUT_LISTENER_H_
|
||||
#define CHROME_BROWSER_UI_SHORTCUT_GLOBAL_SHORTCUT_LISTENER_H_
|
||||
|
||||
#include <map>
|
||||
|
||||
#include "base/basictypes.h"
|
||||
#include "ui/events/keycodes/keyboard_codes.h"
|
||||
|
||||
namespace ui {
|
||||
class Accelerator;
|
||||
}
|
||||
|
||||
namespace atom {
|
||||
|
||||
namespace api {
|
||||
|
||||
// Platform-neutral implementation of a class that keeps track of observers and
|
||||
// monitors keystrokes. It relays messages to the appropriate observer when a
|
||||
// global shortcut has been struck by the user.
|
||||
class GlobalShortcutListener {
|
||||
public:
|
||||
class Observer {
|
||||
public:
|
||||
// Called when your global shortcut (|accelerator|) is struck.
|
||||
virtual void OnKeyPressed(const ui::Accelerator& accelerator) = 0;
|
||||
};
|
||||
|
||||
virtual ~GlobalShortcutListener();
|
||||
|
||||
static GlobalShortcutListener* GetInstance();
|
||||
|
||||
// Register an observer for when a certain |accelerator| is struck. Returns
|
||||
// true if register successfully, or false if 1) the specificied |accelerator|
|
||||
// has been registered by another caller or other native applications, or
|
||||
// 2) shortcut handling is suspended.
|
||||
//
|
||||
// Note that we do not support recognizing that an accelerator has been
|
||||
// registered by another application on all platforms. This is a per-platform
|
||||
// consideration.
|
||||
bool RegisterAccelerator(const ui::Accelerator& accelerator,
|
||||
Observer* observer);
|
||||
|
||||
// Stop listening for the given |accelerator|, does nothing if shortcut
|
||||
// handling is suspended.
|
||||
void UnregisterAccelerator(const ui::Accelerator& accelerator,
|
||||
Observer* observer);
|
||||
|
||||
// Stop listening for all accelerators of the given |observer|, does nothing
|
||||
// if shortcut handling is suspended.
|
||||
void UnregisterAccelerators(Observer* observer);
|
||||
|
||||
// Suspend/Resume global shortcut handling. Note that when suspending,
|
||||
// RegisterAccelerator/UnregisterAccelerator/UnregisterAccelerators are not
|
||||
// allowed to be called until shortcut handling has been resumed.
|
||||
void SetShortcutHandlingSuspended(bool suspended);
|
||||
|
||||
// Returns whether shortcut handling is currently suspended.
|
||||
bool IsShortcutHandlingSuspended() const;
|
||||
|
||||
// Returen whether accelerator is registered.
|
||||
virtual bool IsAcceleratorRegistered(const ui::Accelerator& accelerator) = 0;
|
||||
|
||||
protected:
|
||||
GlobalShortcutListener();
|
||||
|
||||
// Called by platform specific implementations of this class whenever a key
|
||||
// is struck. Only called for keys that have an observer registered.
|
||||
void NotifyKeyPressed(const ui::Accelerator& accelerator);
|
||||
|
||||
private:
|
||||
// The following methods are implemented by platform-specific implementations
|
||||
// of this class.
|
||||
//
|
||||
// Start/StopListening are called when transitioning between zero and nonzero
|
||||
// registered accelerators. StartListening will be called after
|
||||
// RegisterAcceleratorImpl and StopListening will be called after
|
||||
// UnregisterAcceleratorImpl.
|
||||
//
|
||||
// For RegisterAcceleratorImpl, implementations return false if registration
|
||||
// did not complete successfully.
|
||||
virtual void StartListening() = 0;
|
||||
virtual void StopListening() = 0;
|
||||
virtual bool RegisterAcceleratorImpl(const ui::Accelerator& accelerator) = 0;
|
||||
virtual void UnregisterAcceleratorImpl(
|
||||
const ui::Accelerator& accelerator) = 0;
|
||||
|
||||
// The map of accelerators that have been successfully registered as global
|
||||
// shortcuts and their observer.
|
||||
typedef std::map<ui::Accelerator, Observer*> AcceleratorMap;
|
||||
AcceleratorMap accelerator_map_;
|
||||
|
||||
// Keeps track of whether shortcut handling is currently suspended.
|
||||
bool shortcut_handling_suspended_;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(GlobalShortcutListener);
|
||||
};
|
||||
|
||||
} // namespace api
|
||||
|
||||
} // namespace atom
|
||||
#endif // CHROME_BROWSER_UI_SHORTCUT_GLOBAL_SHORTCUT_LISTENER_H_
|
|
@ -0,0 +1,111 @@
|
|||
// Copyright (c) 2014 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.
|
||||
|
||||
#ifndef CHROME_BROWSER_UI_SHORTCUT_GLOBAL_SHORTCUT_LISTENER_MAC_H_
|
||||
#define CHROME_BROWSER_UI_SHORTCUT_GLOBAL_SHORTCUT_LISTENER_MAC_H_
|
||||
|
||||
#include "chrome/browser/ui/shortcut/global_shortcut_listener.h"
|
||||
|
||||
#include <Carbon/Carbon.h>
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
|
||||
#include <map>
|
||||
|
||||
#include "base/mac/scoped_nsobject.h"
|
||||
|
||||
namespace atom {
|
||||
|
||||
namespace api {
|
||||
|
||||
// Mac-specific implementation of the GlobalShortcutListener class that
|
||||
// listens for global shortcuts. Handles basic keyboard intercepting and
|
||||
// forwards its output to the base class for processing.
|
||||
//
|
||||
// This class does two things:
|
||||
// 1. Intercepts media keys. Uses an event tap for intercepting media keys
|
||||
// (PlayPause, NextTrack, PreviousTrack).
|
||||
// 2. Binds keyboard shortcuts (hot keys). Carbon RegisterEventHotKey API for
|
||||
// binding to non-media key global hot keys (eg. Command-Shift-1).
|
||||
class GlobalShortcutListenerMac : public GlobalShortcutListener {
|
||||
public:
|
||||
GlobalShortcutListenerMac();
|
||||
virtual ~GlobalShortcutListenerMac();
|
||||
|
||||
virtual bool IsAcceleratorRegistered(const ui::Accelerator& accelerator) OVERRIDE;
|
||||
private:
|
||||
typedef int KeyId;
|
||||
typedef std::map<ui::Accelerator, KeyId> AcceleratorIdMap;
|
||||
typedef std::map<KeyId, ui::Accelerator> IdAcceleratorMap;
|
||||
typedef std::map<KeyId, EventHotKeyRef> IdHotKeyRefMap;
|
||||
|
||||
// Keyboard event callbacks.
|
||||
void OnHotKeyEvent(EventHotKeyID hot_key_id);
|
||||
bool OnMediaKeyEvent(int key_code);
|
||||
|
||||
// GlobalShortcutListener implementation.
|
||||
virtual void StartListening() OVERRIDE;
|
||||
virtual void StopListening() OVERRIDE;
|
||||
virtual bool RegisterAcceleratorImpl(
|
||||
const ui::Accelerator& accelerator) OVERRIDE;
|
||||
virtual void UnregisterAcceleratorImpl(
|
||||
const ui::Accelerator& accelerator) OVERRIDE;
|
||||
|
||||
// Mac-specific functions for registering hot keys with modifiers.
|
||||
bool RegisterHotKey(const ui::Accelerator& accelerator, KeyId hot_key_id);
|
||||
void UnregisterHotKey(const ui::Accelerator& accelerator);
|
||||
|
||||
// Enable and disable the media key event tap.
|
||||
void StartWatchingMediaKeys();
|
||||
void StopWatchingMediaKeys();
|
||||
|
||||
// Enable and disable the hot key event handler.
|
||||
void StartWatchingHotKeys();
|
||||
void StopWatchingHotKeys();
|
||||
|
||||
// Whether or not any media keys are currently registered.
|
||||
bool IsAnyMediaKeyRegistered();
|
||||
|
||||
// Whether or not any hot keys are currently registered.
|
||||
bool IsAnyHotKeyRegistered();
|
||||
|
||||
// The callback for when an event tap happens.
|
||||
static CGEventRef EventTapCallback(
|
||||
CGEventTapProxy proxy, CGEventType type, CGEventRef event, void* refcon);
|
||||
|
||||
// The callback for when a hot key event happens.
|
||||
static OSStatus HotKeyHandler(
|
||||
EventHandlerCallRef next_handler, EventRef event, void* user_data);
|
||||
|
||||
// Whether this object is listening for global shortcuts.
|
||||
bool is_listening_;
|
||||
|
||||
// The hotkey identifier for the next global shortcut that is added.
|
||||
KeyId hot_key_id_;
|
||||
|
||||
// A map of all hotkeys (media keys and shortcuts) mapping to their
|
||||
// corresponding hotkey IDs. For quickly finding if an accelerator is
|
||||
// registered.
|
||||
AcceleratorIdMap accelerator_ids_;
|
||||
|
||||
// The inverse map for quickly looking up accelerators by hotkey id.
|
||||
IdAcceleratorMap id_accelerators_;
|
||||
|
||||
// Keyboard shortcut IDs to hotkeys map for unregistration.
|
||||
IdHotKeyRefMap id_hot_key_refs_;
|
||||
|
||||
// Event tap for intercepting mac media keys.
|
||||
CFMachPortRef event_tap_;
|
||||
CFRunLoopSourceRef event_tap_source_;
|
||||
|
||||
// Event handler for keyboard shortcut hot keys.
|
||||
EventHandlerRef event_handler_;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(GlobalShortcutListenerMac);
|
||||
};
|
||||
|
||||
} // namespace api
|
||||
|
||||
} // namespace atom
|
||||
|
||||
#endif // CHROME_BROWSER_UI_SHORTCUT_GLOBAL_SHORTCUT_LISTENER_MAC_H_
|
|
@ -0,0 +1,392 @@
|
|||
// Copyright (c) 2014 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 "chrome/browser/ui/shortcut/global_shortcut_listener_mac.h"
|
||||
|
||||
#include <ApplicationServices/ApplicationServices.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#include <IOKit/hidsystem/ev_keymap.h>
|
||||
|
||||
#import "base/mac/foundation_util.h"
|
||||
#include "content/public/browser/browser_thread.h"
|
||||
#include "ui/base/accelerators/accelerator.h"
|
||||
#include "ui/events/event.h"
|
||||
#import "ui/events/keycodes/keyboard_code_conversion_mac.h"
|
||||
|
||||
using content::BrowserThread;
|
||||
using atom::api::GlobalShortcutListenerMac;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
bool IsMediaKey(const ui::Accelerator& accelerator) {
|
||||
if (accelerator.modifiers() != 0)
|
||||
return false;
|
||||
return (accelerator.key_code() == ui::VKEY_MEDIA_NEXT_TRACK ||
|
||||
accelerator.key_code() == ui::VKEY_MEDIA_PREV_TRACK ||
|
||||
accelerator.key_code() == ui::VKEY_MEDIA_PLAY_PAUSE ||
|
||||
accelerator.key_code() == ui::VKEY_MEDIA_STOP);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace atom {
|
||||
|
||||
namespace api {
|
||||
|
||||
// static
|
||||
GlobalShortcutListener* GlobalShortcutListener::GetInstance() {
|
||||
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
|
||||
static GlobalShortcutListenerMac* instance =
|
||||
new GlobalShortcutListenerMac();
|
||||
return instance;
|
||||
}
|
||||
|
||||
GlobalShortcutListenerMac::GlobalShortcutListenerMac()
|
||||
: is_listening_(false),
|
||||
hot_key_id_(0),
|
||||
event_tap_(NULL),
|
||||
event_tap_source_(NULL),
|
||||
event_handler_(NULL) {
|
||||
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
|
||||
}
|
||||
|
||||
GlobalShortcutListenerMac::~GlobalShortcutListenerMac() {
|
||||
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
|
||||
|
||||
// By this point, UnregisterAccelerator should have been called for all
|
||||
// keyboard shortcuts. Still we should clean up.
|
||||
if (is_listening_)
|
||||
StopListening();
|
||||
|
||||
// If keys are still registered, make sure we stop the tap. Again, this
|
||||
// should never happen.
|
||||
if (IsAnyMediaKeyRegistered())
|
||||
StopWatchingMediaKeys();
|
||||
|
||||
if (IsAnyHotKeyRegistered())
|
||||
StopWatchingHotKeys();
|
||||
}
|
||||
|
||||
bool GlobalShortcutListenerMac::IsAcceleratorRegistered(
|
||||
const ui::Accelerator& accelerator) {
|
||||
return accelerator_ids_.find(accelerator) != accelerator_ids_.end();
|
||||
}
|
||||
|
||||
void GlobalShortcutListenerMac::StartListening() {
|
||||
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
|
||||
|
||||
DCHECK(!accelerator_ids_.empty());
|
||||
DCHECK(!id_accelerators_.empty());
|
||||
DCHECK(!is_listening_);
|
||||
|
||||
is_listening_ = true;
|
||||
}
|
||||
|
||||
void GlobalShortcutListenerMac::StopListening() {
|
||||
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
|
||||
|
||||
DCHECK(accelerator_ids_.empty()); // Make sure the set is clean.
|
||||
DCHECK(id_accelerators_.empty());
|
||||
DCHECK(is_listening_);
|
||||
|
||||
is_listening_ = false;
|
||||
}
|
||||
|
||||
void GlobalShortcutListenerMac::OnHotKeyEvent(EventHotKeyID hot_key_id) {
|
||||
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
|
||||
|
||||
// This hot key should be registered.
|
||||
DCHECK(id_accelerators_.find(hot_key_id.id) != id_accelerators_.end());
|
||||
// Look up the accelerator based on this hot key ID.
|
||||
const ui::Accelerator& accelerator = id_accelerators_[hot_key_id.id];
|
||||
NotifyKeyPressed(accelerator);
|
||||
}
|
||||
|
||||
bool GlobalShortcutListenerMac::OnMediaKeyEvent(int media_key_code) {
|
||||
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
|
||||
ui::KeyboardCode key_code = MediaKeyCodeToKeyboardCode(media_key_code);
|
||||
// Create an accelerator corresponding to the keyCode.
|
||||
ui::Accelerator accelerator(key_code, 0);
|
||||
// Look for a match with a bound hot_key.
|
||||
if (accelerator_ids_.find(accelerator) != accelerator_ids_.end()) {
|
||||
// If matched, callback to the event handling system.
|
||||
NotifyKeyPressed(accelerator);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GlobalShortcutListenerMac::RegisterAcceleratorImpl(
|
||||
const ui::Accelerator& accelerator) {
|
||||
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
|
||||
DCHECK(accelerator_ids_.find(accelerator) == accelerator_ids_.end());
|
||||
|
||||
if (IsMediaKey(accelerator)) {
|
||||
if (!IsAnyMediaKeyRegistered()) {
|
||||
// If this is the first media key registered, start the event tap.
|
||||
StartWatchingMediaKeys();
|
||||
}
|
||||
} else {
|
||||
// Register hot_key if they are non-media keyboard shortcuts.
|
||||
if (!RegisterHotKey(accelerator, hot_key_id_))
|
||||
return false;
|
||||
|
||||
if (!IsAnyHotKeyRegistered()) {
|
||||
StartWatchingHotKeys();
|
||||
}
|
||||
}
|
||||
|
||||
// Store the hotkey-ID mappings we will need for lookup later.
|
||||
id_accelerators_[hot_key_id_] = accelerator;
|
||||
accelerator_ids_[accelerator] = hot_key_id_;
|
||||
++hot_key_id_;
|
||||
return true;
|
||||
}
|
||||
|
||||
void GlobalShortcutListenerMac::UnregisterAcceleratorImpl(
|
||||
const ui::Accelerator& accelerator) {
|
||||
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
|
||||
DCHECK(accelerator_ids_.find(accelerator) != accelerator_ids_.end());
|
||||
|
||||
// Unregister the hot_key if it's a keyboard shortcut.
|
||||
if (!IsMediaKey(accelerator))
|
||||
UnregisterHotKey(accelerator);
|
||||
|
||||
// Remove hot_key from the mappings.
|
||||
KeyId key_id = accelerator_ids_[accelerator];
|
||||
id_accelerators_.erase(key_id);
|
||||
accelerator_ids_.erase(accelerator);
|
||||
|
||||
if (IsMediaKey(accelerator)) {
|
||||
// If we unregistered a media key, and now no media keys are registered,
|
||||
// stop the media key tap.
|
||||
if (!IsAnyMediaKeyRegistered())
|
||||
StopWatchingMediaKeys();
|
||||
} else {
|
||||
// If we unregistered a hot key, and no more hot keys are registered, remove
|
||||
// the hot key handler.
|
||||
if (!IsAnyHotKeyRegistered()) {
|
||||
StopWatchingHotKeys();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool GlobalShortcutListenerMac::RegisterHotKey(
|
||||
const ui::Accelerator& accelerator, KeyId hot_key_id) {
|
||||
EventHotKeyID event_hot_key_id;
|
||||
|
||||
// Signature uniquely identifies the application that owns this hot_key.
|
||||
event_hot_key_id.signature = base::mac::CreatorCodeForApplication();
|
||||
event_hot_key_id.id = hot_key_id;
|
||||
|
||||
// Translate ui::Accelerator modifiers to cmdKey, altKey, etc.
|
||||
int modifiers = 0;
|
||||
modifiers |= (accelerator.IsShiftDown() ? shiftKey : 0);
|
||||
modifiers |= (accelerator.IsCtrlDown() ? controlKey : 0);
|
||||
modifiers |= (accelerator.IsAltDown() ? optionKey : 0);
|
||||
modifiers |= (accelerator.IsCmdDown() ? cmdKey : 0);
|
||||
|
||||
int key_code = ui::MacKeyCodeForWindowsKeyCode(accelerator.key_code(), 0,
|
||||
NULL, NULL);
|
||||
|
||||
// Register the event hot key.
|
||||
EventHotKeyRef hot_key_ref;
|
||||
OSStatus status = RegisterEventHotKey(key_code, modifiers, event_hot_key_id,
|
||||
GetApplicationEventTarget(), 0, &hot_key_ref);
|
||||
if (status != noErr)
|
||||
return false;
|
||||
|
||||
id_hot_key_refs_[hot_key_id] = hot_key_ref;
|
||||
return true;
|
||||
}
|
||||
|
||||
void GlobalShortcutListenerMac::UnregisterHotKey(
|
||||
const ui::Accelerator& accelerator) {
|
||||
// Ensure this accelerator is already registered.
|
||||
DCHECK(accelerator_ids_.find(accelerator) != accelerator_ids_.end());
|
||||
// Get the ref corresponding to this accelerator.
|
||||
KeyId key_id = accelerator_ids_[accelerator];
|
||||
EventHotKeyRef ref = id_hot_key_refs_[key_id];
|
||||
// Unregister the event hot key.
|
||||
UnregisterEventHotKey(ref);
|
||||
|
||||
// Remove the event from the mapping.
|
||||
id_hot_key_refs_.erase(key_id);
|
||||
}
|
||||
|
||||
void GlobalShortcutListenerMac::StartWatchingMediaKeys() {
|
||||
// Make sure there's no existing event tap.
|
||||
DCHECK(event_tap_ == NULL);
|
||||
DCHECK(event_tap_source_ == NULL);
|
||||
|
||||
// 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_ == NULL) {
|
||||
LOG(ERROR) << "Error: failed to create event tap.";
|
||||
return;
|
||||
}
|
||||
|
||||
event_tap_source_ = CFMachPortCreateRunLoopSource(kCFAllocatorSystemDefault,
|
||||
event_tap_, 0);
|
||||
if (event_tap_source_ == NULL) {
|
||||
LOG(ERROR) << "Error: failed to create new run loop source.";
|
||||
return;
|
||||
}
|
||||
|
||||
CFRunLoopAddSource(CFRunLoopGetCurrent(), event_tap_source_,
|
||||
kCFRunLoopCommonModes);
|
||||
}
|
||||
|
||||
void GlobalShortcutListenerMac::StopWatchingMediaKeys() {
|
||||
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), event_tap_source_,
|
||||
kCFRunLoopCommonModes);
|
||||
// Ensure both event tap and source are initialized.
|
||||
DCHECK(event_tap_ != NULL);
|
||||
DCHECK(event_tap_source_ != NULL);
|
||||
|
||||
// Invalidate the event tap.
|
||||
CFMachPortInvalidate(event_tap_);
|
||||
CFRelease(event_tap_);
|
||||
event_tap_ = NULL;
|
||||
|
||||
// Release the event tap source.
|
||||
CFRelease(event_tap_source_);
|
||||
event_tap_source_ = NULL;
|
||||
}
|
||||
|
||||
void GlobalShortcutListenerMac::StartWatchingHotKeys() {
|
||||
DCHECK(!event_handler_);
|
||||
EventHandlerUPP hot_key_function = NewEventHandlerUPP(HotKeyHandler);
|
||||
EventTypeSpec event_type;
|
||||
event_type.eventClass = kEventClassKeyboard;
|
||||
event_type.eventKind = kEventHotKeyPressed;
|
||||
InstallApplicationEventHandler(
|
||||
hot_key_function, 1, &event_type, this, &event_handler_);
|
||||
}
|
||||
|
||||
void GlobalShortcutListenerMac::StopWatchingHotKeys() {
|
||||
DCHECK(event_handler_);
|
||||
RemoveEventHandler(event_handler_);
|
||||
event_handler_ = NULL;
|
||||
}
|
||||
|
||||
bool GlobalShortcutListenerMac::IsAnyMediaKeyRegistered() {
|
||||
// Iterate through registered accelerators, looking for media keys.
|
||||
AcceleratorIdMap::iterator it;
|
||||
for (it = accelerator_ids_.begin(); it != accelerator_ids_.end(); ++it) {
|
||||
if (IsMediaKey(it->first))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GlobalShortcutListenerMac::IsAnyHotKeyRegistered() {
|
||||
AcceleratorIdMap::iterator it;
|
||||
for (it = accelerator_ids_.begin(); it != accelerator_ids_.end(); ++it) {
|
||||
if (!IsMediaKey(it->first))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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 NULL prevents the event from propagating.
|
||||
// static
|
||||
CGEventRef GlobalShortcutListenerMac::EventTapCallback(
|
||||
CGEventTapProxy proxy, CGEventType type, CGEventRef event, void* refcon) {
|
||||
GlobalShortcutListenerMac* shortcut_listener =
|
||||
static_cast<GlobalShortcutListenerMac*>(refcon);
|
||||
|
||||
// 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.
|
||||
bool was_handled = shortcut_listener->OnMediaKeyEvent(key_code);
|
||||
|
||||
// Prevent event from proagating to other apps if handled by Chrome.
|
||||
if (was_handled)
|
||||
return NULL;
|
||||
|
||||
// By default, pass the event through.
|
||||
return event;
|
||||
}
|
||||
|
||||
// static
|
||||
OSStatus GlobalShortcutListenerMac::HotKeyHandler(
|
||||
EventHandlerCallRef next_handler, EventRef event, void* user_data) {
|
||||
// Extract the hotkey from the event.
|
||||
EventHotKeyID hot_key_id;
|
||||
OSStatus result = GetEventParameter(event, kEventParamDirectObject,
|
||||
typeEventHotKeyID, NULL, sizeof(hot_key_id), NULL, &hot_key_id);
|
||||
if (result != noErr)
|
||||
return result;
|
||||
|
||||
GlobalShortcutListenerMac* shortcut_listener =
|
||||
static_cast<GlobalShortcutListenerMac*>(user_data);
|
||||
shortcut_listener->OnHotKeyEvent(hot_key_id);
|
||||
return noErr;
|
||||
}
|
||||
|
||||
} // namespace api
|
||||
|
||||
} // namespace atom
|
Loading…
Reference in a new issue