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