Index: chrome/browser/extensions/global_shortcut_listener_mac.mm |
=================================================================== |
--- chrome/browser/extensions/global_shortcut_listener_mac.mm (revision 0) |
+++ chrome/browser/extensions/global_shortcut_listener_mac.mm (working copy) |
@@ -0,0 +1,308 @@ |
+// Copyright (c) 2013 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+#include "chrome/browser/extensions/global_shortcut_listener_mac.h" |
+ |
+#import <Cocoa/Cocoa.h> |
+ |
+#include "content/public/browser/browser_thread.h" |
+#include "ui/events/event.h" |
+#include "ui/base/accelerators/accelerator.h" |
+#import "ui/events/keycodes/keyboard_code_conversion_mac.h" |
+ |
+#define SystemDefinedEventMediaKeys 8 |
+#define EVENT_KEY @"event" |
+#define HANDLED_KEY @"handled" |
+ |
+typedef extensions::GlobalShortcutListenerMac GSL; |
+ |
+@interface GlobalShortcutListenerTap : NSObject { |
+ @public |
+ CFMachPortRef eventTap_; |
+ |
+ @private |
+ CFRunLoopSourceRef eventTapSource_; |
+ CFRunLoopRef tapThreadRunLoop_; |
+ GSL *gsl_; |
+} |
+ |
+- (id)initWithGSL:(GSL *)gsl; |
+- (void)startWatchingMediaKeys; |
+- (void)stopWatchingMediaKeys; |
+- (void)handleKeyEvent:(NSEvent *)event; |
+- (void)handleMediaKeyEvent:(NSEvent *)event; |
+- (BOOL)performEventHandlerOnMainThread:(SEL)selector withEvent:(NSEvent*)event; |
+@end |
+ |
+// Processed events should propagate if they aren't handled by any listeners. |
+// Returning event causes the event to propagate to other applications. |
+// Returning NULL prevents the event from propagating. |
+static CGEventRef tapEventCallback( |
+ CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) { |
+ NSAutoreleasePool *pool = [NSAutoreleasePool new]; |
Finnur
2013/11/07 10:33:07
nit: We prefer Foo* foo over Foo *foo. (multiple o
smus
2013/11/16 00:21:44
Done.
|
+ |
+ GlobalShortcutListenerTap *self = (GlobalShortcutListenerTap*) refcon; |
+ |
+ // Handle the timeout case by re-enabling the tap. |
+ if (type == kCGEventTapDisabledByTimeout) { |
+ LOG(ERROR) << "Event tap was disabled by a timeout."; |
+ CGEventTapEnable(self->eventTap_, TRUE); |
+ return event; |
+ } |
+ |
+ // TODO(smus): do some error handling since eventWithCGEvent can fail. |
+ NSEvent *nsEvent = [NSEvent eventWithCGEvent:event]; |
+ |
+ // Handle regular keys. |
+ if (type == kCGEventKeyDown) { |
+ BOOL wasHandled = [self |
+ performEventHandlerOnMainThread:@selector(handleKeyEvent:) |
+ withEvent:nsEvent]; |
+ return (wasHandled ? NULL : event); |
+ } |
+ |
+ // Handle media keys only (PlayPause, NextTrack, PreviousTrack). |
+ if (type == NX_SYSDEFINED && |
+ [nsEvent subtype] == SystemDefinedEventMediaKeys) { |
+ int keyCode = (([nsEvent data1] & 0xFFFF0000) >> 16); |
+ if (keyCode == NX_KEYTYPE_PLAY || keyCode == NX_KEYTYPE_NEXT || |
+ keyCode == NX_KEYTYPE_PREVIOUS || keyCode == NX_KEYTYPE_FAST || |
+ keyCode == NX_KEYTYPE_REWIND) { |
+ BOOL wasHandled = [self |
+ performEventHandlerOnMainThread:@selector(handleMediaKeyEvent:) |
+ withEvent:nsEvent]; |
+ return (wasHandled ? NULL : event); |
+ } |
+ } |
+ [pool drain]; |
+ // By default, pass the event through. |
+ return event; |
+} |
+ |
+@implementation GlobalShortcutListenerTap |
+ |
+- (id)initWithGSL:(GSL*)gsl { |
+ gsl_ = gsl; |
+ return self; |
+} |
+ |
+- (void)eventTapThread { |
+ tapThreadRunLoop_ = CFRunLoopGetCurrent(); |
+ CFRunLoopAddSource(tapThreadRunLoop_, eventTapSource_, |
+ kCFRunLoopCommonModes); |
+ CFRunLoopRun(); |
+} |
+ |
+- (BOOL)performEventHandlerOnMainThread:(SEL)selector |
+ withEvent:(NSEvent*)event { |
+ NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; |
+ [dict setObject:event forKey:EVENT_KEY]; |
+ [self performSelectorOnMainThread:selector |
+ withObject:dict waitUntilDone:YES]; |
+ // Keep track of the result from the main thread to know if the event has |
+ // been handled. |
+ BOOL wasHandled = [[dict objectForKey:HANDLED_KEY] boolValue]; |
+ [dict release]; |
+ return wasHandled; |
+} |
+ |
+- (ui::KeyboardCode)mediaKeyCodeToKeyboardCode:(int)keyCode { |
+ switch (keyCode) { |
+ case NX_KEYTYPE_PLAY: |
+ return ui::VKEY_MEDIA_PLAY_PAUSE; |
+ case NX_KEYTYPE_PREVIOUS: |
+ case NX_KEYTYPE_REWIND: |
+ return ui::VKEY_MEDIA_PREV_TRACK; |
+ case NX_KEYTYPE_NEXT: |
+ case NX_KEYTYPE_FAST: |
+ return ui::VKEY_MEDIA_NEXT_TRACK; |
+ } |
+ return ui::VKEY_UNKNOWN; |
+} |
+ |
+- (void)startWatchingMediaKeys { |
+ // Prevent having multiple mediaKeys threads |
Finnur
2013/11/07 10:33:07
nit: In general, we end all comments in period (na
smus
2013/11/16 00:21:44
Done.
|
+ [self stopWatchingMediaKeys]; |
Finnur
2013/11/07 10:33:07
Thinking out loud: Doesn't starting twice indicate
smus
2013/11/16 00:21:44
Yep, changed to an assertion.
|
+ |
+ // Add an event tap to intercept the system defined media key events |
+ eventTap_ = CGEventTapCreate(kCGSessionEventTap, |
+ kCGHeadInsertEventTap, |
+ kCGEventTapOptionDefault, |
+ CGEventMaskBit(NX_SYSDEFINED) | CGEventMaskBit(kCGEventKeyDown), |
+ tapEventCallback, |
+ self); |
+ assert(eventTap_ != NULL); |
+ |
+ eventTapSource_ = CFMachPortCreateRunLoopSource(kCFAllocatorSystemDefault, |
+ eventTap_, 0); |
+ assert(eventTapSource_ != NULL); |
+ |
+ // Run the event tap in separate thread to prevent blocking UI. |
+ [NSThread detachNewThreadSelector:@selector(eventTapThread) |
+ toTarget:self withObject:nil]; |
+} |
+- (void)stopWatchingMediaKeys { |
Finnur
2013/11/07 10:33:07
nit: linebreak
smus
2013/11/16 00:21:44
Done.
|
+ if (tapThreadRunLoop_) { |
+ CFRunLoopStop(tapThreadRunLoop_); |
+ tapThreadRunLoop_ = nil; |
+ } |
+ |
+ if (eventTap_) { |
+ CFMachPortInvalidate(eventTap_); |
+ CFRelease(eventTap_); |
+ eventTap_ = nil; |
+ } |
+ |
+ if (eventTapSource_) { |
+ CFRelease(eventTapSource_); |
+ eventTapSource_ = nil; |
+ } |
+} |
+ |
+- (void)handleKeyEvent:(NSMutableDictionary *)dict { |
+ NSEvent *event = [dict objectForKey:EVENT_KEY]; |
+ |
+ ui::KeyboardCode keyCode = ui::KeyboardCodeFromNSEvent(event); |
+ int modifiers = 0; |
+ NSUInteger flags = [event modifierFlags]; |
+ modifiers |= (flags & NSShiftKeyMask) ? ui::EF_SHIFT_DOWN : 0; |
+ modifiers |= (flags & NSAlternateKeyMask ) ? ui::EF_ALT_DOWN : 0; |
+ modifiers |= (flags & NSControlKeyMask) ? ui::EF_CONTROL_DOWN : 0; |
+ modifiers |= (flags & NSCommandKeyMask) ? ui::EF_COMMAND_DOWN : 0; |
+ |
+ bool result = gsl_->OnKeyEvent(keyCode, modifiers); |
+ [dict setObject:[NSNumber numberWithBool:result] forKey:HANDLED_KEY]; |
+} |
+ |
+// Event will have been retained in the other thread. |
+- (void)handleMediaKeyEvent:(NSMutableDictionary *)dict { |
+ NSEvent *event = [dict objectForKey:EVENT_KEY]; |
+ |
+ int keyCode = (([event data1] & 0xFFFF0000) >> 16); |
+ int keyFlags = ([event data1] & 0x0000FFFF); |
+ BOOL keyIsPressed = (((keyFlags & 0xFF00) >> 8)) == 0xA; |
+ //int keyRepeat = (keyFlags & 0x1); |
Finnur
2013/11/07 10:33:07
Remove?
smus
2013/11/16 00:21:44
Done.
|
+ |
+ bool result = false; |
+ if (keyIsPressed) { |
+ result = gsl_->OnMediaKeyEvent([self mediaKeyCodeToKeyboardCode:keyCode]); |
+ } |
Finnur
2013/11/07 10:33:07
nit: When the if clause is single line we omit the
smus
2013/11/16 00:21:44
Not my favorite, but I will comply :)
|
+ [dict setObject:[NSNumber numberWithBool:result] forKey:HANDLED_KEY]; |
+} |
+ |
+ |
+ |
Finnur
2013/11/07 10:33:07
nit: lots of extra linebreaks.
smus
2013/11/16 00:21:44
Done.
|
+@end |
+ |
+ |
+using content::BrowserThread; |
+ |
+namespace { |
+ |
+static base::LazyInstance<extensions::GlobalShortcutListenerMac> instance = |
+ LAZY_INSTANCE_INITIALIZER; |
+ |
+} // namespace |
+ |
+namespace extensions { |
+ |
+// static |
+GlobalShortcutListener* GlobalShortcutListener::GetInstance() { |
+ LOG(ERROR) << "GlobalShortcutListener GetInstance"; |
+ CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
+ return instance.Pointer(); |
+} |
+ |
+GlobalShortcutListenerMac::GlobalShortcutListenerMac() |
+ : is_listening_(false) { |
+ CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
+ |
+ // TODO(implementor): Remove this. |
+ LOG(ERROR) << "GlobalShortcutListenerMac object created"; |
+ |
+ tap_.reset([[GlobalShortcutListenerTap alloc] initWithGSL:this]); |
+} |
+ |
+GlobalShortcutListenerMac::~GlobalShortcutListenerMac() { |
+ if (is_listening_) |
+ StopListening(); |
+} |
+ |
+void GlobalShortcutListenerMac::StartListening() { |
+ DCHECK(!is_listening_); // Don't start twice. |
+ DCHECK(!registered_hot_keys_.empty()); // Also don't start if no hotkey is |
+ // registered. |
+ LOG(ERROR) << "GlobalShortcutListenerMac StartListening"; |
+ is_listening_ = true; |
+ |
+ [tap_ startWatchingMediaKeys]; |
+} |
+ |
+void GlobalShortcutListenerMac::StopListening() { |
+ DCHECK(is_listening_); // No point if we are not already listening. |
+ DCHECK(registered_hot_keys_.empty()); // Make sure the set is clean before |
+ // ending. |
+ LOG(ERROR) << "GlobalShortcutListenerMac StopListening"; |
+ is_listening_ = false; |
+ |
+ [tap_ stopWatchingMediaKeys]; |
+} |
+ |
+void GlobalShortcutListenerMac::RegisterAccelerator( |
+ const ui::Accelerator& accelerator, |
+ GlobalShortcutListener::Observer* observer) { |
+ LOG(ERROR) << "GlobalShortcutListenerMac RegisterAccelerator"; |
+ VLOG(0) << "Registered keyCode: " << accelerator.key_code() |
+ << ", modifiers: " << accelerator.modifiers(); |
+ // To implement: |
+ // 1) Convert modifiers to platform specific modifiers. |
+ // 2) Register for the hotkey. |
+ // 3) If not successful, log why. |
+ // 4) Else, call base class RegisterAccelerator. |
+ registered_hot_keys_.insert(accelerator); |
+ GlobalShortcutListener::RegisterAccelerator(accelerator, observer); |
+} |
+ |
+void GlobalShortcutListenerMac::UnregisterAccelerator( |
+ const ui::Accelerator& accelerator, |
+ GlobalShortcutListener::Observer* observer) { |
+ LOG(ERROR) << "GlobalShortcutListenerMac UnregisterAccelerator"; |
+ // To implement: |
+ // 1) Unregister for the hotkey. |
+ // 2) Call base class UnregisterAccelerator. |
+ |
+ registered_hot_keys_.erase(accelerator); |
+ GlobalShortcutListener::UnregisterAccelerator(accelerator, observer); |
+} |
+ |
+// Returns true iff event was handled. |
+bool GlobalShortcutListenerMac::OnKeyEvent(ui::KeyboardCode keyCode, |
+ int modifiers) { |
+ VLOG(0) << "OnKeyEvent! keyCode: " << keyCode << ", modifiers: " << modifiers; |
+ // Create an accelerator corresponding to the keyCode. |
+ ui::Accelerator accelerator(keyCode, modifiers); |
+ // Look for a match with a bound hotkey. |
+ if (registered_hot_keys_.find(accelerator) != registered_hot_keys_.end()) { |
+ // If matched, callback to the event handling system. |
+ instance.Get().NotifyKeyPressed(accelerator); |
+ return true; |
+ } |
+ return false; |
+} |
+ |
+// Returns true iff event was handled. |
+bool GlobalShortcutListenerMac::OnMediaKeyEvent(ui::KeyboardCode keyCode) { |
+ VLOG(0) << "OnMediaKeyEvent! keyCode: " << keyCode; |
+ // Create an accelerator corresponding to the keyCode. |
+ ui::Accelerator accelerator(keyCode, 0); |
+ // Look for a match with a bound hotkey. |
+ if (registered_hot_keys_.find(accelerator) != registered_hot_keys_.end()) { |
+ // If matched, callback to the event handling system. |
+ instance.Get().NotifyKeyPressed(accelerator); |
+ return true; |
+ } |
+ return false; |
+} |
+ |
+} // namespace extensions |