xref: /haiku/src/preferences/shortcuts/ShortcutsSpec.cpp (revision 02b72520b683730ce13b30416576ded451fb2843)
1 /*
2  * Copyright 1999-2009 Jeremy Friesner
3  * Copyright 2009-2010 Haiku, Inc. All rights reserved.
4  * Distributed under the terms of the MIT License.
5  *
6  * Authors:
7  *		Jeremy Friesner
8  */
9 
10 #include "ShortcutsSpec.h"
11 
12 #include <ctype.h>
13 #include <stdio.h>
14 
15 #include <Beep.h>
16 #include <Catalog.h>
17 #include <ColumnTypes.h>
18 #include <Directory.h>
19 #include <Locale.h>
20 #include <NodeInfo.h>
21 #include <Path.h>
22 #include <Region.h>
23 #include <Window.h>
24 
25 #include "ColumnListView.h"
26 
27 #include "BitFieldTesters.h"
28 #include "CommandActuators.h"
29 #include "KeyInfos.h"
30 #include "MetaKeyStateMap.h"
31 #include "ParseCommandLine.h"
32 
33 
34 #define CLASS "ShortcutsSpec : "
35 
36 #undef B_TRANSLATION_CONTEXT
37 #define B_TRANSLATION_CONTEXT "ShortcutsSpec"
38 
39 const float _height = 20.0f;
40 
41 static MetaKeyStateMap sMetaMaps[ShortcutsSpec::NUM_META_COLUMNS];
42 
43 static bool sFontCached = false;
44 static BFont sViewFont;
45 static float sFontHeight;
46 
47 const char* ShortcutsSpec::sShiftName;
48 const char* ShortcutsSpec::sControlName;
49 const char* ShortcutsSpec::sOptionName;
50 const char* ShortcutsSpec::sCommandName;
51 
52 
53 #define ICON_BITMAP_RECT BRect(0.0f, 0.0f, 15.0f, 15.0f)
54 #define ICON_BITMAP_SPACE B_RGBA32
55 
56 
57 // Returns the (pos)'th char in the string, or '\0' if (pos) if off the end of
58 // the string
59 static char
60 GetLetterAt(const char* str, int pos)
61 {
62 	for (int i = 0; i < pos; i++) {
63 		if (str[i] == '\0')
64 			return '\0';
65 	}
66 	return str[pos];
67 }
68 
69 
70 // Setup the states in a standard manner for a pair of meta-keys.
71 static void
72 SetupStandardMap(MetaKeyStateMap& map, const char* name, uint32 both,
73 	uint32 left, uint32 right)
74 {
75 	map.SetInfo(name);
76 
77 	// In this state, neither key may be pressed.
78 	map.AddState("(None)", new HasBitsFieldTester(0, both));
79 
80 	// Here, either may be pressed. (Remember both is NOT a 2-bit chord, it's
81 	// another bit entirely)
82 	map.AddState("Either", new HasBitsFieldTester(both));
83 
84 	// Here, only the left may be pressed
85 	map.AddState("Left", new HasBitsFieldTester(left, right));
86 
87 	// Here, only the right may be pressed
88 	map.AddState("Right", new HasBitsFieldTester(right, left));
89 
90 	// Here, both must be pressed.
91 	map.AddState("Both", new HasBitsFieldTester(left | right));
92 }
93 
94 
95 MetaKeyStateMap&
96 GetNthKeyMap(int which)
97 {
98 	return sMetaMaps[which];
99 }
100 
101 
102 /*static*/ void
103 ShortcutsSpec::InitializeMetaMaps()
104 {
105 	static bool metaMapsInitialized = false;
106 	if (metaMapsInitialized)
107 		return;
108 	metaMapsInitialized = true;
109 
110 	_InitModifierNames();
111 
112 	SetupStandardMap(sMetaMaps[ShortcutsSpec::SHIFT_COLUMN_INDEX], sShiftName,
113 		B_SHIFT_KEY, B_LEFT_SHIFT_KEY, B_RIGHT_SHIFT_KEY);
114 
115 	SetupStandardMap(sMetaMaps[ShortcutsSpec::CONTROL_COLUMN_INDEX],
116 		sControlName, B_CONTROL_KEY, B_LEFT_CONTROL_KEY, B_RIGHT_CONTROL_KEY);
117 
118 	SetupStandardMap(sMetaMaps[ShortcutsSpec::COMMAND_COLUMN_INDEX],
119 		sCommandName, B_COMMAND_KEY, B_LEFT_COMMAND_KEY, B_RIGHT_COMMAND_KEY);
120 
121 	SetupStandardMap(sMetaMaps[ShortcutsSpec::OPTION_COLUMN_INDEX], sOptionName
122 		, B_OPTION_KEY, B_LEFT_OPTION_KEY, B_RIGHT_OPTION_KEY);
123 }
124 
125 
126 ShortcutsSpec::ShortcutsSpec(const char* cmd)
127 	:
128 	BRow(),
129 	fCommand(NULL),
130 	fBitmap(ICON_BITMAP_RECT, ICON_BITMAP_SPACE),
131 	fLastBitmapName(NULL),
132 	fBitmapValid(false),
133 	fKey(0),
134 	fCursorPtsValid(false)
135 {
136 	for (int i = 0; i < NUM_META_COLUMNS; i++)
137 		fMetaCellStateIndex[i] = 0;
138 	SetCommand(cmd);
139 }
140 
141 
142 ShortcutsSpec::ShortcutsSpec(const ShortcutsSpec& from)
143 	:
144 	BRow(),
145 	fCommand(NULL),
146 	fBitmap(ICON_BITMAP_RECT, ICON_BITMAP_SPACE),
147 	fLastBitmapName(NULL),
148 	fBitmapValid(false),
149 	fKey(from.fKey),
150 	fCursorPtsValid(false)
151 {
152 	for (int i = 0; i < NUM_META_COLUMNS; i++)
153 		fMetaCellStateIndex[i] = from.fMetaCellStateIndex[i];
154 
155 	SetCommand(from.fCommand);
156 	SetSelectedColumn(from.GetSelectedColumn());
157 
158 	for (int i = 0; i < from.CountFields(); i++)
159 		SetField(new BStringField(
160 					static_cast<const BStringField*>(from.GetField(i))->String()), i);
161 }
162 
163 
164 ShortcutsSpec::ShortcutsSpec(BMessage* from)
165 	:
166 	BRow(),
167 	fCommand(NULL),
168 	fBitmap(ICON_BITMAP_RECT, ICON_BITMAP_SPACE),
169 	fLastBitmapName(NULL),
170 	fBitmapValid(false),
171 	fCursorPtsValid(false)
172 {
173 	const char* temp;
174 	if (from->FindString("command", &temp) != B_NO_ERROR) {
175 		printf(CLASS);
176 		printf(" Error, no command string in archive BMessage!\n");
177 		temp = "";
178 	}
179 
180 	SetCommand(temp);
181 
182 	if (from->FindInt32("key", (int32*) &fKey) != B_NO_ERROR) {
183 		printf(CLASS);
184 		printf(" Error, no key int32 in archive BMessage!\n");
185 	}
186 
187 	for (int i = 0; i < NUM_META_COLUMNS; i++)
188 		if (from->FindInt32("mcidx", i, (int32*)&fMetaCellStateIndex[i])
189 			!= B_NO_ERROR) {
190 			printf(CLASS);
191 			printf(" Error, no modifiers int32 in archive BMessage!\n");
192 		}
193 
194 	for (int i = 0; i <= STRING_COLUMN_INDEX; i++)
195 		SetField(new BStringField(GetCellText(i)), i);
196 }
197 
198 
199 void
200 ShortcutsSpec::SetCommand(const char* command)
201 {
202 	delete[] fCommand;
203 		// out with the old (if any)...
204 	fCommandLen = strlen(command) + 1;
205 	fCommandNul = fCommandLen - 1;
206 	fCommand = new char[fCommandLen];
207 	strcpy(fCommand, command);
208 	SetField(new BStringField(command), STRING_COLUMN_INDEX);
209 }
210 
211 
212 const char*
213 ShortcutsSpec::GetColumnName(int i)
214 {
215 	return sMetaMaps[i].GetName();
216 }
217 
218 
219 status_t
220 ShortcutsSpec::Archive(BMessage* into, bool deep) const
221 {
222 	status_t ret = BArchivable::Archive(into, deep);
223 	if (ret != B_NO_ERROR)
224 		return ret;
225 
226 	into->AddString("class", "ShortcutsSpec");
227 
228 	// These fields are for our prefs panel's benefit only
229 	into->AddString("command", fCommand);
230 	into->AddInt32("key", fKey);
231 
232 	// Assemble a BitFieldTester for the input_server add-on to use...
233 	MinMatchFieldTester test(NUM_META_COLUMNS, false);
234 	for (int i = 0; i < NUM_META_COLUMNS; i++) {
235 		// for easy parsing by prefs applet on load-in
236 		into->AddInt32("mcidx", fMetaCellStateIndex[i]);
237 		test.AddSlave(sMetaMaps[i].GetNthStateTester(fMetaCellStateIndex[i]));
238 	}
239 
240 	BMessage testerMsg;
241 	ret = test.Archive(&testerMsg);
242 	if (ret != B_NO_ERROR)
243 		return ret;
244 
245 	into->AddMessage("modtester", &testerMsg);
246 
247 	// And also create a CommandActuator for the input_server add-on to execute
248 	CommandActuator* act = CreateCommandActuator(fCommand);
249 	BMessage actMsg;
250 	ret = act->Archive(&actMsg);
251 	if (ret != B_NO_ERROR)
252 		return ret;
253 	delete act;
254 
255 	into->AddMessage("act", &actMsg);
256 
257 	return ret;
258 }
259 
260 
261 BArchivable*
262 ShortcutsSpec::Instantiate(BMessage* from)
263 {
264 	bool validateOK = false;
265 	if (validate_instantiation(from, "ShortcutsSpec"))
266 		validateOK = true;
267 	else // test the old one.
268 		if (validate_instantiation(from, "SpicyKeysSpec"))
269 			validateOK = true;
270 
271 	if (!validateOK)
272 		return NULL;
273 
274 	return new ShortcutsSpec(from);
275 }
276 
277 
278 ShortcutsSpec::~ShortcutsSpec()
279 {
280 	delete[] fCommand;
281 	delete[] fLastBitmapName;
282 }
283 
284 
285 void
286 ShortcutsSpec::_CacheViewFont(BView* owner)
287 {
288 	if (sFontCached == false) {
289 		sFontCached = true;
290 		owner->GetFont(&sViewFont);
291 		font_height fh;
292 		sViewFont.GetHeight(&fh);
293 		sFontHeight = fh.ascent - fh.descent;
294 	}
295 }
296 
297 
298 const char*
299 ShortcutsSpec::GetCellText(int whichColumn) const
300 {
301 	const char* temp = ""; // default
302 	switch (whichColumn) {
303 		case KEY_COLUMN_INDEX:
304 		{
305 			if ((fKey > 0) && (fKey <= 0xFF)) {
306 				temp = GetKeyName(fKey);
307 				if (temp == NULL)
308 					temp = "";
309 			} else if (fKey > 0xFF) {
310 				sprintf(fScratch, "#%" B_PRIx32, fKey);
311 				return fScratch;
312 			}
313 			break;
314 		}
315 
316 		case STRING_COLUMN_INDEX:
317 			temp = fCommand;
318 			break;
319 
320 		default:
321 			if ((whichColumn >= 0) && (whichColumn < NUM_META_COLUMNS))
322 				temp = sMetaMaps[whichColumn].GetNthStateDesc(
323 							fMetaCellStateIndex[whichColumn]);
324 			if (temp[0] == '(')
325 				temp = "";
326 			break;
327 	}
328 	return temp;
329 }
330 
331 
332 bool
333 ShortcutsSpec::ProcessColumnMouseClick(int whichColumn)
334 {
335 	if ((whichColumn >= 0) && (whichColumn < NUM_META_COLUMNS)) {
336 		// same as hitting space for these columns: cycle entry
337 		const char temp = B_SPACE;
338 
339 		// 3rd arg isn't correct but it isn't read for this case anyway
340 		return ProcessColumnKeyStroke(whichColumn, &temp, 0);
341 	}
342 	return false;
343 }
344 
345 
346 bool
347 ShortcutsSpec::ProcessColumnTextString(int whichColumn, const char* string)
348 {
349 	switch (whichColumn) {
350 		case STRING_COLUMN_INDEX:
351 			SetCommand(string);
352 			return true;
353 			break;
354 
355 		case KEY_COLUMN_INDEX:
356 		{
357 			fKey = FindKeyCode(string);
358 			SetField(new BStringField(GetCellText(whichColumn)),
359 				KEY_COLUMN_INDEX);
360 			return true;
361 			break;
362 		}
363 
364 		default:
365 			return ProcessColumnKeyStroke(whichColumn, string, 0);
366 	}
367 }
368 
369 
370 bool
371 ShortcutsSpec::_AttemptTabCompletion()
372 {
373 	bool result = false;
374 
375 	int32 argc;
376 	char** argv = ParseArgvFromString(fCommand, argc);
377 	if (argc > 0) {
378 		// Try to complete the path partially expressed in the last argument!
379 		char* arg = argv[argc - 1];
380 		char* fileFragment = strrchr(arg, '/');
381 		if (fileFragment != NULL) {
382 			const char* directoryName = (fileFragment == arg) ? "/" : arg;
383 			*fileFragment = '\0';
384 			fileFragment++;
385 			int fragmentLength = strlen(fileFragment);
386 
387 			BDirectory dir(directoryName);
388 			if (dir.InitCheck() == B_NO_ERROR) {
389 				BEntry nextEnt;
390 				BPath nextPath;
391 				BList matchList;
392 				int maxEntryLen = 0;
393 
394 				// Read in all the files in the directory whose names start
395 				// with our fragment.
396 				while (dir.GetNextEntry(&nextEnt) == B_NO_ERROR) {
397 					if (nextEnt.GetPath(&nextPath) == B_NO_ERROR) {
398 						char* filePath = strrchr(nextPath.Path(), '/') + 1;
399 						if (strncmp(filePath, fileFragment, fragmentLength) == 0) {
400 							int len = strlen(filePath);
401 							if (len > maxEntryLen)
402 								maxEntryLen = len;
403 							char* newStr = new char[len + 1];
404 							strcpy(newStr, filePath);
405 							matchList.AddItem(newStr);
406 						}
407 					}
408 				}
409 
410 				// Now slowly extend our keyword to its full length, counting
411 				// numbers of matches at each step. If the match list length
412 				// is 1, we can use that whole entry. If it's greater than one,
413 				// we can use just the match length.
414 				int matchLen = matchList.CountItems();
415 				if (matchLen > 0) {
416 					int i;
417 					BString result(fileFragment);
418 					for (i = fragmentLength; i < maxEntryLen; i++) {
419 						// See if all the matching entries have the same letter
420 						// in the next position... if so, we can go farther.
421 						char commonLetter = '\0';
422 						for (int j = 0; j < matchLen; j++) {
423 							char nextLetter = GetLetterAt(
424 								(char*)matchList.ItemAt(j), i);
425 							if (commonLetter == '\0')
426 								commonLetter = nextLetter;
427 
428 							if ((commonLetter != '\0')
429 								&& (commonLetter != nextLetter)) {
430 								commonLetter = '\0';// failed;
431 								beep();
432 								break;
433 							}
434 						}
435 						if (commonLetter == '\0')
436 							break;
437 						else
438 							result.Append(commonLetter, 1);
439 					}
440 
441 					// free all the strings we allocated
442 					for (int k = 0; k < matchLen; k++)
443 						delete [] ((char*)matchList.ItemAt(k));
444 
445 					DoStandardEscapes(result);
446 
447 					BString wholeLine;
448 					for (int l = 0; l < argc - 1; l++) {
449 						wholeLine += argv[l];
450 						wholeLine += " ";
451 					}
452 
453 					BString file(directoryName);
454 					DoStandardEscapes(file);
455 
456 					if (directoryName[strlen(directoryName) - 1] != '/')
457 						file += "/";
458 
459 					file += result;
460 
461 					// Remove any trailing slash...
462 					const char* fileStr = file.String();
463 					if (fileStr[strlen(fileStr) - 1] == '/')
464 						file.RemoveLast("/");
465 
466 					// and re-append it iff the file is a dir.
467 					BDirectory testFileAsDir(file.String());
468 					if ((strcmp(file.String(), "/") != 0)
469 						&& (testFileAsDir.InitCheck() == B_NO_ERROR))
470 						file.Append("/");
471 
472 					wholeLine += file;
473 
474 					SetCommand(wholeLine.String());
475 					result = true;
476 				}
477 			}
478 			*(fileFragment - 1) = '/';
479 		}
480 	}
481 	FreeArgv(argv);
482 
483 	return result;
484 }
485 
486 
487 bool
488 ShortcutsSpec::ProcessColumnKeyStroke(int whichColumn, const char* bytes,
489 	int32 key)
490 {
491 	bool result = false;
492 
493 	switch (whichColumn) {
494 		case KEY_COLUMN_INDEX:
495 			if (key > -1) {
496 				if ((int32)fKey != key) {
497 					fKey = key;
498 					result = true;
499 				}
500 			}
501 			break;
502 
503 		case STRING_COLUMN_INDEX:
504 		{
505 			switch (bytes[0]) {
506 				case B_BACKSPACE:
507 				case B_DELETE:
508 					if (fCommandNul > 0) {
509 						// trim a char off the string
510 						fCommand[fCommandNul - 1] = '\0';
511 						fCommandNul--;	// note new nul position
512 						result = true;
513 					}
514 					break;
515 
516 				case B_TAB:
517 					if (_AttemptTabCompletion()) {
518 						result = true;
519 					} else
520 						beep();
521 					break;
522 
523 				default:
524 				{
525 					uint32 newCharLen = strlen(bytes);
526 					if ((newCharLen > 0) && (bytes[0] >= ' ')) {
527 						bool reAllocString = false;
528 						// Make sure we have enough room in our command string
529 						// to add these chars...
530 						while (fCommandLen - fCommandNul <= newCharLen) {
531 							reAllocString = true;
532 							// enough for a while...
533 							fCommandLen = (fCommandLen + 10) * 2;
534 						}
535 
536 						if (reAllocString) {
537 							char* temp = new char[fCommandLen];
538 							strcpy(temp, fCommand);
539 							delete [] fCommand;
540 							fCommand = temp;
541 							// fCommandNul is still valid since it's an offset
542 							// and the string length is the same for now
543 						}
544 
545 						// Here we should be guaranteed enough room.
546 						strncat(fCommand, bytes, fCommandLen);
547 						fCommandNul += newCharLen;
548 						result = true;
549 					}
550 				}
551 			}
552 			break;
553 		}
554 
555 		default:
556 			if (whichColumn < 0 || whichColumn >= NUM_META_COLUMNS)
557 				break;
558 
559 			MetaKeyStateMap * map = &sMetaMaps[whichColumn];
560 			int curState = fMetaCellStateIndex[whichColumn];
561 			int origState = curState;
562 			int numStates = map->GetNumStates();
563 
564 			switch(bytes[0]) {
565 				case B_RETURN:
566 					// cycle to the previous state
567 					curState = (curState + numStates - 1) % numStates;
568 					break;
569 
570 				case B_SPACE:
571 					// cycle to the next state
572 					curState = (curState + 1) % numStates;
573 					break;
574 
575 				default:
576 				{
577 					// Go to the state starting with the given letter, if
578 					// any
579 					char letter = bytes[0];
580 					if (islower(letter))
581 						letter = toupper(letter); // convert to upper case
582 
583 					if ((letter == B_BACKSPACE) || (letter == B_DELETE))
584 						letter = '(';
585 							// so space bar will blank out an entry
586 
587 					for (int i = 0; i < numStates; i++) {
588 						const char* desc = map->GetNthStateDesc(i);
589 
590 						if (desc) {
591 							if (desc[0] == letter) {
592 								curState = i;
593 								break;
594 							}
595 						} else {
596 							puts(B_TRANSLATE(
597 								"Error, NULL state description?"));
598 						}
599 					}
600 				}
601 			}
602 			fMetaCellStateIndex[whichColumn] = curState;
603 
604 			if (curState != origState)
605 				result = true;
606 	}
607 
608 	SetField(new BStringField(GetCellText(whichColumn)), whichColumn);
609 
610 	return result;
611 }
612 
613 
614 /*static*/ void
615 ShortcutsSpec::_InitModifierNames()
616 {
617 	sShiftName = B_TRANSLATE_COMMENT("Shift",
618 		"Name for modifier on keyboard");
619 	sControlName = B_TRANSLATE_COMMENT("Control",
620 		"Name for modifier on keyboard");
621 // TODO: Wrapping in __INTEL__ define probably won't work to extract catkeys?
622 #if __INTEL__
623 	sOptionName = B_TRANSLATE_COMMENT("Option",
624 		"Name for modifier on keyboard");
625 	sCommandName = B_TRANSLATE_COMMENT("Alt",
626 		"Name for modifier on keyboard");
627 #else
628 	sOptionName = B_TRANSLATE_COMMENT("Option",
629 		"Name for modifier on keyboard");
630 	sCommandName = B_TRANSLATE_COMMENT("Command",
631 		"Name for modifier on keyboard");
632 #endif
633 }
634