More better names.
[enjoyable.git] / Classes / NJKeyInputField.m
1 //
2 // NJKeyInputField.h
3 // Enjoyable
4 //
5 // Copyright 2013 Joe Wreschnig.
6 //
7
8 #import "NJKeyInputField.h"
9
10 #include <Carbon/Carbon.h>
11 // Only used for kVK_... codes.
12
13 enum {
14 kVK_RightCommand = kVK_Command - 1,
15 kVK_Insert = 0x72,
16 kVK_Power = 0x7f,
17 kVK_ApplicationMenu = 0x6e,
18 kVK_MAX = 0xFFFF,
19 };
20
21 const CGKeyCode NJKeyInputFieldEmpty = kVK_MAX;
22
23 @interface NJKeyInputField () <NSTextFieldDelegate>
24 @end
25
26 @implementation NJKeyInputField {
27 NSTextField *field;
28 NSImageView *warning;
29 }
30
31 - (id)initWithFrame:(NSRect)frameRect {
32 if ((self = [super initWithFrame:frameRect])) {
33 field = [[NSTextField alloc] initWithFrame:self.bounds];
34 field.alignment = NSCenterTextAlignment;
35 field.editable = NO;
36 field.selectable = NO;
37 field.delegate = self;
38 [self addSubview:field];
39
40 warning = [[NSImageView alloc] init];
41 warning.image = [NSImage imageNamed:@"NSInvalidDataFreestanding"];
42 CGSize imgSize = warning.image.size;
43 CGRect bounds = self.bounds;
44 warning.frame = CGRectMake(bounds.size.width - (imgSize.width + 4),
45 (bounds.size.height - imgSize.height) / 2,
46 imgSize.width, imgSize.height);
47
48 warning.toolTip = NSLocalizedString(@"invalid key code",
49 @"shown when the user types an invalid key code");
50 warning.hidden = YES;
51 [self addSubview:warning];
52 }
53 return self;
54 }
55
56 - (void)clear {
57 self.keyCode = NJKeyInputFieldEmpty;
58 [self.delegate keyInputFieldDidClear:self];
59 [self resignIfFirstResponder];
60 }
61
62 - (BOOL)hasKeyCode {
63 return self.keyCode != NJKeyInputFieldEmpty;
64 }
65
66 + (NSString *)displayNameForKeyCode:(CGKeyCode)keyCode {
67 switch (keyCode) {
68 case kVK_F1: return @"F1";
69 case kVK_F2: return @"F2";
70 case kVK_F3: return @"F3";
71 case kVK_F4: return @"F4";
72 case kVK_F5: return @"F5";
73 case kVK_F6: return @"F6";
74 case kVK_F7: return @"F7";
75 case kVK_F8: return @"F8";
76 case kVK_F9: return @"F9";
77 case kVK_F10: return @"F10";
78 case kVK_F11: return @"F11";
79 case kVK_F12: return @"F12";
80 case kVK_F13: return @"F13";
81 case kVK_F14: return @"F14";
82 case kVK_F15: return @"F15";
83 case kVK_F16: return @"F16";
84 case kVK_F17: return @"F17";
85 case kVK_F18: return @"F18";
86 case kVK_F19: return @"F19";
87 case kVK_F20: return @"F20";
88
89 case kVK_Escape: return @"⎋";
90 case kVK_ANSI_Grave: return @"`";
91
92 case kVK_ANSI_1: return @"1";
93 case kVK_ANSI_2: return @"2";
94 case kVK_ANSI_3: return @"3";
95 case kVK_ANSI_4: return @"4";
96 case kVK_ANSI_5: return @"5";
97 case kVK_ANSI_6: return @"6";
98 case kVK_ANSI_7: return @"7";
99 case kVK_ANSI_8: return @"8";
100 case kVK_ANSI_9: return @"9";
101 case kVK_ANSI_0: return @"0";
102 case kVK_ANSI_Minus: return @"-";
103 case kVK_ANSI_Equal: return @"=";
104
105 case kVK_Function: return @"Fn";
106 case kVK_CapsLock: return @"⇪";
107 case kVK_Command: return NSLocalizedString(@"Left ⌘", @"keyboard key");
108 case kVK_RightCommand: return NSLocalizedString(@"Right ⌘", @"keyboard key");
109 case kVK_Option: return NSLocalizedString(@"Left ⌥", @"keyboard key");
110 case kVK_RightOption: return NSLocalizedString(@"Right ⌥", @"keyboard key");
111 case kVK_Control: return NSLocalizedString(@"Left ⌃", @"keyboard key");
112 case kVK_RightControl: return NSLocalizedString(@"Right ⌃", @"keyboard key");
113 case kVK_Shift: return NSLocalizedString(@"Left ⇧", @"keyboard key");
114 case kVK_RightShift: return NSLocalizedString(@"Right ⇧", @"keyboard key");
115
116 case kVK_Home: return @"↖";
117 case kVK_PageUp: return @"⇞";
118 case kVK_End: return @"↘";
119 case kVK_PageDown: return @"⇟";
120
121 case kVK_ForwardDelete: return @"⌦";
122 case kVK_Delete: return @"⌫";
123
124 case kVK_Tab: return @"⇥";
125 case kVK_Return: return @"↩";
126 case kVK_Space: return @"␣";
127
128 case kVK_ANSI_A: return @"A";
129 case kVK_ANSI_B: return @"B";
130 case kVK_ANSI_C: return @"C";
131 case kVK_ANSI_D: return @"D";
132 case kVK_ANSI_E: return @"E";
133 case kVK_ANSI_F: return @"F";
134 case kVK_ANSI_G: return @"G";
135 case kVK_ANSI_H: return @"H";
136 case kVK_ANSI_I: return @"I";
137 case kVK_ANSI_J: return @"J";
138 case kVK_ANSI_K: return @"K";
139 case kVK_ANSI_L: return @"L";
140 case kVK_ANSI_M: return @"M";
141 case kVK_ANSI_N: return @"N";
142 case kVK_ANSI_O: return @"O";
143 case kVK_ANSI_P: return @"P";
144 case kVK_ANSI_Q: return @"Q";
145 case kVK_ANSI_R: return @"R";
146 case kVK_ANSI_S: return @"S";
147 case kVK_ANSI_T: return @"T";
148 case kVK_ANSI_U: return @"U";
149 case kVK_ANSI_V: return @"V";
150 case kVK_ANSI_W: return @"W";
151 case kVK_ANSI_X: return @"X";
152 case kVK_ANSI_Y: return @"Y";
153 case kVK_ANSI_Z: return @"Z";
154 case kVK_ANSI_LeftBracket: return @"[";
155 case kVK_ANSI_RightBracket: return @"]";
156 case kVK_ANSI_Backslash: return @"\\";
157 case kVK_ANSI_Semicolon: return @";";
158 case kVK_ANSI_Quote: return @"'";
159 case kVK_ANSI_Comma: return @",";
160 case kVK_ANSI_Period: return @".";
161 case kVK_ANSI_Slash: return @"/";
162
163 case kVK_ANSI_Keypad0: return NSLocalizedString(@"Key Pad 0", @"numeric pad key");
164 case kVK_ANSI_Keypad1: return NSLocalizedString(@"Key Pad 1", @"numeric pad key");
165 case kVK_ANSI_Keypad2: return NSLocalizedString(@"Key Pad 2", @"numeric pad key");
166 case kVK_ANSI_Keypad3: return NSLocalizedString(@"Key Pad 3", @"numeric pad key");
167 case kVK_ANSI_Keypad4: return NSLocalizedString(@"Key Pad 4", @"numeric pad key");
168 case kVK_ANSI_Keypad5: return NSLocalizedString(@"Key Pad 5", @"numeric pad key");
169 case kVK_ANSI_Keypad6: return NSLocalizedString(@"Key Pad 6", @"numeric pad key");
170 case kVK_ANSI_Keypad7: return NSLocalizedString(@"Key Pad 7", @"numeric pad key");
171 case kVK_ANSI_Keypad8: return NSLocalizedString(@"Key Pad 8", @"numeric pad key");
172 case kVK_ANSI_Keypad9: return NSLocalizedString(@"Key Pad 9", @"numeric pad key");
173 case kVK_ANSI_KeypadClear: return @"⌧";
174 case kVK_ANSI_KeypadEnter: return @"⌤";
175
176 case kVK_ANSI_KeypadEquals:
177 return NSLocalizedString(@"Key Pad =", @"numeric pad key");
178 case kVK_ANSI_KeypadDivide:
179 return NSLocalizedString(@"Key Pad /", @"numeric pad key");
180 case kVK_ANSI_KeypadMultiply:
181 return NSLocalizedString(@"Key Pad *", @"numeric pad key");
182 case kVK_ANSI_KeypadMinus:
183 return NSLocalizedString(@"Key Pad -", @"numeric pad key");
184 case kVK_ANSI_KeypadPlus:
185 return NSLocalizedString(@"Key Pad +", @"numeric pad key");
186 case kVK_ANSI_KeypadDecimal:
187 return NSLocalizedString(@"Key Pad .", @"numeric pad key");
188
189 case kVK_LeftArrow: return @"←";
190 case kVK_RightArrow: return @"→";
191 case kVK_UpArrow: return @"↑";
192 case kVK_DownArrow: return @"↓";
193
194 case kVK_JIS_Yen: return @"¥";
195 case kVK_JIS_Underscore: return @"_";
196 case kVK_JIS_KeypadComma:
197 return NSLocalizedString(@"Key Pad ,", @"numeric pad key");
198 case kVK_JIS_Eisu: return @"英数";
199 case kVK_JIS_Kana: return @"かな";
200
201 case kVK_Power: return @"⌽";
202 case kVK_VolumeUp: return @"🔊";
203 case kVK_VolumeDown: return @"🔉";
204
205 case kVK_Insert:
206 return NSLocalizedString(@"Insert", "keyboard key");
207 case kVK_ApplicationMenu:
208 return NSLocalizedString(@"Menu", "keyboard key");
209
210 case kVK_MAX: // NJKeyInputFieldEmpty
211 return @"";
212 default:
213 return [[NSString alloc] initWithFormat:
214 NSLocalizedString(@"key 0x%x", @"unknown key code"),
215 keyCode];
216 }
217 }
218
219 - (BOOL)acceptsFirstResponder {
220 return self.isEnabled;
221 }
222
223 - (BOOL)becomeFirstResponder {
224 field.backgroundColor = NSColor.selectedTextBackgroundColor;
225 return [super becomeFirstResponder];
226 }
227
228 - (BOOL)resignFirstResponder {
229 field.backgroundColor = NSColor.textBackgroundColor;
230 return [super resignFirstResponder];
231 }
232
233 - (void)setKeyCode:(CGKeyCode)keyCode {
234 _keyCode = keyCode;
235 field.stringValue = [NJKeyInputField displayNameForKeyCode:keyCode];
236 }
237
238 - (void)keyDown:(NSEvent *)event {
239 static const NSUInteger IGNORE = NSAlternateKeyMask | NSCommandKeyMask;
240 if (!event.isARepeat) {
241 if ((event.modifierFlags & IGNORE) && event.keyCode == kVK_Delete) {
242 // Allow Alt/Command+Delete to clear the field.
243 self.keyCode = NJKeyInputFieldEmpty;
244 [self.delegate keyInputFieldDidClear:self];
245 } else if (!(event.modifierFlags & IGNORE)) {
246 self.keyCode = event.keyCode;
247 [self.delegate keyInputField:self didChangeKey:self.keyCode];
248 }
249 [self resignIfFirstResponder];
250 }
251 }
252
253 static BOOL isValidKeyCode(long code) {
254 return code < 0xFFFF && code >= 0;
255 }
256
257 - (void)controlTextDidChange:(NSNotification *)obj {
258 char *error = NULL;
259 long code = strtol(field.stringValue.UTF8String, &error, 16);
260 warning.hidden = (isValidKeyCode(code) && !*error) || !field.stringValue.length;
261 }
262
263 - (void)controlTextDidEndEditing:(NSNotification *)obj {
264 [field.cell setPlaceholderString:@""];
265 field.editable = NO;
266 field.selectable = NO;
267 warning.hidden = YES;
268 char *error = NULL;
269 const char *s = field.stringValue.UTF8String;
270 long code = strtol(s, &error, 16);
271
272 if (!*error && isValidKeyCode(code) && field.stringValue.length) {
273 self.keyCode = code;
274 [self.delegate keyInputField:self didChangeKey:self.keyCode];
275 } else {
276 self.keyCode = self.keyCode;
277 }
278 }
279
280 - (void)mouseDown:(NSEvent *)theEvent {
281 if (self.isEnabled) {
282 if (theEvent.modifierFlags & NSCommandKeyMask) {
283 field.editable = YES;
284 field.selectable = YES;
285 field.stringValue = @"";
286 [field.cell setPlaceholderString:
287 NSLocalizedString(@"enter key code",
288 @"shown when user must enter a key code to map to")];
289 [self.window makeFirstResponder:field];
290 } else {
291 if (self.window.firstResponder == self)
292 [self.window makeFirstResponder:nil];
293 else if (self.acceptsFirstResponder)
294 [self.window makeFirstResponder:self];
295 }
296 }
297 }
298
299 - (void)flagsChanged:(NSEvent *)theEvent {
300 // Many keys are only available on MacBook keyboards by using the
301 // Fn modifier key (e.g. Fn+Left for Home), so delay processing
302 // modifiers until the up event is received in order to let the
303 // user type these virtual keys. However, there is no actual event
304 // for modifier key up - so detect it by checking to see if any
305 // modifiers are still down.
306 if (!field.isEditable
307 && !(theEvent.modifierFlags & NSDeviceIndependentModifierFlagsMask)) {
308 self.keyCode = theEvent.keyCode;
309 [self.delegate keyInputField:self didChangeKey:_keyCode];
310 }
311 }
312
313 @end