WSJT-X/MultiSettings.cpp

700 lines
23 KiB
C++
Raw Normal View History

#include "MultiSettings.hpp"
#include <stdexcept>
#include <QObject>
#include <QSettings>
#include <QString>
#include <QStringList>
#include <QDir>
#include <QFont>
#include <QApplication>
#include <QStandardPaths>
#include <QMainWindow>
#include <QMenu>
#include <QAction>
#include <QActionGroup>
#include <QMessageBox>
#include <QDialog>
#include <QLineEdit>
#include <QRegularExpression>
#include <QRegularExpressionValidator>
#include <QFormLayout>
#include <QVBoxLayout>
#include <QDialogButtonBox>
#include <QPushButton>
#include <QComboBox>
#include <QLabel>
#include <QList>
#include <QMetaObject>
#include "SettingsGroup.hpp"
#include "qt_helpers.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";
// calculate a useable and unique settings file path
QString settings_path ()
{
auto 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
{
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
{
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 ();
bool reposition ();
void create_menu_actions (QMainWindow * main_window, QMenu * menu);
bool exit ();
QSettings settings_;
private:
using Dictionary = QMap<QString, QVariant>;
// 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);
// switch to this configuration
void select_configuration (QMainWindow *);
// clone this configuration
void clone_configuration (QMenu *);
// update this configuration from another
void clone_into_configuration (QMainWindow *);
// reset configuration to default values
void reset_configuration (QMainWindow *);
// change configuration name
void rename_configuration (QMainWindow *);
// remove a configuration
void delete_configuration (QMainWindow *);
QFont original_font_;
QString current_;
// action to take on restart
enum class RepositionType {unchanged, replace, save_and_replace} reposition_type_;
Dictionary new_settings_;
bool exit_flag_; // false means loop around with new
// configuration
QActionGroup * configurations_group_;
QAction * select_action_;
QAction * clone_action_;
QAction * clone_into_action_;
QAction * reset_action_;
QAction * rename_action_;
QAction * delete_action_;
QList<QMetaObject::Connection> action_connections_;
QMenu * active_sub_menu_;
};
#include "MultiSettings.moc"
MultiSettings::MultiSettings ()
{
}
MultiSettings::~MultiSettings ()
{
}
QSettings * MultiSettings::settings ()
{
return &m_->settings_;
}
void MultiSettings::create_menu_actions (QMainWindow * main_window, QMenu * menu)
{
m_->create_menu_actions (main_window, menu);
}
bool MultiSettings::exit ()
{
return m_->exit ();
}
MultiSettings::impl::impl ()
: settings_ {settings_path (), QSettings::IniFormat}
, reposition_type_ {RepositionType::unchanged}
, exit_flag_ {true}
, configurations_group_ {new QActionGroup {this}}
, select_action_ {new QAction {tr ("&Switch To"), this}}
, clone_action_ {new QAction {tr ("&Clone"), this}}
, clone_into_action_ {new QAction {tr ("Clone &Into ..."), this}}
, reset_action_ {new QAction {tr ("R&eset"), this}}
, rename_action_ {new QAction {tr ("&Rename ..."), this}}
, delete_action_ {new QAction {tr ("&Delete"), this}}
, active_sub_menu_ {nullptr}
{
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
{
SettingsGroup alternatives {&settings_, multi_settings_root_group};
current_ = settings_.value (multi_settings_current_name_key).toString ();
if (!current_.size ())
{
current_ = tr (default_string);
settings_.setValue (multi_settings_current_name_key, current_);
}
}
settings_.sync ();
}
// do actions that can only be done once all the windows are closed
bool MultiSettings::impl::reposition ()
{
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 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 (""); // 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;
}
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)
{
SettingsGroup alternatives {&settings_, multi_settings_root_group};
// get the current configuration name
auto 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);
QStringList available_configurations;
// get the existing alternatives
available_configurations = settings_.childGroups ();
// add all the other configurations
for (auto const& configuration_name: available_configurations)
{
create_sub_menu (menu, configuration_name, configurations_group_);
}
// hook up configuration actions
action_connections_ << connect (select_action_, &QAction::triggered, [this, main_window] (bool) {
select_configuration (main_window);
});
action_connections_ << connect (clone_action_, &QAction::triggered, [this, menu] (bool) {
clone_configuration (menu);
});
action_connections_ << connect (clone_into_action_, &QAction::triggered, [this, main_window] (bool) {
clone_into_configuration (main_window);
});
action_connections_ << connect (rename_action_, &QAction::triggered, [this, main_window] (bool) {
rename_configuration (main_window);
});
action_connections_ << connect (reset_action_, &QAction::triggered, [this, main_window] (bool) {
reset_configuration (main_window);
});
action_connections_ << connect (delete_action_, &QAction::triggered, [this, main_window] (bool) {
delete_configuration (main_window);
});
}
// 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 ()
{
for (auto const& connection: action_connections_)
{
disconnect (connection);
}
action_connections_.clear ();
// 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)
{
QMenu * sub_menu = parent_menu->addMenu (menu_title);
if (action_group) action_group->addAction (sub_menu->menuAction ());
sub_menu->menuAction ()->setCheckable (true);
sub_menu->addAction (select_action_);
sub_menu->addSeparator ();
sub_menu->addAction (clone_action_);
sub_menu->addAction (clone_into_action_);
sub_menu->addAction (rename_action_);
sub_menu->addAction (reset_action_);
sub_menu->addAction (delete_action_);
// disable disallowed actions before showing sub menu
connect (sub_menu, &QMenu::aboutToShow, [this, sub_menu] () {
bool is_current {sub_menu->menuAction ()->text () == current_};
select_action_->setEnabled (!is_current);
delete_action_->setEnabled (!is_current);
SettingsGroup alternatives {&settings_, multi_settings_root_group};
clone_into_action_->setEnabled (settings_.childGroups ().size ());
active_sub_menu_ = 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 (QMainWindow * main_window)
{
if (active_sub_menu_)
{
auto const& target_name = active_sub_menu_->title ();
if (target_name != current_)
{
{
// position to the alternative settings
SettingsGroup alternatives {&settings_, multi_settings_root_group};
// save the target settings
SettingsGroup target_group {&settings_, target_name};
new_settings_ = get_settings ();
}
// and set up the restart
current_ = target_name;
reposition_type_ = RepositionType::save_and_replace;
exit_flag_ = false;
main_window->close ();
}
}
}
void MultiSettings::impl::clone_configuration (QMenu * menu)
{
if (active_sub_menu_)
{
auto const& source_name = active_sub_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));
SettingsGroup new_group {&settings_, new_name};
load_from (source_settings);
// insert the new configuration sub menu in the parent menu
create_sub_menu (menu, new_name, configurations_group_);
}
}
void MultiSettings::impl::clone_into_configuration (QMainWindow * main_window)
{
if (active_sub_menu_)
{
auto const& target_name = active_sub_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 (QMessageBox::Yes == QMessageBox::question (main_window,
tr ("Clone Into Configuration"),
tr ("Confirm overwrite of all values for configuration \"%1\" with values from \"%2\"?")
.arg (target_name)
.arg (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
reposition_type_ = RepositionType::replace;
exit_flag_ = false;
main_window->close ();
}
else
{
SettingsGroup alternatives {&settings_, multi_settings_root_group};
SettingsGroup target_group {&settings_, target_name};
settings_.remove (""); // purge entire group
load_from (new_settings_);
new_settings_.clear ();
}
}
}
}
}
void MultiSettings::impl::reset_configuration (QMainWindow * main_window)
{
if (active_sub_menu_)
{
auto const& target_name = active_sub_menu_->title ();
if (QMessageBox::Yes != QMessageBox::question (main_window,
tr ("Reset Configuration"),
tr ("Confirm reset to default values for configuration \"%1\"?")
.arg (target_name)))
{
return;
}
if (target_name == current_)
{
// restart with default settings
reposition_type_ = RepositionType::replace;
new_settings_.clear ();
exit_flag_ = false;
main_window->close ();
}
else
{
SettingsGroup alternatives {&settings_, multi_settings_root_group};
SettingsGroup target_group {&settings_, target_name};
settings_.remove (""); // purge entire group
// add a placeholder to stop alternative configuration name
// being lost
settings_.setValue (multi_settings_place_holder_key, QVariant {});
settings_.sync ();
}
}
}
void MultiSettings::impl::rename_configuration (QMainWindow * main_window)
{
if (active_sub_menu_)
{
auto const& target_name = active_sub_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 ();
}
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 (""); // 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
active_sub_menu_->setTitle (dialog.new_name ());
}
}
}
void MultiSettings::impl::delete_configuration (QMainWindow * main_window)
{
if (active_sub_menu_)
{
auto const& target_name = active_sub_menu_->title ();
if (target_name == current_)
{
return; // suicide not allowed here
}
else
{
if (QMessageBox::Yes != QMessageBox::question (main_window,
tr ("Delete Configuration"),
tr ("Confirm deletion of configuration \"%1\"?")
.arg (target_name)))
{
return;
}
SettingsGroup alternatives {&settings_, multi_settings_root_group};
SettingsGroup target_group {&settings_, target_name};
// purge the configuration data
settings_.remove (""); // purge entire group
settings_.sync ();
}
// update the menu
active_sub_menu_->deleteLater (), active_sub_menu_ = nullptr;
}
}