Merge pull request #1410 from ispyisail/feature/settings-profile-parts

Add saving and loading only part of the settings
This commit is contained in:
ispyisail
2026-10-09 22:35:37 +13:00
committed by GitHub
4 changed files with 427 additions and 28 deletions
+143 -10
View File
@@ -61,14 +61,19 @@
#include <iostream>
#define QUOTE(x) STRINGIFY(x)
#define STRINGIFY(x) #x
#include <QCheckBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFileDialog>
#include <QFileInfo>
#include <QFontDatabase>
#include <QLabel>
#include <QProcessEnvironment>
#include <QRegularExpression>
#include <QSettings>
#include <QStyleFactory>
#include <QStyleHints>
#include <QVBoxLayout>
#ifdef BUILD_WITHOUT_KF
# include "ui/nokde/kautosavefile.h"
#else
@@ -2291,6 +2296,88 @@ void QETApp::customizeQET(int tab)
dialog.setParent(nullptr, dialog.windowFlags());
}
namespace {
/**
@brief partLabel
@return the name of @a part shown to the user
*/
QString partLabel(ConfigProfile::Part part)
{
switch (part) {
case ConfigProfile::Part::Controls:
return QETApp::tr("Toolbars, keyboard shortcuts, mouse and trackpad gestures");
case ConfigProfile::Part::NewProject:
return QETApp::tr("Defaults for new projects: folio, title block, "
"wires, numbering");
case ConfigProfile::Part::Other:
return QETApp::tr("All other settings: appearance, grid, language...");
case ConfigProfile::Part::Folders:
return QETApp::tr("Folders of the collections");
}
return QString();
}
/**
@brief choosePartsToSave
Ask which parts of the settings to save, all of them ticked to begin
with.
@return the parts chosen, or an empty list if the user cancelled
*/
QList<ConfigProfile::Part> choosePartsToSave(QWidget *parent)
{
QDialog dialog(parent);
dialog.setWindowTitle(QETApp::tr("Save settings as...", "dialog title"));
auto *layout = new QVBoxLayout(&dialog);
layout->addWidget(new QLabel(QETApp::tr("Settings to save:"), &dialog));
QMap<ConfigProfile::Part, QCheckBox *> boxes;
for (ConfigProfile::Part part : ConfigProfile::choosableParts()) {
auto *box = new QCheckBox(partLabel(part), &dialog);
box->setChecked(true);
layout->addWidget(box);
boxes.insert(part, box);
}
auto *note = new QLabel(QETApp::tr("The folders of your collections are "
"saved only when every part is ticked."),
&dialog);
note->setWordWrap(true);
layout->addWidget(note);
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok
| QDialogButtonBox::Cancel,
&dialog);
layout->addWidget(buttons);
QObject::connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
QObject::connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
//Nothing ticked, nothing to save
auto update_ok = [&boxes, buttons]() {
bool any = false;
for (QCheckBox *box : std::as_const(boxes)) {
any = any || box->isChecked();
}
buttons->button(QDialogButtonBox::Ok)->setEnabled(any);
};
for (QCheckBox *box : std::as_const(boxes)) {
QObject::connect(box, &QCheckBox::toggled, &dialog, update_ok);
}
if (dialog.exec() != QDialog::Accepted) {
return {};
}
QList<ConfigProfile::Part> parts;
for (ConfigProfile::Part part : ConfigProfile::choosableParts()) {
if (boxes.value(part)->isChecked()) {
parts << part;
}
}
return parts;
}
}
/**
@brief QETApp::exportConfiguration
Save the settings of QElectroTech to a file the user chooses, to keep
@@ -2298,11 +2385,19 @@ void QETApp::customizeQET(int tab)
#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.
The user first chooses which parts of the settings to save, to share
the defaults for new projects alone for example (discussion #1405).
*/
void QETApp::exportConfiguration()
{
QWidget *parent_widget = qApp->activeWindow();
const QList<ConfigProfile::Part> parts = choosePartsToSave(parent_widget);
if (parts.isEmpty()) {
return;
}
QString path = QFileDialog::getSaveFileName(
parent_widget,
tr("Save settings as...", "dialog title"),
@@ -2317,7 +2412,7 @@ void QETApp::exportConfiguration()
QSettings live_settings;
QSettings file_settings(path, QSettings::IniFormat);
ConfigProfile::exportTo(live_settings, file_settings);
ConfigProfile::exportTo(live_settings, file_settings, parts);
if (file_settings.status() != QSettings::NoError) {
QET::QetMessageBox::critical(
@@ -2330,7 +2425,10 @@ void QETApp::exportConfiguration()
/**
@brief QETApp::importConfiguration
Replace the settings of QElectroTech with a file saved by
exportConfiguration(), then close QElectroTech.
exportConfiguration(), then close QElectroTech. A file holding only
some parts of the settings replaces only those parts. A file holding
only the defaults for new projects needs no restart: they are read
again for every new project.
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
@@ -2352,8 +2450,10 @@ void QETApp::importConfiguration()
}
QSettings file_settings(path, QSettings::IniFormat);
const QList<ConfigProfile::Part> parts = ConfigProfile::partsOf(file_settings);
if (file_settings.status() != QSettings::NoError
|| !ConfigProfile::isProfile(file_settings))
|| !ConfigProfile::isProfile(file_settings)
|| parts.isEmpty())
{
QET::QetMessageBox::critical(
parent_widget,
@@ -2362,24 +2462,57 @@ void QETApp::importConfiguration()
return;
}
const bool complete = parts.contains(ConfigProfile::Part::Folders);
const bool restart = ConfigProfile::needsRestart(parts);
QString question;
if (complete) {
question = tr("These settings will replace your current ones, except the "
"window layout and the list of recent files.\n"
"\n"
"QElectroTech will then close. Start it again to use the new "
"settings.\n"
"\n"
"Do you want to continue?");
} else {
QStringList labels;
for (ConfigProfile::Part part : parts) {
labels << QStringLiteral("• ") + partLabel(part);
}
question = tr("These settings will replace your current ones for:\n"
"%1\n"
"All your other settings are kept.")
.arg(labels.join(QLatin1Char('\n')));
question += QStringLiteral("\n\n");
question += restart
? tr("QElectroTech will then close. Start it again to use the new "
"settings.")
: tr("Projects already open keep their own defaults.");
question += QStringLiteral("\n\n") + tr("Do you want to continue?");
}
//Said before anything closes: once the last window is closed,
//QElectroTech quits by itself (checkRemainingWindows()).
const auto answer = QET::QetMessageBox::question(
parent_widget,
tr("Load settings", "message box title"),
tr("These settings will replace your current ones, except the "
"window layout and the list of recent files.\n"
"\n"
"QElectroTech will then close. Start it again to use the new "
"settings.\n"
"\n"
"Do you want to continue?"),
question,
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No);
if (answer != QMessageBox::Yes) {
return;
}
if (!restart) {
QSettings live_settings;
ConfigProfile::importFrom(file_settings, live_settings);
QET::QetMessageBox::information(
parent_widget,
tr("Load settings", "message box title"),
tr("The settings are loaded. They are used for the next "
"new project."));
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
+147 -16
View File
@@ -18,6 +18,7 @@
#ifndef CONFIGPROFILE_H
#define CONFIGPROFILE_H
#include <QList>
#include <QSettings>
#include <QString>
#include <QStringList>
@@ -28,71 +29,201 @@
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
sizes, positions and dock layouts (".../geometry", ".../state",
"dialoggeometry/..."), and the recent-files lists ("...-recentfiles/..."). They are not written to
the file, and loading a profile keeps the current ones.
A file can hold every other setting (a complete profile), or only some
parts of them (discussion #1405), for example the defaults for new
projects alone, to hand to someone else. Loading a file replaces only
the parts it holds and keeps every other setting.
*/
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.
/// the live settings. 1 = a complete profile, 2 = some parts only.
/// A QElectroTech that only knows 1 refuses a file of some parts:
/// it would load it as a complete one and lose every other setting.
inline const QString marker_key{QStringLiteral("qelectrotech-configuration/format")};
/// Key listing the parts a file of format 2 holds, see partName().
inline const QString parts_key{QStringLiteral("qelectrotech-configuration/parts")};
/// The parts the settings are split into.
enum class Part {
Controls, ///< toolbars, keyboard shortcuts, shortcut bar, mouse and trackpad gestures
NewProject, ///< defaults for new projects: folio, title block, wires, numbering...
Other, ///< everything else: appearance, grid, language...
Folders ///< folders of the collections, title blocks and macros
};
/// The parts a user can choose to save. Folders are not one of
/// them: they are only saved in a complete profile, because they
/// rarely exist on another computer.
inline QList<Part> choosableParts()
{
return {Part::Controls, Part::NewProject, Part::Other};
}
/// 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.startsWith(QLatin1String("dialoggeometry/"))
|| key.contains(QLatin1String("-recentfiles/"))
|| key == marker_key;
|| key == marker_key
|| key == parts_key;
}
/// The part @a key belongs to. Every key belongs to exactly one
/// part: a key not listed here is Other.
inline Part partOf(const QString &key)
{
static const QStringList controls{
QStringLiteral("shortcuts/"),
QStringLiteral("toolbars/"),
QStringLiteral("diagrameditor/toolbars/"),
QStringLiteral("diagrameditor/custom_toolbars/"),
QStringLiteral("diagrameditor/shortcut_bar/"),
QStringLiteral("diagrameditor/gestures/"),
QStringLiteral("diagrameditor/mouse_gestures"),
QStringLiteral("diagrameditor/context_toolbar"),
QStringLiteral("diagramview/gestures")};
static const QStringList new_project{
//BorderProperties, TitleBlockProperties, ConductorProperties,
//report, cross-reference and guide defaults all start so
QStringLiteral("diagrameditor/default"),
QStringLiteral("autonum/")};
for (const QString &prefix : controls) {
if (key.startsWith(prefix)) return Part::Controls;
}
for (const QString &prefix : new_project) {
if (key.startsWith(prefix)) return Part::NewProject;
}
if (key.startsWith(QLatin1String("elements-collections/"))
&& key.endsWith(QLatin1String("-path"))) {
return Part::Folders;
}
return Part::Other;
}
/// Name of @a part in the parts_key of a file.
inline QString partName(Part part)
{
switch (part) {
case Part::Controls: return QStringLiteral("controls");
case Part::NewProject: return QStringLiteral("newproject");
case Part::Other: return QStringLiteral("other");
case Part::Folders: return QStringLiteral("folders");
}
return QString();
}
/// True if @a file was exported by exportTo().
inline bool isProfile(const QSettings &file)
{
return file.value(marker_key).toInt() == 1;
const int format = file.value(marker_key).toInt();
return format == 1 || format == 2;
}
/// 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)
/// The parts @a file holds: every part for a complete profile.
inline QList<Part> partsOf(const QSettings &file)
{
const QList<Part> all{Part::Controls, Part::NewProject,
Part::Other, Part::Folders};
if (file.value(marker_key).toInt() != 2) {
return all;
}
const QStringList names = file.value(parts_key).toString()
.split(QLatin1Char(','), Qt::SkipEmptyParts);
QList<Part> parts;
for (Part part : all) {
if (names.contains(partName(part))) {
parts << part;
}
}
return parts;
}
/// Replace the contents of @a file with the keys of @a live that
/// belong to @a parts, except the local ones. With every choosable
/// part, the file is a complete profile, folders included. Returns
/// the number of keys written.
inline int exportTo(const QSettings &live, QSettings &file,
QList<Part> parts = choosableParts())
{
const bool complete = parts.contains(Part::Controls)
&& parts.contains(Part::NewProject)
&& parts.contains(Part::Other);
if (complete) {
parts << Part::Folders;
} else {
parts.removeAll(Part::Folders);
}
file.clear();
int written = 0;
const QStringList keys = live.allKeys();
for (const QString &key : keys) {
if (isLocalKey(key)) {
if (isLocalKey(key) || !parts.contains(partOf(key))) {
continue;
}
file.setValue(key, live.value(key));
++written;
}
file.setValue(marker_key, 1);
if (complete) {
file.setValue(marker_key, 1);
} else {
QStringList names;
for (Part part : std::as_const(parts)) {
names << partName(part);
}
file.setValue(marker_key, 2);
file.setValue(parts_key, names.join(QLatin1Char(',')));
}
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.
/// Replace every key of @a live that belongs to a part @a file
/// holds, except the local ones, with the keys of @a file. A key
/// of such a part that is not in @a file is removed rather than
/// kept: loading a profile must not leave behind a setting the
/// previous profile made. Keys of the other parts are kept. Local
/// keys in @a file are ignored.
inline void importFrom(const QSettings &file, QSettings &live)
{
const QList<Part> parts = partsOf(file);
const QStringList live_keys = live.allKeys();
for (const QString &key : live_keys) {
if (!isLocalKey(key)) {
if (!isLocalKey(key) && parts.contains(partOf(key))) {
live.remove(key);
}
}
const QStringList file_keys = file.allKeys();
for (const QString &key : file_keys) {
if (!isLocalKey(key)) {
if (!isLocalKey(key) && parts.contains(partOf(key))) {
live.setValue(key, file.value(key));
}
}
live.sync();
}
/// True if loading @a parts needs QElectroTech to start again.
/// The defaults for new projects are read again for every new
/// project; every other part is read once, at start.
inline bool needsRestart(const QList<Part> &parts)
{
for (Part part : parts) {
if (part != Part::NewProject) {
return true;
}
}
return false;
}
}
#endif // CONFIGPROFILE_H
+5 -2
View File
@@ -187,10 +187,13 @@ target_link_libraries(tst_symbolscale 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)
# borderproperties.cpp: the real writer of the folio size defaults, to check
# that what it writes falls in the part for new projects.
add_executable(tst_configprofile tst_configprofile.cpp
${QET_DIR}/sources/borderproperties.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)
target_link_libraries(tst_configprofile PRIVATE Qt::Test Qt::Gui Qt::Xml)
# itemgroups.cpp keeps group membership and the selection rule apart from
# Diagram, so it is tested here on a plain scene of rectangles.
+132
View File
@@ -2,6 +2,7 @@
#include <QTemporaryDir>
#include "utils/configprofile.h"
#include "borderproperties.h"
class tst_configprofile : public QObject
{
@@ -17,6 +18,7 @@ private slots:
QVERIFY(ConfigProfile::isLocalKey("diagrameditor/geometry"));
QVERIFY(ConfigProfile::isLocalKey("elementeditor/state"));
QVERIFY(ConfigProfile::isLocalKey("projects-recentfiles/file1"));
QVERIFY(ConfigProfile::isLocalKey("dialoggeometry/ConfigDialog"));
QVERIFY(ConfigProfile::isLocalKey(ConfigProfile::marker_key));
QVERIFY(!ConfigProfile::isLocalKey("diagrameditor/Xgrid"));
QVERIFY(!ConfigProfile::isLocalKey("shortcuts/mainwindow.fullscreen"));
@@ -31,6 +33,7 @@ private slots:
live.setValue("lang", "de");
live.setValue("diagrameditor/geometry", QByteArray("xyz"));
live.setValue("projects-recentfiles/file1", "/home/a/b.qet");
live.setValue("dialoggeometry/ConfigDialog", QByteArray("xyz"));
QSettings file(path("profile1.conf"), QSettings::IniFormat);
QCOMPARE(ConfigProfile::exportTo(live, file), 2);
@@ -38,6 +41,7 @@ private slots:
QCOMPARE(file.value("diagrameditor/Xgrid").toInt(), 7);
QVERIFY(!file.contains("diagrameditor/geometry"));
QVERIFY(!file.contains("projects-recentfiles/file1"));
QVERIFY(!file.contains("dialoggeometry/ConfigDialog"));
}
// Import replaces every setting, removes the ones the profile does not
@@ -87,6 +91,134 @@ private slots:
QCOMPARE(b.value(k), a.value(k));
}
// Every key belongs to one part; keys nobody listed are Other.
void partOfKeys()
{
using P = ConfigProfile::Part;
QCOMPARE(ConfigProfile::partOf("shortcuts/mainwindow.fullscreen"), P::Controls);
QCOMPARE(ConfigProfile::partOf("toolbars/icon_size"), P::Controls);
QCOMPARE(ConfigProfile::partOf("diagrameditor/toolbars/diagram"), P::Controls);
QCOMPARE(ConfigProfile::partOf("diagrameditor/custom_toolbars/names"), P::Controls);
QCOMPARE(ConfigProfile::partOf("diagrameditor/shortcut_bar/canvas"), P::Controls);
QCOMPARE(ConfigProfile::partOf("diagrameditor/gestures/directions"), P::Controls);
QCOMPARE(ConfigProfile::partOf("diagrameditor/mouse_gestures"), P::Controls);
QCOMPARE(ConfigProfile::partOf("diagrameditor/context_toolbar"), P::Controls);
QCOMPARE(ConfigProfile::partOf("diagramview/gestures"), P::Controls);
QCOMPARE(ConfigProfile::partOf("diagrameditor/defaultconductortype"), P::NewProject);
QCOMPARE(ConfigProfile::partOf("diagrameditor/defaultreportlabel"), P::NewProject);
QCOMPARE(ConfigProfile::partOf("diagrameditor/defaultxrefcoil"), P::NewProject);
QCOMPARE(ConfigProfile::partOf("diagrameditor/defaultguides/1/position"), P::NewProject);
QCOMPARE(ConfigProfile::partOf("autonum/conductor/current"), P::NewProject);
QCOMPARE(ConfigProfile::partOf("elements-collections/custom-collection-path"), P::Folders);
QCOMPARE(ConfigProfile::partOf("elements-collections/macros-path"), P::Folders);
QCOMPARE(ConfigProfile::partOf("diagrameditor/Xgrid"), P::Other);
QCOMPARE(ConfigProfile::partOf("diagrameditor/auto_break_conductor"), P::Other);
QCOMPARE(ConfigProfile::partOf("lang"), P::Other);
QCOMPARE(ConfigProfile::partOf("customColors"), P::Other);
}
// What the New project page writes for the folio size is the
// NewProject part, whatever key names BorderProperties uses.
void folioDefaultsAreNewProject()
{
QSettings live(path("live_border.conf"), QSettings::IniFormat);
BorderProperties().toSettings(live, "diagrameditor/default");
const QStringList keys = live.allKeys();
QVERIFY(!keys.isEmpty());
for (const QString &key : keys)
QCOMPARE(ConfigProfile::partOf(key), ConfigProfile::Part::NewProject);
}
// A file of some parts holds only their keys, never the folders, and
// is format 2 so that an older QElectroTech refuses it.
void exportSomeParts()
{
QSettings live(path("live4.conf"), QSettings::IniFormat);
live.setValue("shortcuts/mainwindow.fullscreen", "F11");
live.setValue("diagrameditor/defaultconductortype", "Multi");
live.setValue("diagrameditor/Xgrid", 7);
live.setValue("elements-collections/custom-collection-path", "/home/a/sym");
QSettings file(path("profile4.conf"), QSettings::IniFormat);
QCOMPARE(ConfigProfile::exportTo(live, file, {ConfigProfile::Part::NewProject}), 1);
QCOMPARE(file.value(ConfigProfile::marker_key).toInt(), 2);
QVERIFY(ConfigProfile::isProfile(file));
QCOMPARE(ConfigProfile::partsOf(file), QList<ConfigProfile::Part>{ConfigProfile::Part::NewProject});
QVERIFY(file.contains("diagrameditor/defaultconductortype"));
QVERIFY(!file.contains("shortcuts/mainwindow.fullscreen"));
QVERIFY(!file.contains("diagrameditor/Xgrid"));
QVERIFY(!file.contains("elements-collections/custom-collection-path"));
//Other without the two others: still no folders
QSettings file2(path("profile4b.conf"), QSettings::IniFormat);
ConfigProfile::exportTo(live, file2, {ConfigProfile::Part::Other});
QVERIFY(file2.contains("diagrameditor/Xgrid"));
QVERIFY(!file2.contains("elements-collections/custom-collection-path"));
}
// Every choosable part is a complete profile: format 1, folders
// included, readable by the QElectroTech that only knows format 1.
void exportEveryPartIsComplete()
{
QSettings live(path("live5.conf"), QSettings::IniFormat);
live.setValue("diagrameditor/Xgrid", 7);
live.setValue("elements-collections/custom-collection-path", "/home/a/sym");
QSettings file(path("profile5.conf"), QSettings::IniFormat);
ConfigProfile::exportTo(live, file, ConfigProfile::choosableParts());
QCOMPARE(file.value(ConfigProfile::marker_key).toInt(), 1);
QVERIFY(!file.contains(ConfigProfile::parts_key));
QVERIFY(file.contains("elements-collections/custom-collection-path"));
QCOMPARE(ConfigProfile::partsOf(file).size(), 4);
}
// Loading some parts replaces those parts only, removes their keys the
// file lacks, and keeps every other setting, folders included.
void importSomePartsKeepsTheRest()
{
QSettings file(path("profile6.conf"), QSettings::IniFormat);
file.setValue("diagrameditor/defaultconductortype", "Single");
file.setValue("diagrameditor/Xgrid", 99); // not in its parts: ignored
file.setValue(ConfigProfile::marker_key, 2);
file.setValue(ConfigProfile::parts_key, "newproject");
QSettings live(path("live6.conf"), QSettings::IniFormat);
live.setValue("diagrameditor/defaultconductortype", "Multi");
live.setValue("autonum/folio/current", "mine");
live.setValue("diagrameditor/Xgrid", 10);
live.setValue("shortcuts/mainwindow.fullscreen", "F11");
live.setValue("elements-collections/custom-collection-path", "/home/me/sym");
ConfigProfile::importFrom(file, live);
QCOMPARE(live.value("diagrameditor/defaultconductortype").toString(), QString("Single"));
QVERIFY(!live.contains("autonum/folio/current"));
QCOMPARE(live.value("diagrameditor/Xgrid").toInt(), 10);
QCOMPARE(live.value("shortcuts/mainwindow.fullscreen").toString(), QString("F11"));
QCOMPARE(live.value("elements-collections/custom-collection-path").toString(),
QString("/home/me/sym"));
QVERIFY(!live.contains(ConfigProfile::parts_key));
}
void restartOnlyWhenNeeded()
{
using P = ConfigProfile::Part;
QVERIFY(!ConfigProfile::needsRestart({P::NewProject}));
QVERIFY(ConfigProfile::needsRestart({P::NewProject, P::Controls}));
QVERIFY(ConfigProfile::needsRestart({P::Other}));
}
// A file of format 2 naming no known part is refused by the caller.
void unknownPartsGiveNothing()
{
QSettings file(path("profile7.conf"), QSettings::IniFormat);
file.setValue(ConfigProfile::marker_key, 2);
file.setValue(ConfigProfile::parts_key, "colours");
QVERIFY(ConfigProfile::partsOf(file).isEmpty());
}
void plainFileIsNotAProfile()
{
QSettings other(path("other.conf"), QSettings::IniFormat);