Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(477)

Side by Side Diff: chrome/browser/extensions/global_shortcut_listener_mac.mm

Issue 60353008: Mac global keybindings (Closed) Base URL: https://src.chromium.org/chrome/trunk/src/
Patch Set: Created 7 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/extensions/global_shortcut_listener_mac.h"
6
7 #include <ApplicationServices/ApplicationServices.h>
8 #import <Cocoa/Cocoa.h>
9 #include <IOKit/hidsystem/ev_keymap.h>
10
11 #include "base/mac/foundation_util.h"
12 #include "chrome/browser/extensions/api/commands/command_service.h"
13 #include "content/public/browser/browser_thread.h"
14 #include "ui/base/accelerators/accelerator.h"
15 #include "ui/events/event.h"
16 #import "ui/events/keycodes/keyboard_code_conversion_mac.h"
17
18 using content::BrowserThread;
19 using extensions::GlobalShortcutListenerMac;
20
21 namespace {
22
23 // The media keys subtype. No official docs found, but widely known.
24 // http://lists.apple.com/archives/cocoa-dev/2007/Aug/msg00499.html
25 const int kSystemDefinedEventMediaKeys = 8;
26
27 OSStatus HotKeyHandler(
28 EventHandlerCallRef next_handler, EventRef event, void* user_data) {
29 // Extract the hotkey from the event.
30 EventHotKeyID hot_key_id;
31 int result = GetEventParameter(event, kEventParamDirectObject,
32 typeEventHotKeyID, NULL, sizeof(hot_key_id), NULL, &hot_key_id);
33 if (result != noErr)
34 return result;
35
36 GlobalShortcutListenerMac* shortcut_listener =
37 static_cast<GlobalShortcutListenerMac*>(user_data);
38 shortcut_listener->OnKeyEvent(hot_key_id);
39 return noErr;
40 }
41
42 ui::KeyboardCode MediaKeyCodeToKeyboardCode(int key_code) {
43 switch (key_code) {
44 case NX_KEYTYPE_PLAY:
45 return ui::VKEY_MEDIA_PLAY_PAUSE;
46 case NX_KEYTYPE_PREVIOUS:
47 case NX_KEYTYPE_REWIND:
48 return ui::VKEY_MEDIA_PREV_TRACK;
49 case NX_KEYTYPE_NEXT:
50 case NX_KEYTYPE_FAST:
51 return ui::VKEY_MEDIA_NEXT_TRACK;
52 }
53 return ui::VKEY_UNKNOWN;
54 }
55
56 static GlobalShortcutListenerMac* g_instance = NULL;
57
58 } // namespace
59
60 namespace extensions {
61
62 // static
63 GlobalShortcutListener* GlobalShortcutListener::GetInstance() {
64 CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
65 if (g_instance == NULL) {
Mark Mentovai 2013/12/10 03:25:21 Yes, this is what I meant. But also: - the “sta
66 g_instance = new GlobalShortcutListenerMac();
67 }
68 return g_instance;
69 }
70
71 GlobalShortcutListenerMac::GlobalShortcutListenerMac()
72 : is_listening_(false),
73 hot_key_id_(0),
74 event_tap_(NULL),
75 event_tap_source_(NULL) {
76 CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
77 }
78
79 GlobalShortcutListenerMac::~GlobalShortcutListenerMac() {
80 // By this point, UnregisterAccelerator should have been called for all
81 // keyboard shortcuts.
82 if (is_listening_)
83 StopListening();
84
85 // If media keys are still registered, make sure we stop the tap.
86 if (IsAnyMediaKeyRegistered())
87 StopWatchingMediaKeys();
88 }
89
90 void GlobalShortcutListenerMac::StartListening() {
91 DCHECK(!is_listening_); // Don't start twice.
92 DCHECK(!hot_key_ids_.empty()); // Don't start if no hot key registered.
93 DCHECK(!id_hot_keys_.empty());
94 is_listening_ = true;
95
96 // Install an event handler for hot keys.
97 EventHandlerUPP hot_key_function = NewEventHandlerUPP(HotKeyHandler);
98 EventTypeSpec event_type;
99 event_type.eventClass = kEventClassKeyboard;
100 event_type.eventKind = kEventHotKeyPressed;
101 InstallApplicationEventHandler(
102 hot_key_function, 1, &event_type, this, &event_handler_);
103 }
104
105 void GlobalShortcutListenerMac::StopListening() {
106 DCHECK(is_listening_); // No point if we are not already listening.
107 DCHECK(hot_key_ids_.empty()); // Make sure the set is clean.
108 DCHECK(id_hot_keys_.empty());
109 is_listening_ = false;
110
111 // Remove the hot key event handler.
112 RemoveEventHandler(event_handler_);
113 }
114
115 void GlobalShortcutListenerMac::OnKeyEvent(EventHotKeyID hot_key_id) {
116 // Look up the accelerator based on this hot key ID.
117 const ui::Accelerator& accelerator = id_hot_keys_[hot_key_id.id];
118 NotifyKeyPressed(accelerator);
119 }
120
121 bool GlobalShortcutListenerMac::OnMediaKeyEvent(int media_key_code) {
122 ui::KeyboardCode key_code = MediaKeyCodeToKeyboardCode(media_key_code);
123 // Create an accelerator corresponding to the keyCode.
124 ui::Accelerator accelerator(key_code, 0);
125 // Look for a match with a bound hot_key.
126 if (hot_key_ids_.find(accelerator) != hot_key_ids_.end()) {
127 // If matched, callback to the event handling system.
128 NotifyKeyPressed(accelerator);
129 return true;
130 }
131 return false;
132 }
133
134 void GlobalShortcutListenerMac::RegisterAccelerator(
135 const ui::Accelerator& accelerator,
136 GlobalShortcutListener::Observer* observer) {
137 if (hot_key_ids_.find(accelerator) != hot_key_ids_.end()) {
138 // The shortcut has already been registered. Some shortcuts, such as
139 // MediaKeys can have multiple targets, all keyed off of the same
140 // accelerator.
141 return;
142 }
143
144 // If this is the first media key registered, start the event tap.
145 if (CommandService::IsMediaKey(accelerator) && !IsAnyMediaKeyRegistered()) {
146 StartWatchingMediaKeys();
147 }
148
149 // Register hot_key if they are non-media keyboard shortcuts.
150 if (!CommandService::IsMediaKey(accelerator))
151 RegisterHotKey(accelerator, hot_key_id_);
152
153 // Store the hotkey-ID mappings we will need for lookup later.
154 id_hot_keys_[hot_key_id_] = accelerator;
155 hot_key_ids_[accelerator] = hot_key_id_;
156 ++hot_key_id_;
157 GlobalShortcutListener::RegisterAccelerator(accelerator, observer);
158 }
159
160 void GlobalShortcutListenerMac::UnregisterAccelerator(
161 const ui::Accelerator& accelerator,
162 GlobalShortcutListener::Observer* observer) {
163 // Unregister the hot_key if it's a keyboard shortcut.
164 if (!CommandService::IsMediaKey(accelerator))
165 UnregisterHotKey(accelerator);
166
167 // Remove hot_key from the mappings.
168 int id = hot_key_ids_[accelerator];
169 id_hot_keys_.erase(id);
170 hot_key_ids_.erase(accelerator);
171 GlobalShortcutListener::UnregisterAccelerator(accelerator, observer);
172
173 // If we unregistered a media key, and now if no media keys are registered,
174 // stop the media key tap.
175 if (CommandService::IsMediaKey(accelerator) && !IsAnyMediaKeyRegistered()) {
176 StopWatchingMediaKeys();
177 }
178 }
179
180 void GlobalShortcutListenerMac::RegisterHotKey(
181 const ui::Accelerator& accelerator, int hot_key_id) {
182 EventHotKeyRef hot_key_ref;
183 EventHotKeyID event_hot_key_id;
184
185 // Signature uniquely identifies the application that owns this hot_key.
186 event_hot_key_id.signature = base::mac::CreatorCodeForApplication();
187 event_hot_key_id.id = hot_key_id;
188
189 // Translate ui::Accelerator modifiers to cmdKey, altKey, etc.
190 int modifiers = 0;
191 modifiers |= (accelerator.IsShiftDown() ? shiftKey : 0);
192 modifiers |= (accelerator.IsCtrlDown() ? controlKey : 0);
193 modifiers |= (accelerator.IsAltDown() ? optionKey : 0);
194 modifiers |= (accelerator.IsCmdDown() ? cmdKey : 0);
195
196 unichar character;
197 unichar character_nomods;
198 int key_code = ui::MacKeyCodeForWindowsKeyCode(accelerator.key_code(), 0,
199 &character, &character_nomods);
200
201 // Register the event hot key.
202 RegisterEventHotKey(key_code, modifiers, event_hot_key_id,
203 GetApplicationEventTarget(), 0, &hot_key_ref);
204
205 id_hot_key_refs_[hot_key_id] = hot_key_ref;
206 }
207
208 void GlobalShortcutListenerMac::UnregisterHotKey(
209 const ui::Accelerator& accelerator) {
210 // Get the ref corresponding to this accelerator.
211 int id = hot_key_ids_[accelerator];
212 EventHotKeyRef ref = id_hot_key_refs_[id];
213 // Unregister the event hot key.
214 UnregisterEventHotKey(ref);
215
216 // Remove the event from the mapping.
217 id_hot_key_refs_.erase(id);
218 }
219
220 void GlobalShortcutListenerMac::StartWatchingMediaKeys() {
221 // Make sure there's no existing event tap.
222 DCHECK(event_tap_ == NULL);
223
224 // Add an event tap to intercept the system defined media key events.
225 event_tap_ = CGEventTapCreate(kCGSessionEventTap,
226 kCGHeadInsertEventTap,
227 kCGEventTapOptionDefault,
228 CGEventMaskBit(NX_SYSDEFINED),
229 EventTapCallback,
230 this);
231 if (event_tap_ == NULL) {
232 LOG(ERROR) << "Error: failed to create event tap.";
233 return;
234 }
235
236 event_tap_source_ = CFMachPortCreateRunLoopSource(kCFAllocatorSystemDefault,
237 event_tap_, 0);
238 if (event_tap_source_ == NULL) {
239 LOG(ERROR) << "Error: failed to create new run loop source.";
240 return;
241 }
242
243 CFRunLoopAddSource(CFRunLoopGetCurrent(), event_tap_source_,
244 kCFRunLoopCommonModes);
245 }
246
247 void GlobalShortcutListenerMac::StopWatchingMediaKeys() {
248 CFRunLoopRemoveSource(CFRunLoopGetCurrent(), event_tap_source_,
249 kCFRunLoopCommonModes);
250
251 // Invalidate the event tap.
252 DCHECK(event_tap_ != NULL);
253 CFMachPortInvalidate(event_tap_);
254 CFRelease(event_tap_);
255 event_tap_ = NULL;
256
257 // Release the event tap source.
258 DCHECK(event_tap_source_ != NULL);
259 CFRelease(event_tap_source_);
260 event_tap_source_ = NULL;
261 }
262
263 bool GlobalShortcutListenerMac::IsAnyMediaKeyRegistered() {
264 // Iterate through registered accelerators, looking for media keys.
265 HotKeyIdMap::iterator iter;
266 for (iter = hot_key_ids_.begin(); iter != hot_key_ids_.end(); ++iter) {
267 if (CommandService::IsMediaKey(iter->first))
268 return true;
269 }
270 return false;
271 }
272
273 // Processed events should propagate if they aren't handled by any listeners.
274 // For events that don't matter, this handler should return as quickly as
275 // possible.
276 // Returning event causes the event to propagate to other applications.
277 // Returning NULL prevents the event from propagating.
278 // static
279 CGEventRef GlobalShortcutListenerMac::EventTapCallback(
280 CGEventTapProxy proxy, CGEventType type, CGEventRef event, void* refcon) {
281 GlobalShortcutListenerMac* shortcut_listener =
282 static_cast<GlobalShortcutListenerMac*>(refcon);
283
284 // Handle the timeout case by re-enabling the tap.
285 if (type == kCGEventTapDisabledByTimeout) {
286 CGEventTapEnable(shortcut_listener->event_tap_, TRUE);
287 return event;
288 }
289
290 // Convert the CGEvent to an NSEvent for access to the data1 field.
291 NSEvent* ns_event = [NSEvent eventWithCGEvent:event];
292 if (ns_event == nil) {
293 return event;
294 }
295
296 // Ignore events that are not system defined media keys.
297 if (type != NX_SYSDEFINED ||
298 [ns_event type] != NSSystemDefined ||
299 [ns_event subtype] != kSystemDefinedEventMediaKeys) {
300 return event;
301 }
302
303 NSInteger data1 = [ns_event data1];
304 // Ignore media keys that aren't previous, next and play/pause.
305 // Magical constants are from http://weblog.rogueamoeba.com/2007/09/29/
306 int key_code = ((data1 & 0xFFFF0000) >> 16);
307 if (key_code != NX_KEYTYPE_PLAY && key_code != NX_KEYTYPE_NEXT &&
308 key_code != NX_KEYTYPE_PREVIOUS && key_code != NX_KEYTYPE_FAST &&
309 key_code != NX_KEYTYPE_REWIND) {
310 return event;
311 }
312
313 int key_flags = (data1 & 0x0000FFFF);
314 bool is_key_pressed = (((key_flags & 0xFF00) >> 8)) == 0xA;
315
316 // If the key wasn't pressed (eg. was released), ignore this event.
317 if (!is_key_pressed)
318 return event;
319
320 // Now we have a media key that we care about. Send it to the caller.
321 bool was_handled = shortcut_listener->OnMediaKeyEvent(key_code);
322
323 // Prevent event from proagating to other apps if handled by Chrome.
324 if (was_handled)
325 return NULL;
326
327 // By default, pass the event through.
328 return event;
329 }
330
331 } // namespace extensions
OLDNEW
« no previous file with comments | « chrome/browser/extensions/global_shortcut_listener_mac.cc ('k') | chrome/chrome_browser_extensions.gypi » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698