⛏️ index : haiku.git

/*
 * Copyright 1999-2009 Jeremy Friesner
 * Copyright 2009-2010 Haiku, Inc. All rights reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *		Jeremy Friesner
 */

#include "ShortcutsSpec.h"

#include <ctype.h>
#include <stdio.h>

#include <Beep.h>
#include <Catalog.h>
#include <ColumnTypes.h>
#include <Directory.h>
#include <Locale.h>
#include <NodeInfo.h>
#include <Path.h>
#include <Region.h>
#include <Window.h>

#include "ColumnListView.h"

#include "BitFieldTesters.h"
#include "CommandActuators.h"
#include "KeyInfos.h"
#include "MetaKeyStateMap.h"
#include "ParseCommandLine.h"


#define CLASS "ShortcutsSpec : "

#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "ShortcutsSpec"

const float _height = 20.0f;

static MetaKeyStateMap sMetaMaps[ShortcutsSpec::NUM_META_COLUMNS];

static bool sFontCached = false;
static BFont sViewFont;
static float sFontHeight;

const char* ShortcutsSpec::sShiftName;
const char* ShortcutsSpec::sControlName;
const char* ShortcutsSpec::sOptionName;
const char* ShortcutsSpec::sCommandName;


#define ICON_BITMAP_RECT BRect(0.0f, 0.0f, 15.0f, 15.0f)
#define ICON_BITMAP_SPACE B_RGBA32


// Returns the (pos)'th char in the string, or '\0' if (pos) if off the end of
// the string
static char
GetLetterAt(const char* str, int pos)
{
	for (int i = 0; i < pos; i++) {
		if (str[i] == '\0')
			return '\0';
	}
	return str[pos];
}


// Setup the states in a standard manner for a pair of meta-keys.
static void
SetupStandardMap(MetaKeyStateMap& map, const char* name, uint32 both,
	uint32 left, uint32 right)
{
	map.SetInfo(name);

	// In this state, neither key may be pressed.
	map.AddState(B_TRANSLATE("(None)"), new HasBitsFieldTester(0, both));

	// Here, either may be pressed. (Remember both is NOT a 2-bit chord, it's
	// another bit entirely)
	map.AddState(B_TRANSLATE("Either"), new HasBitsFieldTester(both));

	// Here, only the left may be pressed
	map.AddState(B_TRANSLATE("Left"), new HasBitsFieldTester(left, right));

	// Here, only the right may be pressed
	map.AddState(B_TRANSLATE("Right"), new HasBitsFieldTester(right, left));

	// Here, both must be pressed.
	map.AddState(B_TRANSLATE("Both"), new HasBitsFieldTester(left | right));
}


MetaKeyStateMap&
GetNthKeyMap(int which)
{
	return sMetaMaps[which];
}


/*static*/ void
ShortcutsSpec::InitializeMetaMaps()
{
	static bool metaMapsInitialized = false;
	if (metaMapsInitialized)
		return;
	metaMapsInitialized = true;

	_InitModifierNames();

	SetupStandardMap(sMetaMaps[ShortcutsSpec::SHIFT_COLUMN_INDEX], sShiftName,
		B_SHIFT_KEY, B_LEFT_SHIFT_KEY, B_RIGHT_SHIFT_KEY);

	SetupStandardMap(sMetaMaps[ShortcutsSpec::CONTROL_COLUMN_INDEX],
		sControlName, B_CONTROL_KEY, B_LEFT_CONTROL_KEY, B_RIGHT_CONTROL_KEY);

	SetupStandardMap(sMetaMaps[ShortcutsSpec::COMMAND_COLUMN_INDEX],
		sCommandName, B_COMMAND_KEY, B_LEFT_COMMAND_KEY, B_RIGHT_COMMAND_KEY);

	SetupStandardMap(sMetaMaps[ShortcutsSpec::OPTION_COLUMN_INDEX], sOptionName
		, B_OPTION_KEY, B_LEFT_OPTION_KEY, B_RIGHT_OPTION_KEY);
}


ShortcutsSpec::ShortcutsSpec(const char* cmd)
	:
	BRow(),
	fCommand(NULL),
	fBitmap(ICON_BITMAP_RECT, ICON_BITMAP_SPACE),
	fLastBitmapName(NULL),
	fBitmapValid(false),
	fKey(0),
	fCursorPtsValid(false)
{
	for (int i = 0; i < NUM_META_COLUMNS; i++)
		fMetaCellStateIndex[i] = 0;
	SetCommand(cmd);
}


