⛏️ index : haiku.git

/*
 * Copyright 2011-2023 Haiku, Inc. All rights reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *		John Scipione, jscipione@gmail.com
 *		Jorge Acereda, jacereda@gmail.com
 */


#include "ModifierKeysWindow.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <Button.h>
#include <Catalog.h>
#include <ControlLook.h>
#include <FindDirectory.h>
#include <LayoutBuilder.h>
#include <Locale.h>
#include <MenuField.h>
#include <MenuItem.h>
#include <Message.h>
#include <Path.h>
#include <PopUpMenu.h>
#include <Resources.h>
#include <SeparatorView.h>
#include <Size.h>
#include <StringView.h>

#include "KeymapApplication.h"
#include "StatusMenuField.h"


enum {
	CAPS_KEY = 0x00000001,
	SHIFT_KEY = 0x00000002,
	CONTROL_KEY = 0x00000004,
	OPTION_KEY = 0x00000008,
	COMMAND_KEY = 0x00000010,
};

enum {
	MENU_ITEM_CAPS,
	MENU_ITEM_SHIFT,
	MENU_ITEM_CONTROL,
	MENU_ITEM_OPTION,
	MENU_ITEM_COMMAND,
	MENU_ITEM_SEPARATOR,
	MENU_ITEM_DISABLED,

	MENU_ITEM_FIRST = MENU_ITEM_CAPS,
	MENU_ITEM_LAST = MENU_ITEM_DISABLED
};


static const uint32 kMsgUpdateStatus = 'stat';
static const uint32 kMsgUpdateModifier = 'upmd';
static const uint32 kMsgApplyModifiers = 'apmd';
static const uint32 kMsgRevertModifiers = 'rvmd';

static const int32 kUnset = 0;
static const int32 kDisabled = -1;


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "Modifier keys window"


ModifierKeysWindow::ModifierKeysWindow()
	:
	BWindow(BRect(0, 0, 360, 220), B_TRANSLATE("Modifier keys"),
		B_FLOATING_WINDOW, B_NOT_RESIZABLE | B_NOT_ZOOMABLE | B_AUTO_UPDATE_SIZE_LIMITS)
{
	get_key_map(&fCurrentMap, &fCurrentBuffer);
	get_key_map(&fSavedMap, &fSavedBuffer);

	BStringView* keyRole = new BStringView("key role",
		B_TRANSLATE_COMMENT("Role", "As in the role of a modifier key"));
	keyRole->SetAlignment(B_ALIGN_RIGHT);
	keyRole->SetFont(be_bold_font);

	BStringView* keyLabel = new BStringView("key label",
		B_TRANSLATE_COMMENT("Key", "As in a computer keyboard key"));
	keyLabel->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET));
	keyLabel->SetFont(be_bold_font);

	_CreateMenuField(&fCapsMenu, (BMenuField**)&fCapsField, MENU_ITEM_CAPS,
		B_TRANSLATE_COMMENT("Caps Lock:", "Caps Lock key role name"));
	_CreateMenuField(&fShiftMenu, (BMenuField**)&fShiftField, MENU_ITEM_SHIFT,
		B_TRANSLATE_COMMENT("Shift:", "Shift key role name"));
	_CreateMenuField(&fControlMenu, (BMenuField**)&fControlField, MENU_ITEM_CONTROL,
		B_TRANSLATE_COMMENT("Control:", "Control key role name"));
	_CreateMenuField(&fOptionMenu,(BMenuField**) &fOptionField, MENU_ITEM_OPTION,
		B_TRANSLATE_COMMENT("Option:", "Option key role name"));
	_CreateMenuField(&fCommandMenu, (BMenuField**)&fCommandField, MENU_ITEM_COMMAND,
		B_TRANSLATE_COMMENT("Command:", "Command key role name"));

	fCancelButton = new BButton("cancelButton", B_TRANSLATE("Cancel"),
		new BMessage(B_QUIT_REQUESTED));

	fRevertButton = new BButton("revertButton", B_TRANSLATE("Revert"),
		new BMessage(kMsgRevertModifiers));
	fRevertButton->SetEnabled(false);

	fOkButton = new BButton("okButton", B_TRANSLATE("Set modifier keys"),
		new BMessage(kMsgApplyModifiers));
	fOkButton->MakeDefault(true);

	BLayoutBuilder::Group<>(this, B_VERTICAL, B_USE_DEFAULT_SPACING)
		.AddGrid(B_USE_DEFAULT_SPACING, B_USE_SMALL_SPACING)
			.Add(keyRole, 0, 0)
			.Add(keyLabel, 1, 0)

			.Add(fCapsField->CreateLabelLayoutItem(), 0, 1)
			.AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING, 1, 1)
				.Add(fCapsField->CreateMenuBarLayoutItem())
				.End()

			.Add(fShiftField->CreateLabelLayoutItem(), 0, 2)
			.AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING, 1, 2)
				.Add(fShiftField->CreateMenuBarLayoutItem())
				.End()

			.Add(fControlField->CreateLabelLayoutItem(), 0, 3)
			.AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING, 1, 3)
				.Add(fControlField->CreateMenuBarLayoutItem())
				.End()

			.Add(fOptionField->CreateLabelLayoutItem(), 0, 4)
			.AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING, 1, 4)
				.Add(fOptionField->CreateMenuBarLayoutItem())
				.End()

			.Add(fCommandField->CreateLabelLayoutItem(), 0, 5)
			.AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING, 1, 5)
				.Add(fCommandField->CreateMenuBarLayoutItem())
				.End()

			.End()
		.AddGlue()
		.AddGroup(B_HORIZONTAL)
			.Add(fRevertButton)
			.AddGlue()
			.Add(fCancelButton)
			.Add(fOkButton)
			.End()
		.SetInsets(B_USE_WINDOW_SPACING)
		.End();

	// mark menu items and update status icons
	_UpdateStatus();
}


