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
GetLetterAt(const char * str,int pos)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
SetupStandardMap(MetaKeyStateMap & map,const char * name,uint32 both,uint32 left,uint32 right)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(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
95 MetaKeyStateMap&
GetNthKeyMap(int which)96 GetNthKeyMap(int which)
97 {
98 return sMetaMaps[which];
99 }
100
101
102 /*static*/ void
InitializeMetaMaps()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
ShortcutsSpec(const char * cmd)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
ShortcutsSpec(const ShortcutsSpec & from)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
ShortcutsSpec(BMessage * from)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
SetCommand(const char * command)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*
GetColumnName(int i)213 ShortcutsSpec::GetColumnName(int i)
214 {
215 return sMetaMaps[i].GetName();
216 }
217
218
219 status_t
Archive(BMessage * into,bool deep) const220 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*
Instantiate(BMessage * from)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
~ShortcutsSpec()278 ShortcutsSpec::~ShortcutsSpec()
279 {
280 delete[] fCommand;
281 delete[] fLastBitmapName;
282 }
283
284
285 void
_CacheViewFont(BView * owner)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*
GetCellText(int whichColumn) const299 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
ProcessColumnMouseClick(int whichColumn)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
ProcessColumnTextString(int whichColumn,const char * string)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
_AttemptTabCompletion()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
ProcessColumnKeyStroke(int whichColumn,const char * bytes,int32 key)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
_InitModifierNames()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 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