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