ModifierKeysWindow::~ModifierKeysWindow()
{
	be_app->PostMessage(kMsgCloseModifierKeysWindow);
}


void
ModifierKeysWindow::MessageReceived(BMessage* message)
{
	switch (message->what) {
		case kMsgUpdateModifier:
		{
			int32 menuitem = MENU_ITEM_FIRST;
			int32 key = kDisabled;

			for (; menuitem <= MENU_ITEM_LAST; menuitem++) {
				if (message->FindInt32(_KeyToString(menuitem), &key) == B_OK)
					break;
			}

			if (key == kDisabled)
				return;

			// menuitem contains the item we want to set
			// key contains the item we want to set it to.

			uint32 leftKey = _KeyToKeyCode(key);
			uint32 rightKey = _KeyToKeyCode(key, true);

			switch (menuitem) {
				case MENU_ITEM_CAPS:
					fCurrentMap->caps_key = leftKey;
					break;

				case MENU_ITEM_SHIFT:
					fCurrentMap->left_shift_key = leftKey;
					fCurrentMap->right_shift_key = rightKey;
					break;

				case MENU_ITEM_CONTROL:
					fCurrentMap->left_control_key = leftKey;
					fCurrentMap->right_control_key = rightKey;
					break;

				case MENU_ITEM_OPTION:
					fCurrentMap->left_option_key = leftKey;
					fCurrentMap->right_option_key = rightKey;
					break;

				case MENU_ITEM_COMMAND:
					fCurrentMap->left_command_key = leftKey;
					fCurrentMap->right_command_key = rightKey;
					break;
			}

			_UpdateStatus();

			// enable/disable revert button
			fRevertButton->SetEnabled(memcmp(fCurrentMap, fSavedMap, sizeof(key_map)));
			break;
		}

		// OK button
		case kMsgApplyModifiers:
		{
			// if duplicate modifiers are found, don't update
			if (_DuplicateKeys() != 0)
				break;

			BMessage* updateModifiers = new BMessage(kMsgUpdateModifierKeys);

			if (fCurrentMap->caps_key != fSavedMap->caps_key)
				updateModifiers->AddUInt32("caps_key", fCurrentMap->caps_key);

			if (fCurrentMap->left_shift_key != fSavedMap->left_shift_key)
				updateModifiers->AddUInt32("left_shift_key", fCurrentMap->left_shift_key);

			if (fCurrentMap->right_shift_key != fSavedMap->right_shift_key)
				updateModifiers->AddUInt32("right_shift_key", fCurrentMap->right_shift_key);

			if (fCurrentMap->left_control_key != fSavedMap->left_control_key)
				updateModifiers->AddUInt32("left_control_key", fCurrentMap->left_control_key);

			if (fCurrentMap->right_control_key != fSavedMap->right_control_key)
				updateModifiers->AddUInt32("right_control_key", fCurrentMap->right_control_key);

			if (fCurrentMap->left_option_key != fSavedMap->left_option_key)
				updateModifiers->AddUInt32("left_option_key", fCurrentMap->left_option_key);

			if (fCurrentMap->right_option_key != fSavedMap->right_option_key)
				updateModifiers->AddUInt32("right_option_key", fCurrentMap->right_option_key);

			if (fCurrentMap->left_command_key != fSavedMap->left_command_key)
				updateModifiers->AddUInt32("left_command_key", fCurrentMap->left_command_key);

			if (fCurrentMap->right_command_key != fSavedMap->right_command_key)
				updateModifiers->AddUInt32("right_command_key", fCurrentMap->right_command_key);

			// KeymapWindow updates the modifiers
			be_app->PostMessage(updateModifiers);

			// we are done here, close the window
			this->PostMessage(B_QUIT_REQUESTED);
			break;
		}

		// Revert button
		case kMsgRevertModifiers:
			memcpy(fCurrentMap, fSavedMap, sizeof(key_map));

			_UpdateStatus();

			fRevertButton->SetEnabled(false);
			break;

		default:
			BWindow::MessageReceived(message);
	}
}


