From 721bd155f596868a1b89765d892657248dc26d55 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Wed, 30 Sep 2026 08:42:12 +1300 Subject: [PATCH 01/10] Reject non-finite values when loading a wire A wire read "nan" or "inf" for its position, its segment lengths and its label position without checking them: QString::toDouble() accepts all three as numbers. PR #792 and #996 closed this gap for elements, terminals and texts; wires were left out. The worst case is a segment length: a single "nan" passes the check that the segments add up to the distance between the two terminals, because every comparison with NaN is false. The whole path then turns to NaN: on 2612_ats_singlephase.qet one bad length out of six was saved back as six "nan" vertical segments, and the next save dropped them and routed the wire automatically, so each save changed the file. Now: - a non-finite segment length is skipped like any unreadable one, so the path fails the coherence check and the wire is routed automatically at load, as already happens for "inf"; - a non-finite wire position is read as 0; - a non-finite label position or rotation is ignored, as if the label had not been moved or rotated. Found by the mutation sweep (python3 -m simulator sweep, mutators inject_nan_coordinate / inject_inf_coordinate). The 24 example projects resave byte-identical with and without this change. Co-Authored-By: Claude Opus 5.5 (1M context) --- sources/qetgraphicsitem/conductor.cpp | 11 ++++++++--- sources/qetgraphicsitem/conductortextitem.cpp | 14 +++++++++----- 2 files changed, 17 insertions(+), 8 deletions(-) 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; } } From fbb59ee1e799bd59f9d09f85180f9521b5ece1f5 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Wed, 30 Sep 2026 09:22:05 +1300 Subject: [PATCH 02/10] Fix a crash when closing a project after its terminal strip window is gone The terminal strip window's tree dock connects to its project's destroyed() signal with a lambda capturing `this`, but without `this` as the connection's context, and never disconnects it. When the dock is deleted before the project, closing the project runs the lambda on freed memory. The terminal strip window is a single instance, parented to the editor window it was first opened from. With two editor windows: 1. open the terminal strip manager from window 1 (project A); 2. open it from window 2 (project B): the same window switches to B, still a child of window 1; 3. close window 1: the terminal strip window and its dock are deleted, the connection to B stays; 4. close window 2, or project B: heap-use-after-free in the lambda (terminalstriptreedockwidget.cpp:66), reported by AddressSanitizer 3 times out of 3. Freed by ~TerminalStripEditorWindow from ~QETDiagramEditor. Passing `this` as the context removes the connection with the dock, as FreeTerminalEditor, FreeTerminalModel and TerminalStripEditor already do for the same signal. With the fix: no error, 3 times out of 3. Co-Authored-By: Claude Opus 5.5 (1M context) --- sources/TerminalStrip/ui/terminalstriptreedockwidget.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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(); }); From 576f085ce8bd48f71763e2a33bb6d8613c6f6155 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sat, 8 Aug 2026 17:07:05 +1200 Subject: [PATCH 03/10] Drop hardcoded light-gray background on disabled PLC-IO type combo masterpropertieswidget.cpp set an inline "background-color: #f0f0f0" stylesheet on the disabled type combo box in the PLC master I/O table. Qt already renders disabled widgets correctly without help; the hardcoded light color just breaks under a dark palette, showing a bright box in an otherwise dark dialog. Dropping the stylesheet lets it render like every other disabled widget in the app. --- sources/ui/masterpropertieswidget.cpp | 1 - 1 file changed, 1 deletion(-) 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); From 47c70e8b536e00c371cdd0a9d86bf06a8495fbf4 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Mon, 3 Aug 2026 11:15:50 +1200 Subject: [PATCH 04/10] Add an adjustable background frame to the element editor Discussion #604. Revives #643, closed on 2026-09-10 only to clear the review backlog, ported onto current master. The element editor shows an infinite grid, with nothing to judge how big a symbol will look on a real folio. Affichage > "Afficher le cadre de fond" draws a dashed blue rectangle centred on the element's origin (the hotspot), sized by default to the drawing area of a default folio (BorderProperties: columns x column width by rows x row height, 1020 x 640). Affichage > "Taille du cadre de fond..." sets another size. Both persist in QSettings (elementeditor/background_frame_*). The frame is drawn in ElementView::drawBackground(), so it is not part of the scene: it is never written to the .elmt file and never in the SVG export (scene render). Changed from #643: resolved against master's new-style connects and the hotspot visibility switch; the toggle gets the unused document-print-frame theme icon (light and dark), without which it showed as a text button in the icon toolbar; the size action is registered with ShortcutManager like the toggle. (cherry picked from commit a245411f9fcf1da2b5fbee9817ca7a6f3b452911) Co-Authored-By: Claude Opus 5.5 --- sources/editor/elementscene.cpp | 44 +++++++++++++++++++++++++ sources/editor/elementscene.h | 7 ++++ sources/editor/elementview.cpp | 13 ++++++++ sources/editor/ui/qetelementeditor.cpp | 45 ++++++++++++++++++++++++++ 4 files changed, 109 insertions(+) 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/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); } /** From a28e83418528d81c25d26c2dd02edae45a64605e Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sat, 8 Aug 2026 17:06:54 +1200 Subject: [PATCH 05/10] Remember dialog size and position across sessions QETElementEditor, QETDiagramEditor and QETTemplateEditor already persist their geometry via QSettings; none of the modal QDialog subclasses did, so any dialog resized to see more of its content (Search and Replace, Diagram properties, Export, ...) is back to its default size the next time it's opened. Adds QET::trackDialogGeometry(), one call at the end of each dialog constructor (after any default resize()), following the same restoreGeometry()/saveGeometry() pattern as the three editors above, stored under [dialoggeometry] and keyed by class name by default. An explicit key is used for PropertiesEditorDialog, a single class templated over several unrelated wrapped editors, so they don't all fight over one saved size. Covers the 36 dialogs of the original change (#691) plus seven added since: RenumberElementsDialog, MaterialEntryDialog, AiAssistantDialog, DuplicateOffsetDialog, ImageTransparentColorDialog, PdfPagesDialog and WiringListDialog. Deliberately not touched: BackupDialog and ImageCropDialog, which setFixedSize() themselves, and MaterialSelectionDialog, which already restores its own size. A size saved while the dialog sat partly off-screen is restored on screen: restoreGeometry() moves it back inside the available screen (checked: saved at 1502,972 on a 1600x1000 screen, reopened at 600,317). Co-Authored-By: Claude Opus 5.5 --- .../ElementsCollection/ui/renamedialog.cpp | 2 ++ sources/NameList/ui/namelistdialog.cpp | 2 ++ .../PropertiesEditor/propertieseditordialog.h | 10 +++++++++ .../ui/replaceadvanceddialog.cpp | 2 ++ .../ui/replaceconductordialog.cpp | 2 ++ .../ui/replaceelementdialog.cpp | 2 ++ .../ui/replacefoliowidget.cpp | 3 +++ .../ui/addterminalstripitemdialog.cpp | 2 ++ .../ui/terminalstripcreatordialog.cpp | 2 ++ sources/autoNum/ui/renumberelementsdialog.cpp | 3 +++ sources/configdialog.cpp | 3 +++ .../ui/elementpropertieseditorwidget.cpp | 2 ++ sources/elementdialog.cpp | 2 ++ sources/elementscategoryeditor.cpp | 1 + sources/exportdialog.cpp | 3 +++ sources/factory/ui/addtabledialog.cpp | 2 ++ .../logging/ui/diagnosticsreportdialog.cpp | 3 +++ sources/materiallist/materialentrydialog.cpp | 3 +++ sources/qet.cpp | 22 +++++++++++++++++++ sources/qet.h | 13 +++++++++++ sources/richtext/richtexteditor.cpp | 3 +++ sources/titleblock/dimensionwidget.cpp | 3 +++ sources/ui/aboutqetdialog.cpp | 1 + sources/ui/aiassistantdialog.cpp | 3 +++ sources/ui/alignmenttextdialog.cpp | 2 ++ sources/ui/bomexportdialog.cpp | 2 ++ sources/ui/compositetexteditdialog.cpp | 3 +++ sources/ui/conductorpropertiesdialog.cpp | 2 ++ sources/ui/contactgroupselectiondialog.cpp | 4 ++++ sources/ui/diagrampropertiesdialog.cpp | 2 ++ sources/ui/dialogwaiting.cpp | 2 ++ sources/ui/duplicateoffsetdialog.cpp | 3 +++ sources/ui/formulaassistantdialog.cpp | 2 ++ sources/ui/imagetransparentcolordialog.cpp | 3 +++ sources/ui/importelementdialog.cpp | 2 ++ sources/ui/importelementtextpatterndialog.cpp | 2 ++ sources/ui/jumptoelementdialog.cpp | 3 +++ sources/ui/marginseditdialog.cpp | 2 ++ sources/ui/multipastedialog.cpp | 2 ++ sources/ui/pdfpagesdialog.cpp | 3 +++ sources/ui/potentialselectordialog.cpp | 3 +++ sources/ui/terminalnumberingdialog.cpp | 3 +++ sources/ui/thirdpartybinaryinstalldialog.cpp | 2 ++ sources/ui/wiringlistdialog.cpp | 3 +++ 44 files changed, 144 insertions(+) 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/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/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/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/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/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); } From 79e87cb3b844fe0fdbef280654cde7d04a00c497 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Wed, 30 Sep 2026 10:35:19 +1300 Subject: [PATCH 06/10] Set up the elements collection's items on the GUI thread ElementsCollectionModel::loadCollections() runs setUpData() for every item on worker threads (QtConcurrent::map), and setUpData() calls setText(), setFlags(), setData() and setToolTip() on items that are already in the model. Each of these changes the model and emits its dataChanged() signal from a worker thread, which QAbstractItemModel does not allow. Loading the shipped collection (8838 elements) emitted dataChanged() 38958 times, all from worker threads. The expensive part (reading every element file) stays on the worker threads. Only the result is moved: ElementCollectionItem::setData() keeps a value set from a worker thread on the item, data() returns it to the same worker so setUpData() still reads back what it has set, and the model applies the kept values on the GUI thread when the map is finished, before emitting loadingFinished(). setUpData() called on the GUI thread (macros collection, a single added or changed element) is unchanged. With this change the same load emits dataChanged() 38958 times, all on the GUI thread. Revives the still-needed part of #516, closed only to clear a review backlog. Its other two changes are left out: the wait in loadMacrosCollection() guarded a model shared with a running map, which no longer happens (the macros always get a model of their own), and qetinformation.h's static QString constants are a size clean-up, not a bug. Co-Authored-By: Claude Opus 5.5 --- .../elementcollectionitem.cpp | 70 +++++++++++++++++++ .../elementcollectionitem.h | 11 +++ .../elementscollectionmodel.cpp | 11 ++- 3 files changed, 90 insertions(+), 2 deletions(-) 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); From 473f792bacb2e4a9452cad8ad72dfdfa1a66ccb8 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Wed, 30 Sep 2026 10:42:26 +1300 Subject: [PATCH 07/10] Add saving and loading the configuration to a file (discussion #610) Revives #640, closed only to clear a review backlog. Two entries in the Configuration menu: "Enregistrer la configuration sous..." saves the settings to a .conf file, "Charger une configuration..." replaces them with one. The file is always ini format, so a profile made on one platform loads on the others. Changes from #640: - Window sizes, positions and dock layouts (".../geometry", ".../state") and the recent-files lists stay out of a profile and are kept on load: they describe this computer, not a preference. - A saved file carries a format marker; loading refuses files that QElectroTech did not save. - Loading closes QElectroTech and asks the user to start it again, instead of starting a second copy: a second copy started while this one is still running hands its arguments over and exits (main.cpp). - The colour dialog's custom colours are reloaded from the profile before quitting, since ~QETApp() saves them over the loaded ones. - Both actions are registered with ShortcutManager, so they appear in command search and can be given a shortcut. - The copy rules live in the header-only utils/configprofile.h and are unit tested (tst_configprofile). Loading still replaces rather than merges: a setting the profile does not have is removed, or switching profiles would keep settings from the previous one. Co-Authored-By: Claude Opus 5.5 --- sources/qetapp.cpp | 106 +++++++++++++++++++++++++++++ sources/qetapp.h | 4 ++ sources/qetmainwindow.cpp | 13 ++++ sources/qetmainwindow.h | 2 + sources/utils/configprofile.h | 98 ++++++++++++++++++++++++++ tests/qttest/CMakeLists.txt | 7 ++ tests/qttest/tst_configprofile.cpp | 100 +++++++++++++++++++++++++++ 7 files changed, 330 insertions(+) create mode 100644 sources/utils/configprofile.h create mode 100644 tests/qttest/tst_configprofile.cpp diff --git a/sources/qetapp.cpp b/sources/qetapp.cpp index f7c086756..829e3a74b 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,9 +56,11 @@ #include #define QUOTE(x) STRINGIFY(x) #define STRINGIFY(x) #x +#include #include #include #include +#include #include #include #ifdef BUILD_WITHOUT_KF @@ -2242,6 +2245,109 @@ void QETApp::configureQET() #endif } +/** + @brief QETApp::exportConfiguration + Save the settings of QElectroTech to a file the user chooses, to keep + them as a named profile or copy them to another computer (discussion + #610). The file is always written in the ini format, whatever the + platform stores its live settings in, so a profile saved on Windows + loads on Linux and macOS. See ConfigProfile for the keys left out. +*/ +void QETApp::exportConfiguration() +{ + QWidget *parent_widget = qApp->activeWindow(); + + QString path = QFileDialog::getSaveFileName( + parent_widget, + tr("Enregistrer la configuration sous...", "dialog title"), + QString(), + tr("Configurations QElectroTech (*.conf)", "file dialog filter")); + if (path.isEmpty()) { + return; + } + if (!path.endsWith(QLatin1String(".conf"), Qt::CaseInsensitive)) { + path += QLatin1String(".conf"); + } + + QSettings live_settings; + QSettings file_settings(path, QSettings::IniFormat); + ConfigProfile::exportTo(live_settings, file_settings); + + if (file_settings.status() != QSettings::NoError) { + QET::QetMessageBox::critical( + parent_widget, + tr("Erreur", "message box title"), + tr("Impossible d'enregistrer la configuration dans « %1 ».").arg(path)); + } +} + +/** + @brief QETApp::importConfiguration + Replace the settings of QElectroTech with a file saved by + exportConfiguration(), then close QElectroTech. + + The settings are read by each part of QElectroTech when it starts, and + there is no signal telling all of them that a setting changed, so the + new settings are applied by starting QElectroTech again. That restart + is left to the user: a second copy started from here would find this + one still running, hand its arguments over to it and exit (main.cpp). +*/ +void QETApp::importConfiguration() +{ + QWidget *parent_widget = qApp->activeWindow(); + + const QString path = QFileDialog::getOpenFileName( + parent_widget, + tr("Charger une configuration...", "dialog title"), + QString(), + tr("Configurations QElectroTech (*.conf)", "file dialog filter")); + if (path.isEmpty()) { + return; + } + + QSettings file_settings(path, QSettings::IniFormat); + if (file_settings.status() != QSettings::NoError + || !ConfigProfile::isProfile(file_settings)) + { + QET::QetMessageBox::critical( + parent_widget, + tr("Erreur", "message box title"), + tr("« %1 » n'est pas une configuration enregistrée par QElectroTech.").arg(path)); + return; + } + + //Said before anything closes: once the last window is closed, + //QElectroTech quits by itself (checkRemainingWindows()). + const auto answer = QET::QetMessageBox::question( + parent_widget, + tr("Charger une configuration", "message box title"), + tr("Cette configuration va remplacer vos réglages actuels, sauf la " + "disposition des fenêtres et la liste des fichiers récents.\n\n" + "QElectroTech va ensuite se fermer. Relancez-le pour utiliser " + "la nouvelle configuration.\n\n" + "Voulez-vous continuer ?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); + if (answer != QMessageBox::Yes) { + return; + } + + //Asks to save any modified project, as quitQET() does. The settings + //are only replaced once every editor is closed, so that the editors + //saving their own settings on close cannot overwrite them, and a + //cancelled close leaves them untouched. + if (!closeEveryEditor()) { + return; + } + + QSettings live_settings; + ConfigProfile::importFrom(file_settings, live_settings); + //~QETApp() saves the colour dialog's custom colours on exit: load + //the profile's into it first, or the old ones overwrite them + QET::loadCustomColors(); + qApp->quit(); +} + /** @brief QETApp::aboutQET Open the dialog about qet. 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/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/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" From 0f259bafb5ed38dfd6fa60cc2e87316c744c56a5 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Wed, 30 Sep 2026 11:03:39 +1300 Subject: [PATCH 08/10] Spell out the Shift-drag hint on symbol texts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hovering a symbol's text shows its value and the hint " to move" (" pour déplacer" in French), which does not say what moves: a plain drag moves the whole symbol, Shift+drag moves the text alone (DynamicElementTextItem::mousePressEvent). English: "Hold Shift and drag to move this text on its own". French: "Maintenir Maj et faire glisser pour déplacer ce texte seul". Translations only, as for #1124: the source string is unchanged, so the other languages keep their current text. Revives #499 (closed 2026-06-11 by its author to test locally first). Co-Authored-By: Claude Opus 5.5 --- lang/qet_en.ts | 2 +- lang/qet_fr.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 From 6c4ee10f4404ee1ea2dc2ee23300970cd6c82d6c Mon Sep 17 00:00:00 2001 From: ispyisail Date: Wed, 30 Sep 2026 11:08:55 +1300 Subject: [PATCH 09/10] Generate cabinet placement thumbnails from manufacturer/reference info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Discussion #602. Right-click a selection holding an element with both a manufacturer and a manufacturer reference: "Générer une vignette d'armoire" adds a thumbnail element for each such device to a "Cabinet thumbnails" folder of the project's embedded collection. The thumbnail is a 120x30 frame whose dynamic text shows %{manufacturer} / %{manufacturer_reference}; both values are also in the definition's element information, so it shows them when placed. A device already filed there is skipped, so running it again, or on several copies of one device, adds nothing. Revives #641, closed 2026-09-10 only to clear a review backlog. Changed from it: the frame and text are placed around the hotspot (they were drawn half outside the element), the frame is 120 wide instead of 60 (device names wrapped out of it), the file name goes through QET::stringToFileName() (a '/' in a reference made a folder), the version and font come from QetVersion and QETApp, and the action is left out of the menu when no selected element qualifies. Co-Authored-By: Claude Opus 5.5 --- sources/diagramview.cpp | 181 +++++++++++++++++++++++++++++++++++++++- sources/diagramview.h | 2 + 2 files changed, 182 insertions(+), 1 deletion(-) diff --git a/sources/diagramview.cpp b/sources/diagramview.cpp index b4c5cf126..b80335eda 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,164 @@ 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); + dir_names.addName(QStringLiteral("fr"), tr("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 From aa09d0eb2a2a78292f279d44d350292f30c0cc69 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Wed, 30 Sep 2026 11:15:11 +1300 Subject: [PATCH 10/10] Store the thumbnails folder's French name as French, not through tr() tr() returns the interface language, so an English or German session would have written its own text under the "fr" name. Co-Authored-By: Claude Opus 5.5 --- sources/diagramview.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sources/diagramview.cpp b/sources/diagramview.cpp index b80335eda..d74e524b4 100644 --- a/sources/diagramview.cpp +++ b/sources/diagramview.cpp @@ -2210,7 +2210,9 @@ void DiagramView::generateCabinetThumbnails() { NamesList dir_names; dir_names.addName(QStringLiteral("en"), dir_name); - dir_names.addName(QStringLiteral("fr"), tr("Vignettes d'armoire")); + //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; }