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 *		Fredrik Mod��en
9 */
10
11
12#include "ShortcutsWindow.h"
13
14#include <math.h>
15#include <stdio.h>
16
17#include <Alert.h>
18#include <Application.h>
19#include <Button.h>
20#include <Catalog.h>
21#include <Clipboard.h>
22#include <ColumnListView.h>
23#include <ColumnTypes.h>
24#include <ControlLook.h>
25#include <File.h>
26#include <FilePanel.h>
27#include <FindDirectory.h>
28#include <Input.h>
29#include <LayoutBuilder.h>
30#include <Locale.h>
31#include <Message.h>
32#include <Menu.h>
33#include <MenuBar.h>
34#include <MenuItem.h>
35#include <MessageFilter.h>
36#include <Path.h>
37#include <PopUpMenu.h>
38#include <Screen.h>
39#include <ScrollBar.h>
40#include <ScrollView.h>
41#include <String.h>
42#include <SupportDefs.h>
43#include <usb/USB_hid.h>
44#include <usb/USB_hid_page_consumer.h>
45
46#include "EditWindow.h"
47#include "KeyInfos.h"
48#include "MetaKeyStateMap.h"
49#include "ParseCommandLine.h"
50#include "PopUpColumn.h"
51#include "ShortcutsFilterConstants.h"
52#include "ShortcutsSpec.h"
53
54
55// Window sizing constraints
56#define MAX_WIDTH 10000
57#define MAX_HEIGHT 10000
58	// SetSizeLimits does not provide a mechanism for specifying an
59	// unrestricted maximum. 10,000 seems to be the most common value used
60	// in other Haiku system applications.
61
62#define WINDOW_SETTINGS_FILE_NAME "Shortcuts_window_settings"
63	// Because the "shortcuts_settings" file (SHORTCUTS_SETTING_FILE_NAME) is
64	// already used as a communications method between this configurator and
65	// the "shortcut_catcher" input_server filter, it should not be overloaded
66	// with window position information, instead, a separate file is used.
67
68#undef B_TRANSLATION_CONTEXT
69#define B_TRANSLATION_CONTEXT "ShortcutsWindow"
70
71#define ERROR "Shortcuts error"
72#define WARNING "Shortcuts warning"
73
74
75// Creates a pop-up-menu that reflects the possible states of the specified
76// meta-key.
77static BPopUpMenu*
78CreateMetaPopUp(int column)
79{
80	MetaKeyStateMap& map = GetNthKeyMap(column);
81	BPopUpMenu* popup = new BPopUpMenu(NULL, false);
82	int stateCount = map.GetNumStates();
83
84	for (int i = 0; i < stateCount; i++)
85		popup->AddItem(new BMenuItem(map.GetNthStateDesc(i), NULL));
86
87	return popup;
88}
89
90
91// Creates a pop-up that allows the user to choose a key-cap visually
92static BPopUpMenu*
93CreateKeysPopUp()
94{
95	BPopUpMenu* popup = new BPopUpMenu(NULL, false);
96	int numKeys = GetNumKeyIndices();
97	for (int i = 0; i < numKeys; i++) {
98		const char* next = GetKeyName(i);
99		if (next != NULL)
100			popup->AddItem(new BMenuItem(next, NULL));
101	}
102
103	return popup;
104}
105
106
107ShortcutsWindow::ShortcutsWindow()
108	:
109	BWindow(BRect(0, 0, 0, 0), B_TRANSLATE_SYSTEM_NAME("Shortcuts"),
110		B_TITLED_WINDOW, B_AUTO_UPDATE_SIZE_LIMITS),
111	fSavePanel(NULL),
112	fOpenPanel(NULL),
113	fSelectPanel(NULL),
114	fKeySetModified(false),
115	fLastOpenWasAppend(false)
116{
117	ShortcutsSpec::InitializeMetaMaps();
118
119	BMenuBar* menuBar = new BMenuBar("Menu Bar");
120
121	BMenu* fileMenu = new BMenu(B_TRANSLATE("File"));
122	fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Open KeySet" B_UTF8_ELLIPSIS),
123		new BMessage(OPEN_KEYSET), 'O'));
124	fileMenu->AddItem(new BMenuItem(
125		B_TRANSLATE("Append KeySet" B_UTF8_ELLIPSIS),
126		new BMessage(APPEND_KEYSET), 'A'));
127	fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Revert to saved"),
128		new BMessage(REVERT_KEYSET), 'R'));
129	fileMenu->AddItem(new BSeparatorItem);
130	fileMenu->AddItem(new BMenuItem(
131		B_TRANSLATE("Save KeySet as" B_UTF8_ELLIPSIS),
132		new BMessage(SAVE_KEYSET_AS), 'S'));
133	fileMenu->AddItem(new BSeparatorItem);
134	fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Quit"),
135		new BMessage(B_QUIT_REQUESTED), 'Q'));
136	menuBar->AddItem(fileMenu);
137
138	fColumnListView = new BColumnListView(NULL,
139		B_WILL_DRAW | B_FRAME_EVENTS, B_FANCY_BORDER, false);
140
141	float cellWidth = be_plain_font->StringWidth("Either") + 20;
142		// ShortcutsSpec does not seem to translate the string "Either".
143
144	for (int i = 0; i < ShortcutsSpec::NUM_META_COLUMNS; i++) {
145		const char* name = ShortcutsSpec::GetColumnName(i);
146		float headerWidth = be_plain_font->StringWidth(name) + 20;
147		float width = max_c(headerWidth, cellWidth);
148
149		fColumnListView->AddColumn(new PopUpColumn(CreateMetaPopUp(i), name,
150				width, width - 1, width * 1.5, B_TRUNCATE_END, false, true, 1),
151			fColumnListView->CountColumns());
152	}
153
154	float keyCellWidth = be_plain_font->StringWidth("Caps Lock") + 20;
155	fColumnListView->AddColumn(new PopUpColumn(CreateKeysPopUp(),
156			B_TRANSLATE("Key"), keyCellWidth, keyCellWidth - 10,
157			keyCellWidth + 30, B_TRUNCATE_END),
158		fColumnListView->CountColumns());
159	BPopUpMenu* popup = new BPopUpMenu(NULL, false);
160	popup->AddItem(new BMenuItem(
161		B_TRANSLATE("(Choose application with file requester)"), NULL));
162	popup->AddItem(new BMenuItem(
163		B_TRANSLATE("*InsertString \"Your Text Here\""), NULL));
164	popup->AddItem(new BMenuItem(
165		B_TRANSLATE("*MoveMouse +20 +0"), NULL));
166	popup->AddItem(new BMenuItem(B_TRANSLATE("*MoveMouseTo 50% 50%"), NULL));
167	popup->AddItem(new BMenuItem(B_TRANSLATE("*MouseButton 1"), NULL));
168	popup->AddItem(new BMenuItem(
169		B_TRANSLATE("*LaunchHandler text/html"), NULL));
170	popup->AddItem(new BMenuItem(
171		B_TRANSLATE("*Multi \"*MoveMouseTo 100% 0\" \"*MouseButton 1\""),
172		NULL));
173	popup->AddItem(new BMenuItem(B_TRANSLATE("*MouseDown"), NULL));
174	popup->AddItem(new BMenuItem(B_TRANSLATE("*MouseUp"), NULL));
175	popup->AddItem(new BMenuItem(
176		B_TRANSLATE("*SendMessage application/x-vnd.Be-TRAK 'Tfnd'"), NULL));
177	popup->AddItem(new BMenuItem(B_TRANSLATE("*Beep"), NULL));
178	fColumnListView->AddColumn(new PopUpColumn(popup, B_TRANSLATE("Application"),
179			300.0, 223.0, 324.0, B_TRUNCATE_END, true),
180		fColumnListView->CountColumns());
181
182	fColumnListView->SetSelectionMessage(new BMessage(HOTKEY_ITEM_SELECTED));
183	fColumnListView->SetSelectionMode(B_SINGLE_SELECTION_LIST);
184	fColumnListView->SetTarget(this);
185
186	fAddButton = new BButton("add", B_TRANSLATE("Add new shortcut"),
187		new BMessage(ADD_HOTKEY_ITEM));
188
189	fRemoveButton = new BButton("remove",
190		B_TRANSLATE("Remove selected shortcut"),
191		new BMessage(REMOVE_HOTKEY_ITEM));
192	fRemoveButton->SetEnabled(false);
193
194	fSaveButton = new BButton("save", B_TRANSLATE("Save & apply"),
195		new BMessage(SAVE_KEYSET));
196	fSaveButton->SetEnabled(false);
197
198	CenterOnScreen();
199
200	fColumnListView->ResizeAllColumnsToPreferred();
201
202	entry_ref windowSettingsRef;
203	if (_GetWindowSettingsFile(&windowSettingsRef)) {
204		// The window settings file is not accepted via B_REFS_RECEIVED; this
205		// is a behind-the-scenes file that the user will never see or
206		// interact with.
207		BFile windowSettingsFile(&windowSettingsRef, B_READ_ONLY);
208		BMessage loadMessage;
209		if (loadMessage.Unflatten(&windowSettingsFile) == B_OK)
210			_LoadWindowSettings(loadMessage);
211	}
212
213	entry_ref keySetRef;
214	if (_GetSettingsFile(&keySetRef)) {
215		BMessage message(B_REFS_RECEIVED);
216		message.AddRef("refs", &keySetRef);
217		message.AddString("startupRef", "please");
218		PostMessage(&message);
219			// tell ourselves to load this file if it exists
220	} else {
221		_AddNewSpec("/bin/setvolume -m", (B_HID_USAGE_PAGE_CONSUMER << 16) | B_HID_UID_CON_MUTE);
222		_AddNewSpec("/bin/setvolume -i", (B_HID_USAGE_PAGE_CONSUMER << 16) | B_HID_UID_CON_VOLUME_INCREMENT);
223		_AddNewSpec("/bin/setvolume -d", (B_HID_USAGE_PAGE_CONSUMER << 16) | B_HID_UID_CON_VOLUME_DECREMENT);
224		fLastSaved = BEntry(&keySetRef);
225		PostMessage(SAVE_KEYSET);
226	}
227
228	BLayoutBuilder::Group<>(this, B_VERTICAL, 0)
229		.Add(menuBar)
230		.AddGroup(B_VERTICAL)
231			.SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET))
232			.SetInsets(B_USE_WINDOW_SPACING)
233			.Add(fColumnListView)
234			.AddGroup(B_HORIZONTAL)
235				.AddGroup(B_HORIZONTAL)
236				.SetExplicitAlignment(BAlignment(B_ALIGN_LEFT, B_ALIGN_TOP))
237				.Add(fAddButton)
238				.Add(fRemoveButton)
239				.End()
240				.AddGroup(B_HORIZONTAL)
241					.SetExplicitAlignment(BAlignment(B_ALIGN_RIGHT, B_ALIGN_TOP))
242					.Add(fSaveButton)
243				.End()
244			.End()
245		.End();
246
247	Show();
248}
249
250
251ShortcutsWindow::~ShortcutsWindow()
252{
253	delete fSavePanel;
254	delete fOpenPanel;
255	delete fSelectPanel;
256	be_app->PostMessage(B_QUIT_REQUESTED);
257}
258
259
260bool
261ShortcutsWindow::QuitRequested()
262{
263	bool result = true;
264
265	if (fKeySetModified) {
266		BAlert* alert = new BAlert(WARNING,
267			B_TRANSLATE("Save changes before closing?"),
268			B_TRANSLATE("Cancel"), B_TRANSLATE("Don't save"),
269			B_TRANSLATE("Save"));
270		alert->SetShortcut(0, B_ESCAPE);
271		alert->SetShortcut(1, 'd');
272		alert->SetShortcut(2, 's');
273		switch (alert->Go()) {
274			case 0:
275				result = false;
276				break;
277
278			case 1:
279				result = true;
280				break;
281
282			case 2:
283				// Save: automatically if possible, otherwise go back and open
284				// up the file requester
285				if (fLastSaved.InitCheck() == B_OK) {
286					if (_SaveKeySet(fLastSaved) == false) {
287						BAlert* alert = new BAlert(ERROR,
288							B_TRANSLATE("Shortcuts was unable to save your "
289								"KeySet file!"),
290							B_TRANSLATE("Oh no"));
291						alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
292						alert->Go();
293						result = true; // quit anyway
294					}
295				} else {
296					PostMessage(SAVE_KEYSET);
297					result = false;
298				}
299				break;
300		}
301	}
302
303	if (result) {
304		fColumnListView->DeselectAll();
305
306		// Save the window position.
307		entry_ref ref;
308		if (_GetWindowSettingsFile(&ref)) {
309			BEntry entry(&ref);
310			_SaveWindowSettings(entry);
311		}
312	}
313
314	return result;
315}
316
317
318bool
319ShortcutsWindow::_GetSettingsFile(entry_ref* eref)
320{
321	BPath path;
322	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK)
323		return false;
324	else
325		path.Append(SHORTCUTS_SETTING_FILE_NAME);
326	BEntry entry(path.Path(), true);
327	entry.GetRef(eref);
328	return entry.Exists();
329}
330
331
332// Saves a settings file to (saveEntry). Returns true iff successful.
333bool
334ShortcutsWindow::_SaveKeySet(BEntry& saveEntry)
335{
336	BFile saveTo(&saveEntry, B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE);
337	if (saveTo.InitCheck() != B_OK)
338		return false;
339
340	BMessage saveMessage;
341	for (int i = 0; i < fColumnListView->CountRows(); i++) {
342		BMessage next;
343		if (((ShortcutsSpec*)fColumnListView->RowAt(i))->Archive(&next)
344				== B_OK) {
345			saveMessage.AddMessage("spec", &next);
346		} else
347			printf("Error archiving ShortcutsSpec #%i!\n", i);
348	}
349
350	bool result = (saveMessage.Flatten(&saveTo) == B_OK);
351
352	if (result) {
353		fKeySetModified = false;
354		fSaveButton->SetEnabled(false);
355	}
356
357	return result;
358}
359
360
361// Appends new entries from the file specified in the "spec" entry of
362// (loadMessage). Returns true iff successful.
363bool
364ShortcutsWindow::_LoadKeySet(const BMessage& loadMessage)
365{
366	int i = 0;
367	BMessage message;
368	while (loadMessage.FindMessage("spec", i++, &message) == B_OK) {
369		ShortcutsSpec* spec
370			= (ShortcutsSpec*)ShortcutsSpec::Instantiate(&message);
371		if (spec != NULL)
372			fColumnListView->AddRow(spec);
373		else
374			printf("_LoadKeySet: Error parsing spec!\n");
375	}
376
377	return true;
378}
379
380
381// Gets the filesystem location of the "Shortcuts_window_settings" file.
382bool
383ShortcutsWindow::_GetWindowSettingsFile(entry_ref* eref)
384{
385	BPath path;
386	if (find_directory(B_USER_SETTINGS_DIRECTORY, &path) != B_OK)
387		return false;
388	else
389		path.Append(WINDOW_SETTINGS_FILE_NAME);
390
391	return BEntry(path.Path(), true).GetRef(eref) == B_OK;
392}
393
394
395// Saves the application settings file to (saveEntry).  Because this is a
396// non-essential file, errors are ignored when writing the settings.
397void
398ShortcutsWindow::_SaveWindowSettings(BEntry& saveEntry)
399{
400	BFile saveTo(&saveEntry, B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE);
401	if (saveTo.InitCheck() != B_OK)
402		return;
403
404	BMessage saveMsg;
405	saveMsg.AddRect("window frame", Frame());
406
407	BMessage columnsState;
408	fColumnListView->SaveState(&columnsState);
409	saveMsg.AddMessage ("columns state", &columnsState);
410
411	saveMsg.Flatten(&saveTo);
412}
413
414
415// Loads the application settings file from (loadMessage) and resizes
416// the interface to match the previously saved settings. Because this
417// is a non-essential file, errors are ignored when loading the settings.
418void
419ShortcutsWindow::_LoadWindowSettings(const BMessage& loadMessage)
420{
421	BRect frame;
422	if (loadMessage.FindRect("window frame", &frame) == B_OK) {
423		// ensure the frame does not resize below the computed minimum.
424		float width = max_c(Bounds().right, frame.right - frame.left);
425		float height = max_c(Bounds().bottom, frame.bottom - frame.top);
426		ResizeTo(width, height);
427
428		// ensure the frame is not placed outside of the screen.
429		BScreen screen(this);
430		float left = min_c(screen.Frame().right - width, frame.left);
431		float top = min_c(screen.Frame().bottom - height, frame.top);
432		MoveTo(left, top);
433	}
434
435	BMessage columnsStateMessage;
436	if (loadMessage.FindMessage ("columns state", &columnsStateMessage) == B_OK)
437		fColumnListView->LoadState(&columnsStateMessage);
438}
439
440
441// Creates a new entry and adds it to the GUI. (defaultCommand) will be the
442// text in the entry, or NULL if no text is desired.
443void
444ShortcutsWindow::_AddNewSpec(const char* defaultCommand, uint32 keyCode)
445{
446	_MarkKeySetModified();
447
448	ShortcutsSpec* spec;
449	BRow* curSel = fColumnListView->CurrentSelection();
450	if (curSel)
451		spec = new ShortcutsSpec(*((ShortcutsSpec*)curSel));
452	else {
453		spec = new ShortcutsSpec("");
454		for (int i = 0; i < fColumnListView->CountColumns(); i++)
455			spec->SetField(new BStringField(""), i);
456	}
457
458	fColumnListView->AddRow(spec);
459	fColumnListView->AddToSelection(spec);
460	fColumnListView->ScrollTo(spec);
461	if (defaultCommand)
462		spec->SetCommand(defaultCommand);
463	if (keyCode != 0) {
464		spec->ProcessColumnTextString(ShortcutsSpec::KEY_COLUMN_INDEX,
465			GetFallbackKeyName(keyCode).String());
466	}
467}
468
469
470void
471ShortcutsWindow::MessageReceived(BMessage* message)
472{
473	switch (message->what) {
474		case OPEN_KEYSET:
475		case APPEND_KEYSET:
476			fLastOpenWasAppend = (message->what == APPEND_KEYSET);
477			if (fOpenPanel)
478				fOpenPanel->Show();
479			else {
480				BMessenger messenger(this);
481				fOpenPanel = new BFilePanel(B_OPEN_PANEL, &messenger, NULL,
482					0, false);
483				fOpenPanel->Show();
484			}
485			fOpenPanel->SetButtonLabel(B_DEFAULT_BUTTON, fLastOpenWasAppend ?
486				B_TRANSLATE("Append") : B_TRANSLATE("Open"));
487			break;
488
489		// send a message to myself, to get me to reload the settings file
490		case REVERT_KEYSET:
491		{
492			fLastOpenWasAppend = false;
493			BMessage reload(B_REFS_RECEIVED);
494			entry_ref eref;
495			_GetSettingsFile(&eref);
496			reload.AddRef("refs", &eref);
497			reload.AddString("startupRef", "yeah");
498			PostMessage(&reload);
499			break;
500		}
501
502		// respond to drag-and-drop messages here
503		case B_SIMPLE_DATA:
504		{
505			int i = 0;
506
507			entry_ref ref;
508			while (message->FindRef("refs", i++, &ref) == B_OK) {
509				BEntry entry(&ref);
510				if (entry.InitCheck() == B_OK) {
511					BPath path(&entry);
512
513					if (path.InitCheck() == B_OK) {
514						// Add a new item with the given path.
515						BString str(path.Path());
516						DoStandardEscapes(str);
517						_AddNewSpec(str.String());
518					}
519				}
520			}
521			break;
522		}
523
524		// respond to FileRequester's messages here
525		case B_REFS_RECEIVED:
526		{
527			// Find file ref
528			entry_ref ref;
529			bool isStartMsg = message->HasString("startupRef");
530			if (message->FindRef("refs", &ref) == B_OK) {
531				// load the file into (fileMsg)
532				BMessage fileMsg;
533				{
534					BFile file(&ref, B_READ_ONLY);
535					if ((file.InitCheck() != B_OK)
536						|| (fileMsg.Unflatten(&file) != B_OK)) {
537						if (isStartMsg) {
538							// use this to save to anyway
539							fLastSaved = BEntry(&ref);
540							break;
541						} else {
542							BAlert* alert = new BAlert(ERROR,
543								B_TRANSLATE("Shortcuts was couldn't open your "
544								"KeySet file!"), B_TRANSLATE("OK"));
545							alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
546							alert->Go(NULL);
547							break;
548						}
549					}
550				}
551
552				if (fLastOpenWasAppend == false) {
553					// Clear the menu...
554					while (fColumnListView->CountRows()) {
555						ShortcutsSpec* row =
556							static_cast<ShortcutsSpec*>(fColumnListView->RowAt(0));
557						fColumnListView->RemoveRow(row);
558						delete row;
559					}
560				}
561
562				if (_LoadKeySet(fileMsg)) {
563					if (isStartMsg) fLastSaved = BEntry(&ref);
564					fSaveButton->SetEnabled(isStartMsg == false);
565
566					// If we just loaded in the Shortcuts settings file, then
567					// no need to tell the user to save on exit.
568					entry_ref eref;
569					_GetSettingsFile(&eref);
570					if (ref == eref) fKeySetModified = false;
571				} else {
572					BAlert* alert = new BAlert(ERROR,
573						B_TRANSLATE("Shortcuts was unable to parse your "
574						"KeySet file!"), B_TRANSLATE("OK"));
575					alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
576					alert->Go(NULL);
577					break;
578				}
579			}
580			break;
581		}
582
583		// these messages come from the pop-up menu of the Applications column
584		case SELECT_APPLICATION:
585		{
586			ShortcutsSpec* row =
587				static_cast<ShortcutsSpec*>(fColumnListView->CurrentSelection());
588			if (row != NULL) {
589				entry_ref aref;
590				if (message->FindRef("refs", &aref) == B_OK) {
591					BEntry ent(&aref);
592					if (ent.InitCheck() == B_OK) {
593						BPath path;
594						if ((ent.GetPath(&path) == B_OK)
595							&& (row->
596								ProcessColumnTextString(ShortcutsSpec::STRING_COLUMN_INDEX,
597									path.Path()))) {
598							_MarkKeySetModified();
599						}
600					}
601				}
602			}
603			break;
604		}
605
606		case SAVE_KEYSET:
607		{
608			bool showSaveError = false;
609
610			const char* name;
611			entry_ref entry;
612			if ((message->FindString("name", &name) == B_OK)
613				&& (message->FindRef("directory", &entry) == B_OK)) {
614				BDirectory dir(&entry);
615				BEntry saveTo(&dir, name, true);
616				showSaveError = ((saveTo.InitCheck() != B_OK)
617					|| (_SaveKeySet(saveTo) == false));
618			} else if (fLastSaved.InitCheck() == B_OK) {
619				// We've saved this before, save over previous file.
620				showSaveError = (_SaveKeySet(fLastSaved) == false);
621			} else
622				PostMessage(SAVE_KEYSET_AS);
623					// open the save requester...
624
625			if (showSaveError) {
626				BAlert* alert = new BAlert(ERROR,
627					B_TRANSLATE("Shortcuts wasn't able to save your keyset."),
628					B_TRANSLATE("OK"));
629				alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
630				alert->Go(NULL);
631			}
632			break;
633		}
634
635		case SAVE_KEYSET_AS:
636		{
637			if (fSavePanel)
638				fSavePanel->Show();
639			else {
640				BMessage message(SAVE_KEYSET);
641				BMessenger messenger(this);
642				fSavePanel = new BFilePanel(B_SAVE_PANEL, &messenger, NULL, 0,
643					false, &message);
644				fSavePanel->Show();
645			}
646			break;
647		}
648
649		case ADD_HOTKEY_ITEM:
650			_AddNewSpec(NULL);
651			break;
652
653		case REMOVE_HOTKEY_ITEM:
654		{
655			BRow* item = fColumnListView->CurrentSelection();
656			if (item) {
657				int index = fColumnListView->IndexOf(item);
658				fColumnListView->RemoveRow(item);
659				delete item;
660				_MarkKeySetModified();
661
662				// Rules for new selection: If there is an item at (index),
663				// select it. Otherwise, if there is an item at (index-1),
664				// select it. Otherwise, select nothing.
665				int num = fColumnListView->CountRows();
666				if (num > 0) {
667					if (index < num)
668						fColumnListView->AddToSelection(
669							fColumnListView->RowAt(index));
670					else {
671						if (index > 0)
672							index--;
673						if (index < num)
674							fColumnListView->AddToSelection(
675								fColumnListView->RowAt(index));
676					}
677				}
678			}
679			break;
680		}
681
682		// Received when the user clicks on the ColumnListView
683		case HOTKEY_ITEM_SELECTED:
684		{
685			if (fColumnListView->CountRows() > 0)
686				fRemoveButton->SetEnabled(true);
687			else
688				fRemoveButton->SetEnabled(false);
689			break;
690		}
691
692		// Received when an entry is to be modified in response to GUI activity
693		case HOTKEY_ITEM_MODIFIED:
694		{
695			int32 row, column;
696
697			if ((message->FindInt32("row", &row) == B_OK)
698				&& (message->FindInt32("column", &column) == B_OK)) {
699				int32 key;
700				const char* bytes;
701
702				if (row >= 0) {
703					ShortcutsSpec* item = (ShortcutsSpec*)
704						fColumnListView->RowAt(row);
705					bool repaintNeeded = false; // default
706
707					if (message->HasInt32("mouseClick")) {
708						repaintNeeded = item->ProcessColumnMouseClick(column);
709					} else if ((message->FindString("bytes", &bytes) == B_OK)
710						&& (message->FindInt32("key", &key) == B_OK)) {
711						repaintNeeded = item->ProcessColumnKeyStroke(column,
712							bytes, key);
713					} else if (message->FindInt32("unmappedkey", &key) ==
714						B_OK) {
715						repaintNeeded = ((column == item->KEY_COLUMN_INDEX)
716							&& ((key > 0xFF) || (GetKeyName(key) != NULL))
717							&& (item->ProcessColumnKeyStroke(column, NULL,
718							key)));
719					} else if (message->FindString("text", &bytes) == B_OK) {
720						if ((bytes[0] == '(')&&(bytes[1] == 'C')) {
721							if (fSelectPanel)
722								fSelectPanel->Show();
723							else {
724								BMessage message(SELECT_APPLICATION);
725								BMessenger m(this);
726								fSelectPanel = new BFilePanel(B_OPEN_PANEL, &m,
727									NULL, 0, false, &message);
728								fSelectPanel->Show();
729							}
730							fSelectPanel->SetButtonLabel(B_DEFAULT_BUTTON,
731								B_TRANSLATE("Select"));
732						} else
733							repaintNeeded = item->ProcessColumnTextString(
734								column, bytes);
735					}
736
737					if (repaintNeeded) {
738						fColumnListView->Invalidate(row);
739						_MarkKeySetModified();
740					}
741				}
742			}
743			break;
744		}
745
746		default:
747			BWindow::MessageReceived(message);
748			break;
749	}
750}
751
752
753void
754ShortcutsWindow::_MarkKeySetModified()
755{
756	if (fKeySetModified == false) {
757		fKeySetModified = true;
758		fSaveButton->SetEnabled(true);
759	}
760}
761
762
763void
764ShortcutsWindow::Quit()
765{
766	BWindow::Quit();
767}
768
769
770void
771ShortcutsWindow::DispatchMessage(BMessage* message, BHandler* handler)
772{
773	switch (message->what) {
774		case B_SIMPLE_DATA:
775			MessageReceived(message);
776			break;
777
778		case B_COPY:
779		case B_CUT:
780			if (be_clipboard->Lock()) {
781				ShortcutsSpec* row =
782					static_cast<ShortcutsSpec*>(fColumnListView->CurrentSelection());
783				if (row) {
784					BMessage* data = be_clipboard->Data();
785					data->RemoveName("text/plain");
786					data->AddData("text/plain", B_MIME_TYPE,
787						row->GetCellText(ShortcutsSpec::STRING_COLUMN_INDEX),
788						strlen(row->GetCellText(ShortcutsSpec::STRING_COLUMN_INDEX)));
789					be_clipboard->Commit();
790
791					if (message->what == B_CUT) {
792						row->ProcessColumnTextString(
793							ShortcutsSpec::STRING_COLUMN_INDEX, "");
794						_MarkKeySetModified();
795					}
796				}
797				be_clipboard->Unlock();
798			}
799			break;
800
801		case B_PASTE:
802			if (be_clipboard->Lock()) {
803				BMessage* data = be_clipboard->Data();
804				const char* text;
805				ssize_t textLen;
806				if (data->FindData("text/plain", B_MIME_TYPE, (const void**)
807					&text, &textLen) == B_OK) {
808					ShortcutsSpec* row =
809					static_cast<ShortcutsSpec*>(fColumnListView->CurrentSelection());
810					if (row) {
811						for (ssize_t i = 0; i < textLen; i++) {
812							char buf[2] = {text[i], 0x00};
813							row->ProcessColumnKeyStroke(
814								ShortcutsSpec::STRING_COLUMN_INDEX, buf, 0);
815						}
816					}
817					_MarkKeySetModified();
818				}
819				be_clipboard->Unlock();
820			}
821			break;
822
823		case B_KEY_DOWN:
824		case B_UNMAPPED_KEY_DOWN:
825		{
826			ShortcutsSpec* selected;
827			int32 modifiers = message->GetInt32("modifiers", 0);
828			// These should not block key detection here:
829			modifiers &= ~(B_CAPS_LOCK | B_SCROLL_LOCK | B_NUM_LOCK);
830			if (modifiers != 0)
831				BWindow::DispatchMessage(message, handler);
832			else if (handler == fColumnListView
833				&& (selected =
834					static_cast<ShortcutsSpec*>(fColumnListView->CurrentSelection()))) {
835				uint32 keyCode = message->GetInt32("key", 0);
836				const char* keyName = GetKeyName(keyCode);
837				selected->ProcessColumnTextString(
838						ShortcutsSpec::KEY_COLUMN_INDEX,
839						keyName != NULL ? keyName : GetFallbackKeyName(keyCode).String());
840				_MarkKeySetModified();
841			}
842			break;
843		}
844		default:
845			BWindow::DispatchMessage(message, handler);
846			break;
847	}
848}
849