aboutsummaryrefslogtreecommitdiff
path: root/AppKit/GTMHotKeyTextField.m
diff options
context:
space:
mode:
Diffstat (limited to 'AppKit/GTMHotKeyTextField.m')
-rw-r--r--AppKit/GTMHotKeyTextField.m1009
1 files changed, 1009 insertions, 0 deletions
diff --git a/AppKit/GTMHotKeyTextField.m b/AppKit/GTMHotKeyTextField.m
new file mode 100644
index 0000000..3a604be
--- /dev/null
+++ b/AppKit/GTMHotKeyTextField.m
@@ -0,0 +1,1009 @@
+// GTMHotKeyTextField.m
+//
+// Copyright 2006-2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy
+// of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+//
+
+#import "GTMHotKeyTextField.h"
+
+#import <Carbon/Carbon.h>
+#import "GTMSystemVersion.h"
+#import "GTMObjectSingleton.h"
+
+#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
+typedef struct __TISInputSource* TISInputSourceRef;
+static TISInputSourceRef(*GTM_TISCopyCurrentKeyboardLayoutInputSource)(void) = NULL;
+static void * (*GTM_TISGetInputSourceProperty)(TISInputSourceRef inputSource,
+ CFStringRef propertyKey) = NULL;
+static CFStringRef kGTM_TISPropertyUnicodeKeyLayoutData = NULL;
+#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
+
+
+@interface GTMHotKeyTextField (PrivateMethods)
+- (void)setupBinding:(id)bound withPath:(NSString *)path;
+- (void)updateDisplayedPrettyString;
++ (BOOL)isValidHotKey:(NSDictionary *)hotKey;
++ (NSString *)displayStringForHotKey:(NSDictionary *)hotKey;
+@end
+
+@interface GTMHotKeyFieldEditor (PrivateMethods)
+- (NSDictionary *)hotKeyDictionary;
+- (void)setHotKeyDictionary:(NSDictionary *)hotKey;
+- (BOOL)shouldBypassEvent:(NSEvent *)theEvent;
+- (void)processEventToHotKeyAndString:(NSEvent *)theEvent;
+- (void)windowResigned:(NSNotification *)notification;
+- (NSDictionary *)hotKeyDictionaryForEvent:(NSEvent *)event;
+@end
+
+@implementation GTMHotKeyTextField
+
+- (void)finalize {
+ if (boundObject_ && boundKeyPath_) {
+ [boundObject_ removeObserver:self forKeyPath:boundKeyPath_];
+ }
+ [super finalize];
+}
+
+- (void)dealloc {
+
+ if (boundObject_ && boundKeyPath_) {
+ [boundObject_ removeObserver:self forKeyPath:boundKeyPath_];
+ }
+ [boundObject_ release];
+ [boundKeyPath_ release];
+ [hotKeyDict_ release];
+ [super dealloc];
+
+}
+
+#pragma mark Bindings
+
+
+- (void)bind:(NSString *)binding toObject:(id)observableController
+ withKeyPath:(NSString *)keyPath
+ options:(NSDictionary *)options {
+
+ if ([binding isEqualToString:NSValueBinding]) {
+ // Update to our new binding
+ [self setupBinding:observableController withPath:keyPath];
+ // TODO: Should deal with the bind options
+ }
+ [super bind:binding
+ toObject:observableController
+ withKeyPath:keyPath
+ options:options];
+
+}
+
+- (void)unbind:(NSString *)binding {
+
+ // Clean up value on unbind
+ if ([binding isEqualToString:NSValueBinding]) {
+ if (boundObject_ && boundKeyPath_) {
+ [boundObject_ removeObserver:self forKeyPath:boundKeyPath_];
+ }
+ [boundObject_ release];
+ boundObject_ = nil;
+ [boundKeyPath_ release];
+ boundKeyPath_ = nil;
+ }
+ [super unbind:binding];
+
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
+ change:(NSDictionary *)change context:(void *)context {
+
+ if ((object == boundObject_) && [boundKeyPath_ isEqualToString:keyPath]) {
+ // Our binding has changed, update
+ id changedValue = [change objectForKey:NSKeyValueChangeNewKey];
+ // NSUserDefaultsController does not appear to pass on the new object and,
+ // perhaps other controllers may not, so if we get a nil or NSNull back
+ // here let's directly retrieve the hotKeyDict_ from the object.
+ if (!changedValue || changedValue == [NSNull null]) {
+ changedValue = [object valueForKeyPath:keyPath];
+ }
+ [hotKeyDict_ autorelease];
+ hotKeyDict_ = [changedValue copy];
+ [self updateDisplayedPrettyString];
+ }
+
+}
+
+// Private convenience method for attaching to a new binding
+- (void)setupBinding:(id)bound withPath:(NSString *)path {
+
+ // Release previous
+ if (boundObject_ && boundKeyPath_) {
+ [boundObject_ removeObserver:self forKeyPath:boundKeyPath_];
+ }
+ [boundObject_ release];
+ [boundKeyPath_ release];
+ // Set new
+ boundObject_ = [bound retain];
+ boundKeyPath_ = [path copy];
+ // Make ourself an observer
+ [boundObject_ addObserver:self
+ forKeyPath:boundKeyPath_
+ options:NSKeyValueObservingOptionNew
+ context:nil];
+ // Pull in any current value
+ [hotKeyDict_ autorelease];
+ hotKeyDict_ = [[boundObject_ valueForKeyPath:boundKeyPath_] copy];
+ // Update the display string
+ [self updateDisplayedPrettyString];
+
+}
+
+#pragma mark Defeating NSControl
+
+- (double)doubleValue {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't take numbers.");
+ return 0.0;
+
+}
+
+- (void)setDoubleValue:(double)value {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't take numbers.");
+ return;
+
+}
+
+- (float)floatValue {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't take numbers.");
+ return 0.0f;
+
+}
+
+- (void)setFloatValue:(float)value {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't take numbers.");
+ return;
+
+}
+
+- (int)intValue {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't take numbers.");
+ return 0;
+
+}
+
+- (void)setIntValue:(int)value {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't take numbers.");
+ return;
+
+}
+
+#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5
+
+- (int)integerValue {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't take numbers.");
+ return 0;
+
+}
+
+- (void)setIntegerValue:(NSInteger)value {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't take numbers.");
+ return;
+
+}
+
+#endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5
+
+- (id)objectValue {
+
+ return [self hotKeyValue];
+
+}
+
+- (void)setObjectValue:(id)object {
+
+ [self setHotKeyValue:object];
+
+}
+
+- (NSString *)stringValue {
+
+ return [[self class] displayStringForHotKey:hotKeyDict_];
+
+}
+
+- (void)setStringValue:(NSString *)string {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields want dictionaries, not strings.");
+ return;
+
+}
+
+- (NSAttributedString *)attributedStringValue {
+
+ NSString *prettyString = [self stringValue];
+ if (!prettyString) return nil;
+ return [[[NSAttributedString alloc] initWithString:prettyString] autorelease];
+
+}
+
+- (void)setAttributedStringValue:(NSAttributedString *)string {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields want dictionaries, not strings.");
+ return;
+
+}
+
+- (void)takeDoubleValueFrom:(id)sender {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't take numbers.");
+ return;
+
+}
+
+- (void)takeFloatValueFrom:(id)sender {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't take numbers.");
+ return;
+
+}
+
+- (void)takeIntValueFrom:(id)sender {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't take numbers.");
+ return;
+
+}
+
+- (void)takeObjectValueFrom:(id)sender {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO,
+ @"Hot key fields want dictionaries via bindings, not from controls.");
+ return;
+
+}
+
+- (void)takeStringValueFrom:(id)sender {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields want dictionaries, not strings.");
+ return;
+
+}
+
+- (id)formatter {
+
+ return nil;
+
+}
+
+- (void)setFormatter:(NSFormatter *)newFormatter {
+
+ // Defeating NSControl
+ _GTMDevAssert(NO, @"Hot key fields don't accept formatters.");
+ return;
+
+}
+
+#pragma mark Hot Key Support
+
++ (BOOL)isValidHotKey:(NSDictionary *)hotKeyDict {
+ if (!hotKeyDict ||
+ ![hotKeyDict isKindOfClass:[NSDictionary class]] ||
+ ![hotKeyDict objectForKey:kGTMHotKeyModifierFlagsKey] ||
+ ![hotKeyDict objectForKey:kGTMHotKeyKeyCodeKey] ||
+ ![hotKeyDict objectForKey:kGTMHotKeyDoubledModifierKey]) {
+ return NO;
+ }
+ return YES;
+}
+
+- (void)setHotKeyValue:(NSDictionary *)hotKey {
+
+ // Sanity only if set, nil is OK
+ if (hotKey && ![[self class] isValidHotKey:hotKey]) {
+ return;
+ }
+
+ // If we are bound we want to round trip through that interface
+ if (boundObject_ && boundKeyPath_) {
+ // If the change is accepted this will call us back as an observer
+ [boundObject_ setValue:hotKey forKeyPath:boundKeyPath_];
+ return;
+ }
+
+ // Otherwise we directly update ourself
+ [hotKeyDict_ autorelease];
+ hotKeyDict_ = [hotKey copy];
+ [self updateDisplayedPrettyString];
+
+}
+
+- (NSDictionary *)hotKeyValue {
+
+ return hotKeyDict_;
+
+}
+
+// Private method to update the displayed text of the field with the
+// user-readable representation.
+- (void)updateDisplayedPrettyString {
+
+ // Basic validation
+ if (![[self class] isValidHotKey:hotKeyDict_]) {
+ [super setStringValue:@""];
+ return;
+ }
+
+ // Pretty string
+ NSString *prettyString = [[self class] displayStringForHotKey:hotKeyDict_];
+ if (!prettyString) {
+ prettyString = @"";
+ }
+ [super setStringValue:prettyString];
+
+}
+
++ (NSString *)displayStringForHotKey:(NSDictionary *)hotKeyDict {
+
+ if (!hotKeyDict) return nil;
+
+ NSBundle *bundle = [NSBundle bundleForClass:[self class]];
+
+ // Modifiers
+ unsigned int flags
+ = [[hotKeyDict objectForKey:kGTMHotKeyModifierFlagsKey] unsignedIntValue];
+ NSString *mods = [GTMHotKeyTextField stringForModifierFlags:flags];
+ if (!mods || ![mods length]) return nil;
+ // Handle double modifier case
+ if ([[hotKeyDict objectForKey:kGTMHotKeyDoubledModifierKey] boolValue]) {
+ return [NSString stringWithFormat:@"%@ + %@", mods, mods];
+ }
+ // Keycode
+ unsigned int keycode
+ = [[hotKeyDict objectForKey:kGTMHotKeyKeyCodeKey] unsignedIntValue];
+ NSString *keystroke = [GTMHotKeyTextField stringForKeycode:keycode
+ useGlyph:NO
+ resourceBundle:bundle];
+ if (!keystroke || ![keystroke length]) return nil;
+ return [NSString stringWithFormat:@"%@%@", mods, keystroke];
+
+}
+
+
+#pragma mark Field Editor Callbacks
+
+- (BOOL)textShouldBeginEditing:(GTMHotKeyFieldEditor *)fieldEditor {
+
+ // Sanity
+ if (![fieldEditor isKindOfClass:[GTMHotKeyFieldEditor class]]) {
+ _GTMDevLog(@"Field editor not appropriate for field, check window delegate");
+ return NO;
+ }
+
+ // We don't call super from here, because we are defeating default behavior
+ // as a result we have to call the delegate ourself.
+ id myDelegate = [self delegate];
+ SEL selector = @selector(control:textShouldBeginEditing:);
+ if ([myDelegate respondsToSelector:selector]) {
+ if (![myDelegate control:self textShouldBeginEditing:fieldEditor]) return NO;
+ }
+
+ // Update the field editor internal hotkey representation
+ [fieldEditor setHotKeyDictionary:hotKeyDict_]; // OK if its nil
+ return YES;
+
+}
+
+- (void)textDidChange:(NSNotification *)notification {
+
+ // Sanity
+ GTMHotKeyFieldEditor *fieldEditor = [notification object];
+ if (![fieldEditor isKindOfClass:[GTMHotKeyFieldEditor class]]) {
+ _GTMDevLog(@"Field editor not appropriate for field, check window delegate");
+ return;
+ }
+
+ // When the field changes we want to read in the current hotkey value so
+ // bindings can validate
+ [self setHotKeyValue:[fieldEditor hotKeyDictionary]];
+
+ // Let super handle the notifications
+ [super textDidChange:notification];
+
+}
+
+- (BOOL)textShouldEndEditing:(GTMHotKeyFieldEditor *)fieldEditor {
+
+ // Sanity
+ if (![fieldEditor isKindOfClass:[GTMHotKeyFieldEditor class]]) {
+ _GTMDevLog(@"Field editor not appropriate for field, check window delegate");
+ return NO;
+ }
+
+ // Again we are defeating default behavior so we have to do delegate handling
+ // ourself. In this case our goal is simply to prevent the superclass from
+ // doing its own KVO, but we can also skip [[self cell] isEntryAcceptable:].
+ // We'll also ignore the delegate control:textShouldEndEditing:. The field
+ // editor is done whether they like it or not.
+ id myDelegate = [self delegate];
+ SEL selector = @selector(control:textShouldEndEditing:);
+ if ([myDelegate respondsToSelector:selector]) {
+ [myDelegate control:self textShouldEndEditing:fieldEditor];
+ }
+
+ // The end is always allowed, so set new value
+ [self setHotKeyValue:[fieldEditor hotKeyDictionary]];
+
+ return YES;
+
+}
+
+#pragma mark Class methods building strings for use w/in the UI.
+
+#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
++ (void)initialize {
+ if (!GTM_TISCopyCurrentKeyboardLayoutInputSource
+ && [GTMSystemVersion isLeopardOrGreater]) {
+ CFBundleRef hiToolbox
+ = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.HIToolbox"));
+ if (hiToolbox) {
+ kGTM_TISPropertyUnicodeKeyLayoutData
+ = *(CFStringRef*)CFBundleGetDataPointerForName(hiToolbox,
+ CFSTR("kTISPropertyUnicodeKeyLayoutData"));
+ GTM_TISCopyCurrentKeyboardLayoutInputSource
+ = CFBundleGetFunctionPointerForName(hiToolbox,
+ CFSTR("TISCopyCurrentKeyboardLayoutInputSource"));
+ GTM_TISGetInputSourceProperty
+ = CFBundleGetFunctionPointerForName(hiToolbox,
+ CFSTR("TISGetInputSourceProperty"));
+ }
+ }
+}
+#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
+
+#pragma mark Useful String Class Methods
+
+// These are not in a category on NSString because this class could be used
+// within multiple preference panes at the same time. If we put it in a category
+// it would require setting up some magic so that the categories didn't conflict
+// between the multiple pref panes. By putting it in the class, you can just
+// #define the class name to something else, and then you won't have any
+// conflicts.
+
++ (NSString *)stringForModifierFlags:(unsigned int)flags {
+
+ UniChar modChars[4]; // We only look for 4 flags
+ unsigned int charCount = 0;
+ // These are in the same order as the menu manager shows them
+ if (flags & NSControlKeyMask) modChars[charCount++] = kControlUnicode;
+ if (flags & NSAlternateKeyMask) modChars[charCount++] = kOptionUnicode;
+ if (flags & NSShiftKeyMask) modChars[charCount++] = kShiftUnicode;
+ if (flags & NSCommandKeyMask) modChars[charCount++] = kCommandUnicode;
+ if (charCount == 0) return nil;
+ return [NSString stringWithCharacters:modChars length:charCount];
+
+}
+
++ (NSString *)stringForKeycode:(UInt16)keycode
+ useGlyph:(BOOL)useGlyph
+ resourceBundle:(NSBundle *)bundle {
+
+ // Some keys never move in any layout (to the best of our knowledge at least)
+ // so we can hard map them.
+ UniChar key = 0;
+ NSString *localizedKey = nil;
+
+ switch (keycode) {
+
+ // Of the hard mapped keys some can be represented with pretty and obvioous
+ // Unicode or simple strings without localization.
+
+ // Arrow keys
+ case 123: key = NSLeftArrowFunctionKey; break;
+ case 124: key = NSRightArrowFunctionKey; break;
+ case 125: key = NSDownArrowFunctionKey; break;
+ case 126: key = NSUpArrowFunctionKey; break;
+ case 122: key = NSF1FunctionKey; break;
+ case 120: key = NSF2FunctionKey; break;
+ case 99: key = NSF3FunctionKey; break;
+ case 118: key = NSF4FunctionKey; break;
+ case 96: key = NSF5FunctionKey; break;
+ case 97: key = NSF6FunctionKey; break;
+ case 98: key = NSF7FunctionKey; break;
+ case 100: key = NSF8FunctionKey; break;
+ case 101: key = NSF9FunctionKey; break;
+ case 109: key = NSF10FunctionKey; break;
+ case 103: key = NSF11FunctionKey; break;
+ case 111: key = NSF12FunctionKey; break;
+ case 105: key = NSF13FunctionKey; break;
+ case 107: key = NSF14FunctionKey; break;
+ case 113: key = NSF15FunctionKey; break;
+ case 106: key = NSF16FunctionKey; break;
+ // Forward delete is a terrible name so we'll use the glyph Apple puts on
+ // their current keyboards
+ case 117: key = 0x2326; break;
+
+ // Now we have keys that can be hard coded but don't have good glyph
+ // representations. Sure, the Apple menu manager has glyphs for them, but
+ // an informal poll of Google developers shows no one really knows what
+ // they mean, so its probably a good idea to use strings. Unfortunately
+ // this also means localization (*sigh*). We'll use the real English
+ // strings here as keys so that even if localization is missed we'll do OK
+ // in output.
+
+ // Whitespace
+ case 36: key = '\r'; localizedKey = @"Return"; break;
+ case 76: key = 0x3; localizedKey = @"Enter"; break;
+ case 48: key = 0x9; localizedKey = @"Tab"; break;
+ case 49: key = 0xA0; localizedKey = @"Space"; break;
+ // Control keys
+ case 51: key = 0x8; localizedKey = @"Delete"; break;
+ case 71: key = NSClearDisplayFunctionKey; localizedKey = @"Clear"; break;
+ case 53: key = 0x1B; localizedKey = @"Esc"; break;
+ case 115: key = NSHomeFunctionKey; localizedKey = @"Home"; break;
+ case 116: key = NSPageUpFunctionKey; localizedKey = @"Page Up"; break;
+ case 119: key = NSEndFunctionKey; localizedKey = @"End"; break;
+ case 121: key = NSPageDownFunctionKey; localizedKey = @"Page Down"; break;
+ case 114: key = NSHelpFunctionKey; localizedKey = @"Help"; break;
+ // Keypad keys
+ // There is no good way we could find to glyph these. We tried a variety
+ // of Unicode glyphs, and the menu manager wouldn't take them. We tried
+ // subscript numbers, circled numbers and superscript numbers with no
+ // luck. It may be a bit confusing to the user, but we're happy to hear
+ // any suggestions.
+ case 65: key = '.'; localizedKey = @"Keypad ."; break;
+ case 67: key = '*'; localizedKey = @"Keypad *"; break;
+ case 69: key = '+'; localizedKey = @"Keypad +"; break;
+ case 75: key = '/'; localizedKey = @"Keypad /"; break;
+ case 78: key = '-'; localizedKey = @"Keypad -"; break;
+ case 81: key = '='; localizedKey = @"Keypad ="; break;
+ case 82: key = '0'; localizedKey = @"Keypad 0"; break;
+ case 83: key = '1'; localizedKey = @"Keypad 1"; break;
+ case 84: key = '2'; localizedKey = @"Keypad 2"; break;
+ case 85: key = '3'; localizedKey = @"Keypad 3"; break;
+ case 86: key = '4'; localizedKey = @"Keypad 4"; break;
+ case 87: key = '5'; localizedKey = @"Keypad 5"; break;
+ case 88: key = '6'; localizedKey = @"Keypad 6"; break;
+ case 89: key = '7'; localizedKey = @"Keypad 7"; break;
+ case 91: key = '8'; localizedKey = @"Keypad 8"; break;
+ case 92: key = '9'; localizedKey = @"Keypad 9"; break;
+
+ }
+
+ // If they asked for strings, and we have one return it. Otherwise, return
+ // any key we've picked.
+ if (!useGlyph && localizedKey) {
+ return NSLocalizedStringFromTableInBundle(localizedKey, @"KeyCode",
+ bundle, @"");
+ } else if (key != 0) {
+ return [NSString stringWithFormat:@"%C", key];
+ }
+
+ // Everything else should be printable so look it up in the current keyboard
+ UCKeyboardLayout *uchrData = NULL;
+
+ OSStatus err = noErr;
+#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
+ // layout
+ KeyboardLayoutRef currentLayout = NULL;
+ // Get the layout kind
+ SInt32 currentLayoutKind = -1;
+ if ([GTMSystemVersion isLeopardOrGreater]
+ && kGTM_TISPropertyUnicodeKeyLayoutData
+ && GTM_TISGetInputSourceProperty
+ && GTM_TISCopyCurrentKeyboardLayoutInputSource) {
+ // On Leopard we use the new improved TIS interfaces which work for input
+ // sources as well as keyboard layouts.
+ TISInputSourceRef inputSource
+ = GTM_TISCopyCurrentKeyboardLayoutInputSource();
+ if (inputSource) {
+ CFDataRef uchrDataRef
+ = GTM_TISGetInputSourceProperty(inputSource,
+ kGTM_TISPropertyUnicodeKeyLayoutData);
+ if(uchrDataRef) {
+ uchrData = (UCKeyboardLayout*)CFDataGetBytePtr(uchrDataRef);
+ }
+ CFRelease(inputSource);
+ }
+ } else {
+ // Tiger we use keyboard layouts as it's the best we can officially do.
+ err = KLGetCurrentKeyboardLayout(&currentLayout);
+ if (err != noErr) { // COV_NF_START
+ _GTMDevLog(@"failed to fetch the keyboard layout, err=%d", err);
+ return nil;
+ } // COV_NF_END
+
+ err = KLGetKeyboardLayoutProperty(currentLayout,
+ kKLKind,
+ (const void **)&currentLayoutKind);
+ if (err != noErr) { // COV_NF_START
+ _GTMDevLog(@"failed to fetch the keyboard layout kind property, err=%d",
+ err);
+ return nil;
+ } // COV_NF_END
+
+ if (currentLayoutKind != kKLKCHRKind) {
+ err = KLGetKeyboardLayoutProperty(currentLayout,
+ kKLuchrData,
+ (const void **)&uchrData);
+ if (err != noErr) { // COV_NF_START
+ _GTMDevLog(@"failed to fetch the keyboard layout uchar data, err=%d",
+ err);
+ return nil;
+ } // COV_NF_END
+ }
+ }
+#else
+ TISInputSourceRef inputSource = TISCopyCurrentKeyboardLayoutInputSource();
+ if (inputSource) {
+ CFDataRef uchrDataRef
+ = TISGetInputSourceProperty(inputSource, kTISPropertyUnicodeKeyLayoutData);
+ if(uchrDataRef) {
+ uchrData = (UCKeyboardLayout*)CFDataGetBytePtr(uchrDataRef);
+ }
+ CFRelease(inputSource);
+ }
+#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
+
+ NSString *keystrokeString = nil;
+ if (uchrData) {
+ // uchr layout data is available, this is our preference
+ UniCharCount uchrCharLength = 0;
+ UniChar uchrChars[256] = { 0 };
+ UInt32 uchrDeadKeyState = 0;
+ err = UCKeyTranslate(uchrData,
+ keycode,
+ kUCKeyActionDisplay,
+ 0, // No modifiers
+ LMGetKbdType(),
+ kUCKeyTranslateNoDeadKeysMask,
+ &uchrDeadKeyState,
+ sizeof(uchrChars) / sizeof(UniChar),
+ &uchrCharLength,
+ uchrChars);
+ if (err != noErr) { // COV_NF_START
+ _GTMDevLog(@"failed to translate the keycode, err=%d", err);
+ return nil;
+ } // COV_NF_END
+ if (uchrCharLength < 1) return nil;
+ keystrokeString = [NSString stringWithCharacters:uchrChars
+ length:uchrCharLength];
+ }
+#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
+ else if (currentLayoutKind == kKLKCHRKind) {
+ // Only KCHR layout data is available, go old school
+ void *KCHRData = NULL;
+ err = KLGetKeyboardLayoutProperty(currentLayout, kKLKCHRData,
+ (const void **)&KCHRData);
+ if (err != noErr) { // COV_NF_START
+ _GTMDevLog(@"failed to fetch the keyboard layout uchar data, err=%d",
+ err);
+ return nil;
+ } // COV_NF_END
+ // Turn into character code
+ UInt32 keyTranslateState = 0;
+ UInt32 twoKCHRChars = KeyTranslate(KCHRData, keycode, &keyTranslateState);
+ if (!twoKCHRChars) return nil;
+ // Unpack the fields
+ char firstChar = (char)((twoKCHRChars & 0x00FF0000) >> 16);
+ char secondChar = (char)(twoKCHRChars & 0x000000FF);
+ // May have one or two characters
+ if (firstChar && secondChar) {
+ NSString *str1
+ = [[[NSString alloc] initWithBytes:&firstChar
+ length:1
+ encoding:NSMacOSRomanStringEncoding] autorelease];
+ NSString *str2
+ = [[[NSString alloc] initWithBytes:&secondChar
+ length:1
+ encoding:NSMacOSRomanStringEncoding] autorelease];
+ keystrokeString = [NSString stringWithFormat:@"%@%@",
+ [str1 uppercaseString],
+ [str2 uppercaseString]];
+ } else {
+ keystrokeString = [[[NSString alloc] initWithBytes:&secondChar
+ length:1
+ encoding:NSMacOSRomanStringEncoding] autorelease];
+ [keystrokeString uppercaseString];
+ }
+ }
+#endif // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
+
+ // Sanity we got a stroke
+ if (!keystrokeString || ![keystrokeString length]) return nil;
+
+ // Sanity check the keystroke string for unprintable characters
+ NSMutableCharacterSet *validChars
+ = [[[NSCharacterSet alphanumericCharacterSet] mutableCopy] autorelease];
+ [validChars formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]];
+ [validChars formUnionWithCharacterSet:[NSCharacterSet symbolCharacterSet]];
+ for (unsigned int i = 0; i < [keystrokeString length]; i++) {
+ if (![validChars characterIsMember:[keystrokeString characterAtIndex:i]]) {
+ return nil;
+ }
+ }
+
+ if (!useGlyph) {
+ // menus want glyphs in the original lowercase forms, so we only upper this
+ // if we aren't using it as a glyph.
+ keystrokeString = [keystrokeString uppercaseString];
+ }
+
+ return keystrokeString;
+
+}
+
+@end
+
+@implementation GTMHotKeyFieldEditor
+
+GTMOBJECT_SINGLETON_BOILERPLATE(GTMHotKeyFieldEditor, sharedHotKeyFieldEditor)
+
+- (id)init {
+
+ self = [super init];
+ if (!self) return nil;
+ [self setFieldEditor:YES]; // We are a field editor
+
+ return self;
+
+}
+
+- (void)dealloc {
+
+ [hotKeyDict_ release];
+ [super dealloc];
+
+}
+
+- (NSArray *)acceptableDragTypes {
+
+ // Don't take drags
+ return [NSArray array];
+
+}
+
+- (NSArray *)readablePasteboardTypes {
+
+ // No pasting
+ return [NSArray array];
+
+}
+
+- (NSArray *)writablePasteboardTypes {
+
+ // No copying
+ return [NSArray array];
+
+}
+
+- (BOOL)becomeFirstResponder {
+
+ // We need to lose focus any time the window is not key
+ NSNotificationCenter *dc = [NSNotificationCenter defaultCenter];
+ [dc addObserver:self
+ selector:@selector(windowResigned:)
+ name:NSWindowDidResignKeyNotification
+ object:[self window]];
+ return [super becomeFirstResponder];
+
+}
+
+- (BOOL)resignFirstResponder {
+
+ // No longer interested in window resign
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ return [super resignFirstResponder];
+
+}
+
+// Private method we use to get out of global hotkey capture when the window
+// is no longer front
+- (void)windowResigned:(NSNotification *)notification {
+
+ // Lose our focus
+ [[self window] makeFirstResponder:[self window]];
+
+}
+
+- (BOOL)shouldDrawInsertionPoint {
+
+ // Show an insertion point, because we'll kill our own focus after
+ // each entry
+ return YES;
+
+}
+
+- (NSRange)selectionRangeForProposedRange:(NSRange)proposedSelRange
+ granularity:(NSSelectionGranularity)granularity {
+
+ // Always select everything
+ return NSMakeRange(0, [[self textStorage] length]);
+
+}
+
+- (void)keyDown:(NSEvent *)theEvent {
+
+ if ([self shouldBypassEvent:theEvent]) {
+ [super keyDown:theEvent];
+ } else {
+ // Try to eat the event
+ [self processEventToHotKeyAndString:theEvent];
+ }
+
+}
+
+- (BOOL)performKeyEquivalent:(NSEvent *)theEvent {
+
+ if ([self shouldBypassEvent:theEvent]) {
+ return [super performKeyEquivalent:theEvent];
+ } else {
+ // We always eat these key strokes while we have focus
+ [self processEventToHotKeyAndString:theEvent];
+ return YES;
+ }
+
+}
+
+// Private do method that tell us to ignore certain events
+- (BOOL)shouldBypassEvent:(NSEvent *)theEvent {
+
+ UInt16 keyCode = [theEvent keyCode];
+ NSUInteger modifierFlags
+ = [theEvent modifierFlags] & NSDeviceIndependentModifierFlagsMask;
+
+ // Ignore all events containing tabs. They have special meaning to fields
+ // and some (Cmd Tab variants) are always consumed by the Dock, so users
+ // just shouldn't be able to use them.
+ if (keyCode == 48) { // Tab
+ // Just to be extra clear if the user is trying to use Dock hotkeys beep
+ // at them
+ if ((modifierFlags == NSCommandKeyMask) ||
+ (modifierFlags == (NSCommandKeyMask | NSShiftKeyMask))) {
+ NSBeep();
+ }
+ return YES;
+ }
+
+ // Don't eat Cmd-Q. Users could have it as a hotkey, but its more likely
+ // they're trying to quit
+ if ((keyCode == 12) && (modifierFlags == NSCommandKeyMask)) {
+ return YES;
+ }
+ // Same for Cmd-W, user is probably trying to close the window
+ if ((keyCode == 13) && (modifierFlags == NSCommandKeyMask)) {
+ return YES;
+ }
+
+ return NO;
+
+}
+
+// Private method that turns events into strings and dictionaries for our
+// hotkey plumbing.
+- (void)processEventToHotKeyAndString:(NSEvent *)theEvent {
+
+ // Construct a dictionary of the event as a hotkey pref
+ NSDictionary *newHotKey = [self hotKeyDictionaryForEvent:theEvent];
+ if (!newHotKey) {
+ NSBeep();
+ return; // No action, but don't give up focus
+ }
+ NSString *prettyString = [GTMHotKeyTextField displayStringForHotKey:newHotKey];
+ if (!prettyString) {
+ NSBeep();
+ return;
+ }
+
+ // Replacement range
+ NSRange replaceRange = NSMakeRange(0, [[self textStorage] length]);
+
+ // Ask for permission to replace
+ if (![self shouldChangeTextInRange:replaceRange
+ replacementString:prettyString]) {
+ // If replacement was disallowed, change nothing, including hotKeyDict_
+ NSBeep();
+ return;
+ }
+
+ // Replacement was allowed, update
+ [hotKeyDict_ autorelease];
+ hotKeyDict_ = [newHotKey retain];
+
+ // Set string on self, allowing super to handle attribute copying
+ [self setString:prettyString];
+
+ // Finish the change
+ [self didChangeText];
+
+ // Force editing to end. This sends focus off into space slightly, but
+ // its better than constantly capturing user events. This is exactly
+ // like the Apple editor in their Keyboard pref pane.
+ [[[self delegate] cell] endEditing:self];
+
+}
+
+- (NSDictionary *)hotKeyDictionary {
+
+ return hotKeyDict_;
+
+}
+
+- (void)setHotKeyDictionary:(NSDictionary *)hotKey {
+
+ [hotKeyDict_ autorelease];
+ hotKeyDict_ = [hotKey copy];
+ // Update content
+ NSString *prettyString = nil;
+ if (hotKeyDict_) {
+ prettyString = [GTMHotKeyTextField displayStringForHotKey:hotKey];
+ }
+ if (!prettyString) {
+ prettyString = @"";
+ }
+ [self setString:prettyString];
+
+}
+
+- (NSDictionary *)hotKeyDictionaryForEvent:(NSEvent *)event{
+
+ if (!event) return nil;
+
+ // Check event
+ NSUInteger flags = [event modifierFlags];
+ UInt16 keycode = [event keyCode];
+ // If the event has no modifiers do nothing
+ NSUInteger allModifiers = (NSCommandKeyMask | NSAlternateKeyMask |
+ NSControlKeyMask | NSShiftKeyMask);
+ if (!(flags & allModifiers)) return nil;
+ // If the event has high bits in keycode do nothing
+ if (keycode & 0xFF00) return nil;
+
+ // Clean the flags to only contain things we care about
+ UInt32 cleanFlags = 0;
+ if (flags & NSCommandKeyMask) cleanFlags |= NSCommandKeyMask;
+ if (flags & NSAlternateKeyMask) cleanFlags |= NSAlternateKeyMask;
+ if (flags & NSControlKeyMask) cleanFlags |= NSControlKeyMask;
+ if (flags & NSShiftKeyMask) cleanFlags |= NSShiftKeyMask;
+
+ return [NSDictionary dictionaryWithObjectsAndKeys:
+ [NSNumber numberWithBool:NO],
+ kGTMHotKeyDoubledModifierKey,
+ [NSNumber numberWithUnsignedInt:keycode],
+ kGTMHotKeyKeyCodeKey,
+ [NSNumber numberWithUnsignedInt:cleanFlags],
+ kGTMHotKeyModifierFlagsKey,
+ nil];
+
+}
+@end
+