#include "config.h" #include "log.h" #include "shortcut.h" #include #include #include #include #include #include #include #include #include #include #include #include #include KeyValueConfigBase::~KeyValueConfigBase() { } void KeyValueConfigBase::load() { reset(); QFile file(this->getConfigFilepath()); if (!file.exists()) { if (!file.open(QIODevice::WriteOnly)) { logError(QString("Could not create config file '%1'").arg(this->getConfigFilepath())); } else { file.close(); this->onNewConfigFileCreated(); this->save(); } } if (!file.open(QIODevice::ReadOnly)) { logError(QString("Could not open config file '%1': ").arg(this->getConfigFilepath()) + file.errorString()); } QTextStream in(&file); in.setCodec("UTF-8"); QList configLines; QRegularExpression re("^(?[^=]+)=(?.*)$"); while (!in.atEnd()) { QString line = in.readLine().trimmed(); int commentIndex = line.indexOf("#"); if (commentIndex >= 0) { line = line.left(commentIndex).trimmed(); } if (line.length() == 0) { continue; } QRegularExpressionMatch match = re.match(line); if (!match.hasMatch()) { logWarn(QString("Invalid config line in %1: '%2'").arg(this->getConfigFilepath()).arg(line)); continue; } this->parseConfigKeyValue(match.captured("key").toLower(), match.captured("value")); } this->setUnreadKeys(); file.close(); } void KeyValueConfigBase::save() { QString text = ""; QMap map = this->getKeyValueMap(); for (QMap::iterator it = map.begin(); it != map.end(); it++) { text += QString("%1=%2\n").arg(it.key()).arg(it.value()); } QFile file(this->getConfigFilepath()); if (file.open(QIODevice::WriteOnly)) { file.write(text.toUtf8()); file.close(); } else { logError(QString("Could not open config file '%1' for writing: ").arg(this->getConfigFilepath()) + file.errorString()); } } const QMap mapSortOrderMap = { {MapSortOrder::Group, "group"}, {MapSortOrder::Layout, "layout"}, {MapSortOrder::Area, "area"}, }; const QMap mapSortOrderReverseMap = { {"group", MapSortOrder::Group}, {"layout", MapSortOrder::Layout}, {"area", MapSortOrder::Area}, }; PorymapConfig porymapConfig; QString PorymapConfig::getConfigFilepath() { // porymap config file is in the same directory as porymap itself. QString settingsPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); QDir dir(settingsPath); if (!dir.exists()) dir.mkpath(settingsPath); QString configPath = dir.absoluteFilePath("porymap.cfg"); return configPath; } void PorymapConfig::parseConfigKeyValue(QString key, QString value) { if (key == "recent_project") { this->recentProject = value; } else if (key == "recent_map") { this->recentMap = value; } else if (key == "pretty_cursors") { bool ok; this->prettyCursors = value.toInt(&ok); if (!ok) { logWarn(QString("Invalid config value for pretty_cursors: '%1'. Must be 0 or 1.").arg(value)); } } else if (key == "map_sort_order") { QString sortOrder = value.toLower(); if (mapSortOrderReverseMap.contains(sortOrder)) { this->mapSortOrder = mapSortOrderReverseMap.value(sortOrder); } else { this->mapSortOrder = MapSortOrder::Group; logWarn(QString("Invalid config value for map_sort_order: '%1'. Must be 'group', 'area', or 'layout'.").arg(value)); } } else if (key == "main_window_geometry") { this->mainWindowGeometry = bytesFromString(value); } else if (key == "main_window_state") { this->mainWindowState = bytesFromString(value); } else if (key == "map_splitter_state") { this->mapSplitterState = bytesFromString(value); } else if (key == "main_splitter_state") { this->mainSplitterState = bytesFromString(value); } else if (key == "collision_opacity") { bool ok; this->collisionOpacity = qMax(0, qMin(100, value.toInt(&ok))); if (!ok) { logWarn(QString("Invalid config value for collision_opacity: '%1'. Must be an integer.").arg(value)); this->collisionOpacity = 50; } } else if (key == "tileset_editor_geometry") { this->tilesetEditorGeometry = bytesFromString(value); } else if (key == "tileset_editor_state") { this->tilesetEditorState = bytesFromString(value); } else if (key == "palette_editor_geometry") { this->paletteEditorGeometry = bytesFromString(value); } else if (key == "palette_editor_state") { this->paletteEditorState = bytesFromString(value); } else if (key == "region_map_editor_geometry") { this->regionMapEditorGeometry = bytesFromString(value); } else if (key == "region_map_editor_state") { this->regionMapEditorState = bytesFromString(value); } else if (key == "metatiles_zoom") { bool ok; this->metatilesZoom = qMax(10, qMin(100, value.toInt(&ok))); if (!ok) { logWarn(QString("Invalid config value for metatiles_zoom: '%1'. Must be an integer.").arg(value)); this->metatilesZoom = 30; } } else if (key == "show_player_view") { bool ok; this->showPlayerView = value.toInt(&ok); if (!ok) { logWarn(QString("Invalid config value for show_player_view: '%1'. Must be 0 or 1.").arg(value)); } } else if (key == "show_cursor_tile") { bool ok; this->showCursorTile = value.toInt(&ok); if (!ok) { logWarn(QString("Invalid config value for show_cursor_tile: '%1'. Must be 0 or 1.").arg(value)); } } else if (key == "monitor_files") { bool ok; this->monitorFiles = value.toInt(&ok); if (!ok) { logWarn(QString("Invalid config value for monitor_files: '%1'. Must be 0 or 1.").arg(value)); } } else if (key == "region_map_dimensions") { bool ok1, ok2; QStringList dims = value.split("x"); int w = dims[0].toInt(&ok1); int h = dims[1].toInt(&ok2); if (!ok1 || !ok2) { logWarn("Cannot parse region map dimensions. Using default values instead."); this->regionMapDimensions = QSize(32, 20); } else { this->regionMapDimensions = QSize(w, h); } } else if (key == "theme") { this->theme = value; } else { logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->getConfigFilepath()).arg(key)); } } QMap PorymapConfig::getKeyValueMap() { QMap map; map.insert("recent_project", this->recentProject); map.insert("recent_map", this->recentMap); map.insert("pretty_cursors", this->prettyCursors ? "1" : "0"); map.insert("map_sort_order", mapSortOrderMap.value(this->mapSortOrder)); map.insert("main_window_geometry", stringFromByteArray(this->mainWindowGeometry)); map.insert("main_window_state", stringFromByteArray(this->mainWindowState)); map.insert("map_splitter_state", stringFromByteArray(this->mapSplitterState)); map.insert("main_splitter_state", stringFromByteArray(this->mainSplitterState)); map.insert("tileset_editor_geometry", stringFromByteArray(this->tilesetEditorGeometry)); map.insert("tileset_editor_state", stringFromByteArray(this->tilesetEditorState)); map.insert("palette_editor_geometry", stringFromByteArray(this->paletteEditorGeometry)); map.insert("palette_editor_state", stringFromByteArray(this->paletteEditorState)); map.insert("region_map_editor_geometry", stringFromByteArray(this->regionMapEditorGeometry)); map.insert("region_map_editor_state", stringFromByteArray(this->regionMapEditorState)); map.insert("collision_opacity", QString("%1").arg(this->collisionOpacity)); map.insert("metatiles_zoom", QString("%1").arg(this->metatilesZoom)); map.insert("show_player_view", this->showPlayerView ? "1" : "0"); map.insert("show_cursor_tile", this->showCursorTile ? "1" : "0"); map.insert("monitor_files", this->monitorFiles ? "1" : "0"); map.insert("region_map_dimensions", QString("%1x%2").arg(this->regionMapDimensions.width()) .arg(this->regionMapDimensions.height())); map.insert("theme", this->theme); return map; } QString PorymapConfig::stringFromByteArray(QByteArray bytearray) { QString ret; for (auto ch : bytearray) { ret += QString::number(static_cast(ch)) + ":"; } return ret; } QByteArray PorymapConfig::bytesFromString(QString in) { QByteArray ba; QStringList split = in.split(":"); for (auto ch : split) { ba.append(static_cast(ch.toInt())); } return ba; } void PorymapConfig::setRecentProject(QString project) { this->recentProject = project; this->save(); } void PorymapConfig::setRecentMap(QString map) { this->recentMap = map; this->save(); } void PorymapConfig::setMapSortOrder(MapSortOrder order) { this->mapSortOrder = order; this->save(); } void PorymapConfig::setPrettyCursors(bool enabled) { this->prettyCursors = enabled; this->save(); } void PorymapConfig::setMonitorFiles(bool monitor) { this->monitorFiles = monitor; this->save(); } void PorymapConfig::setMainGeometry(QByteArray mainWindowGeometry_, QByteArray mainWindowState_, QByteArray mapSplitterState_, QByteArray mainSplitterState_) { this->mainWindowGeometry = mainWindowGeometry_; this->mainWindowState = mainWindowState_; this->mapSplitterState = mapSplitterState_; this->mainSplitterState = mainSplitterState_; this->save(); } void PorymapConfig::setTilesetEditorGeometry(QByteArray tilesetEditorGeometry_, QByteArray tilesetEditorState_) { this->tilesetEditorGeometry = tilesetEditorGeometry_; this->tilesetEditorState = tilesetEditorState_; this->save(); } void PorymapConfig::setPaletteEditorGeometry(QByteArray paletteEditorGeometry_, QByteArray paletteEditorState_) { this->paletteEditorGeometry = paletteEditorGeometry_; this->paletteEditorState = paletteEditorState_; this->save(); } void PorymapConfig::setRegionMapEditorGeometry(QByteArray regionMapEditorGeometry_, QByteArray regionMapEditorState_) { this->regionMapEditorGeometry = regionMapEditorGeometry_; this->regionMapEditorState = regionMapEditorState_; this->save(); } void PorymapConfig::setCollisionOpacity(int opacity) { this->collisionOpacity = opacity; // don't auto-save here because this can be called very frequently. } void PorymapConfig::setMetatilesZoom(int zoom) { this->metatilesZoom = zoom; // don't auto-save here because this can be called very frequently. } void PorymapConfig::setShowPlayerView(bool enabled) { this->showPlayerView = enabled; this->save(); } void PorymapConfig::setShowCursorTile(bool enabled) { this->showCursorTile = enabled; this->save(); } void PorymapConfig::setRegionMapDimensions(int width, int height) { this->regionMapDimensions = QSize(width, height); } void PorymapConfig::setTheme(QString theme) { this->theme = theme; } QString PorymapConfig::getRecentProject() { return this->recentProject; } QString PorymapConfig::getRecentMap() { return this->recentMap; } MapSortOrder PorymapConfig::getMapSortOrder() { return this->mapSortOrder; } bool PorymapConfig::getPrettyCursors() { return this->prettyCursors; } QMap PorymapConfig::getMainGeometry() { QMap geometry; geometry.insert("main_window_geometry", this->mainWindowGeometry); geometry.insert("main_window_state", this->mainWindowState); geometry.insert("map_splitter_state", this->mapSplitterState); geometry.insert("main_splitter_state", this->mainSplitterState); return geometry; } QMap PorymapConfig::getTilesetEditorGeometry() { QMap geometry; geometry.insert("tileset_editor_geometry", this->tilesetEditorGeometry); geometry.insert("tileset_editor_state", this->tilesetEditorState); return geometry; } QMap PorymapConfig::getPaletteEditorGeometry() { QMap geometry; geometry.insert("palette_editor_geometry", this->paletteEditorGeometry); geometry.insert("palette_editor_state", this->paletteEditorState); return geometry; } QMap PorymapConfig::getRegionMapEditorGeometry() { QMap geometry; geometry.insert("region_map_editor_geometry", this->regionMapEditorGeometry); geometry.insert("region_map_editor_state", this->regionMapEditorState); return geometry; } int PorymapConfig::getCollisionOpacity() { return this->collisionOpacity; } int PorymapConfig::getMetatilesZoom() { return this->metatilesZoom; } bool PorymapConfig::getShowPlayerView() { return this->showPlayerView; } bool PorymapConfig::getShowCursorTile() { return this->showCursorTile; } bool PorymapConfig::getMonitorFiles() { return this->monitorFiles; } QSize PorymapConfig::getRegionMapDimensions() { return this->regionMapDimensions; } QString PorymapConfig::getTheme() { return this->theme; } const QMap baseGameVersionMap = { {BaseGameVersion::pokeruby, "pokeruby"}, {BaseGameVersion::pokefirered, "pokefirered"}, {BaseGameVersion::pokeemerald, "pokeemerald"}, }; const QMap baseGameVersionReverseMap = { {"pokeruby", BaseGameVersion::pokeruby}, {"pokefirered", BaseGameVersion::pokefirered}, {"pokeemerald", BaseGameVersion::pokeemerald}, }; ProjectConfig projectConfig; QString ProjectConfig::getConfigFilepath() { // porymap config file is in the same directory as porymap itself. return QDir(this->projectDir).filePath("porymap.project.cfg"); } void ProjectConfig::parseConfigKeyValue(QString key, QString value) { if (key == "base_game_version") { QString baseGameVersion = value.toLower(); if (baseGameVersionReverseMap.contains(baseGameVersion)) { this->baseGameVersion = baseGameVersionReverseMap.value(baseGameVersion); } else { this->baseGameVersion = BaseGameVersion::pokeemerald; logWarn(QString("Invalid config value for base_game_version: '%1'. Must be 'pokeruby', 'pokefirered' or 'pokeemerald'.").arg(value)); } } else if (key == "use_encounter_json") { bool ok; this->useEncounterJson = value.toInt(&ok); if (!ok) { logWarn(QString("Invalid config value for use_encounter_json: '%1'. Must be 0 or 1.").arg(value)); } } else if (key == "use_poryscript") { bool ok; this->usePoryScript = value.toInt(&ok); if (!ok) { logWarn(QString("Invalid config value for use_poryscript: '%1'. Must be 0 or 1.").arg(value)); } } else if (key == "use_custom_border_size") { bool ok; this->useCustomBorderSize = value.toInt(&ok); if (!ok) { logWarn(QString("Invalid config value for use_custom_border_size: '%1'. Must be 0 or 1.").arg(value)); } } else if (key == "enable_event_weather_trigger") { bool ok; this->enableEventWeatherTrigger = value.toInt(&ok); if (!ok) { logWarn(QString("Invalid config value for enable_event_weather_trigger: '%1'. Must be 0 or 1.").arg(value)); } } else if (key == "enable_event_secret_base") { bool ok; this->enableEventSecretBase = value.toInt(&ok); if (!ok) { logWarn(QString("Invalid config value for enable_event_secret_base: '%1'. Must be 0 or 1.").arg(value)); } } else if (key == "enable_hidden_item_quantity") { bool ok; this->enableHiddenItemQuantity = value.toInt(&ok); if (!ok) { logWarn(QString("Invalid config value for enable_hidden_item_quantity: '%1'. Must be 0 or 1.").arg(value)); } } else if (key == "enable_hidden_item_requires_itemfinder") { bool ok; this->enableHiddenItemRequiresItemfinder = value.toInt(&ok); if (!ok) { logWarn(QString("Invalid config value for enable_hidden_item_requires_itemfinder: '%1'. Must be 0 or 1.").arg(value)); } } else if (key == "enable_heal_location_respawn_data") { bool ok; this->enableHealLocationRespawnData = value.toInt(&ok); if (!ok) { logWarn(QString("Invalid config value for enable_heal_location_respawn_data: '%1'. Must be 0 or 1.").arg(value)); } } else if (key == "enable_object_event_in_connection") { bool ok; this->enableObjectEventInConnection = value.toInt(&ok); if (!ok) { logWarn(QString("Invalid config value for enable_object_event_in_connection: '%1'. Must be 0 or 1.").arg(value)); } } else if (key == "enable_floor_number") { bool ok; this->enableFloorNumber = value.toInt(&ok); if (!ok) { logWarn(QString("Invalid config value for enable_floor_number: '%1'. Must be 0 or 1.").arg(value)); } } else if (key == "enable_triple_layer_metatiles") { bool ok; this->enableTripleLayerMetatiles = value.toInt(&ok); if (!ok) { logWarn(QString("Invalid config value for enable_triple_layer_metatiles: '%1'. Must be 0 or 1.").arg(value)); } } else if (key == "custom_scripts") { this->customScripts.clear(); QList paths = value.split(","); paths.removeDuplicates(); for (QString script : paths) { if (!script.isEmpty()) { this->customScripts.append(script); } } } else { logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->getConfigFilepath()).arg(key)); } readKeys.append(key); } void ProjectConfig::setUnreadKeys() { // Set game-version specific defaults for any config field that wasn't read bool isPokefirered = this->baseGameVersion == BaseGameVersion::pokefirered; if (!readKeys.contains("use_custom_border_size")) this->useCustomBorderSize = isPokefirered; if (!readKeys.contains("enable_event_weather_trigger")) this->enableEventWeatherTrigger = !isPokefirered; if (!readKeys.contains("enable_event_secret_base")) this->enableEventSecretBase = !isPokefirered; if (!readKeys.contains("enable_hidden_item_quantity")) this->enableHiddenItemQuantity = isPokefirered; if (!readKeys.contains("enable_hidden_item_requires_itemfinder")) this->enableHiddenItemRequiresItemfinder = isPokefirered; if (!readKeys.contains("enable_heal_location_respawn_data")) this->enableHealLocationRespawnData = isPokefirered; if (!readKeys.contains("enable_object_event_in_connection")) this->enableObjectEventInConnection = isPokefirered; if (!readKeys.contains("enable_floor_number")) this->enableFloorNumber = isPokefirered; } QMap ProjectConfig::getKeyValueMap() { QMap map; map.insert("base_game_version", baseGameVersionMap.value(this->baseGameVersion)); map.insert("use_encounter_json", QString::number(this->useEncounterJson)); map.insert("use_poryscript", QString::number(this->usePoryScript)); map.insert("use_custom_border_size", QString::number(this->useCustomBorderSize)); map.insert("enable_event_weather_trigger", QString::number(this->enableEventWeatherTrigger)); map.insert("enable_event_secret_base", QString::number(this->enableEventSecretBase)); map.insert("enable_hidden_item_quantity", QString::number(this->enableHiddenItemQuantity)); map.insert("enable_hidden_item_requires_itemfinder", QString::number(this->enableHiddenItemRequiresItemfinder)); map.insert("enable_heal_location_respawn_data", QString::number(this->enableHealLocationRespawnData)); map.insert("enable_object_event_in_connection", QString::number(this->enableObjectEventInConnection)); map.insert("enable_floor_number", QString::number(this->enableFloorNumber)); map.insert("enable_triple_layer_metatiles", QString::number(this->enableTripleLayerMetatiles)); map.insert("custom_scripts", this->customScripts.join(",")); return map; } void ProjectConfig::onNewConfigFileCreated() { QString dirName = QDir(this->projectDir).dirName().toLower(); if (baseGameVersionReverseMap.contains(dirName)) { this->baseGameVersion = baseGameVersionReverseMap.value(dirName); logInfo(QString("Auto-detected base_game_version as '%1'").arg(dirName)); } else { QDialog dialog(nullptr, Qt::WindowTitleHint); dialog.setWindowTitle("Project Configuration"); dialog.setWindowModality(Qt::NonModal); QFormLayout form(&dialog); QComboBox *baseGameVersionComboBox = new QComboBox(); baseGameVersionComboBox->addItem("pokeruby", BaseGameVersion::pokeruby); baseGameVersionComboBox->addItem("pokefirered", BaseGameVersion::pokefirered); baseGameVersionComboBox->addItem("pokeemerald", BaseGameVersion::pokeemerald); form.addRow(new QLabel("Game Version"), baseGameVersionComboBox); QDialogButtonBox buttonBox(QDialogButtonBox::Ok, Qt::Horizontal, &dialog); connect(&buttonBox, SIGNAL(accepted()), &dialog, SLOT(accept())); form.addRow(&buttonBox); if (dialog.exec() == QDialog::Accepted) { this->baseGameVersion = static_cast(baseGameVersionComboBox->currentData().toInt()); } } bool isPokefirered = this->baseGameVersion == BaseGameVersion::pokefirered; this->useCustomBorderSize = isPokefirered; this->enableEventWeatherTrigger = !isPokefirered; this->enableEventSecretBase = !isPokefirered; this->enableHiddenItemQuantity = isPokefirered; this->enableHiddenItemRequiresItemfinder = isPokefirered; this->enableHealLocationRespawnData = isPokefirered; this->enableObjectEventInConnection = isPokefirered; this->enableFloorNumber = isPokefirered; this->useEncounterJson = true; this->usePoryScript = false; this->enableTripleLayerMetatiles = false; this->customScripts.clear(); } void ProjectConfig::setProjectDir(QString projectDir) { this->projectDir = projectDir; } QString ProjectConfig::getProjectDir() { return this->projectDir; } void ProjectConfig::setBaseGameVersion(BaseGameVersion baseGameVersion) { this->baseGameVersion = baseGameVersion; this->save(); } BaseGameVersion ProjectConfig::getBaseGameVersion() { return this->baseGameVersion; } void ProjectConfig::setEncounterJsonActive(bool active) { this->useEncounterJson = active; this->save(); } bool ProjectConfig::getEncounterJsonActive() { return this->useEncounterJson; } void ProjectConfig::setUsePoryScript(bool usePoryScript) { this->usePoryScript = usePoryScript; this->save(); } bool ProjectConfig::getUsePoryScript() { return this->usePoryScript; } void ProjectConfig::setUseCustomBorderSize(bool enable) { this->useCustomBorderSize = enable; this->save(); } bool ProjectConfig::getUseCustomBorderSize() { return this->useCustomBorderSize; } void ProjectConfig::setEventWeatherTriggerEnabled(bool enable) { this->enableEventWeatherTrigger = enable; this->save(); } bool ProjectConfig::getEventWeatherTriggerEnabled() { return this->enableEventWeatherTrigger; } void ProjectConfig::setEventSecretBaseEnabled(bool enable) { this->enableEventSecretBase = enable; this->save(); } bool ProjectConfig::getEventSecretBaseEnabled() { return this->enableEventSecretBase; } void ProjectConfig::setHiddenItemQuantityEnabled(bool enable) { this->enableHiddenItemQuantity = enable; this->save(); } bool ProjectConfig::getHiddenItemQuantityEnabled() { return this->enableHiddenItemQuantity; } void ProjectConfig::setHiddenItemRequiresItemfinderEnabled(bool enable) { this->enableHiddenItemRequiresItemfinder = enable; this->save(); } bool ProjectConfig::getHiddenItemRequiresItemfinderEnabled() { return this->enableHiddenItemRequiresItemfinder; } void ProjectConfig::setHealLocationRespawnDataEnabled(bool enable) { this->enableHealLocationRespawnData = enable; this->save(); } bool ProjectConfig::getHealLocationRespawnDataEnabled() { return this->enableHealLocationRespawnData; } void ProjectConfig::setObjectEventInConnectionEnabled(bool enable) { this->enableObjectEventInConnection = enable; this->save(); } bool ProjectConfig::getObjectEventInConnectionEnabled() { return this->enableObjectEventInConnection; } void ProjectConfig::setFloorNumberEnabled(bool enable) { this->enableFloorNumber = enable; this->save(); } bool ProjectConfig::getFloorNumberEnabled() { return this->enableFloorNumber; } void ProjectConfig::setTripleLayerMetatilesEnabled(bool enable) { this->enableTripleLayerMetatiles = enable; this->save(); } bool ProjectConfig::getTripleLayerMetatilesEnabled() { return this->enableTripleLayerMetatiles; } void ProjectConfig::setCustomScripts(QList scripts) { this->customScripts = scripts; this->save(); } QList ProjectConfig::getCustomScripts() { return this->customScripts; } ShortcutsConfig shortcutsConfig; QString ShortcutsConfig::getConfigFilepath() { QString settingsPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); QDir dir(settingsPath); if (!dir.exists()) dir.mkpath(settingsPath); QString configPath = dir.absoluteFilePath("porymap.shortcuts.cfg"); return configPath; } void ShortcutsConfig::parseConfigKeyValue(QString key, QString value) { QStringList keySequences = value.split(' '); for (auto keySequence : keySequences) user_shortcuts.insert(key, keySequence); } QMap ShortcutsConfig::getKeyValueMap() { QMap map; for (auto cfg_key : user_shortcuts.uniqueKeys()) { auto keySequences = user_shortcuts.values(cfg_key); QStringList values; for (auto keySequence : keySequences) values.append(keySequence.toString()); QString value = values.join(' '); map.insert(cfg_key, value); } return map; } void ShortcutsConfig::setDefaultShortcuts(const QObjectList &objects) { storeShortcutsFromList(StoreType::Default, objects); save(); } void ShortcutsConfig::setDefaultShortcuts(const QMultiMap &objects_keySequences) { for (auto *object : objects_keySequences.uniqueKeys()) storeShortcuts(StoreType::Default, cfgKey(object), objects_keySequences.values(object)); save(); } QList ShortcutsConfig::defaultShortcuts(const QObject *object) const { return default_shortcuts.values(cfgKey(object)); } void ShortcutsConfig::setUserShortcuts(const QObjectList &objects) { storeShortcutsFromList(StoreType::User, objects); save(); } void ShortcutsConfig::setUserShortcuts(const QMultiMap &objects_keySequences) { for (auto *object : objects_keySequences.uniqueKeys()) storeShortcuts(StoreType::User, cfgKey(object), objects_keySequences.values(object)); save(); } QList ShortcutsConfig::userShortcuts(const QObject *object) const { return user_shortcuts.values(cfgKey(object)); } void ShortcutsConfig::storeShortcutsFromList(StoreType storeType, const QObjectList &objects) { for (const auto *object : objects) if (objectNameIsValid(object)) storeShortcuts(storeType, cfgKey(object), currentShortcuts(object)); } void ShortcutsConfig::storeShortcuts( StoreType storeType, const QString &cfgKey, const QList &keySequences) { bool storeUser = (storeType == User) || !user_shortcuts.contains(cfgKey); if (storeType == Default) default_shortcuts.remove(cfgKey); if (storeUser) user_shortcuts.remove(cfgKey); if (keySequences.isEmpty()) { if (storeType == Default) default_shortcuts.insert(cfgKey, QKeySequence()); if (storeUser) user_shortcuts.insert(cfgKey, QKeySequence()); } else { for (auto keySequence : keySequences) { if (storeType == Default) default_shortcuts.insert(cfgKey, keySequence); if (storeUser) user_shortcuts.insert(cfgKey, keySequence); } } } bool ShortcutsConfig::objectNameIsValid(const QObject *object) { // Qt internal action names start with "_q_" so we filter those out. return !object->objectName().isEmpty() && !object->objectName().startsWith("_q_"); } /* Creates a config key from the object's name prepended with the parent * window's object name, and converts camelCase to snake_case. */ QString ShortcutsConfig::cfgKey(const QObject *object) const { auto cfg_key = QString(); auto *parentWidget = static_cast(object->parent()); if (parentWidget) cfg_key = parentWidget->window()->objectName() + '_'; cfg_key += object->objectName(); QRegularExpression re("[A-Z]"); int i = cfg_key.indexOf(re, 1); while (i != -1) { if (cfg_key.at(i - 1) != '_') cfg_key.insert(i++, '_'); i = cfg_key.indexOf(re, i + 1); } return cfg_key.toLower(); } QList ShortcutsConfig::currentShortcuts(const QObject *object) const { if (object->inherits("QAction")) { const auto *action = qobject_cast(object); return action->shortcuts(); } else if (object->inherits("QAbstractButton")) { const auto *button = qobject_cast(object); return { button->shortcut() }; } else if (object->inherits("Shortcut")) { const auto *shortcut = qobject_cast(object); return shortcut->keys(); } else if (object->inherits("QShortcut")) { const auto *qshortcut = qobject_cast(object); return { qshortcut->key() }; } else if (object->property("shortcut").isValid()) { return { object->property("shortcut").value() }; } else { return { }; } }