#include "MultiSettings.hpp" #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 #include "SettingsGroup.hpp" #include "qt_helpers.hpp" #include "SettingsGroup.hpp" #include "widgets/MessageBox.hpp" #include "pimpl_impl.hpp" 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& current_name, QStringList const& current_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& current_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_; }; } 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 = QMap; // 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& current_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& current_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& current_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& current_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& current_group = settings_.group (); if (current_group.size ()) settings_.endGroup (); SettingsGroup alternatives {&settings_, multi_settings_root_group}; // get the current configuration name auto const& current_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& current_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& current_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& current_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& current_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_ && MessageBox::Yes == MessageBox::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_ || MessageBox::Yes != MessageBox::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& current_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& current_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_ || MessageBox::Yes != MessageBox::query_message (main_window_, tr ("Delete Configuration"), tr ("Confirm deletion of configuration \"%1\"?") .arg (unescape_ampersands (target_name)))) { return; } auto const& current_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; }