From b2d9d890ae07a4a7cf6e176cab7643dd6d321dc7 Mon Sep 17 00:00:00 2001 From: Hartmnt Date: Thu, 2 Jan 2025 20:50:58 +0000 Subject: [PATCH] FEAT(client): profiles --- src/mumble/JSONSerialization.cpp | 15 ++++++ src/mumble/JSONSerialization.h | 2 + src/mumble/MainWindow.cpp | 2 +- src/mumble/Settings.cpp | 81 ++++++++++++++++++++++++-------- src/mumble/Settings.h | 10 +++- src/mumble/SettingsKeys.h | 4 ++ src/mumble/SettingsMacros.h | 9 ++++ src/mumble/main.cpp | 2 +- 8 files changed, 103 insertions(+), 22 deletions(-) diff --git a/src/mumble/JSONSerialization.cpp b/src/mumble/JSONSerialization.cpp index d6050365161..c94d4f1ca3d 100644 --- a/src/mumble/JSONSerialization.cpp +++ b/src/mumble/JSONSerialization.cpp @@ -242,6 +242,21 @@ void from_json(const nlohmann::json &j, OverlaySettings &settings) { } +void to_json(nlohmann::json &j, const Profiles &settings) { +#define PROCESS(category, key, variable) save(j, SettingsKeys::key, settings.variable); + + PROCESS_ALL_PROFILE_SETTINGS + +#undef PROCESS +} + +void from_json(const nlohmann::json &j, Profiles &settings) { +#define PROCESS(category, key, variable) load(j, SettingsKeys::key, settings.variable, settings.variable, true); + + PROCESS_ALL_PROFILE_SETTINGS + +#undef PROCESS +} void to_json(nlohmann::json &j, const QString &string) { j = string.toStdString(); diff --git a/src/mumble/JSONSerialization.h b/src/mumble/JSONSerialization.h index 198317ca82e..430ea9e83bb 100644 --- a/src/mumble/JSONSerialization.h +++ b/src/mumble/JSONSerialization.h @@ -121,6 +121,8 @@ void to_json(nlohmann::json &j, const Settings &settings); void from_json(const nlohmann::json &j, Settings &settings); void to_json(nlohmann::json &j, const OverlaySettings &settings); void from_json(const nlohmann::json &j, OverlaySettings &settings); +void to_json(nlohmann::json &j, const Profiles &settings); +void from_json(const nlohmann::json &j, Profiles &settings); void to_json(nlohmann::json &j, const QString &string); void from_json(const nlohmann::json &j, QString &string); diff --git a/src/mumble/MainWindow.cpp b/src/mumble/MainWindow.cpp index dee417bd85a..eba10686480 100644 --- a/src/mumble/MainWindow.cpp +++ b/src/mumble/MainWindow.cpp @@ -1224,7 +1224,7 @@ void MainWindow::openUrl(const QUrl &url) { try { Settings newSettings; - newSettings.load(f.fileName()); + newSettings.loadFile(f.fileName()); std::swap(newSettings, Global::get().s); diff --git a/src/mumble/Settings.cpp b/src/mumble/Settings.cpp index 4a02bc0908a..068d71fbb99 100644 --- a/src/mumble/Settings.cpp +++ b/src/mumble/Settings.cpp @@ -151,7 +151,24 @@ void Settings::save(const QString &path) const { throw std::runtime_error("Expected settings file to have \".json\" extension"); } - nlohmann::json settingsJSON = *this; + nlohmann::json settingsJSON = profiles; + + qWarning() << "settingsJSON " << QString::fromStdString(settingsJSON.dump(4)); + + nlohmann::json profilesJSON = settingsJSON.at(SettingsKeys::PROFILES); + + qWarning() << "profilesJSON " << QString::fromStdString(profilesJSON.dump(4)); + + nlohmann::json activeProfileJSON = *this; + + qWarning() << "activeProfile " << QString::fromStdString(activeProfileJSON.dump(4)); + + profilesJSON.erase(profiles.activeProfileName.toStdString()); + profilesJSON.push_back({ profiles.activeProfileName, activeProfileJSON }); + + qWarning() << "profilesJSON (after) " << QString::fromStdString(profilesJSON.dump(4)); + + qWarning() << "settingsJSON (after) " << QString::fromStdString(settingsJSON.dump(4)); QFile tmpFile(QString::fromLatin1("%1/mumble_settings.json.tmp") .arg(QStandardPaths::writableLocation(QStandardPaths::TempLocation))); @@ -216,7 +233,7 @@ void Settings::save() const { } } -void Settings::load(const QString &path) { +void Settings::loadFile(const QString &path) { if (path.endsWith(QLatin1String(BACKUP_FILE_EXTENSION))) { // Trim away the backup extension settingsLocation = path.left(path.size() - static_cast< int >(std::strlen(BACKUP_FILE_EXTENSION))); @@ -226,11 +243,29 @@ void Settings::load(const QString &path) { std::ifstream stream(Mumble::QtUtils::qstring_to_path(path)); - nlohmann::json settingsJSON; try { - stream >> settingsJSON; + nlohmann::json profilesJSON; + stream >> profilesJSON; - settingsJSON.get_to(*this); + if (profilesJSON.contains(SettingsKeys::ACTIVE_PROFILE)) { + profilesJSON.get_to(profiles); + + if (profiles.allProfiles.contains(profiles.activeProfileName)) { + *this = profiles.allProfiles[profiles.activeProfileName]; + } else if (profiles.allProfiles.contains("default")) { + qWarning("Failed to load profile '%s'. Falling back to 'default'...", qUtf8Printable(profiles.activeProfileName)); + *this = profiles.allProfiles["default"]; + } else { + qWarning("Failed to 'default' profile. Trying to load settings file as is..."); + profilesJSON.get_to(*this); + } + } else { + // The file does not contain the key "SettingsKeys::ACTIVE_PROFILE" + // We assume the JSON file does not contain any profiles, because it is + // old. We load that instead and convert it to the "default" profile. + qWarning("Migrating settings file to 'default' profile"); + profilesJSON.get_to(*this); + } if (!mumbleQuitNormally) { // These settings were saved without Mumble quitting normally afterwards. In order to prevent loading @@ -254,7 +289,7 @@ void Settings::load(const QString &path) { if (msgBox.exec() == QMessageBox::Yes) { // Load the backup instead qWarning() << "Loading backup settings from" << backupPath; - load(backupPath); + loadFile(backupPath); } } } else { @@ -274,12 +309,13 @@ void Settings::load(const QString &path) { msgBox.exec(); } } + } catch (const nlohmann::json::parse_error &e) { qWarning() << "Failed to load settings from" << path << "due to invalid format: " << e.what(); if (!path.endsWith(QLatin1String(BACKUP_FILE_EXTENSION)) && QFileInfo(path + BACKUP_FILE_EXTENSION).exists()) { qWarning() << "Falling back to backup" << path + BACKUP_FILE_EXTENSION; - load(path + BACKUP_FILE_EXTENSION); + loadFile(path + BACKUP_FILE_EXTENSION); } } @@ -295,11 +331,11 @@ void Settings::load() { if (foundExisting) { // If we found a regular settings file, then use that and be done with it qInfo() << "Loading settings from" << settingsPath; - load(settingsPath); + loadFile(settingsPath); } else if (QFileInfo(settingsPath + BACKUP_FILE_EXTENSION).exists()) { // Load backup settings instead qInfo() << "Loading backup settings from" << settingsPath + BACKUP_FILE_EXTENSION; - load(settingsPath + BACKUP_FILE_EXTENSION); + loadFile(settingsPath + BACKUP_FILE_EXTENSION); } else { // Otherwise check for a legacy settings file and if that is found, load settings from there QString legacySettingsPath = findSettingsLocation(true, &foundExisting); @@ -1123,8 +1159,8 @@ void Settings::legacyLoad(const QString &path) { settings_ptr->endGroup(); - // This field previously populated Settings::uiUpdateCounter, which no longer exists. We require it though, in order - // to determine whether we have to perform some migration work. + // This field previously populated Settings::uiUpdateCounter, which no longer exists. We require it though, in + // order to determine whether we have to perform some migration work. unsigned int configVersion = 0; LOAD(configVersion, "lastupdate"); audioWizardShown = true; @@ -1222,6 +1258,7 @@ void Settings::verifySettingsKeys() const { #define INTERMEDIATE_OPERATION categoryNames.push_back(currentCategoryName); PROCESS_ALL_SETTINGS_WITH_INTERMEDIATE_OPERATION PROCESS_ALL_OVERLAY_SETTINGS_WITH_INTERMEDIATE_OPERATION + PROCESS_ALL_PROFILE_SETTINGS_WITH_INTERMEDIATE_OPERATION // Assert that all entries in categoryNames are unique std::sort(categoryNames.begin(), categoryNames.end()); @@ -1241,6 +1278,7 @@ void Settings::verifySettingsKeys() const { keyNames.clear(); PROCESS_ALL_SETTINGS_WITH_INTERMEDIATE_OPERATION PROCESS_ALL_OVERLAY_SETTINGS_WITH_INTERMEDIATE_OPERATION + PROCESS_ALL_PROFILE_SETTINGS_WITH_INTERMEDIATE_OPERATION #undef PROCESS #undef INTERMEDIATE_OPERATION @@ -1255,15 +1293,20 @@ void Settings::verifySettingsKeys() const { PROCESS_ALL_OVERLAY_SETTINGS std::sort(variableNames.begin(), variableNames.end()); assert(std::unique(variableNames.begin(), variableNames.end()) == variableNames.end()); + variableNames.clear(); + + PROCESS_ALL_PROFILE_SETTINGS + std::sort(variableNames.begin(), variableNames.end()); + assert(std::unique(variableNames.begin(), variableNames.end()) == variableNames.end()); #undef PROCESS } QString Settings::findSettingsLocation(bool legacy, bool *foundExistingFile) const { // In order to make sure we'll find (mostly legacy) settings files, even if they end up being in a slightly - // different dir than we currently expect, we construct a search path list that we'll traverse while searching for - // the settings file. In case we find a suitable settings file within the search path, then we'll continue to use - // this path instead of creating a new one (in the location that we currently think is best to use). + // different dir than we currently expect, we construct a search path list that we'll traverse while searching + // for the settings file. In case we find a suitable settings file within the search path, then we'll continue + // to use this path instead of creating a new one (in the location that we currently think is best to use). QStringList paths; paths << QCoreApplication::instance()->applicationDirPath(); paths << QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); @@ -1300,11 +1343,11 @@ QString Settings::findSettingsLocation(bool legacy, bool *foundExistingFile) con chosenPath = QString::fromLatin1("%1/%2") .arg(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation)) .arg(settingsFileNames[0]); - // Note: QStandardPaths::AppConfigLocation will return a directory of the style // where - // is the path to the general config related directory on the respective OS, is the name of our - // organization and is our application's name. In our case (at the time of writing this) = - // = Mumble, leading to a doubly nested "Mumble" directory. This should only be a cosmetic issue - // though. + // Note: QStandardPaths::AppConfigLocation will return a directory of the style // + // where is the path to the general config related directory on the respective OS, is the name + // of our organization and is our application's name. In our case (at the time of writing + // this) = = Mumble, leading to a doubly nested "Mumble" directory. This should only be + // a cosmetic issue though. } return chosenPath; diff --git a/src/mumble/Settings.h b/src/mumble/Settings.h index ede2f449cc4..833465a1fa1 100644 --- a/src/mumble/Settings.h +++ b/src/mumble/Settings.h @@ -35,6 +35,7 @@ class QSettings; struct MigratedPath; +struct Settings; // Global helper classes to spread variables around across threads // especially helpful to initialize things like the stored @@ -184,6 +185,11 @@ struct OverlaySettings { friend bool operator!=(const OverlaySettings &lhs, const OverlaySettings &rhs); }; +struct Profiles { + QString activeProfileName = QStringLiteral("default"); + QMap< QString, Settings > allProfiles = {}; +}; + struct Settings { enum AudioTransmit { Continuous, VAD, PushToTalk }; enum VADSource { Amplitude, SignalToNoise }; @@ -558,6 +564,8 @@ struct Settings { /// A flag used in order to determine whether or not to offer loading the setting's backup file instead bool mumbleQuitNormally = false; + Profiles profiles; + bool doEcho() const; bool doPositionalAudio() const; @@ -566,8 +574,8 @@ struct Settings { void save(const QString &path) const; void save() const; - void load(const QString &path); void load(); + void loadFile(const QString &path); void legacyLoad(const QString &path = {}); diff --git a/src/mumble/SettingsKeys.h b/src/mumble/SettingsKeys.h index 3e1f59e523a..395871e33dd 100644 --- a/src/mumble/SettingsKeys.h +++ b/src/mumble/SettingsKeys.h @@ -34,6 +34,10 @@ namespace SettingsKeys { * loading settings. */ +// Meta +const SettingsKey ACTIVE_PROFILE = { "active_profile" }; +const SettingsKey PROFILES = { "profiles" }; + // Audio settings const SettingsKey UNMUTE_ON_UNDEAF_KEY = { "unmute_on_undeaf" }; const SettingsKey MUTE_KEY = { "mute" }; diff --git a/src/mumble/SettingsMacros.h b/src/mumble/SettingsMacros.h index dfcbee89974..1e119f2118a 100644 --- a/src/mumble/SettingsMacros.h +++ b/src/mumble/SettingsMacros.h @@ -11,6 +11,10 @@ // Mappings between SettingsKey objects and the corresponding fields in the Settings struct +#define PROFILE_SETTINGS \ + PROCESS(profiles, ACTIVE_PROFILE, activeProfileName) \ + PROCESS(profiles, PROFILES, allProfiles) + #define MISC_SETTINGS \ PROCESS(misc, DATABASE_LOCATION_KEY, qsDatabaseLocation) \ PROCESS(misc, IMAGE_DIRECTORY_KEY, qsImagePath) \ @@ -342,6 +346,7 @@ #define PROCESS_ALL_OVERLAY_SETTINGS OVERLAY_SETTINGS +#define PROCESS_ALL_PROFILE_SETTINGS PROFILE_SETTINGS #define PROCESS_ALL_SETTINGS_WITH_INTERMEDIATE_OPERATION \ MISC_SETTINGS \ @@ -391,5 +396,9 @@ OVERLAY_SETTINGS \ INTERMEDIATE_OPERATION +#define PROCESS_ALL_PROFILE_SETTINGS_WITH_INTERMEDIATE_OPERATION \ + PROFILE_SETTINGS \ + INTERMEDIATE_OPERATION + #endif // MUMBLE_MUMBLE_SETTINGS_MACROS_H_ diff --git a/src/mumble/main.cpp b/src/mumble/main.cpp index 63edcfe29d2..9d97f5110d9 100644 --- a/src/mumble/main.cpp +++ b/src/mumble/main.cpp @@ -549,7 +549,7 @@ int main(int argc, char **argv) { if (settingsFile.isEmpty()) { Global::get().s.load(); } else { - Global::get().s.load(settingsFile); + Global::get().s.loadFile(settingsFile); } if (!Global::get().migratedDBPath.isEmpty()) { // We have migrated the DB to a new location. Make sure that the settings hold the correct (new) path and that