| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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& ) |
| 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> ) 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 |
|
| 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& ) |
| 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> ) 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; |
| 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; |
| 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 |
} |