js8call/JS8_Main/MultiSettings.cpp

799 lines
29 KiB
C++
Raw Permalink Normal View History

#include "MultiSettings.h"
#include <stdexcept>
#include <QAction>
#include <QActionGroup>
#include <QApplication>
#include <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QDir>
#include <QFont>
#include <QFormLayout>
#include <QLabel>
#include <QLineEdit>
#include <QList>
#include <QMainWindow>
#include <QMenu>
#include <QMetaObject>
#include <QObject>
#include <QPushButton>
#include <QRegularExpression>
#include <QRegularExpressionValidator>
#include <QSettings>
#include <QStandardPaths>
#include <QString>
#include <QStringList>
#include <QVBoxLayout>
#include "JS8MessageBox.h"
#include "JS8_Include/SettingsGroup.h"
#include "qt_helpers.h"
#include "JS8_Include/pimpl_impl.h"
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_;
};
} // 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<QSettings *>(&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_ &&
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 &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_ ||
JS8MessageBox::Yes !=
JS8MessageBox::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;
}