ShortcutsSpec::ShortcutsSpec(const ShortcutsSpec& from)
	:
	BRow(),
	fCommand(NULL),
	fBitmap(ICON_BITMAP_RECT, ICON_BITMAP_SPACE),
	fLastBitmapName(NULL),
	fBitmapValid(false),
	fKey(from.fKey),
	fCursorPtsValid(false)
{
	for (int i = 0; i < NUM_META_COLUMNS; i++)
		fMetaCellStateIndex[i] = from.fMetaCellStateIndex[i];

	SetCommand(from.fCommand);
	SetSelectedColumn(from.GetSelectedColumn());

	for (int i = 0; i < from.CountFields(); i++)
		SetField(new BStringField(
					static_cast<const BStringField*>(from.GetField(i))->String()), i);
}


ShortcutsSpec::ShortcutsSpec(BMessage* from)
	:
	BRow(),
	fCommand(NULL),
	fBitmap(ICON_BITMAP_RECT, ICON_BITMAP_SPACE),
	fLastBitmapName(NULL),
	fBitmapValid(false),
	fCursorPtsValid(false)
{
	const char* temp;
	if (from->FindString("command", &temp) != B_NO_ERROR) {
		printf(CLASS);
		printf(" Error, no command string in archive BMessage!\n");
		temp = "";
	}

	SetCommand(temp);

	if (from->FindInt32("key", (int32*) &fKey) != B_NO_ERROR) {
		printf(CLASS);
		printf(" Error, no key int32 in archive BMessage!\n");
	}

	for (int i = 0; i < NUM_META_COLUMNS; i++)
		if (from->FindInt32("mcidx", i, (int32*)&fMetaCellStateIndex[i])
			!= B_NO_ERROR) {
			printf(CLASS);
			printf(" Error, no modifiers int32 in archive BMessage!\n");
		}

	for (int i = 0; i <= STRING_COLUMN_INDEX; i++)
		SetField(new BStringField(GetCellText(i)), i);
}


void
ShortcutsSpec::SetCommand(const char* command)
{
	delete[] fCommand;
		// out with the old (if any)...
	fCommandLen = strlen(command) + 1;
	fCommandNul = fCommandLen - 1;
	fCommand = new char[fCommandLen];
	strcpy(fCommand, command);
	SetField(new BStringField(command), STRING_COLUMN_INDEX);
}


const char*
ShortcutsSpec::GetColumnName(int i)
{
	return sMetaMaps[i].GetName();
}


status_t
ShortcutsSpec::Archive(BMessage* into, bool deep) const
{
	status_t ret = BArchivable::Archive(into, deep);
	if (ret != B_NO_ERROR)
		return ret;

	into->AddString("class", "ShortcutsSpec");

	// These fields are for our prefs panel's benefit only
	into->AddString("command", fCommand);
	into->AddInt32("key", fKey);

	// Assemble a BitFieldTester for the input_server add-on to use...
	MinMatchFieldTester test(NUM_META_COLUMNS, false);
	for (int i = 0; i < NUM_META_COLUMNS; i++) {
		// for easy parsing by prefs applet on load-in
		into->AddInt32("mcidx", fMetaCellStateIndex[i]);
		test.AddSlave(sMetaMaps[i].GetNthStateTester(fMetaCellStateIndex[i]));
	}

	BMessage testerMsg;
	ret = test.Archive(&testerMsg);
	if (ret != B_NO_ERROR)
		return ret;

	into->AddMessage("modtester", &testerMsg);

	// And also create a CommandActuator for the input_server add-on to execute
	CommandActuator* act = CreateCommandActuator(fCommand);
	BMessage actMsg;
	ret = act->Archive(&actMsg);
	if (ret != B_NO_ERROR)
		return ret;
	delete act;

	into->AddMessage("act", &actMsg);

	return ret;
}


BArchivable*
ShortcutsSpec::Instantiate(BMessage* from)
{
	bool validateOK = false;
	if (validate_instantiation(from, "ShortcutsSpec"))
		validateOK = true;
	else // test the old one.
		if (validate_instantiation(from, "SpicyKeysSpec"))
			validateOK = true;

	if (!validateOK)
		return NULL;

	return new ShortcutsSpec(from);
}


ShortcutsSpec::~ShortcutsSpec()
{
	delete[] fCommand;
	delete[] fLastBitmapName;
}


void
ShortcutsSpec::_CacheViewFont(BView* owner)
{
	if (sFontCached == false) {
		sFontCached = true;
		owner->GetFont(&sViewFont);
		font_height fh;
		sViewFont.GetHeight(&fh);
		sFontHeight = fh.ascent - fh.descent;
	}
}


