1/*
2 * Copyright 2011-2023 Haiku, Inc. All rights reserved.
3 * Distributed under the terms of the MIT License.
4 *
5 * Authors:
6 *		John Scipione, jscipione@gmail.com
7 *		Jorge Acereda, jacereda@gmail.com
8 */
9
10
11#include "ModifierKeysWindow.h"
12
13#include <stdio.h>
14#include <stdlib.h>
15#include <string.h>
16
17#include <Button.h>
18#include <Catalog.h>
19#include <ControlLook.h>
20#include <FindDirectory.h>
21#include <LayoutBuilder.h>
22#include <Locale.h>
23#include <MenuField.h>
24#include <MenuItem.h>
25#include <Message.h>
26#include <Path.h>
27#include <PopUpMenu.h>
28#include <Resources.h>
29#include <SeparatorView.h>
30#include <Size.h>
31#include <StringView.h>
32
33#include "KeymapApplication.h"
34#include "StatusMenuField.h"
35
36
37enum {
38	CAPS_KEY = 0x00000001,
39	SHIFT_KEY = 0x00000002,
40	CONTROL_KEY = 0x00000004,
41	OPTION_KEY = 0x00000008,
42	COMMAND_KEY = 0x00000010,
43};
44
45enum {
46	MENU_ITEM_CAPS,
47	MENU_ITEM_SHIFT,
48	MENU_ITEM_CONTROL,
49	MENU_ITEM_OPTION,
50	MENU_ITEM_COMMAND,
51	MENU_ITEM_SEPARATOR,
52	MENU_ITEM_DISABLED,
53
54	MENU_ITEM_FIRST = MENU_ITEM_CAPS,
55	MENU_ITEM_LAST = MENU_ITEM_DISABLED
56};
57
58
59static const uint32 kMsgUpdateStatus = 'stat';
60static const uint32 kMsgUpdateModifier = 'upmd';
61static const uint32 kMsgApplyModifiers = 'apmd';
62static const uint32 kMsgRevertModifiers = 'rvmd';
63
64static const int32 kUnset = 0;
65static const int32 kDisabled = -1;
66
67
68#undef B_TRANSLATION_CONTEXT
69#define B_TRANSLATION_CONTEXT "Modifier keys window"
70
71
72ModifierKeysWindow::ModifierKeysWindow()
73	:
74	BWindow(BRect(0, 0, 360, 220), B_TRANSLATE("Modifier keys"),
75		B_FLOATING_WINDOW, B_NOT_RESIZABLE | B_NOT_ZOOMABLE | B_AUTO_UPDATE_SIZE_LIMITS)
76{
77	get_key_map(&fCurrentMap, &fCurrentBuffer);
78	get_key_map(&fSavedMap, &fSavedBuffer);
79
80	BStringView* keyRole = new BStringView("key role",
81		B_TRANSLATE_COMMENT("Role", "As in the role of a modifier key"));
82	keyRole->SetAlignment(B_ALIGN_RIGHT);
83	keyRole->SetFont(be_bold_font);
84
85	BStringView* keyLabel = new BStringView("key label",
86		B_TRANSLATE_COMMENT("Key", "As in a computer keyboard key"));
87	keyLabel->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET));
88	keyLabel->SetFont(be_bold_font);
89
90	_CreateMenuField(&fCapsMenu, (BMenuField**)&fCapsField, MENU_ITEM_CAPS,
91		B_TRANSLATE_COMMENT("Caps Lock:", "Caps Lock key role name"));
92	_CreateMenuField(&fShiftMenu, (BMenuField**)&fShiftField, MENU_ITEM_SHIFT,
93		B_TRANSLATE_COMMENT("Shift:", "Shift key role name"));
94	_CreateMenuField(&fControlMenu, (BMenuField**)&fControlField, MENU_ITEM_CONTROL,
95		B_TRANSLATE_COMMENT("Control:", "Control key role name"));
96	_CreateMenuField(&fOptionMenu,(BMenuField**) &fOptionField, MENU_ITEM_OPTION,
97		B_TRANSLATE_COMMENT("Option:", "Option key role name"));
98	_CreateMenuField(&fCommandMenu, (BMenuField**)&fCommandField, MENU_ITEM_COMMAND,
99		B_TRANSLATE_COMMENT("Command:", "Command key role name"));
100
101	fCancelButton = new BButton("cancelButton", B_TRANSLATE("Cancel"),
102		new BMessage(B_QUIT_REQUESTED));
103
104	fRevertButton = new BButton("revertButton", B_TRANSLATE("Revert"),
105		new BMessage(kMsgRevertModifiers));
106	fRevertButton->SetEnabled(false);
107
108	fOkButton = new BButton("okButton", B_TRANSLATE("Set modifier keys"),
109		new BMessage(kMsgApplyModifiers));
110	fOkButton->MakeDefault(true);
111
112	BLayoutBuilder::Group<>(this, B_VERTICAL, B_USE_DEFAULT_SPACING)
113		.AddGrid(B_USE_DEFAULT_SPACING, B_USE_SMALL_SPACING)
114			.Add(keyRole, 0, 0)
115			.Add(keyLabel, 1, 0)
116
117			.Add(fCapsField->CreateLabelLayoutItem(), 0, 1)
118			.AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING, 1, 1)
119				.Add(fCapsField->CreateMenuBarLayoutItem())
120				.End()
121
122			.Add(fShiftField->CreateLabelLayoutItem(), 0, 2)
123			.AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING, 1, 2)
124				.Add(fShiftField->CreateMenuBarLayoutItem())
125				.End()
126
127			.Add(fControlField->CreateLabelLayoutItem(), 0, 3)
128			.AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING, 1, 3)
129				.Add(fControlField->CreateMenuBarLayoutItem())
130				.End()
131
132			.Add(fOptionField->CreateLabelLayoutItem(), 0, 4)
133			.AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING, 1, 4)
134				.Add(fOptionField->CreateMenuBarLayoutItem())
135				.End()
136
137			.Add(fCommandField->CreateLabelLayoutItem(), 0, 5)
138			.AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING, 1, 5)
139				.Add(fCommandField->CreateMenuBarLayoutItem())
140				.End()
141
142			.End()
143		.AddGlue()
144		.AddGroup(B_HORIZONTAL)
145			.Add(fRevertButton)
146			.AddGlue()
147			.Add(fCancelButton)
148			.Add(fOkButton)
149			.End()
150		.SetInsets(B_USE_WINDOW_SPACING)
151		.End();
152
153	// mark menu items and update status icons
154	_UpdateStatus();
155}
156
157
158ModifierKeysWindow::~ModifierKeysWindow()
159{
160	be_app->PostMessage(kMsgCloseModifierKeysWindow);
161}
162
163
164void
165ModifierKeysWindow::MessageReceived(BMessage* message)
166{
167	switch (message->what) {
168		case kMsgUpdateModifier:
169		{
170			int32 menuitem = MENU_ITEM_FIRST;
171			int32 key = kDisabled;
172
173			for (; menuitem <= MENU_ITEM_LAST; menuitem++) {
174				if (message->FindInt32(_KeyToString(menuitem), &key) == B_OK)
175					break;
176			}
177
178			if (key == kDisabled)
179				return;
180
181			// menuitem contains the item we want to set
182			// key contains the item we want to set it to.
183
184			uint32 leftKey = _KeyToKeyCode(key);
185			uint32 rightKey = _KeyToKeyCode(key, true);
186
187			switch (menuitem) {
188				case MENU_ITEM_CAPS:
189					fCurrentMap->caps_key = leftKey;
190					break;
191
192				case MENU_ITEM_SHIFT:
193					fCurrentMap->left_shift_key = leftKey;
194					fCurrentMap->right_shift_key = rightKey;
195					break;
196
197				case MENU_ITEM_CONTROL:
198					fCurrentMap->left_control_key = leftKey;
199					fCurrentMap->right_control_key = rightKey;
200					break;
201
202				case MENU_ITEM_OPTION:
203					fCurrentMap->left_option_key = leftKey;
204					fCurrentMap->right_option_key = rightKey;
205					break;
206
207				case MENU_ITEM_COMMAND:
208					fCurrentMap->left_command_key = leftKey;
209					fCurrentMap->right_command_key = rightKey;
210					break;
211			}
212
213			_UpdateStatus();
214
215			// enable/disable revert button
216			fRevertButton->SetEnabled(memcmp(fCurrentMap, fSavedMap, sizeof(key_map)));
217			break;
218		}
219
220		// OK button
221		case kMsgApplyModifiers:
222		{
223			// if duplicate modifiers are found, don't update
224			if (_DuplicateKeys() != 0)
225				break;
226
227			BMessage* updateModifiers = new BMessage(kMsgUpdateModifierKeys);
228
229			if (fCurrentMap->caps_key != fSavedMap->caps_key)
230				updateModifiers->AddUInt32("caps_key", fCurrentMap->caps_key);
231
232			if (fCurrentMap->left_shift_key != fSavedMap->left_shift_key)
233				updateModifiers->AddUInt32("left_shift_key", fCurrentMap->left_shift_key);
234
235			if (fCurrentMap->right_shift_key != fSavedMap->right_shift_key)
236				updateModifiers->AddUInt32("right_shift_key", fCurrentMap->right_shift_key);
237
238			if (fCurrentMap->left_control_key != fSavedMap->left_control_key)
239				updateModifiers->AddUInt32("left_control_key", fCurrentMap->left_control_key);
240
241			if (fCurrentMap->right_control_key != fSavedMap->right_control_key)
242				updateModifiers->AddUInt32("right_control_key", fCurrentMap->right_control_key);
243
244			if (fCurrentMap->left_option_key != fSavedMap->left_option_key)
245				updateModifiers->AddUInt32("left_option_key", fCurrentMap->left_option_key);
246
247			if (fCurrentMap->right_option_key != fSavedMap->right_option_key)
248				updateModifiers->AddUInt32("right_option_key", fCurrentMap->right_option_key);
249
250			if (fCurrentMap->left_command_key != fSavedMap->left_command_key)
251				updateModifiers->AddUInt32("left_command_key", fCurrentMap->left_command_key);
252
253			if (fCurrentMap->right_command_key != fSavedMap->right_command_key)
254				updateModifiers->AddUInt32("right_command_key", fCurrentMap->right_command_key);
255
256			// KeymapWindow updates the modifiers
257			be_app->PostMessage(updateModifiers);
258
259			// we are done here, close the window
260			this->PostMessage(B_QUIT_REQUESTED);
261			break;
262		}
263
264		// Revert button
265		case kMsgRevertModifiers:
266			memcpy(fCurrentMap, fSavedMap, sizeof(key_map));
267
268			_UpdateStatus();
269
270			fRevertButton->SetEnabled(false);
271			break;
272
273		default:
274			BWindow::MessageReceived(message);
275	}
276}
277
278
279//	#pragma mark - ModifierKeysWindow private methods
280
281
282void
283ModifierKeysWindow::_CreateMenuField(BPopUpMenu** _menu, BMenuField** _menuField, uint32 key,
284	const char* label)
285{
286	const char* keyName = _KeyToString(key);
287	const char* name = B_TRANSLATE_NOCOLLECT(keyName);
288	BPopUpMenu* menu = new BPopUpMenu(name, true, true);
289
290	for (int32 key = MENU_ITEM_FIRST; key <= MENU_ITEM_LAST; key++) {
291		if (key == MENU_ITEM_SEPARATOR) {
292			// add separator item
293			BSeparatorItem* separator = new BSeparatorItem;
294			menu->AddItem(separator, MENU_ITEM_SEPARATOR);
295			continue;
296		}
297
298		BMessage* message = new BMessage(kMsgUpdateModifier);
299		message->AddInt32(keyName, key);
300		BMenuItem* item = new BMenuItem(B_TRANSLATE_NOCOLLECT(_KeyToString(key)), message);
301		menu->AddItem(item, key);
302	}
303
304	menu->SetExplicitAlignment(BAlignment(B_ALIGN_USE_FULL_WIDTH, B_ALIGN_VERTICAL_UNSET));
305
306	BMenuField* menuField = new StatusMenuField(label, menu);
307	menuField->SetAlignment(B_ALIGN_RIGHT);
308
309	*_menu = menu;
310	*_menuField = menuField;
311}
312
313
314void
315ModifierKeysWindow::_MarkMenuItems()
316{
317	_MarkMenuItem("caps lock", fCapsMenu, fCurrentMap->caps_key, fCurrentMap->caps_key);
318		// mark but don't set unmatched for Caps Lock
319	fShiftField->SetUnmatched(_MarkMenuItem("shift", fShiftMenu,
320		fCurrentMap->left_shift_key, fCurrentMap->right_shift_key) == false);
321	fControlField->SetUnmatched(_MarkMenuItem("control", fControlMenu,
322		fCurrentMap->left_control_key, fCurrentMap->right_control_key) == false);
323	fOptionField->SetUnmatched(_MarkMenuItem("option", fOptionMenu,
324		fCurrentMap->left_option_key, fCurrentMap->right_option_key) == false);
325	fCommandField->SetUnmatched(_MarkMenuItem("command", fCommandMenu,
326		fCurrentMap->left_command_key, fCurrentMap->right_command_key) == false);
327}
328
329
330bool
331ModifierKeysWindow::_MarkMenuItem(const char* role, BPopUpMenu* menu, uint32 left, uint32 right)
332{
333	for (int32 key = MENU_ITEM_FIRST; key <= MENU_ITEM_LAST; key++) {
334		if (key == MENU_ITEM_SEPARATOR)
335			continue;
336
337		uint32 leftKey = _KeyToKeyCode(key);
338		uint32 rightKey = _KeyToKeyCode(key, true);
339		if (leftKey != rightKey && key == MENU_ITEM_CAPS) {
340			// mark Caps Lock on Caps Lock role if either left or right is caps
341			// otherwise mark Caps Lock if left side is caps
342			uint32 capsKey = _KeyToKeyCode(MENU_ITEM_CAPS);
343			if (strcmp(role, "caps lock") == 0 && (left == capsKey || right == capsKey))
344				menu->ItemAt(key)->SetMarked(true);
345			else if (left == capsKey)
346				menu->ItemAt(key)->SetMarked(true);
347		} else if (left == leftKey && right == rightKey)
348			menu->ItemAt(key)->SetMarked(true);
349	}
350
351	return menu->FindMarked() != NULL;
352}
353
354
355// get the string for a modifier key
356const char*
357ModifierKeysWindow::_KeyToString(int32 key)
358{
359	switch (key) {
360		case MENU_ITEM_CAPS:
361			return B_TRANSLATE_COMMENT("Caps Lock key",
362				"Label of key above Shift, usually Caps Lock");
363
364		case MENU_ITEM_SHIFT:
365			return B_TRANSLATE_COMMENT("Shift key",
366				"Label of key above Ctrl, usually Shift");
367
368		case MENU_ITEM_CONTROL:
369			return B_TRANSLATE_COMMENT("Ctrl key",
370				"Label of key farthest from the spacebar, usually Ctrl"
371				"e.g. Strg for German keyboard");
372
373		case MENU_ITEM_OPTION:
374			return B_TRANSLATE_COMMENT("Win/Cmd key",
375				"Label of the \"Windows\" key (PC)/Command key (Mac)");
376
377		case MENU_ITEM_COMMAND:
378			return B_TRANSLATE_COMMENT("Alt/Opt key",
379				"Label of Alt key (PC)/Option key (Mac)");
380
381		case MENU_ITEM_DISABLED:
382			return B_TRANSLATE_COMMENT("Disabled", "Do nothing");
383	}
384
385	return B_EMPTY_STRING;
386}
387
388
389// get the keycode for a modifier key
390int32
391ModifierKeysWindow::_KeyToKeyCode(int32 key, bool right)
392{
393	switch (key) {
394		case MENU_ITEM_CAPS:
395			return 0x3b;
396
397		case MENU_ITEM_SHIFT:
398			if (right)
399				return 0x56;
400			return 0x4b;
401
402		case MENU_ITEM_CONTROL:
403			if (right)
404				return 0x60;
405			return 0x5c;
406
407		case MENU_ITEM_OPTION:
408			if (right)
409				return 0x67;
410			return 0x66;
411
412		case MENU_ITEM_COMMAND:
413			if (right)
414				return 0x5f;
415			return 0x5d;
416
417		case MENU_ITEM_DISABLED:
418			return kDisabled;
419	}
420
421	return kUnset;
422}
423
424
425// validate duplicate keys
426void
427ModifierKeysWindow::_ValidateDuplicateKeys()
428{
429	uint32 dupMask = _DuplicateKeys();
430	_ValidateDuplicateKey(fCapsField, CAPS_KEY & dupMask);
431	_ValidateDuplicateKey(fShiftField, SHIFT_KEY & dupMask);
432	_ValidateDuplicateKey(fControlField, CONTROL_KEY & dupMask);
433	_ValidateDuplicateKey(fOptionField, OPTION_KEY & dupMask);
434	_ValidateDuplicateKey(fCommandField, COMMAND_KEY & dupMask);
435	fOkButton->SetEnabled(dupMask == 0);
436}
437
438
439void
440ModifierKeysWindow::_ValidateDuplicateKey(StatusMenuField* field, uint32 mask)
441{
442	if (mask != 0) // don't override if false
443		field->SetDuplicate(true);
444}
445
446
447// return a mask marking which keys are duplicates of each other for
448// validation.
449uint32
450ModifierKeysWindow::_DuplicateKeys()
451{
452	uint32 duplicateMask = 0;
453
454	int32 testLeft, testRight, left, right;
455	for (int32 testKey = MENU_ITEM_FIRST; testKey < MENU_ITEM_SEPARATOR; testKey++) {
456		testLeft = kUnset;
457		testRight = kUnset;
458
459		switch (testKey) {
460			case MENU_ITEM_CAPS:
461				testLeft = fCurrentMap->caps_key;
462				testRight = kDisabled;
463				break;
464
465			case MENU_ITEM_SHIFT:
466				testLeft = fCurrentMap->left_shift_key;
467				testRight = fCurrentMap->right_shift_key;
468				break;
469
470			case MENU_ITEM_CONTROL:
471				testLeft = fCurrentMap->left_control_key;
472				testRight = fCurrentMap->right_control_key;
473				break;
474
475			case MENU_ITEM_OPTION:
476				testLeft = fCurrentMap->left_option_key;
477				testRight = fCurrentMap->right_option_key;
478				break;
479
480			case MENU_ITEM_COMMAND:
481				testLeft = fCurrentMap->left_command_key;
482				testRight = fCurrentMap->right_command_key;
483				break;
484		}
485
486		if (testLeft == kUnset && (testRight == kUnset || testRight == kDisabled))
487			continue;
488
489		for (int32 key = MENU_ITEM_FIRST; key < MENU_ITEM_SEPARATOR; key++) {
490			if (key == testKey) {
491				// skip over yourself
492				continue;
493			}
494
495			left = kUnset;
496			right = kUnset;
497
498			switch (key) {
499				case MENU_ITEM_CAPS:
500					left = fCurrentMap->caps_key;
501					right = kDisabled;
502					break;
503
504				case MENU_ITEM_SHIFT:
505					left = fCurrentMap->left_shift_key;
506					right = fCurrentMap->right_shift_key;
507					break;
508
509				case MENU_ITEM_CONTROL:
510					left = fCurrentMap->left_control_key;
511					right = fCurrentMap->right_control_key;
512					break;
513
514				case MENU_ITEM_OPTION:
515					left = fCurrentMap->left_option_key;
516					right = fCurrentMap->right_option_key;
517					break;
518
519				case MENU_ITEM_COMMAND:
520					left = fCurrentMap->left_command_key;
521					right = fCurrentMap->right_command_key;
522					break;
523			}
524
525			if (left == kUnset && (right == kUnset || right == kDisabled))
526				continue;
527
528			// left or right is set
529
530			if (left == testLeft || right == testRight) {
531				duplicateMask |= 1 << testKey;
532				duplicateMask |= 1 << key;
533			}
534		}
535	}
536
537	return duplicateMask;
538}
539
540
541void
542ModifierKeysWindow::_UpdateStatus()
543{
544	// the order is important
545	_MarkMenuItems();
546	_ValidateDuplicateKeys();
547}
548