//	#pragma mark - ModifierKeysWindow private methods


void
ModifierKeysWindow::_CreateMenuField(BPopUpMenu** _menu, BMenuField** _menuField, uint32 key,
	const char* label)
{
	const char* keyName = _KeyToString(key);
	const char* name = B_TRANSLATE_NOCOLLECT(keyName);
	BPopUpMenu* menu = new BPopUpMenu(name, true, true);

	for (int32 key = MENU_ITEM_FIRST; key <= MENU_ITEM_LAST; key++) {
		if (key == MENU_ITEM_SEPARATOR) {
			// add separator item
			BSeparatorItem* separator = new BSeparatorItem;
			menu->AddItem(separator, MENU_ITEM_SEPARATOR);
			continue;
		}

		BMessage* message = new BMessage(kMsgUpdateModifier);
		message->AddInt32(keyName, key);
		StatusMenuItem* item
			= new StatusMenuItem(B_TRANSLATE_NOCOLLECT(_KeyToString(key)), message);
		menu->AddItem(item, key);
	}

	menu->SetExplicitAlignment(BAlignment(B_ALIGN_USE_FULL_WIDTH, B_ALIGN_VERTICAL_UNSET));

	BMenuField* menuField = new StatusMenuField(label, menu);
	menuField->SetAlignment(B_ALIGN_RIGHT);

	*_menu = menu;
	*_menuField = menuField;
}


void
ModifierKeysWindow::_MarkMenuItems()
{
	_MarkMenuItem("caps lock", fCapsMenu, fCurrentMap->caps_key, fCurrentMap->caps_key);
		// mark but don't set unmatched for Caps Lock
	fShiftField->SetUnmatched(_MarkMenuItem("shift", fShiftMenu,
		fCurrentMap->left_shift_key, fCurrentMap->right_shift_key) == false);
	fControlField->SetUnmatched(_MarkMenuItem("control", fControlMenu,
		fCurrentMap->left_control_key, fCurrentMap->right_control_key) == false);
	fOptionField->SetUnmatched(_MarkMenuItem("option", fOptionMenu,
		fCurrentMap->left_option_key, fCurrentMap->right_option_key) == false);
	fCommandField->SetUnmatched(_MarkMenuItem("command", fCommandMenu,
		fCurrentMap->left_command_key, fCurrentMap->right_command_key) == false);
}


bool
ModifierKeysWindow::_MarkMenuItem(const char* role, BPopUpMenu* menu, uint32 left, uint32 right)
{
	for (int32 key = MENU_ITEM_FIRST; key <= MENU_ITEM_LAST; key++) {
		if (key == MENU_ITEM_SEPARATOR)
			continue;

		uint32 leftKey = _KeyToKeyCode(key);
		uint32 rightKey = _KeyToKeyCode(key, true);
		if (leftKey != rightKey && key == MENU_ITEM_CAPS) {
			// mark Caps Lock on Caps Lock role if either left or right is caps
			// otherwise mark Caps Lock if left side is caps
			uint32 capsKey = _KeyToKeyCode(MENU_ITEM_CAPS);
			if (strcmp(role, "caps lock") == 0 && (left == capsKey || right == capsKey))
				menu->ItemAt(key)->SetMarked(true);
			else if (left == capsKey)
				menu->ItemAt(key)->SetMarked(true);
		} else if (left == leftKey && right == rightKey)
			menu->ItemAt(key)->SetMarked(true);
	}

	return menu->FindMarked() != NULL;
}


// get the string for a modifier key
const char*
ModifierKeysWindow::_KeyToString(int32 key)
{
	switch (key) {
		case MENU_ITEM_CAPS:
			return B_TRANSLATE_COMMENT("Caps Lock key",
				"Label of key above Shift, usually Caps Lock");

		case MENU_ITEM_SHIFT:
			return B_TRANSLATE_COMMENT("Shift key",
				"Label of key above Ctrl, usually Shift");

		case MENU_ITEM_CONTROL:
			return B_TRANSLATE_COMMENT("Ctrl key",
				"Label of key farthest from the spacebar, usually Ctrl"
				"e.g. Strg for German keyboard");

		case MENU_ITEM_OPTION:
			return B_TRANSLATE_COMMENT("Win/Cmd key",
				"Label of the \"Windows\" key (PC)/Command key (Mac)");

		case MENU_ITEM_COMMAND:
			return B_TRANSLATE_COMMENT("Alt/Opt key",
				"Label of Alt key (PC)/Option key (Mac)");

		case MENU_ITEM_DISABLED:
			return B_TRANSLATE_COMMENT("Disabled", "Do nothing");
	}

	return B_EMPTY_STRING;
}


