diff --git a/lang/qet_en.ts b/lang/qet_en.ts index e76b18c19..4368b8d93 100644 --- a/lang/qet_en.ts +++ b/lang/qet_en.ts @@ -1936,7 +1936,7 @@ Note: these options DO NOT allow or block auto numberings, only their update pol <Shift> to move -<Shift> to move +Hold Shift and drag to move this text on its own diff --git a/lang/qet_fr.ts b/lang/qet_fr.ts index 6a0a85ece..01e15a8c4 100644 --- a/lang/qet_fr.ts +++ b/lang/qet_fr.ts @@ -1935,7 +1935,7 @@ Remarque: Ces options n'autorisent ou bloquent l'auto numérotation, s <Shift> to move -<Shift> pour déplacer +Maintenir Maj et faire glisser pour déplacer ce texte seul diff --git a/sources/ElementsCollection/elementcollectionitem.cpp b/sources/ElementsCollection/elementcollectionitem.cpp index 3e63c8c8c..ac63291b6 100644 --- a/sources/ElementsCollection/elementcollectionitem.cpp +++ b/sources/ElementsCollection/elementcollectionitem.cpp @@ -18,6 +18,9 @@ #include "elementcollectionitem.h" +#include +#include + /** @brief ElementCollectionItem::ElementCollectionItem Constructor @@ -247,6 +250,73 @@ QList ElementCollectionItem::items() const return list; } +/** + @brief onGuiThread + @return true when called from the thread the application lives in +*/ +static bool onGuiThread() +{ + return QThread::currentThread() == QCoreApplication::instance()->thread(); +} + +/** + @brief ElementCollectionItem::setData + ElementsCollectionModel::loadCollections() runs setUpData() for every + item on worker threads (QtConcurrent::map), while the items are + already in the model. QStandardItem::setData() updates the model and + emits its dataChanged() signal, which is not safe from a worker + thread. So a value set from a worker thread is kept on the item, and + the model applies it on the GUI thread with applyDeferredData() once + every item is set up. setText(), setFlags(), setToolTip() and setIcon() + all end up here. + @param value + @param role +*/ +void ElementCollectionItem::setData(const QVariant &value, int role) +{ + if (role == Qt::EditRole) + role = Qt::DisplayRole; + + if (onGuiThread()) + QStandardItem::setData(value, role); + else + m_deferred_data.insert(role, value); +} + +/** + @brief ElementCollectionItem::data + On a worker thread, a value set by setData() but not applied yet is + returned, so setUpData() reads back what it has just set + (localName() tests text() for example). + @param role + @return +*/ +QVariant ElementCollectionItem::data(int role) const +{ + if (role == Qt::EditRole) + role = Qt::DisplayRole; + + if (!onGuiThread()) + { + const auto it = m_deferred_data.constFind(role); + if (it != m_deferred_data.constEnd()) + return it.value(); + } + return QStandardItem::data(role); +} + +/** + @brief ElementCollectionItem::applyDeferredData + Apply the values set from a worker thread, see setData(). + Must be called on the GUI thread, after the worker is done. +*/ +void ElementCollectionItem::applyDeferredData() +{ + for (auto it = m_deferred_data.constBegin() ; it != m_deferred_data.constEnd() ; ++it) + QStandardItem::setData(it.value(), it.key()); + m_deferred_data.clear(); +} + void setUpData(ElementCollectionItem *eci) { eci->setUpData(); } diff --git a/sources/ElementsCollection/elementcollectionitem.h b/sources/ElementsCollection/elementcollectionitem.h index ad12477c1..057215662 100644 --- a/sources/ElementsCollection/elementcollectionitem.h +++ b/sources/ElementsCollection/elementcollectionitem.h @@ -18,6 +18,7 @@ #ifndef ELEMENTCOLLECTIONITEM2_H #define ELEMENTCOLLECTIONITEM2_H +#include #include /** @@ -56,6 +57,16 @@ class ElementCollectionItem : public QStandardItem QList elementsChild() const; QList directoriesChild() const; QList items() const; + + QVariant data(int role = Qt::UserRole + 1) const override; + void setData(const QVariant &value, int role = Qt::UserRole + 1) override; + void applyDeferredData(); + + private: + /// Values set by setUpData() while it runs on a worker thread, + /// see setData(). Only ever touched by that one worker thread, + /// until applyDeferredData() empties it on the GUI thread. + QHash m_deferred_data; }; void setUpData(ElementCollectionItem *eci); diff --git a/sources/ElementsCollection/elementscollectionmodel.cpp b/sources/ElementsCollection/elementscollectionmodel.cpp index 00ef0d2eb..440f10417 100644 --- a/sources/ElementsCollection/elementscollectionmodel.cpp +++ b/sources/ElementsCollection/elementscollectionmodel.cpp @@ -312,8 +312,15 @@ void ElementsCollectionModel::loadCollections(bool common_collection, this, &ElementsCollectionModel::loadingProgressValueChanged); connect(watcher, &QFutureWatcher::progressRangeChanged, this, &ElementsCollectionModel::loadingProgressRangeChanged); - connect(watcher, &QFutureWatcher::finished, - this, &ElementsCollectionModel::loadingFinished); + //setUpData() ran on worker threads, which only kept the values on + //the items (ElementCollectionItem::setData()): apply them here, on + //the GUI thread, before anyone is told the loading is finished. + connect(watcher, &QFutureWatcher::finished, this, [this]() + { + for (ElementCollectionItem *eci : std::as_const(m_items_list_to_setUp)) + eci->applyDeferredData(); + emit loadingFinished(); + }); connect(watcher, &QFutureWatcher::finished, watcher, &QFutureWatcher::deleteLater); diff --git a/sources/ElementsCollection/ui/renamedialog.cpp b/sources/ElementsCollection/ui/renamedialog.cpp index fe8558b14..c7e761b22 100644 --- a/sources/ElementsCollection/ui/renamedialog.cpp +++ b/sources/ElementsCollection/ui/renamedialog.cpp @@ -20,12 +20,14 @@ #include #include "ui_renamedialog.h" +#include "../../qet.h" RenameDialog::RenameDialog(QString path, QWidget *parent) : QDialog(parent), ui(new Ui::RenameDialog), m_path(std::move(path)) { ui->setupUi(this); + QET::trackDialogGeometry(this); m_name = m_path.split("/").last(); if (m_name.endsWith(".elmt")) m_name.remove(".elmt"); ui->m_label->setText(tr("L'élément « %1 » existe déjà. Que souhaitez-vous faire ?").arg(m_path)); diff --git a/sources/NameList/ui/namelistdialog.cpp b/sources/NameList/ui/namelistdialog.cpp index bb942536b..71e606a4b 100644 --- a/sources/NameList/ui/namelistdialog.cpp +++ b/sources/NameList/ui/namelistdialog.cpp @@ -19,6 +19,7 @@ #include "ui_namelistdialog.h" #include "namelistwidget.h" +#include "../../qet.h" #include #include @@ -27,6 +28,7 @@ NameListDialog::NameListDialog(QWidget *parent) : ui(new Ui::NameListDialog) { ui->setupUi(this); + QET::trackDialogGeometry(this); m_namelist_widget = new NameListWidget(this); ui->m_main_layout->insertWidget(1, m_namelist_widget); diff --git a/sources/PropertiesEditor/propertieseditordialog.h b/sources/PropertiesEditor/propertieseditordialog.h index 50cb96aef..b9e3bae86 100644 --- a/sources/PropertiesEditor/propertieseditordialog.h +++ b/sources/PropertiesEditor/propertieseditordialog.h @@ -18,6 +18,8 @@ #ifndef PROPERTIESEDITORDIALOG_H #define PROPERTIESEDITORDIALOG_H +#include "../qet.h" + #include #include #include @@ -74,6 +76,14 @@ class PropertiesEditorDialog : public QDialog this); vlayout->addWidget(button_box); + // Keyed on the wrapped editor's own class, not this + // dialog's -- every instantiation of this constructor + // template is the same PropertiesEditorDialog class, + // so using its class name here would have every kind + // of wrapped editor (conductor properties, element + // properties, ...) fight over one saved size. + QET::trackDialogGeometry(this, editor->metaObject()->className()); + //Setup connection between button box and the editor connect(button_box, &QDialogButtonBox::clicked, diff --git a/sources/SearchAndReplace/ui/replaceadvanceddialog.cpp b/sources/SearchAndReplace/ui/replaceadvanceddialog.cpp index 81f9f9e83..a728ab374 100644 --- a/sources/SearchAndReplace/ui/replaceadvanceddialog.cpp +++ b/sources/SearchAndReplace/ui/replaceadvanceddialog.cpp @@ -21,6 +21,7 @@ #include "../../qetinformation.h" #include "ui_replaceadvanceddialog.h" +#include "../../qet.h" #include /** @@ -33,6 +34,7 @@ replaceAdvancedDialog::replaceAdvancedDialog(advancedReplaceStruct advanced, QWi ui(new Ui::replaceAdvancedDialog) { ui->setupUi(this); + QET::trackDialogGeometry(this); connect(ui->m_button_box, &QDialogButtonBox::clicked, [this](QAbstractButton *button_) { this->done(ui->m_button_box->buttonRole(button_)); diff --git a/sources/SearchAndReplace/ui/replaceconductordialog.cpp b/sources/SearchAndReplace/ui/replaceconductordialog.cpp index 112452846..9b8fd7017 100644 --- a/sources/SearchAndReplace/ui/replaceconductordialog.cpp +++ b/sources/SearchAndReplace/ui/replaceconductordialog.cpp @@ -20,6 +20,7 @@ #include "../searchandreplaceworker.h" #include "ui_replaceconductordialog.h" +#include "../../qet.h" #include #include @@ -37,6 +38,7 @@ ReplaceConductorDialog::ReplaceConductorDialog( ui(new Ui::ReplaceConductorDialog) { ui->setupUi(this); + QET::trackDialogGeometry(this); initWidget(); setProperties(properties); } diff --git a/sources/SearchAndReplace/ui/replaceelementdialog.cpp b/sources/SearchAndReplace/ui/replaceelementdialog.cpp index d1fa0e34b..ad5dfbf3f 100644 --- a/sources/SearchAndReplace/ui/replaceelementdialog.cpp +++ b/sources/SearchAndReplace/ui/replaceelementdialog.cpp @@ -24,6 +24,7 @@ #include "replaceelementdialog.h" #include "ui_replaceelementdialog.h" +#include "../../qet.h" #include ReplaceElementDialog::ReplaceElementDialog(DiagramContext context, QWidget *parent) : @@ -31,6 +32,7 @@ ReplaceElementDialog::ReplaceElementDialog(DiagramContext context, QWidget *pare ui(new Ui::ReplaceElementDialog) { ui->setupUi(this); + QET::trackDialogGeometry(this); buildWidget(); setContext(context); } diff --git a/sources/SearchAndReplace/ui/replacefoliowidget.cpp b/sources/SearchAndReplace/ui/replacefoliowidget.cpp index ea75b87cf..be11c4352 100644 --- a/sources/SearchAndReplace/ui/replacefoliowidget.cpp +++ b/sources/SearchAndReplace/ui/replacefoliowidget.cpp @@ -18,6 +18,7 @@ #include "replacefoliowidget.h" #include "../../ui/diagramcontextwidget.h" +#include "../../qet.h" #include "../searchandreplaceworker.h" #include "ui_replacefoliowidget.h" @@ -132,6 +133,8 @@ ReplaceFolioDialog::ReplaceFolioDialog(QWidget *parent) : { this->done(m_button_box->buttonRole(button_)); }); + + QET::trackDialogGeometry(this); } ReplaceFolioDialog::~ReplaceFolioDialog() diff --git a/sources/TerminalStrip/ui/addterminalstripitemdialog.cpp b/sources/TerminalStrip/ui/addterminalstripitemdialog.cpp index c61b67ad6..fe4c77d8b 100644 --- a/sources/TerminalStrip/ui/addterminalstripitemdialog.cpp +++ b/sources/TerminalStrip/ui/addterminalstripitemdialog.cpp @@ -23,6 +23,7 @@ #include "../GraphicsItem/terminalstripitem.h" #include "../../diagram.h" +#include "../../qet.h" void AddTerminalStripItemDialog::openDialog(Diagram *diagram, QWidget *parent) { AddTerminalStripItemDialog d(diagram->project(), parent); @@ -46,6 +47,7 @@ AddTerminalStripItemDialog::AddTerminalStripItemDialog(QETProject *project, QWid ui{new Ui::AddTerminalStripItemDialog} { ui->setupUi(this); + QET::trackDialogGeometry(this); fillComboBox(); } diff --git a/sources/TerminalStrip/ui/terminalstripcreatordialog.cpp b/sources/TerminalStrip/ui/terminalstripcreatordialog.cpp index 52363065e..3cf5d7e4c 100644 --- a/sources/TerminalStrip/ui/terminalstripcreatordialog.cpp +++ b/sources/TerminalStrip/ui/terminalstripcreatordialog.cpp @@ -20,6 +20,7 @@ #include "../terminalstrip.h" #include "../../qetproject.h" +#include "../../qet.h" /** * @brief TerminalStripCreatorDialog::TerminalStripCreatorDialog * @param project : Project to add a new terminal strip @@ -31,6 +32,7 @@ TerminalStripCreatorDialog::TerminalStripCreatorDialog(QETProject *project, QWid m_project(project) { ui->setupUi(this); + QET::trackDialogGeometry(this); } /** diff --git a/sources/TerminalStrip/ui/terminalstriptreedockwidget.cpp b/sources/TerminalStrip/ui/terminalstriptreedockwidget.cpp index b2e2aa1db..fd81bd61e 100644 --- a/sources/TerminalStrip/ui/terminalstriptreedockwidget.cpp +++ b/sources/TerminalStrip/ui/terminalstriptreedockwidget.cpp @@ -62,7 +62,9 @@ void TerminalStripTreeDockWidget::setProject(QETProject *project) } m_project = project; if (m_project) { - m_project_destroy_connection = connect(m_project, &QObject::destroyed, [this](){ + //`this` as context: this dock can be deleted before the project + //(with the editor window that owns it), and the connection must go with it + m_project_destroy_connection = connect(m_project, &QObject::destroyed, this, [this](){ this->m_current_strip.clear(); this->reload(); }); diff --git a/sources/autoNum/ui/renumberelementsdialog.cpp b/sources/autoNum/ui/renumberelementsdialog.cpp index a8c4c9dd1..7d9cfe48a 100644 --- a/sources/autoNum/ui/renumberelementsdialog.cpp +++ b/sources/autoNum/ui/renumberelementsdialog.cpp @@ -17,6 +17,7 @@ */ #include "renumberelementsdialog.h" +#include "../../qet.h" #include #include @@ -60,6 +61,8 @@ RenumberElementsDialog::RenumberElementsDialog(const QStringList &scheme_titles, connect(m_all_rb, &QRadioButton::toggled, this, &RenumberElementsDialog::updateUi); connect(m_one_rb, &QRadioButton::toggled, this, &RenumberElementsDialog::updateUi); + + QET::trackDialogGeometry(this); } QString RenumberElementsDialog::selectedSchemeTitle() const diff --git a/sources/configdialog.cpp b/sources/configdialog.cpp index 10a0d0e0e..1e4f2bbe2 100644 --- a/sources/configdialog.cpp +++ b/sources/configdialog.cpp @@ -19,6 +19,7 @@ #include "configdialog.h" #include "ui/configpage/configpages.h" #include "qetapp.h" +#include "qet.h" #include "machine_info.h" @@ -89,6 +90,8 @@ ConfigDialog::ConfigDialog(QWidget *parent) : QDialog(parent) { setWindowFlags(Qt::Sheet); } #endif + + QET::trackDialogGeometry(this); } /// Destructeur diff --git a/sources/diagramview.cpp b/sources/diagramview.cpp index b4c5cf126..d74e524b4 100644 --- a/sources/diagramview.cpp +++ b/sources/diagramview.cpp @@ -45,13 +45,20 @@ #include "ElementsCollection/xmlelementcollection.h" #include "NameList/nameslist.h" #include "elementdialog.h" +#include "qetapp.h" +#include "qetgraphicsitem/element.h" +#include "qetinformation.h" +#include "qetversion.h" #include #include #include #include #include +#include #include +static bool hasCabinetInfo(const DiagramContext &infos); + /** Constructeur @param diagram Schema a afficher ; si diagram vaut 0, un nouveau Diagram est utilise @@ -103,6 +110,10 @@ DiagramView::DiagramView(Diagram *diagram, QWidget *parent) : m_create_template = new QAction(tr("Créer un template", "context menu action"), this); connect(m_create_template, &QAction::triggered, this, &DiagramView::createTemplateFromSelection); + //Setup the action to generate cabinet placement thumbnails (discussion #602) + m_generate_cabinet_thumbnail = new QAction(tr("Générer une vignette d'armoire", "context menu action"), this); + connect(m_generate_cabinet_thumbnail, &QAction::triggered, this, &DiagramView::generateCabinetThumbnails); + //Filled each time the context menu opens, see updateFolioReportMenu() m_folio_report_menu = new QMenu(tr("Renvoi de folio"), this); @@ -1765,7 +1776,8 @@ QList DiagramView::contextMenuActions() const QList list; if (QETDiagramEditor *qde = diagramEditor()) { - if (m_diagram->selectedItems().isEmpty()) + const QList selected_items = m_diagram->selectedItems(); + if (selected_items.isEmpty()) { //Drawing comes first. The row and column actions change //the folio's layout and are rarely wanted, so they sit one @@ -1786,6 +1798,15 @@ QList DiagramView::contextMenuActions() const list << m_multi_paste; list << m_separators.at(0); list << m_create_template; // Add the create template action + //Disabled, and so left out below, unless a selected element + //has both a manufacturer and a manufacturer reference. + m_generate_cabinet_thumbnail->setEnabled(std::any_of( + selected_items.cbegin(), selected_items.cend(), + [](QGraphicsItem *qgi) { + const Element *element = qgraphicsitem_cast(qgi); + return element && hasCabinetInfo(element->elementInformations()); + })); + list << m_generate_cabinet_thumbnail; list << qde->m_conductor_reset; list << m_separators.at(1); list << qde->m_selection_actions_group.actions(); @@ -2073,6 +2094,166 @@ void DiagramView::createTemplateFromSelection() } } +/** + @brief hasCabinetInfo + @return true if @a infos has both a manufacturer and a manufacturer + reference, the two values a cabinet thumbnail shows. +*/ +static bool hasCabinetInfo(const DiagramContext &infos) +{ + return !infos[QETInformation::ELMT_MANUFACTURER].toString().trimmed().isEmpty() + && !infos[QETInformation::ELMT_MANUFACTURER_REF].toString().trimmed().isEmpty(); +} + +/** + @brief cabinetThumbnailDefinition + @return the definition of a thumbnail element, a 120x30 frame showing + the manufacturer and the manufacturer reference in a dynamic text. + The two values are also stored in the definition's element + information, so the thumbnail shows them as soon as it is placed. +*/ +static QDomElement cabinetThumbnailDefinition(QDomDocument &doc, + const QString &display_name, + const QString &manufacturer, + const QString &reference) +{ + QDomElement definition = doc.createElement(QStringLiteral("definition")); + definition.setAttribute(QStringLiteral("version"), QetVersion::currentVersion().toString()); + definition.setAttribute(QStringLiteral("type"), QStringLiteral("element")); + definition.setAttribute(QStringLiteral("link_type"), QStringLiteral("thumbnail")); + definition.setAttribute(QStringLiteral("width"), 120); + definition.setAttribute(QStringLiteral("height"), 30); + definition.setAttribute(QStringLiteral("hotspot_x"), 60); + definition.setAttribute(QStringLiteral("hotspot_y"), 15); + doc.appendChild(definition); + + QDomElement uuid = doc.createElement(QStringLiteral("uuid")); + uuid.setAttribute(QStringLiteral("uuid"), QUuid::createUuid().toString()); + definition.appendChild(uuid); + + NamesList names; + names.addName(QStringLiteral("en"), display_name); + definition.appendChild(names.toXml(doc)); + + QDomElement informations = doc.createElement(QStringLiteral("elementInformations")); + DiagramContext infos; + infos.addValue(QETInformation::ELMT_MANUFACTURER, manufacturer); + infos.addValue(QETInformation::ELMT_MANUFACTURER_REF, reference); + infos.toXml(informations, QStringLiteral("elementInformation")); + definition.appendChild(informations); + + QDomElement description = doc.createElement(QStringLiteral("description")); + + QDomElement rect = doc.createElement(QStringLiteral("rect")); + rect.setAttribute(QStringLiteral("x"), -60); + rect.setAttribute(QStringLiteral("y"), -15); + rect.setAttribute(QStringLiteral("width"), 120); + rect.setAttribute(QStringLiteral("height"), 30); + rect.setAttribute(QStringLiteral("rx"), 0); + rect.setAttribute(QStringLiteral("ry"), 0); + rect.setAttribute(QStringLiteral("style"), + QStringLiteral("line-style:normal;line-weight:normal;filling:none;color:black")); + rect.setAttribute(QStringLiteral("antialias"), QStringLiteral("false")); + description.appendChild(rect); + + QDomElement text = doc.createElement(QStringLiteral("dynamic_text")); + text.setAttribute(QStringLiteral("x"), -57); + text.setAttribute(QStringLiteral("y"), -14); + text.setAttribute(QStringLiteral("z"), 1); + text.setAttribute(QStringLiteral("text_width"), 114); + text.setAttribute(QStringLiteral("Halignment"), QStringLiteral("AlignLeft")); + text.setAttribute(QStringLiteral("Valignment"), QStringLiteral("AlignTop")); + text.setAttribute(QStringLiteral("frame"), QStringLiteral("false")); + text.setAttribute(QStringLiteral("rotation"), 0); + text.setAttribute(QStringLiteral("keep_visual_rotation"), QStringLiteral("false")); + text.setAttribute(QStringLiteral("text_from"), QStringLiteral("CompositeText")); + text.setAttribute(QStringLiteral("uuid"), QUuid::createUuid().toString()); + text.setAttribute(QStringLiteral("font"), QETApp::dynamicTextsItemFont(7).toString()); + + QDomElement text_cache = doc.createElement(QStringLiteral("text")); + text_cache.appendChild(doc.createTextNode(manufacturer + QLatin1Char('\n') + reference)); + text.appendChild(text_cache); + + QDomElement composite_text = doc.createElement(QStringLiteral("composite_text")); + composite_text.appendChild(doc.createTextNode(QStringLiteral("%{manufacturer}\n%{manufacturer_reference}"))); + text.appendChild(composite_text); + + description.appendChild(text); + definition.appendChild(description); + return definition; +} + +/** + @brief DiagramView::generateCabinetThumbnails + Context menu action (discussion #602). For every selected element with + both a manufacturer and a manufacturer reference, add a thumbnail + element showing them to the folder "Cabinet thumbnails" of this + project's embedded collection, from where it can be placed on a + cabinet layout folio like any other element. A device that already + has a thumbnail there is skipped, so placing it several times does + not pile up copies. +*/ +void DiagramView::generateCabinetThumbnails() +{ + QETProject *project = m_diagram->project(); + if (!project || project->isReadOnly()) { + return; + } + XmlElementCollection *collection = project->embeddedElementCollection(); + if (!collection) { + return; + } + + const QString dir_name = QStringLiteral("Cabinet thumbnails"); + const QString dir_path = QStringLiteral("import/") + dir_name; + if (!collection->exist(dir_path)) + { + NamesList dir_names; + dir_names.addName(QStringLiteral("en"), dir_name); + //Stored per language, like an element's names: not tr(), which + //would store the interface's language under "fr" + dir_names.addName(QStringLiteral("fr"), QStringLiteral("Vignettes d'armoire")); + if (!collection->createDir(QStringLiteral("import"), dir_name, dir_names)) { + return; + } + } + + QStringList existing = collection->elementsNames(collection->directory(dir_path)); + int created = 0; + const QList selected_items = m_diagram->selectedItems(); + for (QGraphicsItem *qgi : selected_items) + { + const Element *element = qgraphicsitem_cast(qgi); + if (!element || !hasCabinetInfo(element->elementInformations())) { + continue; + } + const DiagramContext infos = element->elementInformations(); + const QString manufacturer = infos[QETInformation::ELMT_MANUFACTURER].toString().trimmed(); + const QString reference = infos[QETInformation::ELMT_MANUFACTURER_REF].toString().trimmed(); + + //The display name keeps the values as typed; the file name + //cannot hold characters such as '/', which would be read as + //a folder. + const QString display_name = manufacturer + QLatin1Char(' ') + reference; + const QString file_name = QET::stringToFileName(display_name) + QStringLiteral(".elmt"); + if (existing.contains(file_name)) { + continue; + } + + QDomDocument doc; + const QDomElement definition = cabinetThumbnailDefinition(doc, display_name, manufacturer, reference); + if (collection->addElementDefinition(dir_path, file_name, definition)) + { + existing << file_name; + ++created; + } + } + + if (created) { + project->setModified(true); + } +} + /** @return l'editeur de schemas parent ou 0 */ diff --git a/sources/diagramview.h b/sources/diagramview.h index 9ac5a5ad8..11f53ddf2 100644 --- a/sources/diagramview.h +++ b/sources/diagramview.h @@ -58,6 +58,7 @@ class DiagramView : public PaletteGraphicsView QAction *m_paste_here = nullptr; QAction *m_multi_paste = nullptr; QAction *m_create_template = nullptr; + QAction *m_generate_cabinet_thumbnail = nullptr; QMenu *m_folio_report_menu = nullptr; QPoint m_paste_here_pos; QPoint m_press_pos; @@ -197,5 +198,6 @@ class DiagramView : public PaletteGraphicsView void adjustGridToZoom(); void applyReadOnly(); void createTemplateFromSelection(); + void generateCabinetThumbnails(); }; #endif diff --git a/sources/editor/elementscene.cpp b/sources/editor/elementscene.cpp index c0105ae83..1ae830123 100644 --- a/sources/editor/elementscene.cpp +++ b/sources/editor/elementscene.cpp @@ -18,6 +18,7 @@ #include "elementscene.h" #include "../ElementsCollection/terminaluuids.h" +#include "../borderproperties.h" #include "../NameList/ui/namelistdialog.h" #include "../NameList/ui/namelistwidget.h" #include "../QPropertyUndoCommand/qpropertyundocommand.h" @@ -72,6 +73,49 @@ ElementScene::ElementScene(QETElementEditor *editor, QObject *parent) : m_decorator_lock = new QMutex(); connect(&m_undo_stack, &QUndoStack::indexChanged, this, &ElementScene::managePrimitivesGroups); connect(this, &ElementScene::selectionChanged, this, &ElementScene::managePrimitivesGroups); + + QSettings settings; + m_background_frame_visible = settings.value( + QStringLiteral("elementeditor/background_frame_visible"), false).toBool(); + BorderProperties bp = BorderProperties::defaultProperties(); + m_background_frame_size = QSizeF( + settings.value(QStringLiteral("elementeditor/background_frame_width"), + bp.columns_count * bp.columns_width).toReal(), + settings.value(QStringLiteral("elementeditor/background_frame_height"), + bp.rows_count * bp.rows_height).toReal()); +} + +/** + @brief ElementScene::setBackgroundFrameVisible + Toggle the visual-only background frame used to proportion this + element's drawing against a representative folio surface. Like the + hotspot indicator, this frame is never written to the saved .elmt file. + @param visible +*/ +void ElementScene::setBackgroundFrameVisible(bool visible) +{ + if (m_background_frame_visible == visible) { + return; + } + m_background_frame_visible = visible; + QSettings().setValue(QStringLiteral("elementeditor/background_frame_visible"), visible); + update(); +} + +/** + @brief ElementScene::setBackgroundFrameSize + @param size the new size (in scene/grid units) of the background frame +*/ +void ElementScene::setBackgroundFrameSize(const QSizeF &size) +{ + if (m_background_frame_size == size) { + return; + } + m_background_frame_size = size; + QSettings settings; + settings.setValue(QStringLiteral("elementeditor/background_frame_width"), size.width()); + settings.setValue(QStringLiteral("elementeditor/background_frame_height"), size.height()); + update(); } /** diff --git a/sources/editor/elementscene.h b/sources/editor/elementscene.h index a53afff6f..e4f086078 100644 --- a/sources/editor/elementscene.h +++ b/sources/editor/elementscene.h @@ -92,6 +92,8 @@ class ElementScene : public QGraphicsScene QPointer m_single_selected_item; bool m_hotspot_visible = true; + bool m_background_frame_visible = false; + QSizeF m_background_frame_size; // methods public: @@ -142,6 +144,11 @@ class ElementScene : public QGraphicsScene /// not part of the drawn symbol. void setHotspotVisible(bool visible) {m_hotspot_visible = visible;} bool hotspotVisible() const {return m_hotspot_visible;} + + bool backgroundFrameVisible() const {return m_background_frame_visible;} + void setBackgroundFrameVisible(bool visible); + QSizeF backgroundFrameSize() const {return m_background_frame_size;} + void setBackgroundFrameSize(const QSizeF &size); protected: void mouseMoveEvent (QGraphicsSceneMouseEvent *) override; diff --git a/sources/editor/elementview.cpp b/sources/editor/elementview.cpp index c9d254f70..6eed40457 100644 --- a/sources/editor/elementview.cpp +++ b/sources/editor/elementview.cpp @@ -606,6 +606,19 @@ void ElementView::drawBackground(QPainter *p, const QRectF &r) { } } } + + if (m_scene->backgroundFrameVisible()) { + const QSizeF frame_size = m_scene->backgroundFrameSize(); + const QRectF frame_rect(-frame_size.width() / 2.0, -frame_size.height() / 2.0, + frame_size.width(), frame_size.height()); + QPen frame_pen(Qt::blue); + frame_pen.setCosmetic(true); + frame_pen.setStyle(Qt::DashLine); + p -> setPen(frame_pen); + p -> setBrush(Qt::NoBrush); + p -> drawRect(frame_rect); + } + p -> restore(); } diff --git a/sources/editor/ui/elementpropertieseditorwidget.cpp b/sources/editor/ui/elementpropertieseditorwidget.cpp index 6e16ffa1b..78386abd1 100644 --- a/sources/editor/ui/elementpropertieseditorwidget.cpp +++ b/sources/editor/ui/elementpropertieseditorwidget.cpp @@ -22,6 +22,7 @@ #include "../../qetinformation.h" #include "ui_elementpropertieseditorwidget.h" +#include "../../qet.h" #include #include #include @@ -95,6 +96,7 @@ ElementPropertiesEditorWidget::ElementPropertiesEditorWidget(ElementData data, Q m_data(data) { ui->setupUi(this); + QET::trackDialogGeometry(this); setUpInterface(); upDateInterface(); } diff --git a/sources/editor/ui/qetelementeditor.cpp b/sources/editor/ui/qetelementeditor.cpp index 5428ec1b9..8489f8def 100644 --- a/sources/editor/ui/qetelementeditor.cpp +++ b/sources/editor/ui/qetelementeditor.cpp @@ -59,6 +59,10 @@ #include #include #include +#include +#include +#include +#include /** * @brief QETElementEditor::QETElementEditor @@ -1138,6 +1142,47 @@ void QETElementEditor::setupActions() parts_toolbar -> setObjectName("parts"); parts_toolbar -> addActions(m_add_part_action_grp -> actions()); addToolBar(Qt::LeftToolBarArea, parts_toolbar); + + //Background frame action: a visual-only reference rectangle, never + //written to the saved .elmt file, to help proportion the drawing + //against a representative folio surface. + auto *toggle_background_frame_action = new QAction(QET::Icons::DocumentPrintFrame, tr("Afficher le cadre de fond"), this); + toggle_background_frame_action -> setCheckable(true); + toggle_background_frame_action -> setChecked(m_elmt_scene -> backgroundFrameVisible()); + connect(toggle_background_frame_action, &QAction::toggled, m_elmt_scene, &ElementScene::setBackgroundFrameVisible); + ShortcutManager::instance().registerAction(toggle_background_frame_action, "elementeditor.toggle_background_frame", tr("Éditeur d'élément"), QKeySequence()); + ui->m_display_menu->addAction(toggle_background_frame_action); + ui->m_view_toolbar->addAction(toggle_background_frame_action); + + auto *configure_background_frame_action = new QAction(tr("Taille du cadre de fond..."), this); + connect(configure_background_frame_action, &QAction::triggered, this, [this]() { + QDialog dialog(this); + dialog.setWindowTitle(tr("Taille du cadre de fond")); + auto *layout = new QFormLayout(&dialog); + + auto *width_spin = new QDoubleSpinBox(&dialog); + width_spin -> setRange(1.0, 100000.0); + width_spin -> setSuffix(tr(" px")); + width_spin -> setValue(m_elmt_scene -> backgroundFrameSize().width()); + layout -> addRow(tr("Largeur"), width_spin); + + auto *height_spin = new QDoubleSpinBox(&dialog); + height_spin -> setRange(1.0, 100000.0); + height_spin -> setSuffix(tr(" px")); + height_spin -> setValue(m_elmt_scene -> backgroundFrameSize().height()); + layout -> addRow(tr("Hauteur"), height_spin); + + auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); + connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + layout -> addRow(buttons); + + if (dialog.exec() == QDialog::Accepted) { + m_elmt_scene -> setBackgroundFrameSize(QSizeF(width_spin -> value(), height_spin -> value())); + } + }); + ShortcutManager::instance().registerAction(configure_background_frame_action, "elementeditor.configure_background_frame", tr("Éditeur d'élément"), QKeySequence()); + ui->m_display_menu->addAction(configure_background_frame_action); } /** diff --git a/sources/elementdialog.cpp b/sources/elementdialog.cpp index 503aefc6b..64d62bd46 100644 --- a/sources/elementdialog.cpp +++ b/sources/elementdialog.cpp @@ -20,6 +20,7 @@ #include "ElementsCollection/elementcollectionitem.h" #include "ElementsCollection/elementscollectionmodel.h" #include "ElementsCollection/elementstreeview.h" +#include "qet.h" #include "qetapp.h" #include "qetmessagebox.h" #include "qfilenameedit.h" @@ -41,6 +42,7 @@ ElementDialog::ElementDialog(uint mode, QWidget *parent) : { setUpWidget(); setUpConnection(); + QET::trackDialogGeometry(this); } /** diff --git a/sources/elementscategoryeditor.cpp b/sources/elementscategoryeditor.cpp index 600b7e28e..455f4e115 100644 --- a/sources/elementscategoryeditor.cpp +++ b/sources/elementscategoryeditor.cpp @@ -41,6 +41,7 @@ ElementsCategoryEditor::ElementsCategoryEditor(const ElementsLocation &location, m_location(location) { setUpWidget(); + QET::trackDialogGeometry(this); if (m_location.isElement()) { QET::QetMessageBox::warning(this, diff --git a/sources/exportdialog.cpp b/sources/exportdialog.cpp index 95d0f8fb6..4242d726d 100644 --- a/sources/exportdialog.cpp +++ b/sources/exportdialog.cpp @@ -22,6 +22,7 @@ #include "dxfexport.h" #include "exportpropertieswidget.h" #include "factory/elementpicturefactory.h" +#include "qet.h" #include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h" #include "dxfpaintdevice.h" #include "qetgraphicsitem/conductor.h" @@ -108,6 +109,8 @@ ExportDialog::ExportDialog( // ajustement des extensions des fichiers slot_changeFilesExtension(true); + + QET::trackDialogGeometry(this); } /** diff --git a/sources/factory/ui/addtabledialog.cpp b/sources/factory/ui/addtabledialog.cpp index b2de24aad..6ea094a01 100644 --- a/sources/factory/ui/addtabledialog.cpp +++ b/sources/factory/ui/addtabledialog.cpp @@ -22,6 +22,7 @@ #include "../../utils/qetutils.h" #include "ui_addtabledialog.h" +#include "../../qet.h" #include /** @@ -35,6 +36,7 @@ AddTableDialog::AddTableDialog(QWidget *content_widget, QWidget *parent) : ui(new Ui::AddTableDialog) { ui->setupUi(this); + QET::trackDialogGeometry(this); ui->m_header_font_pb->setText(m_header_font.family()); ui->m_table_font_pb->setText(m_table_font.family()); m_content_widget = content_widget; diff --git a/sources/logging/ui/diagnosticsreportdialog.cpp b/sources/logging/ui/diagnosticsreportdialog.cpp index 538b289f8..badfea564 100644 --- a/sources/logging/ui/diagnosticsreportdialog.cpp +++ b/sources/logging/ui/diagnosticsreportdialog.cpp @@ -17,6 +17,7 @@ */ #include "diagnosticsreportdialog.h" +#include "../../qet.h" #include "../../qetmessagebox.h" #include @@ -63,6 +64,8 @@ DiagnosticsReportDialog::DiagnosticsReportDialog( // holds a QString copy but we save the original UTF-8 bytes to avoid // any round-trip surprises. setProperty("qet_report_content", content); + + QET::trackDialogGeometry(this); } void DiagnosticsReportDialog::saveToFile() diff --git a/sources/materiallist/materialentrydialog.cpp b/sources/materiallist/materialentrydialog.cpp index 2795df12f..477dd0805 100644 --- a/sources/materiallist/materialentrydialog.cpp +++ b/sources/materiallist/materialentrydialog.cpp @@ -16,6 +16,7 @@ along with QElectroTech. If not, see . */ #include "materialentrydialog.h" +#include "../qet.h" #include "../qetmessagebox.h" @@ -92,6 +93,8 @@ MaterialEntryDialog::MaterialEntryDialog(const QStringList &columns, QWidget *pa if (!m_edits.isEmpty()) { m_edits.first()->setFocus(); } + + QET::trackDialogGeometry(this); } /** diff --git a/sources/qet.cpp b/sources/qet.cpp index 8f9b79908..4e5db31d8 100644 --- a/sources/qet.cpp +++ b/sources/qet.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -272,6 +273,27 @@ bool QET::infoFlagIsTrue(const QString &value) || v == QLatin1String("on"); } +/** + @brief QET::trackDialogGeometry + @see the declaration in qet.h for the rationale. +*/ +void QET::trackDialogGeometry(QDialog *dialog, const QString &key) +{ + const QString settings_key = QStringLiteral("dialoggeometry/%1").arg( + key.isEmpty() ? QString::fromLatin1(dialog->metaObject()->className()) : key); + + QSettings settings; + const QVariant geometry = settings.value(settings_key); + if (geometry.isValid()) { + dialog->restoreGeometry(geometry.toByteArray()); + } + + QObject::connect(dialog, &QDialog::finished, dialog, [dialog, settings_key]() { + QSettings settings; + settings.setValue(settings_key, dialog->saveGeometry()); + }); +} + /** @brief QET::ElementsAndConductorsSentence Permet de composer rapidement la proposition "x elements et y conducteurs" diff --git a/sources/qet.h b/sources/qet.h index cb5a8a0a8..11438d2c2 100644 --- a/sources/qet.h +++ b/sources/qet.h @@ -24,6 +24,7 @@ #include class QActionGroup; +class QDialog; /** This file provides useful functions and enums that may be used from anywhere else within the QElectroTech application. @@ -204,6 +205,18 @@ namespace QET { QActionGroup *depthActionGroup(QObject *parent = nullptr); void saveCustomColors(); void loadCustomColors(); + + /** + Restore a dialog's last-used size/position from QSettings, and + save it back whenever the dialog closes (accepted, rejected, or + via the window's close button). @a key identifies the dialog in + QSettings; defaults to the dialog's class name, which is enough + to distinguish dialogs one-to-one -- pass an explicit @a key for + a dialog class reused for several different kinds of content + (e.g. a generic properties-editor wrapper), so each kind gets + its own remembered size instead of fighting over one entry. + */ + void trackDialogGeometry(QDialog *dialog, const QString &key = QString()); } Q_DECLARE_METATYPE(QET::DepthOption) diff --git a/sources/qetapp.cpp b/sources/qetapp.cpp index 568cca5de..615428f07 100644 --- a/sources/qetapp.cpp +++ b/sources/qetapp.cpp @@ -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,10 +56,12 @@ #include #define QUOTE(x) STRINGIFY(x) #define STRINGIFY(x) #x +#include #include #include #include #include +#include #include #include #ifdef BUILD_WITHOUT_KF @@ -2246,6 +2249,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. diff --git a/sources/qetapp.h b/sources/qetapp.h index 5c0b15106..2c2fa1d3e 100644 --- a/sources/qetapp.h +++ b/sources/qetapp.h @@ -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); diff --git a/sources/qetgraphicsitem/conductor.cpp b/sources/qetgraphicsitem/conductor.cpp index 4eb9009bc..a82dc5ced 100644 --- a/sources/qetgraphicsitem/conductor.cpp +++ b/sources/qetgraphicsitem/conductor.cpp @@ -1099,8 +1099,10 @@ bool Conductor::fromXml(QDomElement &dom_element) m_uuid = QUuid::createUuid(); } - setPos(dom_element.attribute("x", nullptr).toDouble(), - dom_element.attribute("y", nullptr).toDouble()); + //"nan" and "inf" parse as numbers; a non-finite position is kept at 0 + const qreal x = dom_element.attribute("x", nullptr).toDouble(); + const qreal y = dom_element.attribute("y", nullptr).toDouble(); + setPos(qIsFinite(x) ? x : 0, qIsFinite(y) ? y : 0); bool retval = pathFromXml(dom_element); @@ -1229,7 +1231,10 @@ bool Conductor::pathFromXml(const QDomElement &e) { // cette longueur doit etre un reel bool ok; qreal segment_length = current_segment.attribute("length").toDouble(&ok); - if (!ok) continue; + // toDouble() accepts "nan" and "inf". A NaN length would also pass + // the coherence check below (every comparison with NaN is false) + // and turn the whole path into NaN. + if (!ok || !qIsFinite(segment_length)) continue; if (current_segment.attribute("orientation") == "horizontal") { segments_x << segment_length; diff --git a/sources/qetgraphicsitem/conductortextitem.cpp b/sources/qetgraphicsitem/conductortextitem.cpp index 624b5ea77..a3e3127f0 100644 --- a/sources/qetgraphicsitem/conductortextitem.cpp +++ b/sources/qetgraphicsitem/conductortextitem.cpp @@ -69,13 +69,17 @@ Conductor *ConductorTextItem::parentConductor() const @param e */ void ConductorTextItem::fromXml(const QDomElement &e) { - if (e.hasAttribute("userx")) { - setPos(e.attribute("userx").toDouble(), - e.attribute("usery").toDouble()); + //A non-finite ("nan", "inf") position or rotation is ignored, + //as if the text had never been moved or rotated + const qreal x = e.attribute("userx").toDouble(); + const qreal y = e.attribute("usery").toDouble(); + if (e.hasAttribute("userx") && qIsFinite(x) && qIsFinite(y)) { + setPos(x, y); moved_by_user_ = true; } - if (e.hasAttribute("rotation")) { - setRotation(e.attribute("rotation").toDouble()); + const qreal rotation = e.attribute("rotation").toDouble(); + if (e.hasAttribute("rotation") && qIsFinite(rotation)) { + setRotation(rotation); rotate_by_user_ = true; } } diff --git a/sources/qetmainwindow.cpp b/sources/qetmainwindow.cpp index 1436f2668..36fccebf8 100644 --- a/sources/qetmainwindow.cpp +++ b/sources/qetmainwindow.cpp @@ -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); diff --git a/sources/qetmainwindow.h b/sources/qetmainwindow.h index c3820c4bb..fe76fb7e0 100644 --- a/sources/qetmainwindow.h +++ b/sources/qetmainwindow.h @@ -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 diff --git a/sources/richtext/richtexteditor.cpp b/sources/richtext/richtexteditor.cpp index 9c8b9b0a6..a982db81f 100644 --- a/sources/richtext/richtexteditor.cpp +++ b/sources/richtext/richtexteditor.cpp @@ -48,6 +48,7 @@ #include "richtexteditor_p.h" #include "ui_addlinkdialog.h" #include "../shortcutmanager.h" +#include "../qet.h" //#include @@ -242,6 +243,7 @@ AddLinkDialog::AddLinkDialog(RichTextEditor *editor, QWidget *parent) : m_ui(new Ui::AddLinkDialog) { m_ui->setupUi(this); + QET::trackDialogGeometry(this); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); @@ -862,6 +864,7 @@ RichTextEditorDialog::RichTextEditorDialog(QWidget *parent) : m_editor->setFocus(); resize(600, 400); + QET::trackDialogGeometry(this); } RichTextEditorDialog::~RichTextEditorDialog() diff --git a/sources/titleblock/dimensionwidget.cpp b/sources/titleblock/dimensionwidget.cpp index 228b9ad57..6e2566c06 100644 --- a/sources/titleblock/dimensionwidget.cpp +++ b/sources/titleblock/dimensionwidget.cpp @@ -17,6 +17,8 @@ */ #include "dimensionwidget.h" +#include "../qet.h" + /** Constructor @param complete True for this dialog to show the radio buttons that allow @@ -31,6 +33,7 @@ TitleBlockDimensionWidget::TitleBlockDimensionWidget(bool complete, QWidget *par { initWidgets(); initLayouts(); + QET::trackDialogGeometry(this); } /** diff --git a/sources/ui/aboutqetdialog.cpp b/sources/ui/aboutqetdialog.cpp index afec0e258..37911e122 100644 --- a/sources/ui/aboutqetdialog.cpp +++ b/sources/ui/aboutqetdialog.cpp @@ -34,6 +34,7 @@ AboutQETDialog::AboutQETDialog(QWidget *parent) : ui(new Ui::AboutQETDialog) { ui->setupUi(this); + QET::trackDialogGeometry(this); setAbout(); setAuthors(); setTranslators(); diff --git a/sources/ui/aiassistantdialog.cpp b/sources/ui/aiassistantdialog.cpp index 6d8542d7e..7e0284bdd 100644 --- a/sources/ui/aiassistantdialog.cpp +++ b/sources/ui/aiassistantdialog.cpp @@ -16,6 +16,7 @@ along with QElectroTech. If not, see . */ #include "aiassistantdialog.h" +#include "../qet.h" #include #include @@ -157,6 +158,8 @@ AiAssistantDialog::AiAssistantDialog(QWidget *parent) : connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); refresh(); + + QET::trackDialogGeometry(this); } void AiAssistantDialog::refresh() diff --git a/sources/ui/alignmenttextdialog.cpp b/sources/ui/alignmenttextdialog.cpp index e3331800f..f732d9177 100644 --- a/sources/ui/alignmenttextdialog.cpp +++ b/sources/ui/alignmenttextdialog.cpp @@ -20,11 +20,13 @@ #include "../qetgraphicsitem/dynamicelementtextitem.h" #include "ui_alignmenttextdialog.h" +#include "../qet.h" AlignmentTextDialog::AlignmentTextDialog(Qt::Alignment alignment, QWidget *parent) : QDialog(parent), ui(new Ui::AlignmentTextDialog) { ui->setupUi(this); + QET::trackDialogGeometry(this); if(alignment == (Qt::AlignTop|Qt::AlignLeft)) ui->top_left->setChecked(true); diff --git a/sources/ui/bomexportdialog.cpp b/sources/ui/bomexportdialog.cpp index da2be53ca..703b9e3c6 100644 --- a/sources/ui/bomexportdialog.cpp +++ b/sources/ui/bomexportdialog.cpp @@ -24,6 +24,7 @@ #include "../qetproject.h" #include "ui_bomexportdialog.h" +#include "../qet.h" #include #include #include @@ -40,6 +41,7 @@ BOMExportDialog::BOMExportDialog(QETProject *project, QWidget *parent) : m_project(project) { ui->setupUi(this); + QET::trackDialogGeometry(this); m_query_widget = new ElementQueryWidget(this); ui->m_main_layout->insertWidget(0, m_query_widget); diff --git a/sources/ui/compositetexteditdialog.cpp b/sources/ui/compositetexteditdialog.cpp index 3e0deca34..ac4e9c89b 100644 --- a/sources/ui/compositetexteditdialog.cpp +++ b/sources/ui/compositetexteditdialog.cpp @@ -5,6 +5,7 @@ #include "../qetgraphicsitem/dynamicelementtextitem.h" #include "../qetgraphicsitem/element.h" #include "../qetinformation.h" +#include "../qet.h" #include "ui_compositetexteditdialog.h" #include @@ -15,6 +16,7 @@ CompositeTextEditDialog::CompositeTextEditDialog(DynamicElementTextItem *text, Q m_text(text) { ui->setupUi(this); + QET::trackDialogGeometry(this); m_default_text = m_text->compositeText(); ui->m_plain_text_edit->setPlainText(m_default_text); ui->m_plain_text_edit->setPlaceholderText(tr("Entrée votre texte composé ici, en vous aidant des variables disponible")); @@ -31,6 +33,7 @@ CompositeTextEditDialog::CompositeTextEditDialog(QString text, bool report, QWid ui(new Ui::CompositeTextEditDialog) { ui->setupUi(this); + QET::trackDialogGeometry(this); m_default_text = std::move(text); ui->m_plain_text_edit->setPlainText(m_default_text); ui->m_plain_text_edit->setPlaceholderText(tr("Entrée votre texte composé ici, en vous aidant des variables disponible")); diff --git a/sources/ui/conductorpropertiesdialog.cpp b/sources/ui/conductorpropertiesdialog.cpp index 329cf83a9..1973eb188 100644 --- a/sources/ui/conductorpropertiesdialog.cpp +++ b/sources/ui/conductorpropertiesdialog.cpp @@ -24,6 +24,7 @@ #include "conductorpropertieswidget.h" #include "ui_conductorpropertiesdialog.h" +#include "../qet.h" /** @brief ConductorPropertiesDialog::ConductorPropertiesDialog Constructor @@ -36,6 +37,7 @@ ConductorPropertiesDialog::ConductorPropertiesDialog( ui(new Ui::ConductorPropertiesDialog) { ui->setupUi(this); + QET::trackDialogGeometry(this); m_cpw = new ConductorPropertiesWidget(conductor->properties()); m_cpw -> setHiddenOneTextPerFolio(true); m_cpw->setHiddenAvailableAutonum(true); diff --git a/sources/ui/contactgroupselectiondialog.cpp b/sources/ui/contactgroupselectiondialog.cpp index d1c88a002..e681a79cb 100644 --- a/sources/ui/contactgroupselectiondialog.cpp +++ b/sources/ui/contactgroupselectiondialog.cpp @@ -17,6 +17,8 @@ */ #include "contactgroupselectiondialog.h" +#include "../qet.h" + #include #include #include @@ -207,6 +209,8 @@ ContactGroupSelectionDialog::ContactGroupSelectionDialog( accept(); } }); + + QET::trackDialogGeometry(this); } int ContactGroupSelectionDialog::selectedIndex() const diff --git a/sources/ui/diagrampropertiesdialog.cpp b/sources/ui/diagrampropertiesdialog.cpp index e07de4a6a..75f05fa70 100644 --- a/sources/ui/diagrampropertiesdialog.cpp +++ b/sources/ui/diagrampropertiesdialog.cpp @@ -89,6 +89,8 @@ DiagramPropertiesDialog::DiagramPropertiesDialog(Diagram *diagram, QWidget *pare vlayout.addLayout(glayout); vlayout.addWidget(&boutons); + QET::trackDialogGeometry(this); + // if dialog is accepted if (this -> exec() == QDialog::Accepted && !diagram_is_read_only) { diff --git a/sources/ui/dialogwaiting.cpp b/sources/ui/dialogwaiting.cpp index b203d5d96..498504e1e 100644 --- a/sources/ui/dialogwaiting.cpp +++ b/sources/ui/dialogwaiting.cpp @@ -18,6 +18,7 @@ #include "dialogwaiting.h" #include "ui_dialogwaiting.h" +#include "../qet.h" #include @@ -31,6 +32,7 @@ DialogWaiting::DialogWaiting(QWidget *parent) : ui(new Ui::DialogWaiting) { ui->setupUi(this); + QET::trackDialogGeometry(this); setTitle( "..." ); setDetail( "..." ); } diff --git a/sources/ui/duplicateoffsetdialog.cpp b/sources/ui/duplicateoffsetdialog.cpp index 906bc97cd..86668d570 100644 --- a/sources/ui/duplicateoffsetdialog.cpp +++ b/sources/ui/duplicateoffsetdialog.cpp @@ -16,6 +16,7 @@ along with QElectroTech. If not, see . */ #include "duplicateoffsetdialog.h" +#include "../qet.h" #include #include @@ -76,6 +77,8 @@ DuplicateOffsetDialog::DuplicateOffsetDialog(QWidget *parent) : auto *layout = new QVBoxLayout(this); layout->addLayout(form); layout->addWidget(buttons); + + QET::trackDialogGeometry(this); } QPoint DuplicateOffsetDialog::stepOffset() const diff --git a/sources/ui/formulaassistantdialog.cpp b/sources/ui/formulaassistantdialog.cpp index 228da7aff..0f921f10b 100644 --- a/sources/ui/formulaassistantdialog.cpp +++ b/sources/ui/formulaassistantdialog.cpp @@ -18,6 +18,7 @@ #include "formulaassistantdialog.h" #include "ui_formulaassistantdialog.h" +#include "../qet.h" #include FormulaAssistantDialog::FormulaAssistantDialog(QWidget *parent) : @@ -25,6 +26,7 @@ FormulaAssistantDialog::FormulaAssistantDialog(QWidget *parent) : ui(new Ui::FormulaAssistantDialog) { ui->setupUi(this); + QET::trackDialogGeometry(this); } FormulaAssistantDialog::~FormulaAssistantDialog() diff --git a/sources/ui/imagetransparentcolordialog.cpp b/sources/ui/imagetransparentcolordialog.cpp index ed9f5362c..0318acbee 100644 --- a/sources/ui/imagetransparentcolordialog.cpp +++ b/sources/ui/imagetransparentcolordialog.cpp @@ -16,6 +16,7 @@ along with QElectroTech. If not, see . */ #include "imagetransparentcolordialog.h" +#include "../qet.h" #include #include @@ -141,6 +142,8 @@ ImageTransparentColorDialog::ImageTransparentColorDialog(const QPixmap &basePixm rebuildSwatches(); // shows existingColors immediately, if any updatePreview(); // and the preview already reflects them too, rather than only appearing after the next pick + + QET::trackDialogGeometry(this); } /** diff --git a/sources/ui/importelementdialog.cpp b/sources/ui/importelementdialog.cpp index 47d59220c..65f131f92 100644 --- a/sources/ui/importelementdialog.cpp +++ b/sources/ui/importelementdialog.cpp @@ -17,6 +17,7 @@ */ #include "importelementdialog.h" #include "ui_importelementdialog.h" +#include "../qet.h" #include ImportElementDialog::ImportElementDialog(QWidget *parent) : @@ -24,6 +25,7 @@ ImportElementDialog::ImportElementDialog(QWidget *parent) : ui(new Ui::ImportElementDialog) { ui->setupUi(this); + QET::trackDialogGeometry(this); setUpWidget(); } diff --git a/sources/ui/importelementtextpatterndialog.cpp b/sources/ui/importelementtextpatterndialog.cpp index 4c4429d47..4581fa865 100644 --- a/sources/ui/importelementtextpatterndialog.cpp +++ b/sources/ui/importelementtextpatterndialog.cpp @@ -18,10 +18,12 @@ #include "importelementtextpatterndialog.h" #include "ui_importelementtextpatterndialog.h" +#include "../qet.h" ImportElementTextPatternDialog::ImportElementTextPatternDialog(QWidget *parent) : QDialog(parent), ui(new Ui::ImportElementTextPatternDialog) { ui->setupUi(this); + QET::trackDialogGeometry(this); } ImportElementTextPatternDialog::~ImportElementTextPatternDialog() diff --git a/sources/ui/jumptoelementdialog.cpp b/sources/ui/jumptoelementdialog.cpp index 65b2615b3..566a93305 100644 --- a/sources/ui/jumptoelementdialog.cpp +++ b/sources/ui/jumptoelementdialog.cpp @@ -20,6 +20,7 @@ #include "../diagram.h" #include "../diagramview.h" #include "../qetproject.h" +#include "../qet.h" #include "../qetgraphicsitem/element.h" #include @@ -59,6 +60,8 @@ JumpToElementDialog::JumpToElementDialog(Diagram *diagram, QWidget *parent) : buildCandidates(); updateFilteredList(QString()); m_filter_edit->setFocus(); + + QET::trackDialogGeometry(this); } JumpToElementDialog::~JumpToElementDialog() diff --git a/sources/ui/marginseditdialog.cpp b/sources/ui/marginseditdialog.cpp index 65a8ac1a4..2177cb7d0 100644 --- a/sources/ui/marginseditdialog.cpp +++ b/sources/ui/marginseditdialog.cpp @@ -18,6 +18,7 @@ #include "marginseditdialog.h" #include "ui_marginseditdialog.h" +#include "../qet.h" #include MarginsEditDialog::MarginsEditDialog(QMargins margins, QWidget *parent) : @@ -25,6 +26,7 @@ MarginsEditDialog::MarginsEditDialog(QMargins margins, QWidget *parent) : ui(new Ui::MarginsEditDialog) { ui->setupUi(this); + QET::trackDialogGeometry(this); ui->m_top_sb->setValue(margins.top()); ui->m_left_sb->setValue(margins.left()); ui->m_right_sb->setValue(margins.right()); diff --git a/sources/ui/masterpropertieswidget.cpp b/sources/ui/masterpropertieswidget.cpp index 94d56e145..5d1d5a1ac 100644 --- a/sources/ui/masterpropertieswidget.cpp +++ b/sources/ui/masterpropertieswidget.cpp @@ -605,7 +605,6 @@ void MasterPropertiesWidget::updateUi() // Type combo (read-only — type is defined in the editor) auto *type_cb = new QComboBox(m_plc_table); type_cb->setEnabled(false); - type_cb->setStyleSheet("QComboBox { background-color: #f0f0f0; }"); QStringList plc_types = ElementData::plcIOTypeList(); for (int t = 0; t < plc_types.size(); ++t) { type_cb->addItem(plc_types.at(t), t); diff --git a/sources/ui/multipastedialog.cpp b/sources/ui/multipastedialog.cpp index 953b5bccd..c4c8dda4f 100644 --- a/sources/ui/multipastedialog.cpp +++ b/sources/ui/multipastedialog.cpp @@ -26,6 +26,7 @@ #include "../qetgraphicsitem/conductor.h" #include "../ui_multipastedialog.h" +#include "../qet.h" #include #include @@ -35,6 +36,7 @@ MultiPasteDialog::MultiPasteDialog(Diagram *diagram, QWidget *parent) : m_diagram(diagram) { ui->setupUi(this); + QET::trackDialogGeometry(this); connect(ui->m_x_sb, static_cast(&QSpinBox::valueChanged), this, &MultiPasteDialog::updatePreview); connect(ui->m_y_sb, static_cast(&QSpinBox::valueChanged), this, &MultiPasteDialog::updatePreview); diff --git a/sources/ui/pdfpagesdialog.cpp b/sources/ui/pdfpagesdialog.cpp index 00a460862..586887b40 100644 --- a/sources/ui/pdfpagesdialog.cpp +++ b/sources/ui/pdfpagesdialog.cpp @@ -16,6 +16,7 @@ along with QElectroTech. If not, see . */ #include "pdfpagesdialog.h" +#include "../qet.h" // Whole file is a no-op unless QtPdf is available (see pdfpagesdialog.h). #ifdef QET_HAS_QTPDF @@ -125,6 +126,8 @@ PdfPagesDialog::PdfPagesDialog(QPdfDocument &document, QWidget *parent) // Render initial preview updatePreview(); + + QET::trackDialogGeometry(this); } /** diff --git a/sources/ui/potentialselectordialog.cpp b/sources/ui/potentialselectordialog.cpp index 2a39252c7..7122676a0 100644 --- a/sources/ui/potentialselectordialog.cpp +++ b/sources/ui/potentialselectordialog.cpp @@ -19,6 +19,7 @@ #include "potentialselectordialog.h" #include "../QPropertyUndoCommand/qpropertyundocommand.h" +#include "../qet.h" #include "../autoNum/assignvariables.h" #include "../diagram.h" #include "../qetgraphicsitem/conductor.h" @@ -270,6 +271,7 @@ PotentialSelectorDialog::PotentialSelectorDialog(Conductor *conductor, m_parent_undo(parent_undo) { ui->setupUi(this); + QET::trackDialogGeometry(this); m_potential_selector = new NewConductorPotentialSelector(conductor); buildWidget(); } @@ -292,6 +294,7 @@ PotentialSelectorDialog::PotentialSelectorDialog(Element *report, m_parent_undo(parent_undo) { ui->setupUi(this); + QET::trackDialogGeometry(this); m_potential_selector = new LinkReportPotentialSelector(report); buildWidget(); } diff --git a/sources/ui/terminalnumberingdialog.cpp b/sources/ui/terminalnumberingdialog.cpp index 85f299214..fde1df6da 100644 --- a/sources/ui/terminalnumberingdialog.cpp +++ b/sources/ui/terminalnumberingdialog.cpp @@ -5,6 +5,7 @@ #include "../diagram.h" #include "../qetgraphicsitem/element.h" #include "../undocommand/changeelementinformationcommand.h" +#include "../qet.h" #include #include #include @@ -67,6 +68,8 @@ TerminalNumberingDialog::TerminalNumberingDialog(QWidget *parent, QETProject *pr m_stripCheckboxes.insert(prefix, cb); } } + + QET::trackDialogGeometry(this); } /** diff --git a/sources/ui/thirdpartybinaryinstalldialog.cpp b/sources/ui/thirdpartybinaryinstalldialog.cpp index 80754a269..0eb82fc07 100644 --- a/sources/ui/thirdpartybinaryinstalldialog.cpp +++ b/sources/ui/thirdpartybinaryinstalldialog.cpp @@ -18,6 +18,7 @@ #include "thirdpartybinaryinstalldialog.h" #include "ui_thirdpartybinaryinstalldialog.h" +#include "../qet.h" #include #include #include @@ -32,6 +33,7 @@ ThirdPartyBinaryInstallDialog::ThirdPartyBinaryInstallDialog(const QString &text ui{new Ui::ThirdPartyBinaryInstallDialog} { ui->setupUi(this); + QET::trackDialogGeometry(this); ui->m_label->setText(text); connect(ui->m_download_pb, &QPushButton::clicked, [downloadLink](){ diff --git a/sources/ui/wiringlistdialog.cpp b/sources/ui/wiringlistdialog.cpp index a28f2425e..da2e7746c 100644 --- a/sources/ui/wiringlistdialog.cpp +++ b/sources/ui/wiringlistdialog.cpp @@ -16,6 +16,7 @@ along with QElectroTech. If not, see . */ #include "wiringlistdialog.h" +#include "../qet.h" #include "../dataBase/projectdatabase.h" #include "../qetproject.h" @@ -111,4 +112,6 @@ WiringListDialog::WiringListDialog(QETProject *project, QWidget *parent) : auto *buttons = new QDialogButtonBox(QDialogButtonBox::Close, this); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); layout->addWidget(buttons); + + QET::trackDialogGeometry(this); } diff --git a/sources/utils/configprofile.h b/sources/utils/configprofile.h new file mode 100644 index 000000000..b235bc507 --- /dev/null +++ b/sources/utils/configprofile.h @@ -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 . +*/ +#ifndef CONFIGPROFILE_H +#define CONFIGPROFILE_H + +#include +#include +#include + +/** + 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 diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index c8c159b15..29afaa992 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -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) diff --git a/tests/qttest/tst_configprofile.cpp b/tests/qttest/tst_configprofile.cpp new file mode 100644 index 000000000..e005e6f8d --- /dev/null +++ b/tests/qttest/tst_configprofile.cpp @@ -0,0 +1,100 @@ +#include +#include + +#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"