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