/** * @file MultiSettings.cpp * @brief Implementation of multi-settings dialog */ #include "MultiSettings.h" #include "JS8MessageBox.h" #include "JS8_Include/SettingsGroup.h" #include "JS8_Include/pimpl_impl.h" #include "qt_helpers.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { char const *default_string = QT_TRANSLATE_NOOP("MultiSettings", "Default"); char const *multi_settings_root_group = "MultiSettings"; char const *multi_settings_current_group_key = "CurrentMultiSettingsConfiguration"; char const *multi_settings_current_name_key = "CurrentName"; char const *multi_settings_place_holder_key = "MultiSettingsPlaceHolder"; QString unescape_ampersands(QString s) { return s.replace("&&", "&"); } // calculate a useable and unique settings file path QString settings_path() { auto const &config_directory = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation); QDir config_path{ config_directory}; // will be "." if config_directory is empty if (!config_path.mkpath(".")) { throw std::runtime_error{"Cannot find a usable configuration path \"" + config_path.path().toStdString() + '"'}; } return config_path.absoluteFilePath(QApplication::applicationName() + ".ini"); } // // Dialog to get a valid new configuration name // class NameDialog final : public QDialog { Q_OBJECT public: explicit NameDialog(QString const ¤t_name, QStringList const ¤t_names, QWidget *parent = nullptr) : QDialog{parent} { setWindowTitle(tr("New Configuration Name")); auto form_layout = new QFormLayout(); form_layout->addRow(tr("Old name:"), &old_name_label_); old_name_label_.setText(current_name); form_layout->addRow(tr("&New name:"), &name_line_edit_); auto main_layout = new QVBoxLayout(this); main_layout->addLayout(form_layout); auto button_box = new QDialogButtonBox{QDialogButtonBox::Ok | QDialogButtonBox::Cancel}; button_box->button(QDialogButtonBox::Ok)->setEnabled(false); main_layout->addWidget(button_box); auto name_validator = new QRegularExpressionValidator{ QRegularExpression{R"([^/\\]+)"}, this}; name_line_edit_.setValidator(name_validator); connect(&name_line_edit_, &QLineEdit::textChanged, [current_names, button_box](QString const &name) { bool valid{!current_names.contains(name.trimmed())}; button_box->button(QDialogButtonBox::Ok)->setEnabled(valid); }); connect(button_box, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject); } QString new_name() const { return name_line_edit_.text().trimmed(); } private: QLabel old_name_label_; QLineEdit name_line_edit_; }; // // Dialog to get a valid new existing name // class ExistingNameDialog final : public QDialog { Q_OBJECT public: explicit ExistingNameDialog(QStringList const ¤t_names, QWidget *parent = nullptr) : QDialog{parent} { setWindowTitle(tr("Configuration to Clone From")); name_combo_box_.addItems(current_names); auto form_layout = new QFormLayout(); form_layout->addRow(tr("&Source Configuration Name:"), &name_combo_box_); auto main_layout = new QVBoxLayout(this); main_layout->addLayout(form_layout); auto button_box = new QDialogButtonBox{QDialogButtonBox::Ok | QDialogButtonBox::Cancel}; main_layout->addWidget(button_box); connect(button_box, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject); } QString name() const { return name_combo_box_.currentText(); } private: QComboBox name_combo_box_; }; } // namespace class MultiSettings::impl final : public QObject { Q_OBJECT public: explicit impl(MultiSettings const *parent, QString const &config_name); bool reposition(); void create_menu_actions(QMainWindow *main_window, QMenu *menu); bool exit(); QSettings settings_; QString current_; // switch to this configuration void select_configuration(QString const &target_name); private: using Dictionary = QVariantMap; // create a configuration maintenance sub menu QMenu *create_sub_menu(QMenu *parent, QString const &menu_title, QActionGroup * = nullptr); // extract all settings from the current QSettings group Dictionary get_settings() const; // write the settings values from the dictionary to the current group void load_from(Dictionary const &, bool add_placeholder = true); // clone this configuration void clone_configuration(QMenu *, QMenu const *); // update this configuration from another void clone_into_configuration(QMenu const *); // reset configuration to default values void reset_configuration(QMenu const *); // change configuration name void rename_configuration(QMenu *); // remove a configuration void delete_configuration(QMenu *); // action to take on restart enum class RepositionType { unchanged, replace, save_and_replace }; void restart(RepositionType); MultiSettings const *parent_; // required for emitting signals QMainWindow *main_window_; bool name_change_emit_pending_; // delayed until menu built QFont original_font_; RepositionType reposition_type_; Dictionary new_settings_; bool exit_flag_; // false means loop around with new // configuration QActionGroup *configurations_group_; }; #include "MultiSettings.moc" #include "moc_MultiSettings.cpp" MultiSettings::MultiSettings(QString const &config_name) : m_{this, config_name} {} MultiSettings::~MultiSettings() {} QSettings *MultiSettings::settings() { return &m_->settings_; } QVariant MultiSettings::common_value(QString const &key, QVariant const &default_value) const { QVariant value; QSettings *mutable_settings{const_cast(&m_->settings_)}; auto const ¤t_group = mutable_settings->group(); if (current_group.size()) mutable_settings->endGroup(); { SettingsGroup alternatives{mutable_settings, multi_settings_root_group}; value = mutable_settings->value(key, default_value); } if (current_group.size()) mutable_settings->beginGroup(current_group); return value; } void MultiSettings::set_common_value(QString const &key, QVariant const &value) { auto const ¤t_group = m_->settings_.group(); if (current_group.size()) m_->settings_.endGroup(); { SettingsGroup alternatives{&m_->settings_, multi_settings_root_group}; m_->settings_.setValue(key, value); } if (current_group.size()) m_->settings_.beginGroup(current_group); } void MultiSettings::remove_common_value(QString const &key) { if (!key.size()) return; // we don't allow global delete as it // would break this classes data model auto const ¤t_group = m_->settings_.group(); if (current_group.size()) m_->settings_.endGroup(); { SettingsGroup alternatives{&m_->settings_, multi_settings_root_group}; m_->settings_.remove(key); } if (current_group.size()) m_->settings_.beginGroup(current_group); } void MultiSettings::create_menu_actions(QMainWindow *main_window, QMenu *menu) { m_->create_menu_actions(main_window, menu); } void MultiSettings::select_configuration(QString const &name) { m_->select_configuration(name); } QString MultiSettings::configuration_name() const { return m_->current_; } bool MultiSettings::exit() { return m_->exit(); } MultiSettings::impl::impl(MultiSettings const *parent, QString const &config_name) : settings_{settings_path(), QSettings::IniFormat}, parent_{parent}, main_window_{nullptr}, name_change_emit_pending_{true}, reposition_type_{RepositionType::unchanged}, exit_flag_{true}, configurations_group_{new QActionGroup{this}} { if (!settings_.isWritable()) { throw std::runtime_error{QString{"Cannot access \"%1\" for writing"} .arg(settings_.fileName()) .toStdString()}; } // deal with transient, now defunct, settings key if (settings_.contains(multi_settings_current_group_key)) { current_ = settings_.value(multi_settings_current_group_key).toString(); settings_.remove(multi_settings_current_group_key); if (current_.size()) { { SettingsGroup alternatives{&settings_, multi_settings_root_group}; { SettingsGroup source_group{&settings_, current_}; new_settings_ = get_settings(); } settings_.setValue(multi_settings_current_name_key, tr(default_string)); } reposition_type_ = RepositionType::save_and_replace; reposition(); } else { SettingsGroup alternatives{&settings_, multi_settings_root_group}; settings_.setValue(multi_settings_current_name_key, tr(default_string)); } } // bootstrap QStringList available_configurations; { SettingsGroup alternatives{&settings_, multi_settings_root_group}; available_configurations = settings_.childGroups(); // use last selected configuration current_ = settings_.value(multi_settings_current_name_key).toString(); if (!current_.size()) { // no configurations so use default name current_ = tr(default_string); settings_.setValue(multi_settings_current_name_key, current_); } } if (config_name.size() && available_configurations.contains(config_name) && config_name != current_) { // switch to specified configuration { SettingsGroup alternatives{&settings_, multi_settings_root_group}; // save the target settings SettingsGroup target_group{&settings_, config_name}; new_settings_ = get_settings(); } current_ = config_name; reposition_type_ = RepositionType::save_and_replace; reposition(); } settings_.sync(); } // do actions that can only be done once all the windows are closed bool MultiSettings::impl::reposition() { auto const ¤t_group = settings_.group(); if (current_group.size()) settings_.endGroup(); switch (reposition_type_) { case RepositionType::save_and_replace: { // save the current settings with the other alternatives Dictionary saved_settings{get_settings()}; SettingsGroup alternatives{&settings_, multi_settings_root_group}; // get the current configuration name auto const &previous_group_name = settings_.value(multi_settings_current_name_key, tr(default_string)) .toString(); SettingsGroup save_group{&settings_, previous_group_name}; load_from(saved_settings); } // fall through case RepositionType::replace: // and purge current settings for (auto const &key : settings_.allKeys()) { if (!key.contains(multi_settings_root_group)) { settings_.remove(key); } } // insert the new settings load_from(new_settings_, false); if (!new_settings_.size()) { // if we are clearing the current settings then we must // reset the application font and the font in the // application style sheet, this is necessary since the // application instance is not recreated qApp->setFont(original_font_); qApp->setStyleSheet(qApp->styleSheet() + "* {" + font_as_stylesheet(original_font_) + '}'); } // now we have set up the new current we can safely purge it // from the alternatives { SettingsGroup alternatives{&settings_, multi_settings_root_group}; { SettingsGroup purge_group{&settings_, current_}; settings_.remove(QString{}); // purge entire group } // switch to the specified configuration name settings_.setValue(multi_settings_current_name_key, current_); } settings_.sync(); // fall through case RepositionType::unchanged: new_settings_.clear(); break; } if (current_group.size()) settings_.beginGroup(current_group); reposition_type_ = RepositionType::unchanged; // reset bool exit{exit_flag_}; exit_flag_ = true; // reset exit flag so normal exit works return exit; } // populate a pop up menu with the configurations sub-menus for // maintenance including select, clone, clone from, delete, rename // and, reset void MultiSettings::impl::create_menu_actions(QMainWindow *main_window, QMenu *menu) { main_window_ = main_window; auto const ¤t_group = settings_.group(); if (current_group.size()) settings_.endGroup(); SettingsGroup alternatives{&settings_, multi_settings_root_group}; // get the current configuration name auto const ¤t_configuration_name = settings_.value(multi_settings_current_name_key, tr(default_string)) .toString(); // add the default configuration sub menu QMenu *default_menu = create_sub_menu(menu, current_configuration_name, configurations_group_); // and set as the current configuration default_menu->menuAction()->setChecked(true); // get the existing alternatives auto const &available_configurations = settings_.childGroups(); // add all the other configurations for (auto const &configuration_name : available_configurations) { create_sub_menu(menu, configuration_name, configurations_group_); } if (current_group.size()) settings_.beginGroup(current_group); if (name_change_emit_pending_) { Q_EMIT parent_->configurationNameChanged(unescape_ampersands(current_)); name_change_emit_pending_ = false; } } // call this at the end of the main program loop to determine if the // main window really wants to quit or to run again with a new configuration bool MultiSettings::impl::exit() { // ensure that configuration name changed signal gets fired on restart name_change_emit_pending_ = true; // do any configuration swap required and return exit flag return reposition(); } QMenu *MultiSettings::impl::create_sub_menu(QMenu *parent_menu, QString const &menu_title, QActionGroup *action_group) { auto sub_menu = parent_menu->addMenu(menu_title); if (action_group) action_group->addAction(sub_menu->menuAction()); sub_menu->menuAction()->setCheckable(true); // populate sub-menu actions before showing connect(sub_menu, &QMenu::aboutToShow, [this, parent_menu, sub_menu]() { // depopulate before populating and showing because on Mac OS X // there is an issue with depopulating in QMenu::aboutToHide() // with connections being disconnected before they are actioned while (!sub_menu->actions().isEmpty()) { sub_menu->removeAction(sub_menu->actions().last()); } bool is_current{sub_menu->menuAction()->text() == current_}; if (!is_current) { auto select_action = new QAction{tr("&Switch To"), this}; sub_menu->addAction(select_action); connect(select_action, &QAction::triggered, [this, sub_menu](bool) { select_configuration(sub_menu->title()); }); sub_menu->addSeparator(); } auto clone_action = new QAction{tr("&Clone"), this}; sub_menu->addAction(clone_action); connect(clone_action, &QAction::triggered, [this, parent_menu, sub_menu](bool) { clone_configuration(parent_menu, sub_menu); }); auto const ¤t_group = settings_.group(); if (current_group.size()) settings_.endGroup(); SettingsGroup alternatives{&settings_, multi_settings_root_group}; if (settings_.childGroups().size()) { auto clone_into_action = new QAction{tr("Clone &Into ..."), this}; sub_menu->addAction(clone_into_action); connect( clone_into_action, &QAction::triggered, [this, sub_menu](bool) { clone_into_configuration(sub_menu); }); } if (current_group.size()) settings_.beginGroup(current_group); auto reset_action = new QAction{tr("R&eset"), this}; sub_menu->addAction(reset_action); connect(reset_action, &QAction::triggered, [this, sub_menu](bool) { reset_configuration(sub_menu); }); auto rename_action = new QAction{tr("&Rename ..."), this}; sub_menu->addAction(rename_action); connect(rename_action, &QAction::triggered, [this, sub_menu](bool) { rename_configuration(sub_menu); }); if (!is_current) { auto delete_action = new QAction{tr("&Delete"), this}; sub_menu->addAction(delete_action); connect(delete_action, &QAction::triggered, [this, sub_menu](bool) { delete_configuration(sub_menu); }); } }); return sub_menu; } auto MultiSettings::impl::get_settings() const -> Dictionary { Dictionary settings; for (auto const &key : settings_.allKeys()) { // filter out multi settings group and empty settings // placeholder if (!key.contains(multi_settings_root_group) && !key.contains(multi_settings_place_holder_key)) { settings[key] = settings_.value(key); } } return settings; } void MultiSettings::impl::load_from(Dictionary const &dictionary, bool add_placeholder) { if (dictionary.size()) { for (Dictionary::const_iterator iter = dictionary.constBegin(); iter != dictionary.constEnd(); ++iter) { settings_.setValue(iter.key(), iter.value()); } } else if (add_placeholder) { // add a placeholder key to stop the alternative configuration // name from disappearing settings_.setValue(multi_settings_place_holder_key, QVariant{}); } settings_.sync(); } void MultiSettings::impl::select_configuration(QString const &target_name) { if (main_window_ && target_name != current_) { bool changed{false}; { auto const ¤t_group = settings_.group(); if (current_group.size()) settings_.endGroup(); // position to the alternative settings SettingsGroup alternatives{&settings_, multi_settings_root_group}; if (settings_.childGroups().contains(target_name)) { changed = true; // save the target settings SettingsGroup target_group{&settings_, target_name}; new_settings_ = get_settings(); } if (current_group.size()) settings_.beginGroup(current_group); } if (changed) { // and set up the restart current_ = target_name; Q_EMIT parent_->configurationNameChanged( unescape_ampersands(current_)); restart(RepositionType::save_and_replace); } } } void MultiSettings::impl::clone_configuration(QMenu *parent_menu, QMenu const *menu) { auto const ¤t_group = settings_.group(); if (current_group.size()) settings_.endGroup(); auto const &source_name = menu->title(); // settings to clone Dictionary source_settings; if (source_name == current_) { // grab the data to clone from the current settings source_settings = get_settings(); } SettingsGroup alternatives{&settings_, multi_settings_root_group}; if (source_name != current_) { SettingsGroup source_group{&settings_, source_name}; source_settings = get_settings(); } // find a new unique name QString new_name_root{source_name + " - Copy"}; ; QString new_name{new_name_root}; unsigned index{0}; do { if (index++) new_name = new_name_root + '(' + QString::number(index) + ')'; } while (settings_.childGroups().contains(new_name) || new_name == current_); SettingsGroup new_group{&settings_, new_name}; load_from(source_settings); // insert the new configuration sub menu in the parent menu create_sub_menu(parent_menu, new_name, configurations_group_); if (current_group.size()) settings_.beginGroup(current_group); } void MultiSettings::impl::clone_into_configuration(QMenu const *menu) { Q_ASSERT(main_window_); if (!main_window_) return; auto const ¤t_group = settings_.group(); if (current_group.size()) settings_.endGroup(); auto const &target_name = menu->title(); // get the current configuration name QString current_group_name; QStringList sources; { SettingsGroup alternatives{&settings_, multi_settings_root_group}; current_group_name = settings_.value(multi_settings_current_name_key).toString(); { // get the source configuration name for the clone sources = settings_.childGroups(); sources << current_group_name; sources.removeOne(target_name); } } // pick a source configuration ExistingNameDialog dialog{sources, main_window_}; if (sources.size() && (1 == sources.size() || QDialog::Accepted == dialog.exec())) { QString source_name{1 == sources.size() ? sources.at(0) : dialog.name()}; if (main_window_ && JS8MessageBox::Yes == JS8MessageBox::query_message( main_window_, tr("Clone Into Configuration"), tr("Confirm overwrite of all values for configuration " "\"%1\" with values from \"%2\"?") .arg(unescape_ampersands(target_name)) .arg(unescape_ampersands(source_name)))) { // grab the data to clone from if (source_name == current_group_name) { // grab the data to clone from the current settings new_settings_ = get_settings(); } else { SettingsGroup alternatives{&settings_, multi_settings_root_group}; SettingsGroup source_group{&settings_, source_name}; new_settings_ = get_settings(); } // purge target settings and replace if (target_name == current_) { // restart with new settings restart(RepositionType::replace); } else { SettingsGroup alternatives{&settings_, multi_settings_root_group}; SettingsGroup target_group{&settings_, target_name}; settings_.remove(QString{}); // purge entire group load_from(new_settings_); new_settings_.clear(); } } } if (current_group.size()) settings_.beginGroup(current_group); } void MultiSettings::impl::reset_configuration(QMenu const *menu) { Q_ASSERT(main_window_); if (!main_window_) return; auto const &target_name = menu->title(); if (!main_window_ || JS8MessageBox::Yes != JS8MessageBox::query_message( main_window_, tr("Reset Configuration"), tr("Confirm reset to default values for configuration \"%1\"?") .arg(unescape_ampersands(target_name)))) { return; } if (target_name == current_) { // restart with default settings new_settings_.clear(); restart(RepositionType::replace); } else { auto const ¤t_group = settings_.group(); if (current_group.size()) settings_.endGroup(); SettingsGroup alternatives{&settings_, multi_settings_root_group}; SettingsGroup target_group{&settings_, target_name}; settings_.remove(QString{}); // purge entire group // add a placeholder to stop alternative configuration name // being lost settings_.setValue(multi_settings_place_holder_key, QVariant{}); settings_.sync(); if (current_group.size()) settings_.beginGroup(current_group); } } void MultiSettings::impl::rename_configuration(QMenu *menu) { Q_ASSERT(main_window_); if (!main_window_) return; auto const ¤t_group = settings_.group(); if (current_group.size()) settings_.endGroup(); auto const &target_name = menu->title(); // gather names we cannot use SettingsGroup alternatives{&settings_, multi_settings_root_group}; auto invalid_names = settings_.childGroups(); invalid_names << settings_.value(multi_settings_current_name_key).toString(); // get the new name NameDialog dialog{target_name, invalid_names, main_window_}; if (QDialog::Accepted == dialog.exec()) { if (target_name == current_) { settings_.setValue(multi_settings_current_name_key, dialog.new_name()); settings_.sync(); current_ = dialog.new_name(); Q_EMIT parent_->configurationNameChanged( unescape_ampersands(current_)); } else { // switch to the target group and fetch the configuration data Dictionary target_settings; { // grab the target configuration settings SettingsGroup target_group{&settings_, target_name}; target_settings = get_settings(); // purge the old configuration data settings_.remove(QString{}); // purge entire group } // load into new configuration group name SettingsGroup target_group{&settings_, dialog.new_name()}; load_from(target_settings); } // change the action text in the menu menu->setTitle(dialog.new_name()); } if (current_group.size()) settings_.beginGroup(current_group); } void MultiSettings::impl::delete_configuration(QMenu *menu) { Q_ASSERT(main_window_); auto const &target_name = menu->title(); if (target_name == current_) { return; // suicide not allowed here } else { if (!main_window_ || JS8MessageBox::Yes != JS8MessageBox::query_message( main_window_, tr("Delete Configuration"), tr("Confirm deletion of configuration \"%1\"?") .arg(unescape_ampersands(target_name)))) { return; } auto const ¤t_group = settings_.group(); if (current_group.size()) settings_.endGroup(); SettingsGroup alternatives{&settings_, multi_settings_root_group}; SettingsGroup target_group{&settings_, target_name}; // purge the configuration data settings_.remove(QString{}); // purge entire group settings_.sync(); if (current_group.size()) settings_.beginGroup(current_group); } // update the menu menu->deleteLater(); } void MultiSettings::impl::restart(RepositionType type) { Q_ASSERT(main_window_); reposition_type_ = type; exit_flag_ = false; main_window_->close(); main_window_ = nullptr; }