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

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

Powered by Google App Engine
This is Rietveld 408576698