mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-30 14:54:13 +02:00
Merge pull request #1166 from ispyisail/revive/640-config-profiles
Add saving and loading the configuration to a file
This commit is contained in:
@@ -30,6 +30,7 @@
|
||||
#include "qetpalette.h"
|
||||
#include "qetstyle.h"
|
||||
#include "utils/qetutils.h"
|
||||
#include "utils/configprofile.h"
|
||||
#include "qetmessagebox.h"
|
||||
#include "qetproject.h"
|
||||
#include "qtextorientationspinboxwidget.h"
|
||||
@@ -55,9 +56,11 @@
|
||||
#include <iostream>
|
||||
#define QUOTE(x) STRINGIFY(x)
|
||||
#define STRINGIFY(x) #x
|
||||
#include <QFileDialog>
|
||||
#include <QFontDatabase>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QRegularExpression>
|
||||
#include <QSettings>
|
||||
#include <QStyleFactory>
|
||||
#include <QStyleHints>
|
||||
#ifdef BUILD_WITHOUT_KF
|
||||
@@ -2242,6 +2245,109 @@ void QETApp::configureQET()
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETApp::exportConfiguration
|
||||
Save the settings of QElectroTech to a file the user chooses, to keep
|
||||
them as a named profile or copy them to another computer (discussion
|
||||
#610). The file is always written in the ini format, whatever the
|
||||
platform stores its live settings in, so a profile saved on Windows
|
||||
loads on Linux and macOS. See ConfigProfile for the keys left out.
|
||||
*/
|
||||
void QETApp::exportConfiguration()
|
||||
{
|
||||
QWidget *parent_widget = qApp->activeWindow();
|
||||
|
||||
QString path = QFileDialog::getSaveFileName(
|
||||
parent_widget,
|
||||
tr("Enregistrer la configuration sous...", "dialog title"),
|
||||
QString(),
|
||||
tr("Configurations QElectroTech (*.conf)", "file dialog filter"));
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!path.endsWith(QLatin1String(".conf"), Qt::CaseInsensitive)) {
|
||||
path += QLatin1String(".conf");
|
||||
}
|
||||
|
||||
QSettings live_settings;
|
||||
QSettings file_settings(path, QSettings::IniFormat);
|
||||
ConfigProfile::exportTo(live_settings, file_settings);
|
||||
|
||||
if (file_settings.status() != QSettings::NoError) {
|
||||
QET::QetMessageBox::critical(
|
||||
parent_widget,
|
||||
tr("Erreur", "message box title"),
|
||||
tr("Impossible d'enregistrer la configuration dans « %1 ».").arg(path));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETApp::importConfiguration
|
||||
Replace the settings of QElectroTech with a file saved by
|
||||
exportConfiguration(), then close QElectroTech.
|
||||
|
||||
The settings are read by each part of QElectroTech when it starts, and
|
||||
there is no signal telling all of them that a setting changed, so the
|
||||
new settings are applied by starting QElectroTech again. That restart
|
||||
is left to the user: a second copy started from here would find this
|
||||
one still running, hand its arguments over to it and exit (main.cpp).
|
||||
*/
|
||||
void QETApp::importConfiguration()
|
||||
{
|
||||
QWidget *parent_widget = qApp->activeWindow();
|
||||
|
||||
const QString path = QFileDialog::getOpenFileName(
|
||||
parent_widget,
|
||||
tr("Charger une configuration...", "dialog title"),
|
||||
QString(),
|
||||
tr("Configurations QElectroTech (*.conf)", "file dialog filter"));
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QSettings file_settings(path, QSettings::IniFormat);
|
||||
if (file_settings.status() != QSettings::NoError
|
||||
|| !ConfigProfile::isProfile(file_settings))
|
||||
{
|
||||
QET::QetMessageBox::critical(
|
||||
parent_widget,
|
||||
tr("Erreur", "message box title"),
|
||||
tr("« %1 » n'est pas une configuration enregistrée par QElectroTech.").arg(path));
|
||||
return;
|
||||
}
|
||||
|
||||
//Said before anything closes: once the last window is closed,
|
||||
//QElectroTech quits by itself (checkRemainingWindows()).
|
||||
const auto answer = QET::QetMessageBox::question(
|
||||
parent_widget,
|
||||
tr("Charger une configuration", "message box title"),
|
||||
tr("Cette configuration va remplacer vos réglages actuels, sauf la "
|
||||
"disposition des fenêtres et la liste des fichiers récents.\n\n"
|
||||
"QElectroTech va ensuite se fermer. Relancez-le pour utiliser "
|
||||
"la nouvelle configuration.\n\n"
|
||||
"Voulez-vous continuer ?"),
|
||||
QMessageBox::Yes | QMessageBox::No,
|
||||
QMessageBox::No);
|
||||
if (answer != QMessageBox::Yes) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Asks to save any modified project, as quitQET() does. The settings
|
||||
//are only replaced once every editor is closed, so that the editors
|
||||
//saving their own settings on close cannot overwrite them, and a
|
||||
//cancelled close leaves them untouched.
|
||||
if (!closeEveryEditor()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QSettings live_settings;
|
||||
ConfigProfile::importFrom(file_settings, live_settings);
|
||||
//~QETApp() saves the colour dialog's custom colours on exit: load
|
||||
//the profile's into it first, or the old ones overwrite them
|
||||
QET::loadCustomColors();
|
||||
qApp->quit();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QETApp::aboutQET
|
||||
Open the dialog about qet.
|
||||
|
||||
@@ -291,6 +291,10 @@ class QETApp : public QObject
|
||||
void openTitleBlockTemplate(const QString &);
|
||||
void openTitleBlockTemplateFiles(const QStringList &);
|
||||
void configureQET();
|
||||
/// Save the settings to a file (discussion #610)
|
||||
void exportConfiguration();
|
||||
/// Replace the settings with a saved file, then close QElectroTech
|
||||
void importConfiguration();
|
||||
void aboutQET();
|
||||
void showDiagnosticsReport();
|
||||
void receiveMessage(int instanceId, QByteArray message);
|
||||
|
||||
@@ -92,6 +92,16 @@ void QETMainWindow::initCommonActions()
|
||||
}
|
||||
});
|
||||
|
||||
export_config_action_ = new QAction(QET::Icons::DocumentExport, tr("Enregistrer la configuration sous..."), this);
|
||||
export_config_action_ -> setStatusTip(tr("Enregistre les réglages de QElectroTech dans un fichier", "status bar tip"));
|
||||
connect(export_config_action_, &QAction::triggered, qet_app, &QETApp::exportConfiguration);
|
||||
ShortcutManager::instance().registerAction(export_config_action_, "mainwindow.export_configuration", tr("Général"), QKeySequence());
|
||||
|
||||
import_config_action_ = new QAction(QET::Icons::DocumentImport, tr("Charger une configuration..."), this);
|
||||
import_config_action_ -> setStatusTip(tr("Remplace les réglages de QElectroTech par ceux d'un fichier, puis ferme QElectroTech", "status bar tip"));
|
||||
connect(import_config_action_, &QAction::triggered, qet_app, &QETApp::importConfiguration);
|
||||
ShortcutManager::instance().registerAction(import_config_action_, "mainwindow.import_configuration", tr("Général"), QKeySequence());
|
||||
|
||||
fullscreen_action_ = new QAction(this);
|
||||
updateFullScreenAction();
|
||||
connect(fullscreen_action_, &QAction::triggered, this, &QETMainWindow::toggleFullScreen);
|
||||
@@ -170,6 +180,9 @@ void QETMainWindow::initCommonMenus()
|
||||
settings_menu_ = new QMenu(tr("&Configuration", "window menu"), this);
|
||||
settings_menu_ -> addAction(fullscreen_action_);
|
||||
settings_menu_ -> addAction(configure_action_);
|
||||
settings_menu_ -> addSeparator();
|
||||
settings_menu_ -> addAction(export_config_action_);
|
||||
settings_menu_ -> addAction(import_config_action_);
|
||||
connect(settings_menu_, &QMenu::aboutToShow, this, &QETMainWindow::checkToolbarsmenu);
|
||||
|
||||
help_menu_ = new QMenu(tr("&Aide", "window menu"), this);
|
||||
|
||||
@@ -56,6 +56,8 @@ class QETMainWindow : public QMainWindow {
|
||||
// attributes
|
||||
protected:
|
||||
QAction *configure_action_; ///< Launch the QElectroTech configuration dialog
|
||||
QAction *export_config_action_; ///< Save the settings to a file
|
||||
QAction *import_config_action_; ///< Replace the settings with a saved file
|
||||
QAction *fullscreen_action_; ///< Toggle full screen
|
||||
QAction *whatsthis_action_; ///< Toggle "What's this" mode
|
||||
QAction *about_qet_; ///< Launch the "About QElectroTech" dialog
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef CONFIGPROFILE_H
|
||||
#define CONFIGPROFILE_H
|
||||
|
||||
#include <QSettings>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
/**
|
||||
Configuration profiles (discussion #610): the settings of QElectroTech
|
||||
saved to a file, and loaded back from one.
|
||||
|
||||
Two kinds of keys stay out of a profile, because they describe this
|
||||
computer rather than how the user wants QElectroTech to behave: window
|
||||
sizes, positions and dock layouts (".../geometry", ".../state"), and
|
||||
the recent-files lists ("...-recentfiles/..."). They are not written to
|
||||
the file, and loading a profile keeps the current ones.
|
||||
*/
|
||||
namespace ConfigProfile
|
||||
{
|
||||
/// Key written to every exported file, so that loading refuses a
|
||||
/// file that was not exported by QElectroTech. Never copied into
|
||||
/// the live settings.
|
||||
inline const QString marker_key{QStringLiteral("qelectrotech-configuration/format")};
|
||||
|
||||
/// True for a key that stays out of a profile (see above).
|
||||
inline bool isLocalKey(const QString &key)
|
||||
{
|
||||
return key.endsWith(QLatin1String("/geometry"))
|
||||
|| key.endsWith(QLatin1String("/state"))
|
||||
|| key.contains(QLatin1String("-recentfiles/"))
|
||||
|| key == marker_key;
|
||||
}
|
||||
|
||||
/// True if @a file was exported by exportTo().
|
||||
inline bool isProfile(const QSettings &file)
|
||||
{
|
||||
return file.value(marker_key).toInt() == 1;
|
||||
}
|
||||
|
||||
/// Replace the contents of @a file with every key of @a live,
|
||||
/// except the local ones. Returns the number of keys written.
|
||||
inline int exportTo(const QSettings &live, QSettings &file)
|
||||
{
|
||||
file.clear();
|
||||
int written = 0;
|
||||
const QStringList keys = live.allKeys();
|
||||
for (const QString &key : keys) {
|
||||
if (isLocalKey(key)) {
|
||||
continue;
|
||||
}
|
||||
file.setValue(key, live.value(key));
|
||||
++written;
|
||||
}
|
||||
file.setValue(marker_key, 1);
|
||||
file.sync();
|
||||
return written;
|
||||
}
|
||||
|
||||
/// Replace every key of @a live, except the local ones, with the
|
||||
/// keys of @a file. A key that is not in @a file is removed rather
|
||||
/// than kept: loading a profile must not leave behind a setting
|
||||
/// the previous profile made. Local keys in @a file are ignored.
|
||||
inline void importFrom(const QSettings &file, QSettings &live)
|
||||
{
|
||||
const QStringList live_keys = live.allKeys();
|
||||
for (const QString &key : live_keys) {
|
||||
if (!isLocalKey(key)) {
|
||||
live.remove(key);
|
||||
}
|
||||
}
|
||||
const QStringList file_keys = file.allKeys();
|
||||
for (const QString &key : file_keys) {
|
||||
if (!isLocalKey(key)) {
|
||||
live.setValue(key, file.value(key));
|
||||
}
|
||||
}
|
||||
live.sync();
|
||||
}
|
||||
}
|
||||
|
||||
#endif // CONFIGPROFILE_H
|
||||
@@ -111,6 +111,13 @@ add_test(NAME tst_alignment COMMAND tst_alignment)
|
||||
target_include_directories(tst_alignment PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_alignment PRIVATE Qt::Test)
|
||||
|
||||
# configprofile.h is header-only: which settings a saved configuration
|
||||
# profile carries, and how loading one replaces the live settings.
|
||||
add_executable(tst_configprofile tst_configprofile.cpp)
|
||||
add_test(NAME tst_configprofile COMMAND tst_configprofile)
|
||||
target_include_directories(tst_configprofile PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_configprofile PRIVATE Qt::Test Qt::Gui)
|
||||
|
||||
# itemgroups.cpp keeps group membership and the selection rule apart from
|
||||
# Diagram, so it is tested here on a plain scene of rectangles.
|
||||
add_executable(tst_itemgroups tst_itemgroups.cpp ${QET_DIR}/sources/itemgroups.cpp)
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#include <QtTest>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
#include "utils/configprofile.h"
|
||||
|
||||
class tst_configprofile : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
|
||||
QString path(const QString &name) const { return m_dir.filePath(name); }
|
||||
|
||||
private slots:
|
||||
void localKeys()
|
||||
{
|
||||
QVERIFY(ConfigProfile::isLocalKey("diagrameditor/geometry"));
|
||||
QVERIFY(ConfigProfile::isLocalKey("elementeditor/state"));
|
||||
QVERIFY(ConfigProfile::isLocalKey("projects-recentfiles/file1"));
|
||||
QVERIFY(ConfigProfile::isLocalKey(ConfigProfile::marker_key));
|
||||
QVERIFY(!ConfigProfile::isLocalKey("diagrameditor/Xgrid"));
|
||||
QVERIFY(!ConfigProfile::isLocalKey("shortcuts/mainwindow.fullscreen"));
|
||||
QVERIFY(!ConfigProfile::isLocalKey("masterpropertieswidget/plc-table-header-state"));
|
||||
}
|
||||
|
||||
// Export leaves out window layout and recent files, and marks the file.
|
||||
void exportLeavesOutLocalKeys()
|
||||
{
|
||||
QSettings live(path("live1.conf"), QSettings::IniFormat);
|
||||
live.setValue("diagrameditor/Xgrid", 7);
|
||||
live.setValue("lang", "de");
|
||||
live.setValue("diagrameditor/geometry", QByteArray("xyz"));
|
||||
live.setValue("projects-recentfiles/file1", "/home/a/b.qet");
|
||||
|
||||
QSettings file(path("profile1.conf"), QSettings::IniFormat);
|
||||
QCOMPARE(ConfigProfile::exportTo(live, file), 2);
|
||||
QVERIFY(ConfigProfile::isProfile(file));
|
||||
QCOMPARE(file.value("diagrameditor/Xgrid").toInt(), 7);
|
||||
QVERIFY(!file.contains("diagrameditor/geometry"));
|
||||
QVERIFY(!file.contains("projects-recentfiles/file1"));
|
||||
}
|
||||
|
||||
// Import replaces every setting, removes the ones the profile does not
|
||||
// have, keeps window layout and recent files, and never copies the marker.
|
||||
void importReplacesButKeepsLocal()
|
||||
{
|
||||
QSettings file(path("profile2.conf"), QSettings::IniFormat);
|
||||
file.setValue("diagrameditor/Xgrid", 5);
|
||||
file.setValue("diagrameditor/geometry", QByteArray("from-file"));
|
||||
file.setValue(ConfigProfile::marker_key, 1);
|
||||
|
||||
QSettings live(path("live2.conf"), QSettings::IniFormat);
|
||||
live.setValue("diagrameditor/Xgrid", 10);
|
||||
live.setValue("lang", "fr");
|
||||
live.setValue("diagrameditor/geometry", QByteArray("mine"));
|
||||
live.setValue("projects-recentfiles/file1", "/home/a/b.qet");
|
||||
|
||||
ConfigProfile::importFrom(file, live);
|
||||
QCOMPARE(live.value("diagrameditor/Xgrid").toInt(), 5);
|
||||
QVERIFY(!live.contains("lang"));
|
||||
QCOMPARE(live.value("diagrameditor/geometry").toByteArray(), QByteArray("mine"));
|
||||
QCOMPARE(live.value("projects-recentfiles/file1").toString(), QString("/home/a/b.qet"));
|
||||
QVERIFY(!live.contains(ConfigProfile::marker_key));
|
||||
}
|
||||
|
||||
// Values survive the round trip with their types, including arrays.
|
||||
void roundTrip()
|
||||
{
|
||||
QSettings a(path("a.conf"), QSettings::IniFormat);
|
||||
a.setValue("print/default/fitinpage", true);
|
||||
a.setValue("diagrameditor/sheet_background_color", QColor(12, 34, 56));
|
||||
a.beginWriteArray("diagrameditor/defaultguides", 2);
|
||||
a.setArrayIndex(0); a.setValue("pos", 100);
|
||||
a.setArrayIndex(1); a.setValue("pos", 250);
|
||||
a.endArray();
|
||||
|
||||
QSettings file(path("profile3.conf"), QSettings::IniFormat);
|
||||
ConfigProfile::exportTo(a, file);
|
||||
QSettings b(path("b.conf"), QSettings::IniFormat);
|
||||
b.setValue("stray", 1);
|
||||
ConfigProfile::importFrom(file, b);
|
||||
|
||||
QStringList ka = a.allKeys(), kb = b.allKeys();
|
||||
ka.sort(); kb.sort();
|
||||
QCOMPARE(kb, ka);
|
||||
for (const QString &k : std::as_const(ka))
|
||||
QCOMPARE(b.value(k), a.value(k));
|
||||
}
|
||||
|
||||
void plainFileIsNotAProfile()
|
||||
{
|
||||
QSettings other(path("other.conf"), QSettings::IniFormat);
|
||||
other.setValue("some/key", 1);
|
||||
QVERIFY(!ConfigProfile::isProfile(other));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(tst_configprofile)
|
||||
|
||||
#include "tst_configprofile.moc"
|
||||
Reference in New Issue
Block a user