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

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, 1 month 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 #import <Cocoa/Cocoa.h>
Robert Sesek 2013/11/19 18:34:58 #include and using blocks should be alphabetized.
smus 2013/11/20 04:28:51 Done.
8 #include <IOKit/hidsystem/ev_keymap.h>
9 #include <ApplicationServices/ApplicationServices.h>
10
11 #include "content/public/browser/browser_thread.h"
12 #include "ui/events/event.h"
13 #include "ui/base/accelerators/accelerator.h"
14 #import "ui/events/keycodes/keyboard_code_conversion_mac.h"
15
16 const int SYSTEM_DEFINED_EVENT_MEDIA_KEYS = 8;
17
18 #define EVENT_KEY @"event"
Robert Sesek 2013/11/19 18:34:58 These are still #defines. This should be: namespa
smus 2013/11/20 04:28:51 Done.
19 #define HANDLED_KEY @"handled"
20
21 using extensions::GlobalShortcutListenerMac;
Robert Sesek 2013/11/19 18:34:58 Alphabetize.
smus 2013/11/20 04:28:51 Done.
22 using content::BrowserThread;
23
24 @interface GlobalShortcutListenerTap : NSObject {
25
26 @private
Robert Sesek 2013/11/19 18:34:58 nit: one space indent, just like C++
smus 2013/11/20 04:28:51 Done.
27 CFMachPortRef eventTap_;
28 CFRunLoopSourceRef eventTapSource_;
29 CFRunLoopRef tapThreadRunLoop_;
30 GlobalShortcutListenerMac* shortcutListener_;
31 }
32
33 - (id)initWithShortcutListener:(GlobalShortcutListenerMac*)shortcutListener;
34 - (void)startWatchingMediaKeys;
35 - (void)stopWatchingMediaKeys;
36 - (void)handleMediaKeyEvent:(NSEvent*)event;
37 - (BOOL)performEventHandlerOnMainThread:(SEL)selector withEvent:(NSEvent*)event;
38 - (void)enableTap;
39
40 @end
41
42 // Processed events should propagate if they aren't handled by any listeners.
43 // Returning event causes the event to propagate to other applications.
44 // Returning NULL prevents the event from propagating.
45 CGEventRef TapEventCallback(
Robert Sesek 2013/11/19 18:34:58 Actually, this should probably be EventTapCallback
smus 2013/11/20 04:28:51 Done.
46 CGEventTapProxy proxy, CGEventType type, CGEventRef event, void* refcon) {
47 NSAutoreleasePool* pool = [NSAutoreleasePool new];
48 CGEventRef out_event = event;
49
50 GlobalShortcutListenerTap* self =
51 static_cast<GlobalShortcutListenerTap*>(refcon);
52
53 // Handle the timeout case by re-enabling the tap.
54 if (type == kCGEventTapDisabledByTimeout) {
55 LOG(INFO) << "Event tap was disabled by a timeout.";
56 [self enableTap];
57 // Release the event as soon as possible.
58 return out_event;
59 }
60
61 // TODO(smus): do some error handling since eventWithCGEvent can fail.
62 NSEvent* ns_event = [NSEvent eventWithCGEvent:event];
63
64 // Handle media keys (PlayPause, NextTrack, PreviousTrack).
65 if (type != NX_SYSDEFINED ||
66 [ns_event subtype] != SYSTEM_DEFINED_EVENT_MEDIA_KEYS) {
67 int key_code = (([ns_event data1] & 0xFFFF0000) >> 16);
68 if (key_code != NX_KEYTYPE_PLAY && key_code != NX_KEYTYPE_NEXT &&
69 key_code != NX_KEYTYPE_PREVIOUS && key_code != NX_KEYTYPE_FAST &&
70 key_code != NX_KEYTYPE_REWIND) {
71 // Release the event as soon as possible.
72 return out_event;
73 }
74 }
75
76 // If we got here, we are dealing with a real media key event.
77 BOOL was_handled = [self
78 performEventHandlerOnMainThread:@selector(handleMediaKeyEvent:)
79 withEvent:ns_event];
80 // Prevent the event from proagating to other mac applications if it was
81 // handled by Chrome.
82 if (was_handled)
83 out_event = NULL;
84
85 [pool drain];
86 // By default, pass the event through.
87 return out_event;
88 }
89
90 OSStatus HotKeyHandler(EventHandlerCallRef next_handler, EventRef event,
Finnur 2013/11/19 13:10:05 nit: I think style-wise we prefer: void func( F
smus 2013/11/20 04:28:51 Done.
91 void *user_data) {
92 VLOG(0) << "HotKeyHandler fired with event: " << event;
93 // Extract the hotkey from the event.
94 EventHotKeyID hotkey_id;
95 int result = GetEventParameter(event, kEventParamDirectObject,
96 typeEventHotKeyID, NULL, sizeof(hotkey_id), NULL, &hotkey_id);
97 if (result != noErr) {
98 return result;
99 }
Finnur 2013/11/19 13:10:05 nit: Single line if.
smus 2013/11/20 04:28:51 Done.
100
101 // Callback to the parent class.
102 GlobalShortcutListenerMac* shortcutListener =
103 static_cast<GlobalShortcutListenerMac*>(user_data);
104 shortcutListener->OnKeyEvent(hotkey_id);
105 return noErr;
106 }
107
108 @implementation GlobalShortcutListenerTap
109
110 - (id)initWithShortcutListener:(GlobalShortcutListenerMac*)shortcutListener{
111 if ((self = [super init])) {
112 shortcutListener_ = shortcutListener;
113 }
Finnur 2013/11/19 13:10:05 nit: Single line if.
smus 2013/11/20 04:28:51 Done.
114 return self;
115 }
116
117 - (void)eventTapThread {
118 tapThreadRunLoop_ = CFRunLoopGetCurrent();
119 CFRunLoopAddSource(tapThreadRunLoop_, eventTapSource_,
120 kCFRunLoopCommonModes);
121 CFRunLoopRun();
122 }
123
124 - (BOOL)performEventHandlerOnMainThread:(SEL)selector
125 withEvent:(NSEvent*)event {
126 NSMutableDictionary* dict = [[NSMutableDictionary alloc] init];
127 [dict setObject:event forKey:EVENT_KEY];
128 [self performSelectorOnMainThread:selector
129 withObject:dict waitUntilDone:YES];
130 // Keep track of the result from the main thread to know if the event has
131 // been handled.
132 BOOL was_handled = [[dict objectForKey:HANDLED_KEY] boolValue];
133 [dict release];
134 return was_handled;
135 }
136
137 - (ui::KeyboardCode)mediaKeyCodeToKeyboardCode:(int)keyCode {
138 switch (keyCode) {
139 case NX_KEYTYPE_PLAY:
140 return ui::VKEY_MEDIA_PLAY_PAUSE;
141 case NX_KEYTYPE_PREVIOUS:
142 case NX_KEYTYPE_REWIND:
143 return ui::VKEY_MEDIA_PREV_TRACK;
144 case NX_KEYTYPE_NEXT:
145 case NX_KEYTYPE_FAST:
146 return ui::VKEY_MEDIA_NEXT_TRACK;
147 }
148 return ui::VKEY_UNKNOWN;
149 }
150
151 - (void)startWatchingMediaKeys {
152 // Make sure there's no existing event tap.
153 if (eventTap_ != NULL) {
154 LOG(ERROR) << "Error watching media keys: existing event tap found.";
155 return;
156 }
157
158 // Add an event tap to intercept the system defined media key events.
159 eventTap_ = CGEventTapCreate(kCGSessionEventTap,
160 kCGHeadInsertEventTap,
161 kCGEventTapOptionDefault,
162 CGEventMaskBit(NX_SYSDEFINED),
163 TapEventCallback,
164 self);
165 if (eventTap_ == NULL) {
166 LOG(ERROR) << "Error watching media keys: failed to create event tap.";
167 return;
168 }
169
170 eventTapSource_ = CFMachPortCreateRunLoopSource(kCFAllocatorSystemDefault,
171 eventTap_, 0);
172 if (eventTapSource_ == NULL) {
173 LOG(ERROR) <<
174 "Error watching media keys: failed to create new run loop source.";
175 return;
176 }
177
178 VLOG(0) << "Starting media key event tap.";
179 // Run the event tap in separate thread to prevent blocking UI.
180 [NSThread detachNewThreadSelector:@selector(eventTapThread)
181 toTarget:self withObject:nil];
182 }
183
184 - (void)stopWatchingMediaKeys {
185 if (tapThreadRunLoop_) {
Finnur 2013/11/19 13:10:05 Under what circumstances can these be nil when you
smus 2013/11/20 04:28:51 Done.
186 CFRunLoopStop(tapThreadRunLoop_);
187 tapThreadRunLoop_ = nil;
188 }
189
190 if (eventTap_) {
191 CFMachPortInvalidate(eventTap_);
192 CFRelease(eventTap_);
193 eventTap_ = nil;
194 }
195
196 if (eventTapSource_) {
197 CFRelease(eventTapSource_);
198 eventTapSource_ = nil;
199 }
200 }
201
202 // Event will have been retained in the other thread.
203 - (void)handleMediaKeyEvent:(NSMutableDictionary*)dict {
204 NSEvent* event = [dict objectForKey:EVENT_KEY];
205
206 int key_code = (([event data1] & 0xFFFF0000) >> 16);
207 int key_flags = ([event data1] & 0x0000FFFF);
208 BOOL is_key_pressed = (((key_flags & 0xFF00) >> 8)) == 0xA;
209
210 bool result = false;
211 if (is_key_pressed) {
212 result = shortcutListener_->OnMediaKeyEvent(
213 [self mediaKeyCodeToKeyboardCode:key_code]);
214 }
215
216 [dict setObject:[NSNumber numberWithBool:result] forKey:HANDLED_KEY];
217 }
218
219 - (void)enableTap {
220 CGEventTapEnable(eventTap_, TRUE);
221 }
222
223 @end
224
225 namespace {
226
227 static base::LazyInstance<extensions::GlobalShortcutListenerMac> g_instance =
228 LAZY_INSTANCE_INITIALIZER;
229
230 } // namespace
231
232 namespace extensions {
233
234 // static
235 GlobalShortcutListener* GlobalShortcutListener::GetInstance() {
236 CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
237 return g_instance.Pointer();
238 }
239
240 GlobalShortcutListenerMac::GlobalShortcutListenerMac()
241 : is_listening_(false) {
242 CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
243
244 tap_.reset([[GlobalShortcutListenerTap alloc] initWithShortcutListener:this]);
245 }
246
247 GlobalShortcutListenerMac::~GlobalShortcutListenerMac() {
248 if (is_listening_)
249 StopListening();
250 }
251
252 void GlobalShortcutListenerMac::StartListening() {
253 DCHECK(!is_listening_); // Don't start twice.
254 DCHECK(!hotkey_ids_.empty()); // Don't start if no hotkey registered.
255 DCHECK(!id_hotkeys_.empty());
256 DCHECK(!id_hotkey_refs_.empty());
257 is_listening_ = true;
258
259 // Start an event tap for observing media keys. Regular keyboard shortcuts
260 // are registered via RegisterEventHotKey.
261 [tap_ startWatchingMediaKeys];
262 }
263
264 void GlobalShortcutListenerMac::StopListening() {
265 DCHECK(is_listening_); // No point if we are not already listening.
266 DCHECK(hotkey_ids_.empty()); // Make sure the set is clean.
267 DCHECK(id_hotkeys_.empty());
268 DCHECK(id_hotkey_refs_.empty());
269 is_listening_ = false;
270
271 [tap_ stopWatchingMediaKeys];
272 }
273
274 void GlobalShortcutListenerMac::RegisterAccelerator(
275 const ui::Accelerator& accelerator,
276 GlobalShortcutListener::Observer* observer) {
277 VLOG(0) << "Registered keyCode: " << accelerator.key_code()
278 << ", modifiers: " << accelerator.modifiers();
279 // Register hotkey if they are keyboard shortcuts.
Finnur 2013/11/19 13:10:05 nit: MediaKeys are keyboard shortcuts, are they no
smus 2013/11/20 04:28:51 Done.
280 if (!IsMediaKey(accelerator))
281 RegisterHotKey(accelerator);
282
283 // Store the hotkey-ID mappings we will need for lookup later.
284 id_hotkeys_[hotkey_id_] = accelerator;
285 hotkey_ids_[accelerator] = hotkey_id_;
286 hotkey_id_ += 1;
287 GlobalShortcutListener::RegisterAccelerator(accelerator, observer);
288 }
289
290 void GlobalShortcutListenerMac::UnregisterAccelerator(
291 const ui::Accelerator& accelerator,
292 GlobalShortcutListener::Observer* observer) {
293 // Unregister the hotkey if it's a keyboard shortcut.
294 if (!IsMediaKey(accelerator))
295 UnregisterHotKey(accelerator);
296
297 // Remove hotkey from the mappings.
298 int id = hotkey_ids_[accelerator];
299 id_hotkeys_.erase(id);
300 hotkey_ids_.erase(accelerator);
301 GlobalShortcutListener::UnregisterAccelerator(accelerator, observer);
302 }
303
304 bool GlobalShortcutListenerMac::OnKeyEvent(EventHotKeyID hotKeyID) {
305 // Look up the accelerator based on this hot key ID.
306 VLOG(0) << "OnKeyEvent! hotKeyID: " << hotKeyID.id;
307 ui::Accelerator accelerator = id_hotkeys_[hotKeyID.id];
308 VLOG(0) << "Key code: " << accelerator.key_code() <<
309 " modifiers: " << accelerator.modifiers();
310 NotifyKeyPressed(accelerator);
311 return true;
312 }
313
314 bool GlobalShortcutListenerMac::IsMediaKey(const ui::Accelerator& accelerator) {
315 // Assume all keys are hot keys unless they have a modifier.
316 return accelerator.modifiers() == 0;
317 }
318
319 // Returns true iff event was handled.
320 bool GlobalShortcutListenerMac::OnMediaKeyEvent(ui::KeyboardCode keyCode) {
321 VLOG(0) << "OnMediaKeyEvent! keyCode: " << keyCode;
322 // Create an accelerator corresponding to the keyCode.
323 ui::Accelerator accelerator(keyCode, 0);
324 // Look for a match with a bound hotkey.
325 if (hotkey_ids_.find(accelerator) != hotkey_ids_.end()) {
326 // If matched, callback to the event handling system.
327 NotifyKeyPressed(accelerator);
328 return true;
329 }
330 return false;
331 }
332
333 void GlobalShortcutListenerMac::RegisterHotKey(
334 const ui::Accelerator& accelerator) {
335 VLOG(0) << "Registering hotkey. Windows keycode: " << accelerator.key_code();
336 EventHotKeyRef hotkey_ref;
337 EventHotKeyID event_hotkey_id;
338 EventHandlerUPP hotkey_function = NewEventHandlerUPP(HotKeyHandler);
339
340 EventTypeSpec event_type;
341 event_type.eventClass = kEventClassKeyboard;
342 event_type.eventKind = kEventHotKeyPressed;
343 InstallApplicationEventHandler(hotkey_function, 1, &event_type, this, NULL);
344
345 // Signature uniquely identifies the application that owns this hotkey.
346 event_hotkey_id.signature = 'chro';
347 event_hotkey_id.id = hotkey_id_;
348
349 // Translate ui::Accelerator modifiers to cmdKey, altKey, etc.
350 int modifiers = 0;
351 modifiers += (accelerator.IsShiftDown() ? shiftKey : 0);
352 modifiers += (accelerator.IsCtrlDown() ? controlKey : 0);
353 modifiers += (accelerator.IsAltDown() ? optionKey : 0);
354 modifiers += (accelerator.IsCmdDown() ? cmdKey : 0);
355
356 unichar character;
357 unichar character_nomods;
358 int key_code = ui::MacKeyCodeForWindowsKeyCode(accelerator.key_code(), 0,
359 &character, &character_nomods);
360 VLOG(0) << "RegisterHotKey. Code: " << key_code << " modifier: " << modifiers;
361
362 // Register the event hot key.
363 RegisterEventHotKey(key_code, modifiers, event_hotkey_id,
364 GetApplicationEventTarget(), 0, &hotkey_ref);
365
366 // Note: hotkey_id_ will be incremented in the caller (RegisterAccelerator).
367 id_hotkey_refs_[hotkey_id_] = hotkey_ref;
368 }
369
370 void GlobalShortcutListenerMac::UnregisterHotKey(
371 const ui::Accelerator& accelerator) {
372 // Get the ref corresponding to this accelerator.
373 int id = hotkey_ids_[accelerator];
374 EventHotKeyRef ref = id_hotkey_refs_[id];
375 // Unregister the event hot key.
376 UnregisterEventHotKey(ref);
377
378 // Remove the event from the mapping.
379 id_hotkey_refs_.erase(id);
380 }
381
382 } // namespace extensions
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698