diff --git a/src/mumble/ConfigDialog.cpp b/src/mumble/ConfigDialog.cpp index 3cc4ebbce5..18992d7449 100644 --- a/src/mumble/ConfigDialog.cpp +++ b/src/mumble/ConfigDialog.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -84,6 +85,7 @@ ConfigDialog::ConfigDialog(QWidget *p) : QDialog(p) { restoreGeometry(Global::get().s.qbaConfigGeometry); } + updateProfileList(); updateTabOrder(); qlwIcons->setFocus(); } @@ -205,6 +207,137 @@ void ConfigDialog::on_qlwIcons_currentItemChanged(QListWidgetItem *current, QLis } } +void ConfigDialog::updateProfileList() { + // Prevent changing the profile unintentionally while filling the ComboBox + const QSignalBlocker blocker(qcbProfiles); + + qcbProfiles->clear(); + + // Always sort the default profile before anything else + qcbProfiles->addItem(Profiles::s_default_profile_name); + + QStringList profiles = Global::get().profiles.allProfiles.keys(); + profiles.sort(); + for (const QString &profile : profiles) { + if (profile == Profiles::s_default_profile_name) { + continue; + } + qcbProfiles->addItem(profile); + } + + qcbProfiles->setCurrentIndex(qcbProfiles->findText(Global::get().profiles.activeProfileName)); + + bool isDefault = qcbProfiles->currentText() == Profiles::s_default_profile_name; + qpbProfileRename->setEnabled(!isDefault); + qpbProfileDelete->setEnabled(!isDefault); +} + +void ConfigDialog::switchProfile(const QString &newProfile, bool saveActiveProfile) { + Profiles &profiles = Global::get().profiles; + + if (saveActiveProfile) { + profiles.allProfiles[profiles.activeProfileName] = Global::get().s; + } + Global::get().s.loadProfile(newProfile); + s = Global::get().s; + for (ConfigWidget *cw : s_existingWidgets.values()) { + cw->load(s); + } + + updateProfileList(); +} + +void ConfigDialog::on_qcbProfiles_currentIndexChanged(int) { + QString selectedProfile = qcbProfiles->currentText(); + + Profiles &profiles = Global::get().profiles; + + if (selectedProfile == profiles.activeProfileName) { + return; + } + + if (!profiles.allProfiles.contains(selectedProfile)) { + return; + } + + switchProfile(selectedProfile, true); +} + +void ConfigDialog::on_qpbProfileAdd_clicked() { + bool ok; + QString profileName = + QInputDialog::getText(this, tr("Creating settings profile"), tr("Enter new settings profile name"), + QLineEdit::Normal, Global::get().profiles.activeProfileName, &ok); + + if (!ok || profileName.isEmpty()) { + return; + } + + if (Global::get().profiles.allProfiles.contains(profileName)) { + QMessageBox::critical(this, tr("Creating settings profile"), + tr("A settings profile with this name already exists")); + return; + } + + // Instead of "resetting" when creating a new profile, use the currently + // (possibly not applied) settings for the new profile + Profiles &profiles = Global::get().profiles; + profiles.allProfiles[profiles.activeProfileName] = Global::get().s; + apply(); + profiles.allProfiles.insert(profileName, Global::get().s); + switchProfile(profileName, false); +} + +void ConfigDialog::on_qpbProfileRename_clicked() { + QString oldProfileName = qcbProfiles->currentText(); + + if (oldProfileName == Profiles::s_default_profile_name) { + return; + } + + bool ok; + QString profileName = + QInputDialog::getText(this, tr("Renaming settings profile"), tr("Enter new settings profile name"), + QLineEdit::Normal, oldProfileName, &ok); + + if (!ok || profileName.isEmpty()) { + return; + } + + if (Global::get().profiles.allProfiles.contains(profileName)) { + QMessageBox::critical(this, tr("Renaming settings profile"), + tr("A settings profile with this name already exists")); + return; + } + + Global::get().profiles.allProfiles.insert(profileName, Global::get().s); + Global::get().profiles.allProfiles.remove(oldProfileName); + switchProfile(profileName, false); +} + +void ConfigDialog::on_qpbProfileDelete_clicked() { + QString oldProfileName = qcbProfiles->currentText(); + + if (oldProfileName == Profiles::s_default_profile_name) { + return; + } + + if (!Global::get().profiles.allProfiles.contains(oldProfileName)) { + return; + } + + QMessageBox::StandardButton confirmation = QMessageBox::question( + this, tr("Delete settings profile"), + tr("Are you sure you want to permanently delete settings profile '%1'").arg(oldProfileName)); + + if (confirmation != QMessageBox::Yes) { + return; + } + + Global::get().profiles.allProfiles.remove(oldProfileName); + switchProfile(Profiles::s_default_profile_name, false); +} + void ConfigDialog::updateTabOrder() { QPushButton *okButton = dialogButtonBox->button(QDialogButtonBox::Ok); QPushButton *cancelButton = dialogButtonBox->button(QDialogButtonBox::Cancel); @@ -228,7 +361,13 @@ void ConfigDialog::updateTabOrder() { } setTabOrder(cancelButton, okButton); - setTabOrder(okButton, qlwIcons); + setTabOrder(okButton, qcbProfiles); + + setTabOrder(qcbProfiles, qpbProfileAdd); + setTabOrder(qpbProfileAdd, qpbProfileRename); + setTabOrder(qpbProfileRename, qpbProfileDelete); + setTabOrder(qpbProfileDelete, qlwIcons); + setTabOrder(qlwIcons, contentFocusWidget); if (resetButton && restoreButton && restoreAllButton) { setTabOrder(contentFocusWidget, resetButton); diff --git a/src/mumble/ConfigDialog.h b/src/mumble/ConfigDialog.h index 5613482803..379a8f63bd 100644 --- a/src/mumble/ConfigDialog.h +++ b/src/mumble/ConfigDialog.h @@ -19,6 +19,8 @@ class ConfigDialog : public QDialog, public Ui::ConfigDialog { Q_DISABLE_COPY(ConfigDialog) void updateTabOrder(); + void updateProfileList(); + void switchProfile(const QString &newProfile, bool saveActiveProfile); protected: static QMutex s_existingWidgetsMutex; @@ -46,6 +48,10 @@ public slots: void on_pageButtonBox_clicked(QAbstractButton *); void on_dialogButtonBox_clicked(QAbstractButton *); void on_qlwIcons_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous); + void on_qcbProfiles_currentIndexChanged(int); + void on_qpbProfileAdd_clicked(); + void on_qpbProfileRename_clicked(); + void on_qpbProfileDelete_clicked(); void apply(); void accept() Q_DECL_OVERRIDE; }; diff --git a/src/mumble/ConfigDialog.ui b/src/mumble/ConfigDialog.ui index 2cbe3f2c0e..81ff3eafb5 100644 --- a/src/mumble/ConfigDialog.ui +++ b/src/mumble/ConfigDialog.ui @@ -35,34 +35,79 @@ - QListView::Adjust + QListView::ResizeMode::Adjust - QListView::Batched + QListView::LayoutMode::Batched true - + + + + Profile + + + + 0 + + + 9 + + + 0 + + + 0 + + + + + + + + Add + + + + + + + Rename + + + + + + + Delete + + + + + + + - Qt::Horizontal + Qt::Orientation::Horizontal - + - Qt::Horizontal + Qt::Orientation::Horizontal - 474 - 22 + 40 + 20 @@ -70,16 +115,16 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok + QDialogButtonBox::StandardButton::Apply|QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok - + diff --git a/src/mumble/Global.h b/src/mumble/Global.h index 012ee826f7..3f6ddf4f7d 100644 --- a/src/mumble/Global.h +++ b/src/mumble/Global.h @@ -51,6 +51,7 @@ struct Global Q_DECL_FINAL { MainWindow *mw; Settings s; + Profiles profiles; boost::shared_ptr< ServerHandler > sh; boost::shared_ptr< AudioInput > ai; boost::shared_ptr< AudioOutput > ao; diff --git a/src/mumble/JSONSerialization.cpp b/src/mumble/JSONSerialization.cpp index d605036516..c94d4f1ca3 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 198317ca82..430ea9e83b 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/Settings.cpp b/src/mumble/Settings.cpp index 4a02bc0908..57c9ddd3ef 100644 --- a/src/mumble/Settings.cpp +++ b/src/mumble/Settings.cpp @@ -151,7 +151,16 @@ void Settings::save(const QString &path) const { throw std::runtime_error("Expected settings file to have \".json\" extension"); } - nlohmann::json settingsJSON = *this; + Profiles &profiles = Global::get().profiles; + nlohmann::json settingsJSON = profiles; + nlohmann::json &profilesJSON = settingsJSON.at(SettingsKeys::PROFILES); + nlohmann::json activeProfileJSON = *this; + + qInfo("Saving settings profile '%s'", qUtf8Printable(profiles.activeProfileName)); + + // Replace the settings loaded from disk with the current (possibly modified) settings + profilesJSON.erase(profiles.activeProfileName.toStdString()); + profilesJSON.push_back({ profiles.activeProfileName, activeProfileJSON }); QFile tmpFile(QString::fromLatin1("%1/mumble_settings.json.tmp") .arg(QStandardPaths::writableLocation(QStandardPaths::TempLocation))); @@ -216,6 +225,33 @@ void Settings::save() const { } } +void Settings::loadProfile(std::optional< QString > requestedProfile) { + Profiles &profiles = Global::get().profiles; + + QString profileName; + if (!requestedProfile) { + profileName = profiles.activeProfileName; + } else { + profileName = requestedProfile.value(); + } + + if (!profiles.allProfiles.contains(profileName)) { + qWarning("Failed to load settings profile '%s'. Falling back to '%s'...", qUtf8Printable(profileName), + qUtf8Printable(Profiles::s_default_profile_name)); + profileName = Profiles::s_default_profile_name; + + if (!profiles.allProfiles.contains(profileName)) { + qWarning("Failed to load fallback settings profile '%s'", qUtf8Printable(Profiles::s_default_profile_name)); + return; + } + } + + qInfo("Loading settings profile '%s'", qUtf8Printable(profileName)); + + *this = profiles.allProfiles[profileName]; + profiles.activeProfileName = profileName; +} + void Settings::load(const QString &path) { if (path.endsWith(QLatin1String(BACKUP_FILE_EXTENSION))) { // Trim away the backup extension @@ -230,7 +266,16 @@ void Settings::load(const QString &path) { try { stream >> settingsJSON; - settingsJSON.get_to(*this); + if (settingsJSON.contains(SettingsKeys::ACTIVE_PROFILE)) { + settingsJSON.get_to(Global::get().profiles); + loadProfile(); + } 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 the file raw instead and convert it to the s_default_profile_name profile. + qWarning("Migrating settings file to 'default' profile"); + settingsJSON.get_to(*this); + } if (!mumbleQuitNormally) { // These settings were saved without Mumble quitting normally afterwards. In order to prevent loading @@ -401,6 +446,8 @@ std::size_t qHash(const ChannelTarget &target) { return qHash(target.channelID); } +const QString Profiles::s_default_profile_name = QLatin1String("default"); + const QString Settings::cqsDefaultPushClickOn = QLatin1String(":/on.ogg"); const QString Settings::cqsDefaultPushClickOff = QLatin1String(":/off.ogg"); @@ -1222,6 +1269,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 +1289,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,6 +1304,11 @@ 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 } diff --git a/src/mumble/Settings.h b/src/mumble/Settings.h index ede2f449cc..473c28841b 100644 --- a/src/mumble/Settings.h +++ b/src/mumble/Settings.h @@ -32,9 +32,11 @@ #include #include +#include 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 +186,13 @@ struct OverlaySettings { friend bool operator!=(const OverlaySettings &lhs, const OverlaySettings &rhs); }; +struct Profiles { + static const QString s_default_profile_name; + + QString activeProfileName = s_default_profile_name; + QMap< QString, Settings > allProfiles = {}; +}; + struct Settings { enum AudioTransmit { Continuous, VAD, PushToTalk }; enum VADSource { Amplitude, SignalToNoise }; @@ -566,6 +575,7 @@ struct Settings { void save(const QString &path) const; void save() const; + void loadProfile(std::optional< QString > requestedProfile = {}); void load(const QString &path); void load(); diff --git a/src/mumble/SettingsKeys.h b/src/mumble/SettingsKeys.h index 3e1f59e523..395871e33d 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 dfcbee8997..1e119f2118 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_