OLD | NEW |
---|---|
(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 | |
OLD | NEW |