Skip to content

Commit

Permalink
Merge pull request electron#534 from hokein/hotkey
Browse files Browse the repository at this point in the history
Implement global shortcut API, fixes electron#439
  • Loading branch information
zcbenz committed Aug 3, 2014
2 parents c4d9dc9 + 0356790 commit 9c038a2
Show file tree
Hide file tree
Showing 15 changed files with 1,382 additions and 0 deletions.
11 changes: 11 additions & 0 deletions atom.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
'atom/browser/api/lib/browser-window.coffee',
'atom/browser/api/lib/content-tracing.coffee',
'atom/browser/api/lib/dialog.coffee',
'atom/browser/api/lib/global-shortcut.coffee',
'atom/browser/api/lib/ipc.coffee',
'atom/browser/api/lib/menu.coffee',
'atom/browser/api/lib/menu-item.coffee',
Expand Down Expand Up @@ -54,6 +55,8 @@
'atom/browser/api/atom_api_auto_updater.h',
'atom/browser/api/atom_api_content_tracing.cc',
'atom/browser/api/atom_api_dialog.cc',
'atom/browser/api/atom_api_global_shortcut.cc',
'atom/browser/api/atom_api_global_shortcut.h',
'atom/browser/api/atom_api_menu.cc',
'atom/browser/api/atom_api_menu.h',
'atom/browser/api/atom_api_menu_views.cc',
Expand Down Expand Up @@ -225,6 +228,14 @@
'atom/renderer/atom_render_view_observer.h',
'atom/renderer/atom_renderer_client.cc',
'atom/renderer/atom_renderer_client.h',
'chromium_src/chrome/browser/extensions/global_shortcut_listener.cc',
'chromium_src/chrome/browser/extensions/global_shortcut_listener.h',
'chromium_src/chrome/browser/extensions/global_shortcut_listener_mac.mm',
'chromium_src/chrome/browser/extensions/global_shortcut_listener_mac.h',
'chromium_src/chrome/browser/extensions/global_shortcut_listener_x11.cc',
'chromium_src/chrome/browser/extensions/global_shortcut_listener_x11.h',
'chromium_src/chrome/browser/extensions/global_shortcut_listener_win.cc',
'chromium_src/chrome/browser/extensions/global_shortcut_listener_win.h',
'chromium_src/chrome/browser/ui/libgtk2ui/app_indicator_icon_menu.cc',
'chromium_src/chrome/browser/ui/libgtk2ui/app_indicator_icon_menu.h',
'chromium_src/chrome/browser/ui/libgtk2ui/gtk2_status_icon.cc',
Expand Down
123 changes: 123 additions & 0 deletions atom/browser/api/atom_api_global_shortcut.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// 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_global_shortcut.h"

#include <string>

#include "atom/browser/ui/accelerator_util.h"
#include "atom/common/native_mate_converters/function_converter.h"
#include "native_mate/dictionary.h"

#include "atom/common/node_includes.h"

using extensions::GlobalShortcutListener;

namespace atom {

namespace api {

GlobalShortcut::GlobalShortcut() {
}

GlobalShortcut::~GlobalShortcut() {
UnregisterAll();
}

void GlobalShortcut::OnKeyPressed(const ui::Accelerator& accelerator) {
if (accelerator_callback_map_.find(accelerator) ==
accelerator_callback_map_.end()) {
// This should never occur, because if it does, GlobalGlobalShortcutListener
// notifes us with wrong accelerator.
NOTREACHED();
return;
}
accelerator_callback_map_[accelerator].Run();
}

bool GlobalShortcut::Register(const std::string& keycode,
const base::Closure& callback) {
ui::Accelerator accelerator;
if (!accelerator_util::StringToAccelerator(keycode, &accelerator)) {
LOG(ERROR) << keycode << " is invalid.";
return false;
}
if (!GlobalShortcutListener::GetInstance()->RegisterAccelerator(
accelerator, this)) {
return false;
}
accelerator_callback_map_[accelerator] = callback;
return true;
}

void GlobalShortcut::Unregister(const std::string& keycode) {
ui::Accelerator accelerator;
if (!accelerator_util::StringToAccelerator(keycode, &accelerator)) {
LOG(ERROR) << "The keycode: " << keycode << " is invalid.";
return;
}
if (accelerator_callback_map_.find(accelerator) ==
accelerator_callback_map_.end()) {
LOG(ERROR) << "The keycode: " << keycode << " isn't registered yet!";
return;
}
accelerator_callback_map_.erase(accelerator);
GlobalShortcutListener::GetInstance()->UnregisterAccelerator(
accelerator, this);
}

void GlobalShortcut::UnregisterAll() {
accelerator_callback_map_.clear();
GlobalShortcutListener::GetInstance()->UnregisterAccelerators(this);
}

bool GlobalShortcut::IsRegistered(const std::string& keycode) {
ui::Accelerator accelerator;
if (!accelerator_util::StringToAccelerator(keycode, &accelerator)) {
LOG(ERROR) << "The keycode: " << keycode << " is invalid.";
return false;
}
return accelerator_callback_map_.find(accelerator) !=
accelerator_callback_map_.end();
}

// static
mate::ObjectTemplateBuilder GlobalShortcut::GetObjectTemplateBuilder(
v8::Isolate* isolate) {
return mate::ObjectTemplateBuilder(isolate)
.SetMethod("register",
base::Bind(&GlobalShortcut::Register,
base::Unretained(this)))
.SetMethod("isRegistered",
base::Bind(&GlobalShortcut::IsRegistered,
base::Unretained(this)))
.SetMethod("unregister",
base::Bind(&GlobalShortcut::Unregister,
base::Unretained(this)))
.SetMethod("unregisterAll",
base::Bind(&GlobalShortcut::UnregisterAll,
base::Unretained(this)));
}

// static
mate::Handle<GlobalShortcut> GlobalShortcut::Create(v8::Isolate* isolate) {
return CreateHandle(isolate, new GlobalShortcut);
}

} // namespace api

} // namespace atom

