PageRenderTime 140ms CodeModel.GetById 11ms app.highlight 122ms RepoModel.GetById 1ms app.codeStats 1ms

/core/externals/google-toolbox-for-mac/AppKit/GTMHotKeyTextField.m

http://macfuse.googlecode.com/
Objective C | 781 lines | 565 code | 96 blank | 120 comment | 114 complexity | 936daa05d8c87862b4f9f1998b68aa8f MD5 | raw file
  1//  GTMHotKeyTextField.m
  2//
  3//  Copyright 2006-2010 Google Inc.
  4//
  5//  Licensed under the Apache License, Version 2.0 (the "License"); you may not
  6//  use this file except in compliance with the License.  You may obtain a copy
  7//  of the License at
  8//
  9//  http://www.apache.org/licenses/LICENSE-2.0
 10//
 11//  Unless required by applicable law or agreed to in writing, software
 12//  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 13//  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 14//  License for the specific language governing permissions and limitations under
 15//  the License.
 16//
 17
 18#import "GTMHotKeyTextField.h"
 19
 20#import <Carbon/Carbon.h>
 21
 22#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
 23#import "GTMSystemVersion.h"
 24typedef struct __TISInputSource* TISInputSourceRef;
 25
 26static TISInputSourceRef(*GTM_TISCopyCurrentKeyboardLayoutInputSource)(void) = NULL;
 27static void * (*GTM_TISGetInputSourceProperty)(TISInputSourceRef inputSource,
 28                                               CFStringRef propertyKey) = NULL;
 29static CFStringRef kGTM_TISPropertyUnicodeKeyLayoutData = NULL;
 30#endif  // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
 31
 32@interface GTMHotKeyTextFieldCell (PrivateMethods)
 33- (void)setupBinding:(id)bound withPath:(NSString *)path;
 34- (void)updateDisplayedPrettyString;
 35+ (NSString *)displayStringForHotKey:(GTMHotKey *)hotKey;
 36+ (BOOL)doesKeyCodeRequireModifier:(UInt16)keycode;
 37@end
 38
 39@interface GTMHotKeyFieldEditor (PrivateMethods)
 40- (GTMHotKeyTextFieldCell *)cell;
 41- (void)setCell:(GTMHotKeyTextFieldCell *)cell;
 42- (BOOL)shouldBypassEvent:(NSEvent *)theEvent;
 43- (void)processEventToHotKeyAndString:(NSEvent *)theEvent;
 44- (void)windowResigned:(NSNotification *)notification;
 45- (GTMHotKey *)hotKeyForEvent:(NSEvent *)event;
 46@end
 47
 48@implementation GTMHotKey
 49
 50+ (id)hotKeyWithKeyCode:(NSUInteger)keyCode
 51              modifiers:(NSUInteger)modifiers
 52     useDoubledModifier:(BOOL)doubledModifier {
 53  return [[[self alloc] initWithKeyCode:keyCode
 54                              modifiers:modifiers
 55                     useDoubledModifier:doubledModifier] autorelease];
 56}
 57
 58- (id)initWithKeyCode:(NSUInteger)keyCode
 59            modifiers:(NSUInteger)modifiers
 60   useDoubledModifier:(BOOL)doubledModifier {
 61  if ((self = [super init])) {
 62    modifiers_ = modifiers;
 63    keyCode_ = keyCode;
 64    doubledModifier_ = doubledModifier;
 65  }
 66  return self;
 67}
 68
 69- (NSUInteger)modifiers {
 70  return modifiers_;
 71}
 72
 73- (NSUInteger)keyCode {
 74  return keyCode_;
 75}
 76
 77- (BOOL)doubledModifier {
 78  return doubledModifier_;
 79}
 80
 81- (BOOL)isEqual:(id)object {
 82  return [object isKindOfClass:[GTMHotKey class]]
 83    && [object modifiers] == [self modifiers]
 84    && [(GTMHotKey *)object keyCode] == [self keyCode]
 85    && [object doubledModifier] == [self doubledModifier];
 86}
 87
 88- (NSUInteger)hash {
 89  return [self modifiers] + [self keyCode] + [self doubledModifier];
 90}
 91
 92- (id)copyWithZone:(NSZone *)zone {
 93  return NSCopyObject(self, 0, zone);
 94}
 95
 96- (NSString *)description {
 97  return [NSString stringWithFormat:@"<%@ %p> - %@",
 98          [self class], self,
 99          [GTMHotKeyTextFieldCell displayStringForHotKey:self]];
100}
101
102@end
103
104@implementation GTMHotKeyTextField
105
106+ (Class)cellClass {
107  return [GTMHotKeyTextFieldCell class];
108}
109
110@end
111
112@implementation GTMHotKeyTextFieldCell
113- (void)dealloc {
114  [hotKey_ release];
115  [super dealloc];
116}
117
118- (id)copyWithZone:(NSZone *)zone {
119  GTMHotKeyTextFieldCell *copy = [super copyWithZone:zone];
120  copy->hotKey_ = nil;
121  [copy setObjectValue:[self objectValue]];
122  return copy;
123}
124
125#pragma mark Defeating NSCell
126
127- (void)logBadValueAccess {
128  _GTMDevLog(@"Hot key fields want hot key dictionaries as object values.");
129}
130
131- (id)objectValue {
132  return hotKey_;
133}
134
135- (void)setObjectValue:(id)object {
136  // Sanity only if set, nil is OK
137  if (object && ![object isKindOfClass:[GTMHotKey class]]) {
138    [self logBadValueAccess];
139    return;
140  }
141  if (![hotKey_ isEqual:object]) {
142    // Otherwise we directly update ourself
143    [hotKey_ autorelease];
144    hotKey_ = [object copy];
145    [self updateDisplayedPrettyString];
146  }
147}
148
149- (NSString *)stringValue {
150  NSString *value = [[self class] displayStringForHotKey:hotKey_];
151  if (!value) {
152    value = @"";
153  }
154  return value;
155}
156
157- (void)setStringValue:(NSString *)string {
158  // Since we are a text cell, lots of AppKit objects will attempt to
159  // set out string value. Our Field editor should already have done
160  // that for us, so check to make sure what AppKit is setting us to is
161  // what we expect.
162  if (![string isEqual:[self stringValue]]) {
163    [self logBadValueAccess];
164  }
165}
166
167- (NSAttributedString *)attributedStringValue {
168  NSAttributedString *attrString = nil;
169  NSString *prettyString = [self stringValue];
170  if (prettyString) {
171    attrString = [[[NSAttributedString alloc]
172                   initWithString:prettyString] autorelease];
173  }
174  return attrString;
175}
176
177- (void)setAttributedStringValue:(NSAttributedString *)string {
178  [self logBadValueAccess];
179}
180
181- (id)formatter {
182  return nil;
183}
184
185- (void)setFormatter:(NSFormatter *)newFormatter {
186  if (newFormatter) {
187    // Defeating NSCell
188    _GTMDevLog(@"Hot key fields don't accept formatters.");
189  }
190}
191
192- (id)_fieldEditor {
193  GTMHotKeyFieldEditor *editor = [GTMHotKeyFieldEditor sharedHotKeyFieldEditor];
194  [editor setCell:self];
195  return editor;
196}
197
198#pragma mark Hot Key Support
199
200// Private method to update the displayed text of the field with the
201// user-readable representation.
202- (void)updateDisplayedPrettyString {
203  // Pretty string
204  NSString *prettyString = [[self class] displayStringForHotKey:hotKey_];
205  if (!prettyString) {
206    prettyString = @"";
207  }
208  [super setObjectValue:prettyString];
209
210}
211
212+ (NSString *)displayStringForHotKey:(GTMHotKey *)hotKey {
213  if (!hotKey) return nil;
214
215  NSBundle *bundle = [NSBundle bundleForClass:[self class]];
216
217  // Modifiers
218  NSUInteger modifiers = [hotKey modifiers];
219  NSString *mods = [[self class] stringForModifierFlags:modifiers];
220  if (modifiers && ![mods length]) return nil;
221  // Handle double modifier case
222  if ([hotKey doubledModifier]) {
223    return [NSString stringWithFormat:@"%@ + %@", mods, mods];
224  }
225  // Keycode
226  NSUInteger keycode = [hotKey keyCode];
227  NSString *keystroke = [[self class] stringForKeycode:keycode
228                                              useGlyph:NO
229                                        resourceBundle:bundle];
230  if (!keystroke || ![keystroke length]) return nil;
231  if ([[self class] doesKeyCodeRequireModifier:keycode]
232      && ![mods length]) {
233    return nil;
234  }
235
236  return [NSString stringWithFormat:@"%@%@", mods, keystroke];
237}
238
239#pragma mark Class methods building strings for use w/in the UI.
240
241#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
242+ (void)initialize {
243  if (!GTM_TISCopyCurrentKeyboardLayoutInputSource
244      && [GTMSystemVersion isLeopardOrGreater]) {
245    CFBundleRef hiToolbox
246      = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.HIToolbox"));
247    if (hiToolbox) {
248      kGTM_TISPropertyUnicodeKeyLayoutData
249        = *(CFStringRef*)CFBundleGetDataPointerForName(hiToolbox,
250                                    CFSTR("kTISPropertyUnicodeKeyLayoutData"));
251      GTM_TISCopyCurrentKeyboardLayoutInputSource
252        = CFBundleGetFunctionPointerForName(hiToolbox,
253                             CFSTR("TISCopyCurrentKeyboardLayoutInputSource"));
254      GTM_TISGetInputSourceProperty
255        = CFBundleGetFunctionPointerForName(hiToolbox,
256                                           CFSTR("TISGetInputSourceProperty"));
257    }
258  }
259}
260#endif  // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
261
262#pragma mark Useful String Class Methods
263
264+ (BOOL)doesKeyCodeRequireModifier:(UInt16)keycode {
265  BOOL doesRequire = YES;
266  switch(keycode) {
267    // These are the keycodes that map to the
268    //unichars in the associated comment.
269    case 122:  //  NSF1FunctionKey
270    case 120:  //  NSF2FunctionKey
271    case 99:   //  NSF3FunctionKey
272    case 118:  //  NSF4FunctionKey
273    case 96:   //  NSF5FunctionKey
274    case 97:   //  NSF6FunctionKey
275    case 98:   //  NSF7FunctionKey
276    case 100:  //  NSF8FunctionKey
277    case 101:  //  NSF9FunctionKey
278    case 109:  //  NSF10FunctionKey
279    case 103:  //  NSF11FunctionKey
280    case 111:  //  NSF12FunctionKey
281    case 105:  //  NSF13FunctionKey
282    case 107:  //  NSF14FunctionKey
283    case 113:  //  NSF15FunctionKey
284    case 106:  //  NSF16FunctionKey
285      doesRequire = NO;
286      break;
287    default:
288      doesRequire = YES;
289      break;
290  }
291  return doesRequire;
292}
293
294// These are not in a category on NSString because this class could be used
295// within multiple preference panes at the same time. If we put it in a category
296// it would require setting up some magic so that the categories didn't conflict
297// between the multiple pref panes. By putting it in the class, you can just
298// #define the class name to something else, and then you won't have any
299// conflicts.
300
301+ (NSString *)stringForModifierFlags:(NSUInteger)flags {
302  UniChar modChars[4];  // We only look for 4 flags
303  unsigned int charCount = 0;
304  // These are in the same order as the menu manager shows them
305  if (flags & NSControlKeyMask) modChars[charCount++] = kControlUnicode;
306  if (flags & NSAlternateKeyMask) modChars[charCount++] = kOptionUnicode;
307  if (flags & NSShiftKeyMask) modChars[charCount++] = kShiftUnicode;
308  if (flags & NSCommandKeyMask) modChars[charCount++] = kCommandUnicode;
309  if (charCount == 0) return @"";
310  return [NSString stringWithCharacters:modChars length:charCount];
311}
312
313+ (NSString *)stringForKeycode:(UInt16)keycode
314                      useGlyph:(BOOL)useGlyph
315                resourceBundle:(NSBundle *)bundle {
316  // Some keys never move in any layout (to the best of our knowledge at least)
317  // so we can hard map them.
318  UniChar key = 0;
319  NSString *localizedKey = nil;
320
321  switch (keycode) {
322
323      // Of the hard mapped keys some can be represented with pretty and obvioous
324      // Unicode or simple strings without localization.
325
326      // Arrow keys
327    case 123: key = NSLeftArrowFunctionKey; break;
328    case 124: key = NSRightArrowFunctionKey; break;
329    case 125: key = NSDownArrowFunctionKey; break;
330    case 126: key = NSUpArrowFunctionKey; break;
331    case 122: key = NSF1FunctionKey; localizedKey = @"F1"; break;
332    case 120: key = NSF2FunctionKey; localizedKey = @"F2"; break;
333    case 99:  key = NSF3FunctionKey; localizedKey = @"F3"; break;
334    case 118: key = NSF4FunctionKey; localizedKey = @"F4"; break;
335    case 96:  key = NSF5FunctionKey; localizedKey = @"F5"; break;
336    case 97:  key = NSF6FunctionKey; localizedKey = @"F6"; break;
337    case 98:  key = NSF7FunctionKey; localizedKey = @"F7"; break;
338    case 100: key = NSF8FunctionKey; localizedKey = @"F8"; break;
339    case 101: key = NSF9FunctionKey; localizedKey = @"F9"; break;
340    case 109: key = NSF10FunctionKey; localizedKey = @"F10"; break;
341    case 103: key = NSF11FunctionKey; localizedKey = @"F11"; break;
342    case 111: key = NSF12FunctionKey; localizedKey = @"F12"; break;
343    case 105: key = NSF13FunctionKey; localizedKey = @"F13"; break;
344    case 107: key = NSF14FunctionKey; localizedKey = @"F14"; break;
345    case 113: key = NSF15FunctionKey; localizedKey = @"F15"; break;
346    case 106: key = NSF16FunctionKey; localizedKey = @"F16"; break;
347      // Forward delete is a terrible name so we'll use the glyph Apple puts on
348      // their current keyboards
349    case 117: key = 0x2326; break;
350
351      // Now we have keys that can be hard coded but don't have good glyph
352      // representations. Sure, the Apple menu manager has glyphs for them, but
353      // an informal poll of Google developers shows no one really knows what
354      // they mean, so its probably a good idea to use strings. Unfortunately
355      // this also means localization (*sigh*). We'll use the real English
356      // strings here as keys so that even if localization is missed we'll do OK
357      // in output.
358
359      // Whitespace
360    case 36: key = '\r'; localizedKey = @"Return"; break;
361    case 76: key = 0x3; localizedKey = @"Enter"; break;
362    case 48: key = 0x9; localizedKey = @"Tab"; break;
363      // 0x2423 is the Open Box
364    case 49: key = 0x2423; localizedKey = @"Space"; break;
365      // Control keys
366    case 51: key = 0x8; localizedKey = @"Delete"; break;
367    case 71: key = NSClearDisplayFunctionKey; localizedKey = @"Clear"; break;
368    case 53: key = 0x1B; localizedKey = @"Esc"; break;
369    case 115: key = NSHomeFunctionKey; localizedKey = @"Home"; break;
370    case 116: key = NSPageUpFunctionKey; localizedKey = @"Page Up"; break;
371    case 119: key = NSEndFunctionKey; localizedKey = @"End"; break;
372    case 121: key = NSPageDownFunctionKey; localizedKey = @"Page Down"; break;
373    case 114: key = NSHelpFunctionKey; localizedKey = @"Help"; break;
374      // Keypad keys
375      // There is no good way we could find to glyph these. We tried a variety
376      // of Unicode glyphs, and the menu manager wouldn't take them. We tried
377      // subscript numbers, circled numbers and superscript numbers with no
378      // luck.  It may be a bit confusing to the user, but we're happy to hear
379      // any suggestions.
380    case 65: key = '.'; localizedKey = @"Keypad ."; break;
381    case 67: key = '*'; localizedKey = @"Keypad *"; break;
382    case 69: key = '+'; localizedKey = @"Keypad +"; break;
383    case 75: key = '/'; localizedKey = @"Keypad /"; break;
384    case 78: key = '-'; localizedKey = @"Keypad -"; break;
385    case 81: key = '='; localizedKey = @"Keypad ="; break;
386    case 82: key = '0'; localizedKey = @"Keypad 0"; break;
387    case 83: key = '1'; localizedKey = @"Keypad 1"; break;
388    case 84: key = '2'; localizedKey = @"Keypad 2"; break;
389    case 85: key = '3'; localizedKey = @"Keypad 3"; break;
390    case 86: key = '4'; localizedKey = @"Keypad 4"; break;
391    case 87: key = '5'; localizedKey = @"Keypad 5"; break;
392    case 88: key = '6'; localizedKey = @"Keypad 6"; break;
393    case 89: key = '7'; localizedKey = @"Keypad 7"; break;
394    case 91: key = '8'; localizedKey = @"Keypad 8"; break;
395    case 92: key = '9'; localizedKey = @"Keypad 9"; break;
396
397  }
398
399  // If they asked for strings, and we have one return it.  Otherwise, return
400  // any key we've picked.
401  if (!useGlyph && localizedKey) {
402    return NSLocalizedStringFromTableInBundle(localizedKey,
403                                              @"GTMHotKeyTextField",
404                                              bundle,
405                                              @"");
406  } else if (key != 0) {
407    return [NSString stringWithFormat:@"%C", key];
408  }
409
410  // Everything else should be printable so look it up in the current keyboard
411  UCKeyboardLayout *uchrData = NULL;
412
413  OSStatus err = noErr;
414#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
415  // layout
416  KeyboardLayoutRef  currentLayout = NULL;
417  // Get the layout kind
418  SInt32 currentLayoutKind = -1;
419  if ([GTMSystemVersion isLeopardOrGreater]
420      && kGTM_TISPropertyUnicodeKeyLayoutData
421      && GTM_TISGetInputSourceProperty
422      && GTM_TISCopyCurrentKeyboardLayoutInputSource) {
423    // On Leopard we use the new improved TIS interfaces which work for input
424    // sources as well as keyboard layouts.
425    TISInputSourceRef inputSource
426      = GTM_TISCopyCurrentKeyboardLayoutInputSource();
427    if (inputSource) {
428      CFDataRef uchrDataRef
429        = GTM_TISGetInputSourceProperty(inputSource,
430                                        kGTM_TISPropertyUnicodeKeyLayoutData);
431      if(uchrDataRef) {
432        uchrData = (UCKeyboardLayout*)CFDataGetBytePtr(uchrDataRef);
433      }
434      CFRelease(inputSource);
435    }
436  } else {
437    // Tiger we use keyboard layouts as it's the best we can officially do.
438    err = KLGetCurrentKeyboardLayout(&currentLayout);
439    if (err != noErr) { // COV_NF_START
440      _GTMDevLog(@"failed to fetch the keyboard layout, err=%d", err);
441      return nil;
442    }  // COV_NF_END
443
444    err = KLGetKeyboardLayoutProperty(currentLayout,
445                                      kKLKind,
446                                      (const void **)&currentLayoutKind);
447    if (err != noErr) { // COV_NF_START
448      _GTMDevLog(@"failed to fetch the keyboard layout kind property, err=%d",
449                 err);
450      return nil;
451    }  // COV_NF_END
452
453    if (currentLayoutKind != kKLKCHRKind) {
454      err = KLGetKeyboardLayoutProperty(currentLayout,
455                                        kKLuchrData,
456                                        (const void **)&uchrData);
457      if (err != noErr) { // COV_NF_START
458        _GTMDevLog(@"failed to fetch the keyboard layout uchar data, err=%d",
459                   err);
460        return nil;
461      }  // COV_NF_END
462    }
463  }
464#else
465  TISInputSourceRef inputSource = TISCopyCurrentKeyboardLayoutInputSource();
466  if (inputSource) {
467    CFDataRef uchrDataRef
468      = TISGetInputSourceProperty(inputSource, kTISPropertyUnicodeKeyLayoutData);
469    if(uchrDataRef) {
470      uchrData = (UCKeyboardLayout*)CFDataGetBytePtr(uchrDataRef);
471    }
472    CFRelease(inputSource);
473  }
474#endif  // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
475
476  NSString *keystrokeString = nil;
477  if (uchrData) {
478    // uchr layout data is available, this is our preference
479    UniCharCount uchrCharLength = 0;
480    UniChar  uchrChars[256] = { 0 };
481    UInt32 uchrDeadKeyState = 0;
482    err = UCKeyTranslate(uchrData,
483                         keycode,
484                         kUCKeyActionDisplay,
485                         0,  // No modifiers
486                         LMGetKbdType(),
487                         kUCKeyTranslateNoDeadKeysMask,
488                         &uchrDeadKeyState,
489                         sizeof(uchrChars) / sizeof(UniChar),
490                         &uchrCharLength,
491                         uchrChars);
492    if (err != noErr) {
493      // COV_NF_START
494      _GTMDevLog(@"failed to translate the keycode, err=%d", (int)err);
495      return nil;
496      // COV_NF_END
497    }
498    if (uchrCharLength < 1) return nil;
499    keystrokeString = [NSString stringWithCharacters:uchrChars
500                                              length:uchrCharLength];
501  }
502#if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
503  else if (currentLayoutKind == kKLKCHRKind) {
504    // Only KCHR layout data is available, go old school
505    void *KCHRData = NULL;
506    err = KLGetKeyboardLayoutProperty(currentLayout, kKLKCHRData,
507                                      (const void **)&KCHRData);
508    if (err != noErr) { // COV_NF_START
509      _GTMDevLog(@"failed to fetch the keyboard layout uchar data, err=%d",
510                 err);
511      return nil;
512    }  // COV_NF_END
513    // Turn into character code
514    UInt32 keyTranslateState = 0;
515    UInt32 twoKCHRChars = KeyTranslate(KCHRData, keycode, &keyTranslateState);
516    if (!twoKCHRChars) return nil;
517    // Unpack the fields
518    char firstChar = (char)((twoKCHRChars & 0x00FF0000) >> 16);
519    char secondChar = (char)(twoKCHRChars & 0x000000FF);
520    // May have one or two characters
521    if (firstChar && secondChar) {
522      NSString *str1
523        = [[[NSString alloc] initWithBytes:&firstChar
524                                    length:1
525                                  encoding:NSMacOSRomanStringEncoding] autorelease];
526      NSString *str2
527        = [[[NSString alloc] initWithBytes:&secondChar
528                                    length:1
529                                  encoding:NSMacOSRomanStringEncoding] autorelease];
530      keystrokeString = [NSString stringWithFormat:@"%@%@",
531                         [str1 uppercaseString],
532                         [str2 uppercaseString]];
533    } else {
534      keystrokeString
535        = [[[NSString alloc] initWithBytes:&secondChar
536                                    length:1
537                                  encoding:NSMacOSRomanStringEncoding] autorelease];
538      [keystrokeString uppercaseString];
539    }
540  }
541#endif  // MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4
542
543  // Sanity we got a stroke
544  if (!keystrokeString || ![keystrokeString length]) return nil;
545
546  // Sanity check the keystroke string for unprintable characters
547  NSMutableCharacterSet *validChars =
548    [[[NSMutableCharacterSet alloc] init] autorelease];
549
550  [validChars formUnionWithCharacterSet:[NSCharacterSet alphanumericCharacterSet]];
551  [validChars formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]];
552  [validChars formUnionWithCharacterSet:[NSCharacterSet symbolCharacterSet]];
553  for (unsigned int i = 0; i < [keystrokeString length]; i++) {
554    if (![validChars characterIsMember:[keystrokeString characterAtIndex:i]]) {
555      return nil;
556    }
557  }
558
559  if (!useGlyph) {
560    // menus want glyphs in the original lowercase forms, so we only upper this
561    // if we aren't using it as a glyph.
562    keystrokeString = [keystrokeString uppercaseString];
563  }
564
565  return keystrokeString;
566}
567
568@end
569
570@implementation GTMHotKeyFieldEditor
571
572+ (GTMHotKeyFieldEditor *)sharedHotKeyFieldEditor {
573  static GTMHotKeyFieldEditor *obj;
574  if (!obj) {
575    obj = [[self alloc] init];
576  }
577  return obj;
578}
579
580- (id)init {
581  if ((self = [super init])) {
582    [self setFieldEditor:YES];  // We are a field editor
583  }
584  return self;
585}
586
587// COV_NF_START
588// Singleton so never called.
589- (void)dealloc {
590  [cell_ release];
591  [super dealloc];
592}
593// COV_NF_END
594
595- (GTMHotKeyTextFieldCell *)cell {
596  return cell_;
597}
598
599- (void)setCell:(GTMHotKeyTextFieldCell *)cell {
600  [cell_ autorelease];
601  cell_ = [cell retain];
602}
603
604- (NSArray *)acceptableDragTypes {
605  // Don't take drags
606  return [NSArray array];
607}
608
609- (NSArray *)readablePasteboardTypes {
610  // No pasting
611  return [NSArray array];
612}
613
614- (NSArray *)writablePasteboardTypes {
615  // No copying
616  return [NSArray array];
617}
618
619- (BOOL)becomeFirstResponder {
620  // We need to lose focus any time the window is not key
621  NSNotificationCenter *dc = [NSNotificationCenter defaultCenter];
622  [dc addObserver:self
623         selector:@selector(windowResigned:)
624             name:NSWindowDidResignKeyNotification
625           object:[self window]];
626  return [super becomeFirstResponder];
627}
628
629- (BOOL)resignFirstResponder {
630  // No longer interested in window resign
631  [[NSNotificationCenter defaultCenter] removeObserver:self];
632  return [super resignFirstResponder];
633}
634
635// Private method we use to get out of global hotkey capture when the window
636// is no longer front
637- (void)windowResigned:(NSNotification *)notification {
638  // Lose our focus
639  NSWindow *window = [self window];
640  [window makeFirstResponder:window];
641
642}
643
644- (BOOL)shouldDrawInsertionPoint {
645  // Show an insertion point, because we'll kill our own focus after
646  // each entry
647  return YES;
648}
649
650- (NSRange)selectionRangeForProposedRange:(NSRange)proposedSelRange
651                              granularity:(NSSelectionGranularity)granularity {
652  // Always select everything
653  return NSMakeRange(0, [[self textStorage] length]);
654}
655
656- (void)keyDown:(NSEvent *)theEvent {
657  if ([self shouldBypassEvent:theEvent]) {
658    [super keyDown:theEvent];
659  } else {
660    // Try to eat the event
661    [self processEventToHotKeyAndString:theEvent];
662  }
663}
664
665- (BOOL)performKeyEquivalent:(NSEvent *)theEvent {
666  if ([self shouldBypassEvent:theEvent]) {
667    return [super performKeyEquivalent:theEvent];
668  } else {
669    // We always eat these key strokes while we have focus
670    [self processEventToHotKeyAndString:theEvent];
671    return YES;
672  }
673}
674
675// Private do method that tell us to ignore certain events
676- (BOOL)shouldBypassEvent:(NSEvent *)theEvent {
677  BOOL bypass = NO;
678  UInt16 keyCode = [theEvent keyCode];
679  NSUInteger modifierFlags
680    = [theEvent modifierFlags] & NSDeviceIndependentModifierFlagsMask;
681
682  if (keyCode == 48) {  // Tab
683    // Ignore all events that the dock cares about
684    // Just to be extra clear if the user is trying to use Dock hotkeys beep
685    // at them
686    if ((modifierFlags == NSCommandKeyMask) ||
687        (modifierFlags == (NSCommandKeyMask | NSShiftKeyMask))) {
688      NSBeep();
689      bypass = YES;
690    } else if (modifierFlags == 0 || modifierFlags == NSShiftKeyMask) {
691      // Probably attempting to tab around the dialog.
692      bypass = YES;
693    }
694
695  } else if ((keyCode == 12) && (modifierFlags == NSCommandKeyMask)) {
696    // Don't eat Cmd-Q. Users could have it as a hotkey, but its more likely
697    // they're trying to quit
698    bypass = YES;
699  } else if ((keyCode == 13) && (modifierFlags == NSCommandKeyMask)) {
700    // Same for Cmd-W, user is probably trying to close the window
701    bypass = YES;
702  }
703  return bypass;
704}
705
706// Private method that turns events into strings and dictionaries for our
707// hotkey plumbing.
708- (void)processEventToHotKeyAndString:(NSEvent *)theEvent {
709  // Construct a dictionary of the event as a hotkey pref
710  GTMHotKey *newHotKey = nil;
711  NSString *prettyString = @"";
712  // 51 is "the delete key"
713  const NSUInteger allModifiers = (NSCommandKeyMask | NSAlternateKeyMask |
714                                   NSControlKeyMask | NSShiftKeyMask);
715  if (!(([theEvent keyCode] == 51 )
716        && (([theEvent modifierFlags] & allModifiers)== 0))) {
717    newHotKey = [self hotKeyForEvent:theEvent];
718    if (!newHotKey) {
719      NSBeep();
720      return;  // No action, but don't give up focus
721    }
722    prettyString = [GTMHotKeyTextFieldCell displayStringForHotKey:newHotKey];
723    if (!prettyString) {
724      NSBeep();
725      return;
726    }
727  }
728
729  // Replacement range
730  NSRange replaceRange = NSMakeRange(0, [[self textStorage] length]);
731
732  // Ask for permission to replace
733  if (![self shouldChangeTextInRange:replaceRange
734                   replacementString:prettyString]) {
735    // If replacement was disallowed, change nothing, including hotKeyDict_
736    NSBeep();
737    return;
738  }
739
740  [[self cell] setObjectValue:newHotKey];
741
742  // Finish the change
743  [self didChangeText];
744
745  // Force editing to end. This sends focus off into space slightly, but
746  // its better than constantly capturing user events. This is exactly
747  // like the Apple editor in their Keyboard pref pane.
748  [[self window] makeFirstResponder:nil];
749}
750
751- (GTMHotKey *)hotKeyForEvent:(NSEvent *)event {
752  if (!event) return nil;
753
754  // Check event
755  NSUInteger flags = [event modifierFlags];
756  UInt16 keycode = [event keyCode];
757  // If the event has no modifiers do nothing
758  NSUInteger allModifiers = (NSCommandKeyMask | NSAlternateKeyMask |
759                             NSControlKeyMask | NSShiftKeyMask);
760
761  BOOL requiresModifiers
762    = [GTMHotKeyTextFieldCell doesKeyCodeRequireModifier:keycode];
763  if (requiresModifiers) {
764    // If we aren't a function key, and have no modifiers do nothing.
765    if (!(flags & allModifiers)) return nil;
766    // If the event has high bits in keycode do nothing
767    if (keycode & 0xFF00) return nil;
768  }
769
770  // Clean the flags to only contain things we care about
771  UInt32 cleanFlags = 0;
772  if (flags & NSCommandKeyMask) cleanFlags |= NSCommandKeyMask;
773  if (flags & NSAlternateKeyMask) cleanFlags |= NSAlternateKeyMask;
774  if (flags & NSControlKeyMask) cleanFlags |= NSControlKeyMask;
775  if (flags & NSShiftKeyMask) cleanFlags |= NSShiftKeyMask;
776  return [GTMHotKey hotKeyWithKeyCode:keycode
777                            modifiers:cleanFlags
778                   useDoubledModifier:NO];
779}
780
781@end