const char*
ShortcutsSpec::GetCellText(int whichColumn) const
{
	const char* temp = ""; // default
	switch (whichColumn) {
		case KEY_COLUMN_INDEX:
		{
			if ((fKey > 0) && (fKey <= 0xFF)) {
				temp = GetKeyName(fKey);
				if (temp == NULL)
					temp = "";
			} else if (fKey > 0xFF) {
				sprintf(fScratch, "#%" B_PRIx32, fKey);
				return fScratch;
			}
			break;
		}

		case STRING_COLUMN_INDEX:
			temp = fCommand;
			break;

		default:
			if ((whichColumn >= 0) && (whichColumn < NUM_META_COLUMNS))
				temp = sMetaMaps[whichColumn].GetNthStateDesc(
							fMetaCellStateIndex[whichColumn]);
			if (temp[0] == '(')
				temp = "";
			break;
	}
	return temp;
}


bool
ShortcutsSpec::ProcessColumnMouseClick(int whichColumn)
{
	if ((whichColumn >= 0) && (whichColumn < NUM_META_COLUMNS)) {
		// same as hitting space for these columns: cycle entry
		const char temp = B_SPACE;

		// 3rd arg isn't correct but it isn't read for this case anyway
		return ProcessColumnKeyStroke(whichColumn, &temp, 0);
	}
	return false;
}


bool
ShortcutsSpec::ProcessColumnTextString(int whichColumn, const char* string)
{
	switch (whichColumn) {
		case STRING_COLUMN_INDEX:
			SetCommand(string);
			return true;
			break;

		case KEY_COLUMN_INDEX:
		{
			fKey = FindKeyCode(string);
			SetField(new BStringField(GetCellText(whichColumn)),
				KEY_COLUMN_INDEX);
			return true;
			break;
		}

		default:
			return ProcessColumnKeyStroke(whichColumn, string, 0);
	}
}


bool
ShortcutsSpec::_AttemptTabCompletion()
{
	bool result = false;

	int32 argc;
	char** argv = ParseArgvFromString(fCommand, argc);
	if (argc > 0) {
		// Try to complete the path partially expressed in the last argument!
		char* arg = argv[argc - 1];
		char* fileFragment = strrchr(arg, '/');
		if (fileFragment != NULL) {
			const char* directoryName = (fileFragment == arg) ? "/" : arg;
			*fileFragment = '\0';
			fileFragment++;
			int fragmentLength = strlen(fileFragment);

			BDirectory dir(directoryName);
			if (dir.InitCheck() == B_NO_ERROR) {
				BEntry nextEnt;
				BPath nextPath;
				BList matchList;
				int maxEntryLen = 0;

				// Read in all the files in the directory whose names start
				// with our fragment.
				while (dir.GetNextEntry(&nextEnt) == B_NO_ERROR) {
					if (nextEnt.GetPath(&nextPath) == B_NO_ERROR) {
						char* filePath = strrchr(nextPath.Path(), '/') + 1;
						if (strncmp(filePath, fileFragment, fragmentLength) == 0) {
							int len = strlen(filePath);
							if (len > maxEntryLen)
								maxEntryLen = len;
							char* newStr = new char[len + 1];
							strcpy(newStr, filePath);
							matchList.AddItem(newStr);
						}
					}
				}

				// Now slowly extend our keyword to its full length, counting
				// numbers of matches at each step. If the match list length
				// is 1, we can use that whole entry. If it's greater than one,
				// we can use just the match length.
				int matchLen = matchList.CountItems();
				if (matchLen > 0) {
					int i;
					BString result(fileFragment);
					for (i = fragmentLength; i < maxEntryLen; i++) {
						// See if all the matching entries have the same letter
						// in the next position... if so, we can go farther.
						char commonLetter = '\0';
						for (int j = 0; j < matchLen; j++) {
							char nextLetter = GetLetterAt(
								(char*)matchList.ItemAt(j), i);
							if (commonLetter == '\0')
								commonLetter = nextLetter;

							if ((commonLetter != '\0')
								&& (commonLetter != nextLetter)) {
								commonLetter = '\0';// failed;
								beep();
								break;
							}
						}
						if (commonLetter == '\0')
							break;
						else
							result.Append(commonLetter, 1);
					}

					// free all the strings we allocated
					for (int k = 0; k < matchLen; k++)
						delete [] ((char*)matchList.ItemAt(k));

					DoStandardEscapes(result);

					BString wholeLine;
					for (int l = 0; l < argc - 1; l++) {
						wholeLine += argv[l];
						wholeLine += " ";
					}

					BString file(directoryName);
					DoStandardEscapes(file);

					if (directoryName[strlen(directoryName) - 1] != '/')
						file += "/";

					file += result;

					// Remove any trailing slash...
					const char* fileStr = file.String();
					if (fileStr[strlen(fileStr) - 1] == '/')
						file.RemoveLast("/");

					// and re-append it iff the file is a dir.
					BDirectory testFileAsDir(file.String());
					if ((strcmp(file.String(), "/") != 0)
						&& (testFileAsDir.InitCheck() == B_NO_ERROR))
						file.Append("/");

					wholeLine += file;

					SetCommand(wholeLine.String());
					result = true;
				}
			}
			*(fileFragment - 1) = '/';
		}
	}
	FreeArgv(argv);

	return result;
}