// get the keycode for a modifier key
int32
ModifierKeysWindow::_KeyToKeyCode(int32 key, bool right)
{
	switch (key) {
		case MENU_ITEM_CAPS:
			return 0x3b;

		case MENU_ITEM_SHIFT:
			if (right)
				return 0x56;
			return 0x4b;

		case MENU_ITEM_CONTROL:
			if (right)
				return 0x60;
			return 0x5c;

		case MENU_ITEM_OPTION:
			if (right)
				return 0x67;
			return 0x66;

		case MENU_ITEM_COMMAND:
			if (right)
				return 0x5f;
			return 0x5d;

		case MENU_ITEM_DISABLED:
			return kDisabled;
	}

	return kUnset;
}


// validate duplicate keys
void
ModifierKeysWindow::_ValidateDuplicateKeys()
{
	uint32 dupMask = _DuplicateKeys();
	_ValidateDuplicateKey(fCapsField, CAPS_KEY & dupMask);
	_ValidateDuplicateKey(fShiftField, SHIFT_KEY & dupMask);
	_ValidateDuplicateKey(fControlField, CONTROL_KEY & dupMask);
	_ValidateDuplicateKey(fOptionField, OPTION_KEY & dupMask);
	_ValidateDuplicateKey(fCommandField, COMMAND_KEY & dupMask);
	fOkButton->SetEnabled(dupMask == 0);
}


void
ModifierKeysWindow::_ValidateDuplicateKey(StatusMenuField* field, uint32 mask)
{
	if (mask != 0) // don't override if false
		field->SetDuplicate(true);
}


// return a mask marking which keys are duplicates of each other for
// validation.
uint32
ModifierKeysWindow::_DuplicateKeys()
{
	uint32 duplicateMask = 0;

	int32 testLeft, testRight, left, right;
	for (int32 testKey = MENU_ITEM_FIRST; testKey < MENU_ITEM_SEPARATOR; testKey++) {
		testLeft = kUnset;
		testRight = kUnset;

		switch (testKey) {
			case MENU_ITEM_CAPS:
				testLeft = fCurrentMap->caps_key;
				testRight = kDisabled;
				break;

			case MENU_ITEM_SHIFT:
				testLeft = fCurrentMap->left_shift_key;
				testRight = fCurrentMap->right_shift_key;
				break;

			case MENU_ITEM_CONTROL:
				testLeft = fCurrentMap->left_control_key;
				testRight = fCurrentMap->right_control_key;
				break;

			case MENU_ITEM_OPTION:
				testLeft = fCurrentMap->left_option_key;
				testRight = fCurrentMap->right_option_key;
				break;

			case MENU_ITEM_COMMAND:
				testLeft = fCurrentMap->left_command_key;
				testRight = fCurrentMap->right_command_key;
				break;
		}

		if (testLeft == kUnset && (testRight == kUnset || testRight == kDisabled))
			continue;

		for (int32 key = MENU_ITEM_FIRST; key < MENU_ITEM_SEPARATOR; key++) {
			if (key == testKey) {
				// skip over yourself
				continue;
			}

			left = kUnset;
			right = kUnset;

			switch (key) {
				case MENU_ITEM_CAPS:
					left = fCurrentMap->caps_key;
					right = kDisabled;
					break;

				case MENU_ITEM_SHIFT:
					left = fCurrentMap->left_shift_key;
					right = fCurrentMap->right_shift_key;
					break;

				case MENU_ITEM_CONTROL:
					left = fCurrentMap->left_control_key;
					right = fCurrentMap->right_control_key;
					break;

				case MENU_ITEM_OPTION:
					left = fCurrentMap->left_option_key;
					right = fCurrentMap->right_option_key;
					break;

				case MENU_ITEM_COMMAND:
					left = fCurrentMap->left_command_key;
					right = fCurrentMap->right_command_key;
					break;
			}

			if (left == kUnset && (right == kUnset || right == kDisabled))
				continue;

			// left or right is set

			if (left == testLeft || right == testRight) {
				duplicateMask |= 1 << testKey;
				duplicateMask |= 1 << key;
			}
		}
	}

	return duplicateMask;
}


void
ModifierKeysWindow::_UpdateStatus()
{
	// the order is important
	_MarkMenuItems();
	_ValidateDuplicateKeys();
}