OXYGENHUB
◆ main openmsx / src / config / SettingsConfig.cc
10.7 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "SettingsConfig.hh"
2
3
#include "CliComm.hh"
4
#include "CommandException.hh"
5
#include "File.hh"
6
#include "FileContext.hh"
7
#include "FileException.hh"
8
#include "FileOperations.hh"
9
#include "GlobalCommandController.hh"
10
#include "HotKey.hh"
11
#include "MSXException.hh"
12
#include "Shortcuts.hh"
13
#include "TclObject.hh"
14
#include "XMLOutputStream.hh"
15
16
#include "MemBuffer.hh"
17
#include "StringOp.hh"
18
#include "outer.hh"
19
#include "rapidsax.hh"
20
#include "unreachable.hh"
21
22
#include <cstdint>
23
24
namespace openmsx {
25
26
struct SettingsParser : rapidsax::NullHandler
27
{
28
	void start(std::string_view tag);
29
	void attribute(std::string_view name, std::string_view value);
30
	void text(std::string_view txt);
31
	void stop();
32
	void doctype(std::string_view txt);
33
34
	// parse result
35
	struct Setting {
36
		std::string_view name;
37
		std::string_view value;
38
	};
39
	std::vector<Setting> settings;
40
	std::vector<HotKey::Data> binds;
41
	std::vector<std::string_view> unbinds;
42
43
	struct ShortcutItem {
44
		Shortcuts::ID id = Shortcuts::ID::INVALID;
45
		Shortcuts::Shortcut shortcut = {};
46
	};
47
	std::vector<ShortcutItem> shortcutItems;
48
49
	std::string_view systemID;
50
51
	// parse state
52
	unsigned unknownLevel = 0;
53
	enum class State : uint8_t{
54
		START,
55
		TOP,
56
		SETTINGS,
57
		SETTING,
58
		BINDINGS,
59
		BIND,
60
		UNBIND,
61
		SHORTCUTS,
62
		SHORTCUT,
63
		END
64
	} state = State::START;
65
	Setting currentSetting;
66
	HotKey::Data currentBind;
67
	std::string_view currentUnbind;
68
	ShortcutItem currentShortcut;
69
};
70
71
SettingsConfig::SettingsConfig(
72
		GlobalCommandController& globalCommandController,
73
		HotKey& hotKey_, Shortcuts& shortcuts_)
74
	: commandController(globalCommandController)
75
	, saveSettingsCommand(commandController)
76
	, loadSettingsCommand(commandController)
77
	, settingsManager(globalCommandController)
78
	, hotKey(hotKey_)
79
	, shortcuts(shortcuts_)
80
{
81
}
82
83
SettingsConfig::~SettingsConfig()
84
{
85
	if (mustSaveSettings) {
86
		try {
87
			saveSetting();
88
		} catch (FileException& e) {
89
			commandController.getCliComm().printWarning(
90
				"Auto-saving of settings failed: ", e.getMessage());
91
		}
92
	}
93
}
94
95
void SettingsConfig::loadSetting(const FileContext& context, std::string_view filename)
96
{
97
	std::string resolved = context.resolve(filename);
98
99
	auto buf = [&] {
100
		try {
101
			return File(resolved).mmap<char>(rapidsax::EXTRA_BUFFER_SPACE);
102
		} catch (FileException& e) {
103
			throw MSXException("Failed to read settings file '", filename,
104
			                   "': ", e.getMessage());
105
		}
106
	}();
107
	SettingsParser parser;
108
	rapidsax::parse<0>(parser, buf.data());
109
	if (parser.systemID != "settings.dtd") {
110
		throw MSXException(
111
			"Failed to parser settings file '", filename,
112
		        "': systemID doesn't match (expected 'settings.dtd' got '",
113
			parser.systemID, "')\n"
114
			"You're probably using an old incompatible file format.");
115
	}
116
117
	settingValues.clear();
118
	settingValues.reserve(unsigned(parser.settings.size()));
119
	for (const auto& [name, value] : parser.settings) {
120
		settingValues.emplace(name, value);
121
	}
122
123
	hotKey.loadInit();
124
	for (const auto& bind : parser.binds) {
125
		try {
126
			hotKey.loadBind(bind);
127
		} catch (MSXException& e) {
128
			commandController.getCliComm().printWarning(
129
				"Couldn't restore key-binding: ", e.getMessage());
130
		}
131
	}
132
	for (const auto& key : parser.unbinds) {
133
		try {
134
			hotKey.loadUnbind(key);
135
		} catch (MSXException& e) {
136
			commandController.getCliComm().printWarning(
137
				"Couldn't restore key-binding: ", e.getMessage());
138
		}
139
	}
140
141
	shortcuts.setDefaultShortcuts();
142
	for (const auto& item : parser.shortcutItems) {
143
		shortcuts.setShortcut(item.id, item.shortcut);
144
	}
145
146
	getSettingsManager().loadSettings(*this);
147
148
	// only set saveName after file was successfully parsed
149
	saveName = std::move(resolved);
150
}
151
152
void SettingsConfig::setSaveFilename(const FileContext& context, std::string_view filename)
153
{
154
	saveName = context.resolveCreate(filename);
155
}
156
157
const std::string* SettingsConfig::getValueForSetting(std::string_view setting) const
158
{
159
	return lookup(settingValues, setting);
160
}
161
162
void SettingsConfig::setValueForSetting(std::string_view setting, std::string_view value)
163
{
164
	settingValues.insert_or_assign(setting, value);
165
}
166
167
void SettingsConfig::removeValueForSetting(std::string_view setting)
168
{
169
	settingValues.erase(setting);
170
}
171
172
void SettingsConfig::saveSetting(std::string filename)
173
{
174
	if (filename.empty()) filename = saveName;
175
	if (filename.empty()) return;
176
177
	struct SettingsWriter {
178
		explicit SettingsWriter(std::string filename)
179
			: file(std::move(filename), File::OpenMode::TRUNCATE)
180
		{
181
			std::string_view header =
182
				"<!DOCTYPE settings SYSTEM 'settings.dtd'>\n";
183
			write(header);
184
		}
185
186
		void write(std::span<const char> buf) {
187
			file.write(buf);
188
		}
189
		void write1(char c) {
190
			file.write(std::span{&c, 1});
191
		}
192
		void check(bool condition) const {
193
			assert(condition); (void)condition;
194
		}
195
196
	private:
197
		File file;
198
	};
199
200
	SettingsWriter writer(std::move(filename));
201
	XMLOutputStream xml(writer);
202
	xml.with_tag("settings", [&]{
203
		xml.with_tag("settings", [&]{
204
			for (const auto& [name, value] : settingValues) {
205
				xml.with_tag("setting", [&]{
206
					xml.attribute("id", name);
207
					xml.data(value);
208
				});
209
			}
210
		});
211
		hotKey.saveBindings(xml);
212
		shortcuts.saveShortcuts(xml);
213
	});
214
}
215
216
217
// class SaveSettingsCommand
218
219
SettingsConfig::SaveSettingsCommand::SaveSettingsCommand(
220
		CommandController& commandController_)
221
	: Command(commandController_, "save_settings")
222
{
223
}
224
225
void SettingsConfig::SaveSettingsCommand::execute(
226
	std::span<const TclObject> tokens, TclObject& /*result*/)
227
{
228
	checkNumArgs(tokens, Between{1, 2}, Prefix{1}, "?filename?");
229
	auto& settingsConfig = OUTER(SettingsConfig, saveSettingsCommand);
230
	try {
231
		switch (tokens.size()) {
232
		case 1:
233
			settingsConfig.saveSetting();
234
			break;
235
		case 2:
236
			settingsConfig.saveSetting(FileOperations::expandTilde(
237
				std::string(tokens[1].getString())));
238
			break;
239
		}
240
	} catch (FileException& e) {
241
		throw CommandException(std::move(e).getMessage());
242
	}
243
}
244
245
std::string SettingsConfig::SaveSettingsCommand::help(std::span<const TclObject> /*tokens*/) const
246
{
247
	return "Save the current settings.";
248
}
249
250
void SettingsConfig::SaveSettingsCommand::tabCompletion(std::vector<std::string>& tokens) const
251
{
252
	if (tokens.size() == 2) {
253
		completeFileName(tokens, systemFileContext());
254
	}
255
}
256
257
258
// class LoadSettingsCommand
259
260
SettingsConfig::LoadSettingsCommand::LoadSettingsCommand(
261
		CommandController& commandController_)
262
	: Command(commandController_, "load_settings")
263
{
264
}
265
266
void SettingsConfig::LoadSettingsCommand::execute(
267
	std::span<const TclObject> tokens, TclObject& /*result*/)
268
{
269
	checkNumArgs(tokens, 2, "filename");
270
	auto& settingsConfig = OUTER(SettingsConfig, loadSettingsCommand);
271
	settingsConfig.loadSetting(systemFileContext(), tokens[1].getString());
272
}
273
274
std::string SettingsConfig::LoadSettingsCommand::help(std::span<const TclObject> /*tokens*/) const
275
{
276
	return "Load settings from given file.";
277
}
278
279
void SettingsConfig::LoadSettingsCommand::tabCompletion(std::vector<std::string>& tokens) const
280
{
281
	if (tokens.size() == 2) {
282
		completeFileName(tokens, systemFileContext());
283
	}
284
}
285
286
287
void SettingsParser::start(std::string_view tag)
288
{
289
	if (unknownLevel) {
290
		++unknownLevel;
291
		return;
292
	}
293
	switch (state) {
294
	using enum State;
295
	case START:
296
		if (tag == "settings") {
297
			state = TOP;
298
			return;
299
		}
300
		throw MSXException("Expected <settings> as root tag.");
301
	case TOP:
302
		if (tag == "settings") {
303
			state = SETTINGS;
304
			return;
305
		} else if (tag == "bindings") {
306
			state = BINDINGS;
307
			return;
308
		} else if (tag == "shortcuts") {
309
			state = SHORTCUTS;
310
			return;
311
		}
312
		break;
313
	case SETTINGS:
314
		if (tag == "setting") {
315
			state = SETTING;
316
			currentSetting = Setting{};
317
			return;
318
		}
319
		break;
320
	case BINDINGS:
321
		if (tag == "bind") {
322
			state = BIND;
323
			currentBind = HotKey::Data{};
324
			return;
325
		} else if (tag == "unbind") {
326
			state = UNBIND;
327
			currentUnbind = std::string_view{};
328
			return;
329
		}
330
		break;
331
	case SHORTCUTS:
332
		if (tag == "shortcut") {
333
			state = SHORTCUT;
334
			currentShortcut = ShortcutItem{};
335
			return;
336
		}
337
		break;
338
	case SETTING:
339
	case BIND:
340
	case UNBIND:
341
	case SHORTCUT:
342
		break;
343
	case END:
344
		throw MSXException("Unexpected opening tag: ", tag);
345
	default:
346
		UNREACHABLE;
347
	}
348
349
	++unknownLevel;
350
}
351
352
void SettingsParser::attribute(std::string_view name, std::string_view value)
353
{
354
	if (unknownLevel) return;
355
356
	switch (state) {
357
	using enum State;
358
	case SETTING:
359
		if (name == "id") {
360
			currentSetting.name = value;
361
		}
362
		break;
363
	case SHORTCUT:
364
		if (name == "key") {
365
			if (auto keyChord = parseKeyChord(value)) {
366
				currentShortcut.shortcut.keyChord = *keyChord;
367
			} else {
368
				std::cerr << "Parse error: invalid shortcut key \"" << value << "\"\n";
369
			}
370
		} else if (name == "type") {
371
			if (auto type = Shortcuts::parseType(value)) {
372
				currentShortcut.shortcut.type = *type;
373
			} else {
374
				std::cerr << "Parse error: invalid shortcut type \"" << value << "\"\n";
375
			}
376
		}
377
		break;
378
	case BIND:
379
		if (name == "key") {
380
			currentBind.key = value;
381
		} else if (name == "repeat") {
382
			currentBind.repeat = StringOp::stringToBool(value);
383
		} else if (name == "event") {
384
			currentBind.event = StringOp::stringToBool(value);
385
		} else if (name == "msx") {
386
			currentBind.msx = StringOp::stringToBool(value);
387
		}
388
		break;
389
	case UNBIND:
390
		if (name == "key") {
391
			currentUnbind = value;
392
		}
393
		break;
394
	default:
395
		break; //nothing
396
	}
397
}
398
399
void SettingsParser::text(std::string_view txt)
400
{
401
	if (unknownLevel) return;
402
403
	switch (state) {
404
	using enum State;
405
	case SETTING:
406
		currentSetting.value = txt;
407
		break;
408
	case BIND:
409
		currentBind.cmd = txt;
410
		break;
411
	case SHORTCUT:
412
		if (auto value = Shortcuts::parseShortcutName(txt)) {
413
			currentShortcut.id = *value;
414
		} else {
415
			std::cerr << "Parse error: invalid shortcut \"" << txt << "\"\n";
416
		}
417
		break;
418
	default:
419
		break; //nothing
420
	}
421
}
422
423
void SettingsParser::stop()
424
{
425
	if (unknownLevel) {
426
		--unknownLevel;
427
		return;
428
	}
429
430
	switch (state) {
431
	using enum State;
432
	case TOP:
433
		state = END;
434
		break;
435
	case SETTINGS:
436
		state = TOP;
437
		break;
438
	case BINDINGS:
439
		state = TOP;
440
		break;
441
	case SHORTCUTS:
442
		state = TOP;
443
		break;
444
	case SETTING:
445
		if (!currentSetting.name.empty()) {
446
			settings.push_back(currentSetting);
447
		}
448
		state = SETTINGS;
449
		break;
450
	case BIND:
451
		if (!currentBind.key.empty()) {
452
			binds.push_back(currentBind);
453
		}
454
		state = BINDINGS;
455
		break;
456
	case UNBIND:
457
		if (!currentUnbind.empty()) {
458
			unbinds.push_back(currentUnbind);
459
		}
460
		state = BINDINGS;
461
		break;
462
	case SHORTCUT:
463
		if (currentShortcut.id != Shortcuts::ID::INVALID) {
464
			shortcutItems.push_back(currentShortcut);
465
		}
466
		state = SHORTCUTS;
467
		break;
468
	case START:
469
	case END:
470
		throw MSXException("Unexpected closing tag");
471
	default:
472
		UNREACHABLE;
473
	}
474
}
475
476
void SettingsParser::doctype(std::string_view txt)
477
{
478
	auto pos1 = txt.find(" SYSTEM ");
479
	if (pos1 == std::string_view::npos) return;
480
	if ((pos1 + 8) >= txt.size()) return;
481
	char q = txt[pos1 + 8];
482
	if (q != one_of('"', '\'')) return;
483
	auto t = txt.substr(pos1 + 9);
484
	auto pos2 = t.find(q);
485
	if (pos2 == std::string_view::npos) return;
486
487
	systemID = t.substr(0, pos2);
488
}
489
490
} // namespace openmsx