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
39const float _height = 20.0f;
40
41static MetaKeyStateMap sMetaMaps[ShortcutsSpec::NUM_META_COLUMNS];
42
43static bool sFontCached = false;
44static BFont sViewFont;
45static float sFontHeight;
46
47const char* ShortcutsSpec::sShiftName;
48const char* ShortcutsSpec::sControlName;
49const char* ShortcutsSpec::sOptionName;
50const 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
59static char
60GetLetterAt(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.
71static void
72SetupStandardMap(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(B_TRANSLATE("(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(B_TRANSLATE("Either"), new HasBitsFieldTester(both));
83
84	// Here, only the left may be pressed
85	map.AddState(B_TRANSLATE("Left"), new HasBitsFieldTester(left, right));
86
87	// Here, only the right may be pressed
88	map.AddState(B_TRANSLATE("Right"), new HasBitsFieldTester(right, left));
89
90	// Here, both must be pressed.
91	map.AddState(B_TRANSLATE("Both"), new HasBitsFieldTester(left | right));
92}
93
94
95MetaKeyStateMap&
96GetNthKeyMap(int which)
97{
98	return sMetaMaps[which];
99}
100
101
102/*static*/ void
103ShortcutsSpec::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
126ShortcutsSpec::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
142ShortcutsSpec::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
164ShortcutsSpec::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
199void
200ShortcutsSpec::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
212const char*
213ShortcutsSpec::GetColumnName(int i)
214{
215	return sMetaMaps[i].GetName();
216}
217
218
219status_t
220ShortcutsSpec::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
261BArchivable*
262ShortcutsSpec::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
278ShortcutsSpec::~ShortcutsSpec()
279{
280	delete[] fCommand;
281	delete[] fLastBitmapName;
282}
283
284
285void
286ShortcutsSpec::_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
298const char*
299ShortcutsSpec::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
332bool
333ShortcutsSpec::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
346bool
347ShortcutsSpec::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
370bool
371ShortcutsSpec::_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
487bool
488ShortcutsSpec::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
615ShortcutsSpec::_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	sOptionName = B_TRANSLATE_COMMENT("Option",
622		"Name for modifier on keyboard");
623	sCommandName = B_TRANSLATE_COMMENT("Alt",
624		"Name for modifier on keyboard");
625}
626