xref: /haiku/src/preferences/shortcuts/ShortcutsSpec.cpp (revision a629567a9001547736cfe892cdf992be16868fed)
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 <Directory.h>
18 #include <Locale.h>
19 #include <NodeInfo.h>
20 #include <Path.h>
21 #include <Region.h>
22 #include <Window.h>
23 
24 #include "ColumnListView.h"
25 
26 #include "BitFieldTesters.h"
27 #include "Colors.h"
28 #include "CommandActuators.h"
29 #include "MetaKeyStateMap.h"
30 #include "ParseCommandLine.h"
31 
32 
33 #define CLASS "ShortcutsSpec : "
34 
35 #undef B_TRANSLATION_CONTEXT
36 #define B_TRANSLATION_CONTEXT "ShortcutsSpec"
37 
38 const float _height = 20.0f;
39 
40 static MetaKeyStateMap sMetaMaps[ShortcutsSpec::NUM_META_COLUMNS];
41 
42 static bool sFontCached = false;
43 static BFont sViewFont;
44 static float sFontHeight;
45 static BBitmap* sActuatorBitmaps[2];
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 BBitmap*
103 MakeActuatorBitmap(bool lit)
104 {
105 	BBitmap* map = new BBitmap(ICON_BITMAP_RECT, ICON_BITMAP_SPACE, true);
106 	const rgb_color yellow = {255, 255, 0};
107 	const rgb_color red = {200, 200, 200};
108 	const rgb_color black = {0, 0, 0};
109 	const BPoint points[10] = {
110 		BPoint(8, 0), BPoint(9.8, 5.8), BPoint(16, 5.8),
111 		BPoint(11, 9.0), BPoint(13, 16), BPoint(8, 11),
112 		BPoint(3, 16), BPoint(5, 9.0), BPoint(0, 5.8),
113 		BPoint(6.2, 5.8) };
114 
115 	BView* view = new BView(BRect(0, 0, 16, 16), NULL, B_FOLLOW_ALL_SIDES, 0L);
116 	map->AddChild(view);
117 	map->Lock();
118 	view->SetHighColor(B_TRANSPARENT_32_BIT);
119 	view->FillRect(ICON_BITMAP_RECT);
120 	view->SetHighColor(lit ? yellow : red);
121 	view->FillPolygon(points, 10);
122 	view->SetHighColor(black);
123 	view->StrokePolygon(points, 10);
124 	map->Unlock();
125 	map->RemoveChild(view);
126 	delete view;
127 	return map;
128 }
129 
130 
131 /*static*/ void
132 ShortcutsSpec::InitializeMetaMaps()
133 {
134 	static bool metaMapsInitialized = false;
135 	if (metaMapsInitialized)
136 		return;
137 	metaMapsInitialized = true;
138 
139 	_InitModifierNames();
140 
141 	SetupStandardMap(sMetaMaps[ShortcutsSpec::SHIFT_COLUMN_INDEX], sShiftName,
142 		B_SHIFT_KEY, B_LEFT_SHIFT_KEY, B_RIGHT_SHIFT_KEY);
143 
144 	SetupStandardMap(sMetaMaps[ShortcutsSpec::CONTROL_COLUMN_INDEX],
145 		sControlName, B_CONTROL_KEY, B_LEFT_CONTROL_KEY, B_RIGHT_CONTROL_KEY);
146 
147 	SetupStandardMap(sMetaMaps[ShortcutsSpec::COMMAND_COLUMN_INDEX],
148 		sCommandName, B_COMMAND_KEY, B_LEFT_COMMAND_KEY, B_RIGHT_COMMAND_KEY);
149 
150 	SetupStandardMap(sMetaMaps[ShortcutsSpec::OPTION_COLUMN_INDEX], sOptionName
151 		, B_OPTION_KEY, B_LEFT_OPTION_KEY, B_RIGHT_OPTION_KEY);
152 
153 	sActuatorBitmaps[0] = MakeActuatorBitmap(false);
154 	sActuatorBitmaps[1] = MakeActuatorBitmap(true);
155 }
156 
157 
158 ShortcutsSpec::ShortcutsSpec(const char* cmd)
159 	:
160 	CLVListItem(0, false, false, _height),
161 	fCommand(NULL),
162 	fTextOffset(0),
163 	fBitmap(ICON_BITMAP_RECT, ICON_BITMAP_SPACE),
164 	fLastBitmapName(NULL),
165 	fBitmapValid(false),
166 	fKey(0),
167 	fCursorPtsValid(false)
168 {
169 	for (int i = 0; i < NUM_META_COLUMNS; i++)
170 		fMetaCellStateIndex[i] = 0;
171 	SetCommand(cmd);
172 }
173 
174 
175 ShortcutsSpec::ShortcutsSpec(const ShortcutsSpec& from)
176 	:
177 	CLVListItem(0, false, false, _height),
178 	fCommand(NULL),
179 	fTextOffset(from.fTextOffset),
180 	fBitmap(ICON_BITMAP_RECT, ICON_BITMAP_SPACE),
181 	fLastBitmapName(NULL),
182 	fBitmapValid(false),
183 	fKey(from.fKey),
184 	fCursorPtsValid(false)
185 {
186 	for (int i = 0; i < NUM_META_COLUMNS; i++)
187 		fMetaCellStateIndex[i] = from.fMetaCellStateIndex[i];
188 
189 	SetCommand(from.fCommand);
190 	SetSelectedColumn(from.GetSelectedColumn());
191 }
192 
193 
194 ShortcutsSpec::ShortcutsSpec(BMessage* from)
195 	:
196 	CLVListItem(0, false, false, _height),
197 	fCommand(NULL),
198 	fTextOffset(0),
199 	fBitmap(ICON_BITMAP_RECT, ICON_BITMAP_SPACE),
200 	fLastBitmapName(NULL),
201 	fBitmapValid(false),
202 	fCursorPtsValid(false)
203 {
204 	const char* temp;
205 	if (from->FindString("command", &temp) != B_NO_ERROR) {
206 		printf(CLASS);
207 		printf(" Error, no command string in archive BMessage!\n");
208 		temp = "";
209 	}
210 
211 	SetCommand(temp);
212 
213 	if (from->FindInt32("key", (int32*) &fKey) != B_NO_ERROR) {
214 		printf(CLASS);
215 		printf(" Error, no key int32 in archive BMessage!\n");
216 	}
217 
218 	for (int i = 0; i < NUM_META_COLUMNS; i++)
219 		if (from->FindInt32("mcidx", i, (int32*)&fMetaCellStateIndex[i])
220 			!= B_NO_ERROR) {
221 			printf(CLASS);
222 			printf(" Error, no modifiers int32 in archive BMessage!\n");
223 		}
224 }
225 
226 
227 void
228 ShortcutsSpec::SetCommand(const char* command)
229 {
230 	delete[] fCommand;
231 		// out with the old (if any)...
232 	fCommandLen = strlen(command) + 1;
233 	fCommandNul = fCommandLen - 1;
234 	fCommand = new char[fCommandLen];
235 	strcpy(fCommand, command);
236 	_UpdateIconBitmap();
237 }
238 
239 
240 const char*
241 ShortcutsSpec::GetColumnName(int i)
242 {
243 	return sMetaMaps[i].GetName();
244 }
245 
246 
247 status_t
248 ShortcutsSpec::Archive(BMessage* into, bool deep) const
249 {
250 	status_t ret = BArchivable::Archive(into, deep);
251 	if (ret != B_NO_ERROR)
252 		return ret;
253 
254 	into->AddString("class", "ShortcutsSpec");
255 
256 	// These fields are for our prefs panel's benefit only
257 	into->AddString("command", fCommand);
258 	into->AddInt32("key", fKey);
259 
260 	// Assemble a BitFieldTester for the input_server add-on to use...
261 	MinMatchFieldTester test(NUM_META_COLUMNS, false);
262 	for (int i = 0; i < NUM_META_COLUMNS; i++) {
263 		// for easy parsing by prefs applet on load-in
264 		into->AddInt32("mcidx", fMetaCellStateIndex[i]);
265 		test.AddSlave(sMetaMaps[i].GetNthStateTester(fMetaCellStateIndex[i]));
266 	}
267 
268 	BMessage testerMsg;
269 	ret = test.Archive(&testerMsg);
270 	if (ret != B_NO_ERROR)
271 		return ret;
272 
273 	into->AddMessage("modtester", &testerMsg);
274 
275 	// And also create a CommandActuator for the input_server add-on to execute
276 	CommandActuator* act = CreateCommandActuator(fCommand);
277 	BMessage actMsg;
278 	ret = act->Archive(&actMsg);
279 	if (ret != B_NO_ERROR)
280 		return ret;
281 	delete act;
282 
283 	into->AddMessage("act", &actMsg);
284 	return ret;
285 }
286 
287 
288 static bool IsValidActuatorName(const char* c);
289 static bool
290 IsValidActuatorName(const char* c)
291 {
292 	return (strcmp(c, B_TRANSLATE("InsertString")) == 0
293 		|| strcmp(c, B_TRANSLATE("MoveMouse")) == 0
294 		|| strcmp(c, B_TRANSLATE("MoveMouseTo")) == 0
295 		|| strcmp(c, B_TRANSLATE("MouseButton")) == 0
296 		|| strcmp(c, B_TRANSLATE("LaunchHandler")) == 0
297 		|| strcmp(c, B_TRANSLATE("Multi")) == 0
298 		|| strcmp(c, B_TRANSLATE("MouseDown")) == 0
299 		|| strcmp(c, B_TRANSLATE("MouseUp")) == 0
300 		|| strcmp(c, B_TRANSLATE("SendMessage")) == 0
301 		|| strcmp(c, B_TRANSLATE("Beep")) == 0);
302 }
303 
304 
305 BArchivable*
306 ShortcutsSpec::Instantiate(BMessage* from)
307 {
308 	bool validateOK = false;
309 	if (validate_instantiation(from, "ShortcutsSpec"))
310 		validateOK = true;
311 	else // test the old one.
312 		if (validate_instantiation(from, "SpicyKeysSpec"))
313 			validateOK = true;
314 
315 	if (!validateOK)
316 		return NULL;
317 
318 	return new ShortcutsSpec(from);
319 }
320 
321 
322 ShortcutsSpec::~ShortcutsSpec()
323 {
324 	delete[] fCommand;
325 	delete[] fLastBitmapName;
326 }
327 
328 
329 void
330 ShortcutsSpec::_CacheViewFont(BView* owner)
331 {
332 	if (sFontCached == false) {
333 		sFontCached = true;
334 		owner->GetFont(&sViewFont);
335 		font_height fh;
336 		sViewFont.GetHeight(&fh);
337 		sFontHeight = fh.ascent - fh.descent;
338 	}
339 }
340 
341 
342 void
343 ShortcutsSpec::DrawItemColumn(BView* owner, BRect item_column_rect,
344 	int32 column_index, bool columnSelected, bool complete)
345 {
346 	const float STRING_COLUMN_LEFT_MARGIN = 25.0f;
347 		// 16 for the icon, +9 empty
348 
349 	rgb_color color;
350 	bool selected = IsSelected();
351 	if (selected)
352 		color = columnSelected ? BeBackgroundGrey : BeListSelectGrey;
353 	else
354 		color = BeInactiveControlGrey;
355 	owner->SetLowColor(color);
356 	owner->SetDrawingMode(B_OP_COPY);
357 	owner->SetHighColor(color);
358 	owner->FillRect(item_column_rect);
359 
360 	const char* text = GetCellText(column_index);
361 
362 	if (text == NULL)
363 		return;
364 
365 	_CacheViewFont(owner);
366 		// Ensure that sViewFont is configured before using it to calculate
367 		// widths.  The lack of this call was causing the initial display of
368 		// columns to be incorrect, with a "jump" as all the columns correct
369 		// themselves upon the first column resize.
370 
371 	float textWidth = sViewFont.StringWidth(text);
372 	BPoint point;
373 	rgb_color lowColor = color;
374 
375 	if (column_index == STRING_COLUMN_INDEX) {
376 		// left justified
377 		point.Set(item_column_rect.left + STRING_COLUMN_LEFT_MARGIN,
378 			item_column_rect.top + fTextOffset);
379 
380 		item_column_rect.left = point.x;
381 			// keep text from drawing into icon area
382 
383 		// scroll if too wide
384 		float rectWidth = item_column_rect.Width() - STRING_COLUMN_LEFT_MARGIN;
385 		float extra = textWidth - rectWidth;
386 		if (extra > 0.0f)
387 			point.x -= extra;
388 	} else {
389 		if ((column_index < NUM_META_COLUMNS) && (text[0] == '('))
390 			return; // don't draw for this ...
391 
392 		if ((column_index <= NUM_META_COLUMNS) && (text[0] == '\0'))
393 			return; // don't draw for this ...
394 
395 		// centered
396 		point.Set((item_column_rect.left + item_column_rect.right) / 2.0,
397 			item_column_rect.top + fTextOffset);
398 		_CacheViewFont(owner);
399 		point.x -= textWidth / 2.0f;
400 	}
401 
402 	BRegion Region;
403 	Region.Include(item_column_rect);
404 	owner->ConstrainClippingRegion(&Region);
405 	if (column_index != STRING_COLUMN_INDEX) {
406 		const float KEY_MARGIN = 3.0f;
407 		const float CORNER_RADIUS = 3.0f;
408 		_CacheViewFont(owner);
409 
410 		// How about I draw a nice "key" background for this one?
411 		BRect textRect(point.x - KEY_MARGIN, (point.y - sFontHeight) - KEY_MARGIN,
412 			point.x + textWidth + KEY_MARGIN - 2.0f, point.y + KEY_MARGIN);
413 
414 		if (column_index == KEY_COLUMN_INDEX)
415 			lowColor = ReallyLightPurple;
416 		else
417 			lowColor = LightYellow;
418 
419 		owner->SetHighColor(lowColor);
420 		owner->FillRoundRect(textRect, CORNER_RADIUS, CORNER_RADIUS);
421 		owner->SetHighColor(Black);
422 		owner->StrokeRoundRect(textRect, CORNER_RADIUS, CORNER_RADIUS);
423 	}
424 
425 	owner->SetHighColor(Black);
426 	owner->SetLowColor(lowColor);
427 	owner->DrawString(text, point);
428 	// with a cursor at the end if highlighted
429 	if (column_index == STRING_COLUMN_INDEX) {
430 		// Draw cursor
431 		if ((columnSelected) && (selected)) {
432 			point.x += textWidth;
433 			point.y += (fTextOffset / 4.0f);
434 
435 			BPoint pt2 = point;
436 			pt2.y -= fTextOffset;
437 			owner->StrokeLine(point, pt2);
438 
439 			fCursorPt1 = point;
440 			fCursorPt2 = pt2;
441 			fCursorPtsValid = true;
442 		}
443 
444 		BRegion bitmapRegion;
445 		item_column_rect.left	-= (STRING_COLUMN_LEFT_MARGIN - 4.0f);
446 		item_column_rect.right	= item_column_rect.left + 16.0f;
447 		item_column_rect.top	+= 3.0f;
448 		item_column_rect.bottom	= item_column_rect.top + 16.0f;
449 
450 		bitmapRegion.Include(item_column_rect);
451 		owner->ConstrainClippingRegion(&bitmapRegion);
452 		owner->SetDrawingMode(B_OP_ALPHA);
453 
454 		if ((fCommand != NULL) && (fCommand[0] == '*'))
455 			owner->DrawBitmap(sActuatorBitmaps[fBitmapValid ? 1 : 0],
456 				ICON_BITMAP_RECT, item_column_rect);
457 		else
458 			// Draw icon, if any
459 			if (fBitmapValid)
460 				owner->DrawBitmap(&fBitmap, ICON_BITMAP_RECT,
461 					item_column_rect);
462 	}
463 
464 	owner->SetDrawingMode(B_OP_COPY);
465 	owner->ConstrainClippingRegion(NULL);
466 }
467 
468 
469 void
470 ShortcutsSpec::Update(BView* owner, const BFont* font)
471 {
472 	CLVListItem::Update(owner, font);
473 	font_height FontAttributes;
474 	be_plain_font->GetHeight(&FontAttributes);
475 	float fontHeight = ceil(FontAttributes.ascent) +
476 		ceil(FontAttributes.descent);
477 	fTextOffset = ceil(FontAttributes.ascent) + (Height() - fontHeight) / 2.0;
478 }
479 
480 
481 const char*
482 ShortcutsSpec::GetCellText(int whichColumn) const
483 {
484 	const char* temp = ""; // default
485 	switch(whichColumn) {
486 		case KEY_COLUMN_INDEX:
487 		{
488 			if ((fKey > 0) && (fKey <= 0xFF)) {
489 				temp = GetKeyName(fKey);
490 				if (temp == NULL)
491 					temp = "";
492 			} else if (fKey > 0xFF) {
493 				sprintf(fScratch, "#%" B_PRIx32, fKey);
494 				return fScratch;
495 			}
496 			break;
497 		}
498 
499 		case STRING_COLUMN_INDEX:
500 			temp = fCommand;
501 			break;
502 
503 		default:
504 			if ((whichColumn >= 0) && (whichColumn < NUM_META_COLUMNS))
505 				temp = sMetaMaps[whichColumn].GetNthStateDesc(
506 							fMetaCellStateIndex[whichColumn]);
507 			break;
508 	}
509 	return temp;
510 }
511 
512 
513 bool
514 ShortcutsSpec::ProcessColumnMouseClick(int whichColumn)
515 {
516 	if ((whichColumn >= 0) && (whichColumn < NUM_META_COLUMNS)) {
517 		// same as hitting space for these columns: cycle entry
518 		const char temp = B_SPACE;
519 
520 		// 3rd arg isn't correct but it isn't read for this case anyway
521 		return ProcessColumnKeyStroke(whichColumn, &temp, 0);
522 	}
523 	return false;
524 }
525 
526 
527 bool
528 ShortcutsSpec::ProcessColumnTextString(int whichColumn, const char* string)
529 {
530 	switch(whichColumn) {
531 		case STRING_COLUMN_INDEX:
532 			SetCommand(string);
533 			return true;
534 			break;
535 
536 		case KEY_COLUMN_INDEX:
537 		{
538 			fKey = FindKeyCode(string);
539 			return true;
540 			break;
541 		}
542 
543 		default:
544 			return ProcessColumnKeyStroke(whichColumn, string, 0);
545 	}
546 }
547 
548 
549 bool
550 ShortcutsSpec::_AttemptTabCompletion()
551 {
552 	bool result = false;
553 
554 	int32 argc;
555 	char** argv = ParseArgvFromString(fCommand, argc);
556 	if (argc > 0) {
557 		// Try to complete the path partially expressed in the last argument!
558 		char* arg = argv[argc - 1];
559 		char* fileFragment = strrchr(arg, '/');
560 		if (fileFragment != NULL) {
561 			const char* directoryName = (fileFragment == arg) ? "/" : arg;
562 			*fileFragment = '\0';
563 			fileFragment++;
564 			int fragmentLength = strlen(fileFragment);
565 
566 			BDirectory dir(directoryName);
567 			if (dir.InitCheck() == B_NO_ERROR) {
568 				BEntry nextEnt;
569 				BPath nextPath;
570 				BList matchList;
571 				int maxEntryLen = 0;
572 
573 				// Read in all the files in the directory whose names start
574 				// with our fragment.
575 				while (dir.GetNextEntry(&nextEnt) == B_NO_ERROR) {
576 					if (nextEnt.GetPath(&nextPath) == B_NO_ERROR) {
577 						char* filePath = strrchr(nextPath.Path(), '/') + 1;
578 						if (strncmp(filePath, fileFragment, fragmentLength) == 0) {
579 							int len = strlen(filePath);
580 							if (len > maxEntryLen)
581 								maxEntryLen = len;
582 							char* newStr = new char[len + 1];
583 							strcpy(newStr, filePath);
584 							matchList.AddItem(newStr);
585 						}
586 					}
587 				}
588 
589 				// Now slowly extend our keyword to its full length, counting
590 				// numbers of matches at each step. If the match list length
591 				// is 1, we can use that whole entry. If it's greater than one,
592 				// we can use just the match length.
593 				int matchLen = matchList.CountItems();
594 				if (matchLen > 0) {
595 					int i;
596 					BString result(fileFragment);
597 					for (i = fragmentLength; i < maxEntryLen; i++) {
598 						// See if all the matching entries have the same letter
599 						// in the next position... if so, we can go farther.
600 						char commonLetter = '\0';
601 						for (int j = 0; j < matchLen; j++) {
602 							char nextLetter = GetLetterAt(
603 								(char*)matchList.ItemAt(j), i);
604 							if (commonLetter == '\0')
605 								commonLetter = nextLetter;
606 
607 							if ((commonLetter != '\0')
608 								&& (commonLetter != nextLetter)) {
609 								commonLetter = '\0';// failed;
610 								beep();
611 								break;
612 							}
613 						}
614 						if (commonLetter == '\0')
615 							break;
616 						else
617 							result.Append(commonLetter, 1);
618 					}
619 
620 					// free all the strings we allocated
621 					for (int k = 0; k < matchLen; k++)
622 						delete [] ((char*)matchList.ItemAt(k));
623 
624 					DoStandardEscapes(result);
625 
626 					BString wholeLine;
627 					for (int l = 0; l < argc - 1; l++) {
628 						wholeLine += argv[l];
629 						wholeLine += " ";
630 					}
631 
632 					BString file(directoryName);
633 					DoStandardEscapes(file);
634 
635 					if (directoryName[strlen(directoryName) - 1] != '/')
636 						file += "/";
637 
638 					file += result;
639 
640 					// Remove any trailing slash...
641 					const char* fileStr = file.String();
642 					if (fileStr[strlen(fileStr) - 1] == '/')
643 						file.RemoveLast("/");
644 
645 					// and re-append it iff the file is a dir.
646 					BDirectory testFileAsDir(file.String());
647 					if ((strcmp(file.String(), "/") != 0)
648 						&& (testFileAsDir.InitCheck() == B_NO_ERROR))
649 						file.Append("/");
650 
651 					wholeLine += file;
652 
653 					SetCommand(wholeLine.String());
654 					result = true;
655 				}
656 			}
657 			*(fileFragment - 1) = '/';
658 		}
659 	}
660 	FreeArgv(argv);
661 
662 	return result;
663 }
664 
665 
666 bool
667 ShortcutsSpec::ProcessColumnKeyStroke(int whichColumn, const char* bytes,
668 	int32 key)
669 {
670 	bool result = false;
671 
672 	switch(whichColumn) {
673 		case KEY_COLUMN_INDEX:
674 			if (key > -1) {
675 				if ((int32)fKey != key) {
676 					fKey = key;
677 					result = true;
678 				}
679 			}
680 			break;
681 
682 		case STRING_COLUMN_INDEX:
683 		{
684 			switch(bytes[0]) {
685 				case B_BACKSPACE:
686 				case B_DELETE:
687 					if (fCommandNul > 0) {
688 						// trim a char off the string
689 						fCommand[fCommandNul - 1] = '\0';
690 						fCommandNul--;	// note new nul position
691 						result = true;
692 						_UpdateIconBitmap();
693 					}
694 					break;
695 
696 				case B_TAB:
697 					if (_AttemptTabCompletion()) {
698 						_UpdateIconBitmap();
699 						result = true;
700 					} else
701 						beep();
702 					break;
703 
704 				default:
705 				{
706 					uint32 newCharLen = strlen(bytes);
707 					if ((newCharLen > 0) && (bytes[0] >= ' ')) {
708 						bool reAllocString = false;
709 						// Make sure we have enough room in our command string
710 						// to add these chars...
711 						while (fCommandLen - fCommandNul <= newCharLen) {
712 							reAllocString = true;
713 							// enough for a while...
714 							fCommandLen = (fCommandLen + 10) * 2;
715 						}
716 
717 						if (reAllocString) {
718 							char* temp = new char[fCommandLen];
719 							strcpy(temp, fCommand);
720 							delete [] fCommand;
721 							fCommand = temp;
722 							// fCommandNul is still valid since it's an offset
723 							// and the string length is the same for now
724 						}
725 
726 						// Here we should be guaranteed enough room.
727 						strncat(fCommand, bytes, fCommandLen);
728 						fCommandNul += newCharLen;
729 						result = true;
730 						_UpdateIconBitmap();
731 					}
732 				}
733 			}
734 			break;
735 		}
736 
737 		default:
738 			if (whichColumn < 0 || whichColumn >= NUM_META_COLUMNS)
739 				break;
740 
741 			MetaKeyStateMap * map = &sMetaMaps[whichColumn];
742 			int curState = fMetaCellStateIndex[whichColumn];
743 			int origState = curState;
744 			int numStates = map->GetNumStates();
745 
746 			switch(bytes[0]) {
747 				case B_RETURN:
748 					// cycle to the previous state
749 					curState = (curState + numStates - 1) % numStates;
750 					break;
751 
752 				case B_SPACE:
753 					// cycle to the next state
754 					curState = (curState + 1) % numStates;
755 					break;
756 
757 				default:
758 				{
759 					// Go to the state starting with the given letter, if
760 					// any
761 					char letter = bytes[0];
762 					if (islower(letter))
763 						letter = toupper(letter); // convert to upper case
764 
765 					if ((letter == B_BACKSPACE) || (letter == B_DELETE))
766 						letter = '(';
767 							// so space bar will blank out an entry
768 
769 					for (int i = 0; i < numStates; i++) {
770 						const char* desc = map->GetNthStateDesc(i);
771 
772 						if (desc) {
773 							if (desc[0] == letter) {
774 								curState = i;
775 								break;
776 							}
777 						} else {
778 							puts(B_TRANSLATE(
779 								"Error, NULL state description?"));
780 						}
781 					}
782 				}
783 			}
784 			fMetaCellStateIndex[whichColumn] = curState;
785 
786 			if (curState != origState)
787 				result = true;
788 	}
789 
790 	return result;
791 }
792 
793 
794 int
795 ShortcutsSpec::CLVListItemCompare(const CLVListItem* firstItem,
796 	const CLVListItem* secondItem, int32 keyColumn)
797 {
798 	ShortcutsSpec* left = (ShortcutsSpec*) firstItem;
799 	ShortcutsSpec* right = (ShortcutsSpec*) secondItem;
800 
801 	int result = strcmp(left->GetCellText(keyColumn),
802 		right->GetCellText(keyColumn));
803 
804 	return result > 0 ? 1 : (result == 0 ? 0 : -1);
805 }
806 
807 
808 void
809 ShortcutsSpec::Pulse(BView* owner)
810 {
811 	if ((fCursorPtsValid)&&(owner->Window()->IsActive())) {
812 		rgb_color prevColor = owner->HighColor();
813 		rgb_color backgroundColor = (GetSelectedColumn() ==
814 			STRING_COLUMN_INDEX) ? BeBackgroundGrey : BeListSelectGrey;
815 		rgb_color barColor = ((GetSelectedColumn() == STRING_COLUMN_INDEX)
816 			&& ((system_time() % 1000000) > 500000)) ? Black : backgroundColor;
817 		owner->SetHighColor(barColor);
818 		owner->StrokeLine(fCursorPt1, fCursorPt2);
819 		owner->SetHighColor(prevColor);
820 	}
821 }
822 
823 
824 void
825 ShortcutsSpec::_UpdateIconBitmap()
826 {
827 	BString firstWord = ParseArgvZeroFromString(fCommand);
828 
829 	// we only need to change if the first word has changed...
830 	if (fLastBitmapName == NULL || firstWord.Length() == 0
831 		|| firstWord.Compare(fLastBitmapName)) {
832 		if (firstWord.ByteAt(0) == '*')
833 			fBitmapValid = IsValidActuatorName(&firstWord.String()[1]);
834 		else {
835 			fBitmapValid = false;
836 			// default until we prove otherwise
837 
838 			if (firstWord.Length() > 0) {
839 				delete [] fLastBitmapName;
840 				fLastBitmapName = new char[firstWord.Length() + 1];
841 				strcpy(fLastBitmapName, firstWord.String());
842 
843 				BEntry progEntry(fLastBitmapName, true);
844 				if ((progEntry.InitCheck() == B_NO_ERROR)
845 					&& (progEntry.Exists())) {
846 					BNode progNode(&progEntry);
847 					if (progNode.InitCheck() == B_NO_ERROR) {
848 						BNodeInfo progNodeInfo(&progNode);
849 						if ((progNodeInfo.InitCheck() == B_NO_ERROR)
850 						&& (progNodeInfo.GetTrackerIcon(&fBitmap, B_MINI_ICON)
851 							== B_NO_ERROR)) {
852 							fBitmapValid = fBitmap.IsValid();
853 						}
854 					}
855 				}
856 			}
857 		}
858 	}
859 }
860 
861 
862 /*static*/ void
863 ShortcutsSpec::_InitModifierNames()
864 {
865 	sShiftName = B_TRANSLATE_COMMENT("Shift",
866 		"Name for modifier on keyboard");
867 	sControlName = B_TRANSLATE_COMMENT("Control",
868 		"Name for modifier on keyboard");
869 // TODO: Wrapping in __INTEL__ define probably won't work to extract catkeys?
870 #if __INTEL__
871 	sOptionName = B_TRANSLATE_COMMENT("Option",
872 		"Name for modifier on keyboard");
873 	sCommandName = B_TRANSLATE_COMMENT("Alt",
874 		"Name for modifier on keyboard");
875 #else
876 	sOptionName = B_TRANSLATE_COMMENT("Option",
877 		"Name for modifier on keyboard");
878 	sCommandName = B_TRANSLATE_COMMENT("Command",
879 		"Name for modifier on keyboard");
880 #endif
881 }
882