OXYGENHUB
◆ main openmsx / src / input / UnicodeKeymap.cc
6.0 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
e8555c89 Andy McCall 1
#include "UnicodeKeymap.hh"
2
3
#include "File.hh"
4
#include "FileContext.hh"
5
#include "FileException.hh"
6
#include "MSXException.hh"
7
8
#include "narrow.hh"
9
#include "one_of.hh"
10
#include "ranges.hh"
11
#include "stl.hh"
12
13
#include <algorithm>
14
#include <bit>
15
#include <optional>
16
17
namespace openmsx {
18
19
using namespace std::literals;
20
21
/** Parses the given string as a hexadecimal integer.
22
  */
23
[[nodiscard]] static constexpr std::optional<unsigned> parseHex(std::string_view str)
24
{
25
	if (str.empty()) {
26
		return {};
27
	}
28
	unsigned value = 0;
29
	for (const char c : str) {
30
		value *= 16;
31
		if ('0' <= c && c <= '9') {
32
			value += c - '0';
33
		} else if ('A' <= c && c <= 'F') {
34
			value += c - 'A' + 10;
35
		} else if ('a' <= c && c <= 'f') {
36
			value += c - 'a' + 10;
37
		} else {
38
			return {};
39
		}
40
	}
41
	return value;
42
}
43
44
/** Returns true iff the given character is a separator.
45
  * Separators are: comma, whitespace and hash mark.
46
  * Newline (\n) is not considered a separator.
47
  */
48
[[nodiscard]] static constexpr bool isSep(char c)
49
{
50
	return c == one_of(',',             // comma
51
	                   ' ', '\t', '\r', // whitespace
52
	                   '#');            // comment
53
}
54
55
/** Removes separator characters at the start of the given string reference.
56
  * Characters between a hash mark and the following newline are also skipped.
57
  */
58
static constexpr void skipSep(std::string_view& str)
59
{
60
	while (!str.empty()) {
61
		const char c = str.front();
62
		if (!isSep(c)) break;
63
		if (c == '#') {
64
			// Skip till end of line.
65
			while (!str.empty() && str.front() != '\n') str.remove_prefix(1);
66
			break;
67
		}
68
		str.remove_prefix(1);
69
	}
70
}
71
72
/** Returns the next token in the given string.
73
  * The token and any separators preceding it are removed from the string.
74
  */
75
[[nodiscard]] static constexpr std::string_view nextToken(std::string_view& str)
76
{
77
	skipSep(str);
78
	const auto* tokenBegin = str.data();
79
	while (!str.empty() && str.front() != '\n' && !isSep(str.front())) {
80
		// Pop non-separator character.
81
		str.remove_prefix(1);
82
	}
83
	return {tokenBegin, size_t(str.data() - tokenBegin)};
84
}
85
86
87
UnicodeKeymap::UnicodeKeymap(std::string_view keyboardType)
88
{
89
	auto filename = systemFileContext().resolve(
90
		tmpStrCat("unicodemaps/unicodemap.", keyboardType));
91
	try {
92
		auto buf = File(filename).mmap<const char>();
93
		parseUnicodeKeyMapFile(std::string_view(buf.data(), buf.size())); // TODO c++23
94
		// TODO in the future we'll require the presence of
95
		//      "MSX-Video-Characterset" in the keyboard information
96
		//      file, then we don't need this fallback.
97
		if (!msxChars.has_value()) {
98
			msxChars.emplace("MSXVID.TXT");
99
		}
100
	} catch (FileException&) {
101
		throw MSXException("Couldn't load unicode keymap file: ", filename);
102
	}
103
}
104
105
UnicodeKeymap::KeyInfo UnicodeKeymap::get(unsigned unicode) const
106
{
107
	auto m = binary_find(mapData, unicode, {}, &Entry::unicode);
108
	return m ? m->keyInfo : KeyInfo();
109
}
110
111
UnicodeKeymap::KeyInfo UnicodeKeymap::getDeadKey(unsigned n) const
112
{
113
	assert(n < NUM_DEAD_KEYS);
114
	return deadKeys[n];
115
}
116
117
void UnicodeKeymap::parseUnicodeKeyMapFile(std::string_view data)
118
{
119
	std::ranges::fill(relevantMods, 0);
120
121
	while (!data.empty()) {
122
		if (data.front() == '\n') {
123
			// Next line.
124
			data.remove_prefix(1);
125
		}
126
127
		std::string_view token = nextToken(data);
128
		if (token.empty()) {
129
			// Skip empty line.
130
			continue;
131
		}
132
133
		if (token == "MSX-Video-Characterset:") {
134
			auto vidFileName = nextToken(data);
135
			if (vidFileName.empty()) {
136
				throw MSXException("Missing filename for MSX-Video-Characterset");
137
			}
138
			msxChars.emplace(vidFileName);
139
			continue;
140
		}
141
142
		// Parse first token: a unicode value or the keyword DEADKEY.
143
		unsigned unicode = 0;
144
		unsigned deadKeyIndex = 0;
145
		bool isDeadKey = token.starts_with("DEADKEY");
146
		if (isDeadKey) {
147
			token.remove_prefix(strlen("DEADKEY"));
148
			if (token.empty()) {
149
				// The normal keywords are
150
				//    DEADKEY1  DEADKEY2  DEADKEY3
151
				// but for backwards compatibility also still recognize
152
				//    DEADKEY
153
			} else {
154
				auto d = parseHex(token);
155
				if (!d || *d > NUM_DEAD_KEYS) {
156
					throw MSXException(
157
						"Wrong deadkey number in keymap file. "
158
						"It must be 1..", NUM_DEAD_KEYS);
159
				}
160
				deadKeyIndex = *d - 1; // Make index 0 based instead of 1 based
161
			}
162
		} else {
163
			auto u = parseHex(token);
164
			if (!u || *u > 0x1FBFF) {
165
				throw MSXException("Wrong unicode value in keymap file");
166
			}
167
			unicode = *u;
168
		}
169
170
		// Parse second token. It must be <ROW><COL>
171
		token = nextToken(data);
172
		if (token == "--") {
173
			// Skip -- for now, it means the character cannot be typed.
174
			continue;
175
		}
176
		auto rowcol = parseHex(token);
177
		if (!rowcol || *rowcol >= 0x100) {
178
			throw MSXException(
179
				(token.empty() ? "Missing"sv : "Wrong"sv),
180
				" <ROW><COL> value in keymap file");
181
		}
182
		if ((*rowcol >> 4) >= KeyMatrixPosition::NUM_ROWS) {
183
			throw MSXException("Too high row value in keymap file");
184
		}
185
		if ((*rowcol & 0x0F) >= KeyMatrixPosition::NUM_COLS) {
186
			throw MSXException("Too high column value in keymap file");
187
		}
188
		auto pos = KeyMatrixPosition(narrow_cast<uint8_t>(*rowcol));
189
190
		// Parse remaining tokens. It is an optional list of modifier keywords.
191
		uint8_t modMask = 0;
192
		while (true) {
193
			token = nextToken(data);
194
			if (token.empty()) {
195
				break;
196
			} else if (token == "SHIFT") {
197
				modMask |= KeyInfo::SHIFT_MASK;
198
			} else if (token == "CTRL") {
199
				modMask |= KeyInfo::CTRL_MASK;
200
			} else if (token == "GRAPH") {
201
				modMask |= KeyInfo::GRAPH_MASK;
202
			} else if (token == "CAPSLOCK") {
203
				modMask |= KeyInfo::CAPS_MASK;
204
			} else if (token == "CODE") {
205
				modMask |= KeyInfo::CODE_MASK;
206
			} else {
207
				throw MSXException(
208
					"Invalid modifier \"", token, "\" in keymap file");
209
			}
210
		}
211
212
		if (isDeadKey) {
213
			if (modMask != 0) {
214
				throw MSXException(
215
					"DEADKEY entry in keymap file cannot have modifiers");
216
			}
217
			deadKeys[deadKeyIndex] = KeyInfo(pos, 0);
218
		} else {
219
			mapData.emplace_back(Entry{.unicode = unicode, .keyInfo = KeyInfo(pos, modMask)});
220
			// Note: getRowCol() uses 3 bits for column, rowcol uses 4.
221
			relevantMods[pos.getRowCol()] |= modMask;
222
		}
223
	}
224
225
	std::ranges::sort(mapData, {}, &Entry::unicode);
226
}
227
228
} // namespace openmsx