Chromium Code Reviews| 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 |