From 4b3bd9c3cc8e5566032c62f377c4fdf1dcb584c5 Mon Sep 17 00:00:00 2001 From: Haojian Wu Date: Sun, 20 Jul 2014 23:09:26 +0800 Subject: [PATCH] mac: Implement global keyboard shortcut API. --- atom.gyp | 7 + atom/browser/api/atom_api_shortcut.cc | 107 +++++ atom/browser/api/atom_api_shortcut.h | 67 +++ atom/browser/api/lib/shortcut.coffee | 7 + atom/common/node_bindings.cc | 1 + .../ui/shortcut/global_shortcut_listener.cc | 125 ++++++ .../ui/shortcut/global_shortcut_listener.h | 105 +++++ .../shortcut/global_shortcut_listener_mac.h | 111 +++++ .../shortcut/global_shortcut_listener_mac.mm | 392 ++++++++++++++++++ 9 files changed, 922 insertions(+) create mode 100644 atom/browser/api/atom_api_shortcut.cc create mode 100644 atom/browser/api/atom_api_shortcut.h create mode 100644 atom/browser/api/lib/shortcut.coffee create mode 100644 chromium_src/chrome/browser/ui/shortcut/global_shortcut_listener.cc create mode 100644 chromium_src/chrome/browser/ui/shortcut/global_shortcut_listener.h create mode 100644 chromium_src/chrome/browser/ui/shortcut/global_shortcut_listener_mac.h create mode 100644 chromium_src/chrome/browser/ui/shortcut/global_shortcut_listener_mac.mm diff --git a/atom.gyp b/atom.gyp index 6bd28b25d90d..230fdb6469a6 100644 --- a/atom.gyp +++ b/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': [ diff --git a/atom/browser/api/atom_api_shortcut.cc b/atom/browser/api/atom_api_shortcut.cc new file mode 100644 index 000000000000..d752b453ddf6 --- /dev/null +++ b/atom/browser/api/atom_api_shortcut.cc @@ -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 +#include + +#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 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 exports, v8::Handle unused, + v8::Handle context, void* priv) { + using atom::api::Shortcut; + v8::Isolate* isolate = context->GetIsolate(); + v8::Handle constructor = mate::CreateConstructor( + isolate, "Shortcut", base::Bind(&Shortcut::Create)); + mate::Dictionary dict(isolate, exports); + dict.Set("Shortcut", static_cast>(constructor)); +} + +} // namespace + +NODE_MODULE_CONTEXT_AWARE_BUILTIN(atom_browser_shortcut, Initialize) diff --git a/atom/browser/api/atom_api_shortcut.h b/atom/browser/api/atom_api_shortcut.h new file mode 100644 index 000000000000..b71dad380a9d --- /dev/null +++ b/atom/browser/api/atom_api_shortcut.h @@ -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 +#include + +#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 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_ diff --git a/atom/browser/api/lib/shortcut.coffee b/atom/browser/api/lib/shortcut.coffee new file mode 100644 index 000000000000..04cc252ee7f9 --- /dev/null +++ b/atom/browser/api/lib/shortcut.coffee @@ -0,0 +1,7 @@ +EventEmitter = require('events').EventEmitter +bindings = process.atomBinding 'shortcut' + +Shortcut = bindings.Shortcut +Shortcut::__proto__ = EventEmitter.prototype + +module.exports = Shortcut diff --git a/atom/common/node_bindings.cc b/atom/common/node_bindings.cc index 2d5d8364bc08..ffc1967b43c2 100644 --- a/atom/common/node_bindings.cc +++ b/atom/common/node_bindings.cc @@ -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); diff --git a/chromium_src/chrome/browser/ui/shortcut/global_shortcut_listener.cc b/chromium_src/chrome/browser/ui/shortcut/global_shortcut_listener.cc new file mode 100644 index 000000000000..f16ca705d1cf --- /dev/null +++ b/chromium_src/chrome/browser/ui/shortcut/global_shortcut_listener.cc @@ -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 diff --git a/chromium_src/chrome/browser/ui/shortcut/global_shortcut_listener.h b/chromium_src/chrome/browser/ui/shortcut/global_shortcut_listener.h new file mode 100644 index 000000000000..8985edb9c945 --- /dev/null +++ b/chromium_src/chrome/browser/ui/shortcut/global_shortcut_listener.h @@ -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 + +#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 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_ diff --git a/chromium_src/chrome/browser/ui/shortcut/global_shortcut_listener_mac.h b/chromium_src/chrome/browser/ui/shortcut/global_shortcut_listener_mac.h new file mode 100644 index 000000000000..27cbadcb163c --- /dev/null +++ b/chromium_src/chrome/browser/ui/shortcut/global_shortcut_listener_mac.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 +#include + +#include + +#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 AcceleratorIdMap; + typedef std::map IdAcceleratorMap; + typedef std::map 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_ diff --git a/chromium_src/chrome/browser/ui/shortcut/global_shortcut_listener_mac.mm b/chromium_src/chrome/browser/ui/shortcut/global_shortcut_listener_mac.mm new file mode 100644 index 000000000000..47b390e60f3b --- /dev/null +++ b/chromium_src/chrome/browser/ui/shortcut/global_shortcut_listener_mac.mm @@ -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 +#import +#include + +#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(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(user_data); + shortcut_listener->OnHotKeyEvent(hot_key_id); + return noErr; +} + +} // namespace api + +} // namespace atom