namespace {

void Initialize(v8::Handle<v8::Object> exports, v8::Handle<v8::Value> unused,
v8::Handle<v8::Context> context, void* priv) {
v8::Isolate* isolate = context->GetIsolate();
mate::Dictionary dict(isolate, exports);
dict.Set("globalShortcut", atom::api::GlobalShortcut::Create(isolate));
}

} // namespace

NODE_MODULE_CONTEXT_AWARE_BUILTIN(atom_browser_global_shortcut, Initialize)
54 changes: 54 additions & 0 deletions atom/browser/api/atom_api_global_shortcut.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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_GLOBAL_SHORTCUT_H_
#define ATOM_BROWSER_API_ATOM_API_GLOBAL_SHORTCUT_H_

#include <map>
#include <string>

#include "base/callback.h"
#include "chrome/browser/extensions/global_shortcut_listener.h"
#include "native_mate/wrappable.h"
#include "native_mate/handle.h"
#include "ui/base/accelerators/accelerator.h"

namespace atom {

namespace api {

class GlobalShortcut : public extensions::GlobalShortcutListener::Observer,
public mate::Wrappable {
public:
static mate::Handle<GlobalShortcut> Create(v8::Isolate* isolate);

protected:
GlobalShortcut();
virtual ~GlobalShortcut();

// mate::Wrappable implementations:
virtual mate::ObjectTemplateBuilder GetObjectTemplateBuilder(
v8::Isolate* isolate) OVERRIDE;

private:
typedef std::map<ui::Accelerator, base::Closure> AcceleratorCallbackMap;

bool Register(const std::string& keycode, const base::Closure& callback);
bool IsRegistered(const std::string& keycode);
void Unregister(const std::string& keycode);
void UnregisterAll();

// GlobalShortcutListener::Observer implementation.
virtual void OnKeyPressed(const ui::Accelerator& accelerator) OVERRIDE;

AcceleratorCallbackMap accelerator_callback_map_;

DISALLOW_COPY_AND_ASSIGN(GlobalShortcut);
};

} // namespace api

} // namespace atom

#endif // ATOM_BROWSER_API_ATOM_API_GLOBAL_SHORTCUT_H_
5 changes: 5 additions & 0 deletions atom/browser/api/lib/global-shortcut.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
bindings = process.atomBinding 'global_shortcut'

globalShortcut = bindings.globalShortcut

module.exports = globalShortcut
1 change: 1 addition & 0 deletions atom/common/node_bindings.cc
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,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_global_shortcut);
REFERENCE_MODULE(atom_browser_tray);
REFERENCE_MODULE(atom_browser_window);
REFERENCE_MODULE(atom_common_clipboard);
Expand Down
122 changes: 122 additions & 0 deletions chromium_src/chrome/browser/extensions/global_shortcut_listener.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright (c) 2013 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/extensions/global_shortcut_listener.h"

#include "base/logging.h"
#include "content/public/browser/browser_thread.h"
#include "ui/base/accelerators/accelerator.h"

using content::BrowserThread;

namespace extensions {

GlobalShortcutListener::GlobalShortcutListener()
: shortcut_handling_suspended_(false) {
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
}

GlobalShortcutListener::~GlobalShortcutListener() {
CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(accelerator_map_.empty()); // Make sure we've cleaned up.
}

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);
// We should never get asked to unregister something that we didn't register.
DCHECK(it != accelerator_map_.end());
// 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 extensions
Loading

0 comments on commit 9c038a2

Please sign in to comment.