diff options
Diffstat (limited to 'AppKit/GTMHotKeyTextField.m')
-rw-r--r-- | AppKit/GTMHotKeyTextField.m | 1009 |
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(¤tLayout); + 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 **)¤tLayoutKind); + 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 + |