bool
ShortcutsSpec::ProcessColumnKeyStroke(int whichColumn, const char* bytes,
	int32 key)
{
	bool result = false;

	switch (whichColumn) {
		case KEY_COLUMN_INDEX:
			if (key > -1) {
				if ((int32)fKey != key) {
					fKey = key;
					result = true;
				}
			}
			break;

		case STRING_COLUMN_INDEX:
		{
			switch (bytes[0]) {
				case B_BACKSPACE:
				case B_DELETE:
					if (fCommandNul > 0) {
						// trim a char off the string
						fCommand[fCommandNul - 1] = '\0';
						fCommandNul--;	// note new nul position
						result = true;
					}
					break;

				case B_TAB:
					if (_AttemptTabCompletion()) {
						result = true;
					} else
						beep();
					break;

				default:
				{
					uint32 newCharLen = strlen(bytes);
					if ((newCharLen > 0) && (bytes[0] >= ' ')) {
						bool reAllocString = false;
						// Make sure we have enough room in our command string
						// to add these chars...
						while (fCommandLen - fCommandNul <= newCharLen) {
							reAllocString = true;
							// enough for a while...
							fCommandLen = (fCommandLen + 10) * 2;
						}

						if (reAllocString) {
							char* temp = new char[fCommandLen];
							strcpy(temp, fCommand);
							delete [] fCommand;
							fCommand = temp;
							// fCommandNul is still valid since it's an offset
							// and the string length is the same for now
						}

						// Here we should be guaranteed enough room.
						strncat(fCommand, bytes, fCommandLen);
						fCommandNul += newCharLen;
						result = true;
					}
				}
			}
			break;
		}

		default:
			if (whichColumn < 0 || whichColumn >= NUM_META_COLUMNS)
				break;

			MetaKeyStateMap * map = &sMetaMaps[whichColumn];
			int curState = fMetaCellStateIndex[whichColumn];
			int origState = curState;
			int numStates = map->GetNumStates();

			switch(bytes[0]) {
				case B_RETURN:
					// cycle to the previous state
					curState = (curState + numStates - 1) % numStates;
					break;

				case B_SPACE:
					// cycle to the next state
					curState = (curState + 1) % numStates;
					break;

				default:
				{
					// Go to the state starting with the given letter, if
					// any
					char letter = bytes[0];
					if (islower(letter))
						letter = toupper(letter); // convert to upper case

					if ((letter == B_BACKSPACE) || (letter == B_DELETE))
						letter = '(';
							// so space bar will blank out an entry

					for (int i = 0; i < numStates; i++) {
						const char* desc = map->GetNthStateDesc(i);

						if (desc) {
							if (desc[0] == letter) {
								curState = i;
								break;
							}
						} else {
							puts(B_TRANSLATE(
								"Error, NULL state description?"));
						}
					}
				}
			}
			fMetaCellStateIndex[whichColumn] = curState;

			if (curState != origState)
				result = true;
	}

	SetField(new BStringField(GetCellText(whichColumn)), whichColumn);

	return result;
}


/*static*/ void
ShortcutsSpec::_InitModifierNames()
{
	sShiftName = B_TRANSLATE_COMMENT("Shift",
		"Name for modifier on keyboard");
	sControlName = B_TRANSLATE_COMMENT("Control",
		"Name for modifier on keyboard");
	sOptionName = B_TRANSLATE_COMMENT("Option",
		"Name for modifier on keyboard");
	sCommandName = B_TRANSLATE_COMMENT("Alt",
		"Name for modifier on keyboard");
}