From 0d869b7e506a2e26ef12c7679d35c51042857a77 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Wed, 30 Sep 2026 07:05:54 +1300 Subject: [PATCH 01/15] Align the lines of a multi-line symbol text to its alignment The lines of a wrapped or multi-line dynamic text were always centred, whatever horizontal alignment the user picked, on the folio and in the symbol editor alike. Follow the horizontal alignment instead. The alignment still also sets the anchor point, as before, and saved positions are unchanged. Existing multi-line texts set to the default left alignment are now drawn left-aligned instead of centred. Issue #1155 Co-Authored-By: Claude Opus 5.5 (1M context) --- sources/editor/graphicspart/partdynamictextfield.cpp | 6 +++++- sources/qetgraphicsitem/dynamicelementtextitem.cpp | 10 +++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/sources/editor/graphicspart/partdynamictextfield.cpp b/sources/editor/graphicspart/partdynamictextfield.cpp index c65c955df..9c26a70d6 100644 --- a/sources/editor/graphicspart/partdynamictextfield.cpp +++ b/sources/editor/graphicspart/partdynamictextfield.cpp @@ -47,7 +47,7 @@ PartDynamicTextField::PartDynamicTextField(QETElementEditor *editor, QGraphicsIt //Option when text is displayed in multiple line QTextOption option = document() -> defaultTextOption(); - option.setAlignment(Qt::AlignHCenter); + option.setAlignment(m_alignment & Qt::AlignHorizontal_Mask); option.setWrapMode(QTextOption::WordWrap); document() -> setDefaultTextOption(option); } @@ -462,6 +462,10 @@ void PartDynamicTextField::setPlainText(const QString &text) { void PartDynamicTextField::setAlignment(Qt::Alignment alignment) { m_alignment = alignment; + //Lines of a multi-line text follow the horizontal alignment + QTextOption option = document() -> defaultTextOption(); + option.setAlignment(m_alignment & Qt::AlignHorizontal_Mask); + document() -> setDefaultTextOption(option); emit alignmentChanged(m_alignment); } diff --git a/sources/qetgraphicsitem/dynamicelementtextitem.cpp b/sources/qetgraphicsitem/dynamicelementtextitem.cpp index fda123da9..5c5578ed9 100644 --- a/sources/qetgraphicsitem/dynamicelementtextitem.cpp +++ b/sources/qetgraphicsitem/dynamicelementtextitem.cpp @@ -63,9 +63,17 @@ DynamicElementTextItem::DynamicElementTextItem(Element *parent_element) : //Option when text is displayed in multiple line QTextOption option = document()->defaultTextOption(); - option.setAlignment(Qt::AlignHCenter); + option.setAlignment(alignment() & Qt::AlignHorizontal_Mask); option.setWrapMode(QTextOption::WordWrap); document()->setDefaultTextOption(option); + + //Lines of a multi-line text follow the horizontal alignment + connect(this, &DiagramTextItem::alignmentChanged, [this](Qt::Alignment alignment) + { + QTextOption option = document()->defaultTextOption(); + option.setAlignment(alignment & Qt::AlignHorizontal_Mask); + document()->setDefaultTextOption(option); + }); } DynamicElementTextItem::~DynamicElementTextItem() From 721bd155f596868a1b89765d892657248dc26d55 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Wed, 30 Sep 2026 08:42:12 +1300 Subject: [PATCH 02/15] 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 bae336ef5b984e5059a655dfec74dd2dde3c7845 Mon Sep 17 00:00:00 2001 From: plc-user <74435298+plc-user@users.noreply.github.com> Date: Tue, 29 Sep 2026 21:57:43 +0200 Subject: [PATCH 03/15] =?UTF-8?q?Update=20define=5Fdefinitions.cmake=20?= =?UTF-8?q?=E2=80=93=20fix=20only=20whitespace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmake/define_definitions.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/define_definitions.cmake b/cmake/define_definitions.cmake index c7f326abf..89ea2c6d4 100644 --- a/cmake/define_definitions.cmake +++ b/cmake/define_definitions.cmake @@ -63,7 +63,7 @@ message("QET_DIR :" ${QET_DIR}) message("GIT_COMMIT_SHA :" ${GIT_COMMIT_SHA}) if(BUILD_WITH_KF) - message("KF_GIT_TAG :" ${KF_GIT_TAG}) + message("KF_GIT_TAG :" ${KF_GIT_TAG}) else() add_definitions(-DBUILD_WITHOUT_KF) endif() From fbb59ee1e799bd59f9d09f85180f9521b5ece1f5 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Wed, 30 Sep 2026 09:22:05 +1300 Subject: [PATCH 04/15] 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 05/15] 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 06/15] 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 07/15] 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 08/15] 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 09/15] 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 1dd56df04849310a701dbd783a6583f713b8215a Mon Sep 17 00:00:00 2001 From: ispyisail Date: Wed, 30 Sep 2026 10:45:25 +1300 Subject: [PATCH 10/15] Keep three crash-recovery snapshots instead of overwriting one Discussion #598, reviving PR #654. The crash-recovery backup written every 20 minutes went to a single file. If the project was already in a bad state when a backup ran, that bad state replaced the only recovery copy. QETProject now writes the backups in turn to three KAutoSaveFile slots (BackupGenerations), so one bad write only replaces the oldest snapshot. Scope is crash recovery only; the opt-in autosave is unchanged. After a crash, the recovery prompt groups the snapshots by project and offers one row per project with a list to pick the snapshot to reopen, newest selected by default. The snapshots not picked are deleted. Ported onto current master: writeBackup() keeps the "skip if nothing changed" check (bugtracker #273) and offerBackupFiles() keeps its place after the stale-file filter and before the crash report (#901). Tested with the backup interval shortened to 4 s (test build only): after three changes, master holds one recovery file, overwritten each time; this branch holds three, with 4, 5 and 6 folios. After killing QET, the prompt lists the project; picking the oldest snapshot reopens 4 folios, the default reopens 6. Twice each. ctest 34/34 with and without KDE Frameworks; the carried-over KAutoSaveFile test passes in the nokde build. Co-Authored-By: Claude Opus 5.5 --- cmake/qet_compilation_vars.cmake | 2 + sources/qetapp.cpp | 55 +++++----- sources/qetproject.cpp | 27 +++-- sources/qetproject.h | 10 +- sources/ui/backuprestoredialog.cpp | 142 +++++++++++++++++++++++++ sources/ui/backuprestoredialog.h | 60 +++++++++++ tests/catch/src/kautosavefile_test.cpp | 105 ++++++++++++++++++ 7 files changed, 365 insertions(+), 36 deletions(-) create mode 100644 sources/ui/backuprestoredialog.cpp create mode 100644 sources/ui/backuprestoredialog.h diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index d57817384..eab3ba0a7 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -752,6 +752,8 @@ set(QET_SRC_FILES ${QET_DIR}/sources/ui/diagrampropertieseditordockwidget.h ${QET_DIR}/sources/ui/backupdialog.cpp ${QET_DIR}/sources/ui/backupdialog.h + ${QET_DIR}/sources/ui/backuprestoredialog.cpp + ${QET_DIR}/sources/ui/backuprestoredialog.h ${QET_DIR}/sources/ui/dialogwaiting.cpp ${QET_DIR}/sources/ui/dialogwaiting.h ${QET_DIR}/sources/ui/duplicateoffsetdialog.cpp diff --git a/sources/qetapp.cpp b/sources/qetapp.cpp index f7c086756..568cca5de 100644 --- a/sources/qetapp.cpp +++ b/sources/qetapp.cpp @@ -55,6 +55,7 @@ #include #define QUOTE(x) STRINGIFY(x) #define STRINGIFY(x) #x +#include #include #include #include @@ -65,6 +66,9 @@ #else # include #endif +#include "ui/backuprestoredialog.h" + +#include #ifdef QET_ALLOW_OVERRIDE_CED_OPTION QString QETApp::m_overrided_common_elements_dir = QString(); @@ -2839,48 +2843,47 @@ void QETApp::checkBackupFiles() /** @brief QETApp::offerBackupFiles Ask whether to reopen the recovery files left by a previous run, and - open or discard them accordingly. + open or discard them accordingly. A project can leave several recovery + files, one per snapshot (@see QETProject::writeBackup): they are grouped + by project, and the user picks which one to reopen, the newest by default. @param stale_files : the recovery files to offer */ void QETApp::offerBackupFiles(const QList &stale_files) { - QString text; - if(stale_files.size() == 1) { - text.append(tr("Le fichier de restauration suivant a été trouvé,
" - "Voulez-vous l'ouvrir ?

")); - } else { - text.append(tr("Les fichiers de restauration suivant on été trouvé,
" - "Voulez-vous les ouvrir ?

")); + //Group the snapshots by the project they recover, newest first. + QHash> groups; + for (KAutoSaveFile *kasf : stale_files) { + groups[kasf->managedFile().path()].append(kasf); } - for(const KAutoSaveFile *kasf : stale_files) - { -# ifdef Q_OS_WIN - //Remove the first character '/' before the name of the drive - text.append("
" + kasf->managedFile().path().remove(0,1)); -# else - text.append("
" + kasf->managedFile().path()); -# endif + for (auto &snapshots : groups) { + std::sort(snapshots.begin(), snapshots.end(), + [](KAutoSaveFile *a, KAutoSaveFile *b) { + return QFileInfo(*a).lastModified() + > QFileInfo(*b).lastModified(); + }); } - //Open backup file - if (QET::QetMessageBox::question(nullptr, - tr("Fichier de restauration"), - text, - QMessageBox::Ok - |QMessageBox::Cancel - ) - == QMessageBox::Ok) + BackupRestoreDialog dialog(groups, nullptr); + if (dialog.exec() == QDialog::Accepted) { + //The snapshots not picked are no longer needed. + for (KAutoSaveFile *discarded : dialog.discardedFiles()) + { + discarded->open(QIODevice::ReadWrite); + delete discarded; + } + + const QList to_open = dialog.selectedFiles(); //If there are open editors, find those that are visible if (diagramEditors().count()) { diagramEditors().first()->setVisible(true); - diagramEditors().first()->openBackupFiles(stale_files); + diagramEditors().first()->openBackupFiles(to_open); } else { QETDiagramEditor *editor = new QETDiagramEditor(); - editor->openBackupFiles(stale_files); + editor->openBackupFiles(to_open); } } else //Clear backup file diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index a44e127ef..d126b45f9 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -205,7 +205,7 @@ QETProject::QETProject(KAutoSaveFile *backup, QObject *parent) : QETProject::~QETProject() { //Wait for any in-flight async crash-recovery backup to finish: the worker - //writes through &m_backup_file, a member that would otherwise be destroyed + //writes through m_backup_files, a member that would otherwise be destroyed //under it (issue #492). m_backup_future.waitForFinished(); @@ -562,12 +562,16 @@ void QETProject::setFilePath(const QString &filepath) if (filepath == m_file_path) { return; } - //Don't close/re-point the backup file while a backup is still writing it. + //Don't close/re-point the backup files while a backup is still writing one. m_backup_future.waitForFinished(); - if (m_backup_file.isOpen()) { - m_backup_file.close(); + const QUrl managed_file = QUrl::fromLocalFile(filepath); + for (auto &backup_file : m_backup_files) { + if (backup_file.isOpen()) { + backup_file.close(); + } + backup_file.setManagedFile(managed_file); } - m_backup_file.setManagedFile(QUrl::fromLocalFile(filepath)); + m_next_backup_slot = 0; m_file_path = filepath; QFileInfo fi(m_file_path); @@ -2337,14 +2341,17 @@ void QETProject::detachDiagram(Diagram *diagram) /** @brief QETProject::writeBackup - Write a backup file of this project, in the case that QET crash + Write a backup file of this project, in the case that QET crash. + The snapshots are written in turn to m_backup_files, so a write made + while the project is already in a bad state only replaces the oldest + snapshot, and the earlier ones are still there to recover from. */ void QETProject::writeBackup() { if (!m_backup_enabled) return; //Don't launch a new backup while the previous one is still writing: - //both would write through &m_backup_file on different threads. + //both could write through the same m_backup_files slot on different threads. if (m_backup_future.isRunning()) return; //toXml() walks the whole project on the GUI thread, which freezes @@ -2357,8 +2364,10 @@ void QETProject::writeBackup() //Qt5-style QtConcurrent::run(function, reference-args) call did not //survive the Qt6 API change, a lambda behaves identically on both. QDomDocument xml_project(toXml()); - m_backup_future = QtConcurrent::run([this, xml_project]() mutable { - return QET::writeToFile(xml_project, &m_backup_file, nullptr); + KAutoSaveFile *target = &m_backup_files[m_next_backup_slot]; + m_next_backup_slot = (m_next_backup_slot + 1) % BackupGenerations; + m_backup_future = QtConcurrent::run([target, xml_project]() mutable { + return QET::writeToFile(xml_project, target, nullptr); }); } diff --git a/sources/qetproject.h b/sources/qetproject.h index b30b07075..9e260bcc4 100644 --- a/sources/qetproject.h +++ b/sources/qetproject.h @@ -39,6 +39,8 @@ #include #include +#include + class Diagram; class ElementsLocation; class QETResult; @@ -128,6 +130,10 @@ class QETProject : public QObject /// process can destroy the project before the write finishes (crash). static void setBackupEnabled(bool enabled); + /// Number of crash-recovery snapshots kept per project, written in + /// turn by writeBackup(), so one bad write cannot replace the only copy + static constexpr int BackupGenerations = 3; + ///DEFAULT PROPERTIES BorderProperties defaultBorderProperties() const; void setDefaultBorderProperties(const BorderProperties &); @@ -366,7 +372,9 @@ class QETProject : public QObject QTimer m_save_backup_timer, m_autosave_timer; QFuture m_backup_future; - KAutoSaveFile m_backup_file; + /// Crash-recovery snapshots, written in turn by writeBackup() + std::array m_backup_files; + int m_next_backup_slot = 0; QUuid m_uuid = QUuid::createUuid(); QHash m_derived_uuid_keys; QSet m_saved_item_uuids; //symbol and wire uuids the file carries, see derivedItemUuid() diff --git a/sources/ui/backuprestoredialog.cpp b/sources/ui/backuprestoredialog.cpp new file mode 100644 index 000000000..72e46712b --- /dev/null +++ b/sources/ui/backuprestoredialog.cpp @@ -0,0 +1,142 @@ +/* + 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 . +*/ + +#include "backuprestoredialog.h" + +#ifdef BUILD_WITHOUT_KF +# include "nokde/kautosavefile.h" +#else +# include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +/** + @brief BackupRestoreDialog::BackupRestoreDialog + @param groups : managed project path -> its recovery generations + (newest-first) + @param parent : parent widget +*/ +BackupRestoreDialog::BackupRestoreDialog( + const QHash> &groups, + QWidget *parent) : + QDialog(parent), + m_groups(groups) +{ + setWindowTitle(tr("Fichiers de restauration", "window title")); + + auto main_layout = new QVBoxLayout(this); + + auto intro = new QLabel( + tr("Des fichiers de restauration ont été trouvés,
" + "voulez-vous les ouvrir ?

" + "Pour un projet ayant plusieurs versions de restauration, " + "la plus récente est sélectionnée par défaut.", + "dialog message")); + intro->setWordWrap(true); + main_layout->addWidget(intro); + + auto grid = new QGridLayout(); + int row = 0; + for (auto it = m_groups.constBegin(); it != m_groups.constEnd(); ++it) + { + const QString &path = it.key(); + const QList &generations = it.value(); + +# ifdef Q_OS_WIN + QString display_path = path; + display_path.remove(0, 1); +# else + const QString &display_path = path; +# endif + grid->addWidget(new QLabel(display_path), row, 0); + + auto combo = new QComboBox(this); + for (int i = 0; i < generations.size(); ++i) + { + const QDateTime modified = + QFileInfo(*generations.at(i)).lastModified(); + QString label = QLocale::system().toString( + modified, QLocale::ShortFormat); + if (i == 0) { + label = tr("%1 (la plus récente)", "recovery generation label") + .arg(label); + } + combo->addItem(label); + } + combo->setEnabled(generations.size() > 1); + grid->addWidget(combo, row, 1); + + m_combo_for_path.insert(path, combo); + ++row; + } + main_layout->addLayout(grid); + + auto buttons = new QDialogButtonBox( + QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + main_layout->addWidget(buttons); +} + +/** + @brief BackupRestoreDialog::~BackupRestoreDialog +*/ +BackupRestoreDialog::~BackupRestoreDialog() = default; + +/** + @return one recovery generation per project, matching the combo box + selection (the newest generation, unless the user picked another). +*/ +QList BackupRestoreDialog::selectedFiles() const +{ + QList selected; + for (auto it = m_groups.constBegin(); it != m_groups.constEnd(); ++it) + { + const int index = m_combo_for_path.value(it.key())->currentIndex(); + selected << it.value().at(index); + } + return selected; +} + +/** + @return every recovery generation the user did not pick; the caller + should release and delete these. +*/ +QList BackupRestoreDialog::discardedFiles() const +{ + QList discarded; + for (auto it = m_groups.constBegin(); it != m_groups.constEnd(); ++it) + { + const int index = m_combo_for_path.value(it.key())->currentIndex(); + const QList &generations = it.value(); + for (int i = 0; i < generations.size(); ++i) { + if (i != index) { + discarded << generations.at(i); + } + } + } + return discarded; +} diff --git a/sources/ui/backuprestoredialog.h b/sources/ui/backuprestoredialog.h new file mode 100644 index 000000000..30eed692d --- /dev/null +++ b/sources/ui/backuprestoredialog.h @@ -0,0 +1,60 @@ +/* + 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 BACKUPRESTOREDIALOG_H +#define BACKUPRESTOREDIALOG_H + +#include +#include +#include +#include + +class KAutoSaveFile; +class QComboBox; + +/** + @brief Lets the user pick, per crashed project, which recovery + generation to reopen when the crash-recovery rotation + (@see QETProject::writeBackup) left more than one snapshot behind. +*/ +class BackupRestoreDialog : public QDialog +{ + Q_OBJECT + + public: + /// @param groups : managed project file path -> its stale recovery + /// generations, each list already sorted newest-first. Ownership of + /// the KAutoSaveFile objects stays with the caller. + explicit BackupRestoreDialog( + const QHash> &groups, + QWidget *parent = nullptr); + ~BackupRestoreDialog() override; + + /// Valid once accepted: one entry per project, the chosen generation + /// (defaults to the most recent one). + QList selectedFiles() const; + /// The generations the user did not pick; the caller should discard + /// (release + delete) these. + QList discardedFiles() const; + + private: + QHash> m_groups; + QHash m_combo_for_path; +}; + +#endif // BACKUPRESTOREDIALOG_H diff --git a/tests/catch/src/kautosavefile_test.cpp b/tests/catch/src/kautosavefile_test.cpp index 64258c117..6e1e1c829 100644 --- a/tests/catch/src/kautosavefile_test.cpp +++ b/tests/catch/src/kautosavefile_test.cpp @@ -3,14 +3,18 @@ #include #include +#include #include #include #include #include +#include #include +#include #ifdef Q_OS_UNIX +#include #include #include #include @@ -106,3 +110,104 @@ TEST_CASE("Qt-only KAutoSaveFile recovers stale files", "[nokde][autosave]") CHECK_FALSE(QFile::exists(lock_file_name)); #endif } + +TEST_CASE("Multiple KAutoSaveFile generations for one managed file are all " + "found stale after a crash", "[nokde][autosave]") +{ +#ifndef Q_OS_UNIX + SUCCEED("crash-style stale lock test is Unix-only"); +#else + //Simulates QETProject's rotating crash-recovery generations + //(BackupGenerations snapshots written round-robin): several + //KAutoSaveFile instances sharing one managed file, alive at once. + QTemporaryDir data_home; + REQUIRE(data_home.isValid()); + + qputenv("XDG_DATA_HOME", QFile::encodeName(data_home.path())); + QCoreApplication::setOrganizationName(QStringLiteral("QElectroTech")); + QCoreApplication::setApplicationName( + QStringLiteral("KAutoSaveFileGenerationsTest")); + + const auto managed_path = data_home.filePath(QStringLiteral("project.qet")); + QFile managed_file(managed_path); + REQUIRE(managed_file.open(QIODevice::WriteOnly | QIODevice::Text)); + REQUIRE(managed_file.write("\n") > 0); + managed_file.close(); + + constexpr int generations = 3; + + int ready_pipe[2] = {-1, -1}; + REQUIRE(pipe(ready_pipe) == 0); + + const auto child_pid = fork(); + REQUIRE(child_pid >= 0); + + if (child_pid == 0) { + close(ready_pipe[0]); + + std::vector> backups; + for (int i = 0; i < generations; ++i) { + auto backup = std::make_unique( + QUrl::fromLocalFile(managed_path)); + if (!backup->open(QIODevice::WriteOnly + | QIODevice::Truncate + | QIODevice::Text)) { + _exit(2); + } + const QByteArray payload = + "\n"; + if (backup->write(payload) != payload.size()) { + _exit(3); + } + if (!backup->flush()) { + _exit(4); + } + //Give each generation a distinct, increasing mtime. + struct timespec pause{0, 20 * 1000 * 1000}; + nanosleep(&pause, nullptr); + backups.push_back(std::move(backup)); + } + + const char ready = '1'; + if (write(ready_pipe[1], &ready, 1) != 1) { + _exit(5); + } + close(ready_pipe[1]); + + for (;;) { + pause(); + } + } + + close(ready_pipe[1]); + char ready = 0; + REQUIRE(read(ready_pipe[0], &ready, 1) == 1); + close(ready_pipe[0]); + REQUIRE(ready == '1'); + + REQUIRE(kill(child_pid, SIGKILL) == 0); + int status = 0; + REQUIRE(waitpid(child_pid, &status, 0) == child_pid); + REQUIRE(WIFSIGNALED(status)); + REQUIRE(WTERMSIG(status) == SIGKILL); + + auto stale_files = KAutoSaveFile::allStaleFiles(); + REQUIRE(stale_files.size() == generations); + + std::sort(stale_files.begin(), stale_files.end(), + [](KAutoSaveFile *a, KAutoSaveFile *b) { + return QFileInfo(*a).lastModified() < QFileInfo(*b).lastModified(); + }); + + for (int i = 0; i < generations; ++i) { + std::unique_ptr stale_file(stale_files.at(i)); + CHECK(stale_file->managedFile().path() + == QFileInfo(managed_path).absoluteFilePath()); + REQUIRE(stale_file->open(QIODevice::ReadOnly | QIODevice::Text)); + const QByteArray content = stale_file->readAll(); + CHECK(content.contains( + "generation n=\"" + QByteArray::number(i) + "\"")); + } +#endif +} From 0f259bafb5ed38dfd6fa60cc2e87316c744c56a5 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Wed, 30 Sep 2026 11:03:39 +1300 Subject: [PATCH 11/15] 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 12/15] 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 13/15] 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; } From 8b82e348f266652b0df02876ac55a0d8902a22c6 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Wed, 30 Sep 2026 13:47:45 +1300 Subject: [PATCH 14/15] Choose a conductor colour by double-clicking it In the F2 conductor colour dialog and the toolbar's "Other colour..." dialog, a double-click on a basic or custom colour swatch now chooses it and closes the dialog, as OK does (forum topic 3190). Qt's dialog only selects the swatch on a double-click. ColorDialogDoubleClick filters the dialog's QWellArray swatch grids, found by class name once the dialog is shown, because Qt builds them only then when a native dialog was offered but not shown. Where the platform's native colour dialog is used instead (macOS, some Linux desktops) there are no swatches to watch and nothing changes. The toolbar dialog is now an instance rather than QColorDialog::getColor(), so the filter can be installed. Co-Authored-By: Claude Opus 5.5 --- cmake/qet_compilation_vars.cmake | 2 + sources/diagramview.cpp | 2 + sources/ui/conductorcolortoolbutton.cpp | 10 +-- sources/utils/colordialogdoubleclick.cpp | 77 ++++++++++++++++++++++++ sources/utils/colordialogdoubleclick.h | 50 +++++++++++++++ 5 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 sources/utils/colordialogdoubleclick.cpp create mode 100644 sources/utils/colordialogdoubleclick.h diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index eab3ba0a7..9297115f7 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -865,6 +865,8 @@ set(QET_SRC_FILES ${QET_DIR}/sources/utils/conductorcreator.cpp ${QET_DIR}/sources/utils/conductorcreator.h + ${QET_DIR}/sources/utils/colordialogdoubleclick.cpp + ${QET_DIR}/sources/utils/colordialogdoubleclick.h ${QET_DIR}/sources/utils/macosxopenevent.cpp ${QET_DIR}/sources/utils/macosxopenevent.h ${QET_DIR}/sources/utils/qetsettings.cpp diff --git a/sources/diagramview.cpp b/sources/diagramview.cpp index d74e524b4..c7abe1f33 100644 --- a/sources/diagramview.cpp +++ b/sources/diagramview.cpp @@ -18,6 +18,7 @@ #include "diagramview.h" #include "cellruler.h" #include "lastusedstyle.h" +#include "utils/colordialogdoubleclick.h" #include "qetproject.h" #include "QPropertyUndoCommand/qpropertyundocommand.h" #include "diagramcommands.h" @@ -176,6 +177,7 @@ DiagramView::DiagramView(Diagram *diagram, QWidget *parent) : color_dialog -> setWindowFlags(Qt::Sheet); #endif color_dialog->setCurrentColor(initial_properties.color); + ColorDialogDoubleClick::install(color_dialog); // asks the user what color he wishes to apply if (color_dialog->exec() == QDialog::Accepted) diff --git a/sources/ui/conductorcolortoolbutton.cpp b/sources/ui/conductorcolortoolbutton.cpp index e9b91dc08..f85ece30c 100644 --- a/sources/ui/conductorcolortoolbutton.cpp +++ b/sources/ui/conductorcolortoolbutton.cpp @@ -26,6 +26,7 @@ #include "../projectview.h" #include "../qetgraphicsitem/conductor.h" #include "../conductorproperties.h" +#include "../utils/colordialogdoubleclick.h" #include #include @@ -217,10 +218,11 @@ void ConductorColorToolButton::applyColor(const QColor &color) */ void ConductorColorToolButton::chooseOtherColor() { - const QColor c = QColorDialog::getColor(m_current, this, - tr("Choisir une couleur de conducteur")); - if (c.isValid()) { - applyColor(c); + QColorDialog dialog(m_current, this); + dialog.setWindowTitle(tr("Choisir une couleur de conducteur")); + ColorDialogDoubleClick::install(&dialog); + if (dialog.exec() == QDialog::Accepted && dialog.selectedColor().isValid()) { + applyColor(dialog.selectedColor()); } } diff --git a/sources/utils/colordialogdoubleclick.cpp b/sources/utils/colordialogdoubleclick.cpp new file mode 100644 index 000000000..3cde3a591 --- /dev/null +++ b/sources/utils/colordialogdoubleclick.cpp @@ -0,0 +1,77 @@ +/* + 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 . +*/ +#include "colordialogdoubleclick.h" + +#include +#include +#include +#include + +/** + @brief ColorDialogDoubleClick::install + Watch the swatches of @a dialog. The filter is a child of the dialog + and is deleted with it. + @param dialog +*/ +void ColorDialogDoubleClick::install(QColorDialog *dialog) +{ + if (!dialog) { + return; + } + + //The swatches are looked for when the dialog is shown: Qt builds + //them only then when the platform offered a native dialog it + //could not show after all. + dialog->installEventFilter(new ColorDialogDoubleClick(dialog)); +} + +ColorDialogDoubleClick::ColorDialogDoubleClick(QColorDialog *dialog) : + QObject(dialog), + m_dialog(dialog) +{} + +/** + @brief ColorDialogDoubleClick::eventFilter + The first click of a double-click has already selected the swatch + (on its release), so the dialog's current colour is the one + double-clicked: accept it. Queued, so the swatch finishes handling + the second click before the dialog closes. +*/ +bool ColorDialogDoubleClick::eventFilter(QObject *watched, QEvent *event) +{ + if (watched == m_dialog && event->type() == QEvent::Show) { + //QWellArray is Qt's private class behind both swatch grids; + //its name is the only handle on them (in the QtPrivate + //namespace in recent Qt 6). If a Qt release renames it, + //nothing matches and double-click just selects, as before. + //Installing twice only moves the filter to the front. + const auto children = m_dialog->findChildren(); + for (QWidget *child : children) { + if (child->inherits("QtPrivate::QWellArray") + || child->inherits("QWellArray")) { + child->installEventFilter(this); + } + } + } + else if (event->type() == QEvent::MouseButtonDblClick + && static_cast(event)->button() == Qt::LeftButton) + { + QMetaObject::invokeMethod(m_dialog, &QDialog::accept, Qt::QueuedConnection); + } + return QObject::eventFilter(watched, event); +} diff --git a/sources/utils/colordialogdoubleclick.h b/sources/utils/colordialogdoubleclick.h new file mode 100644 index 000000000..62f9ea40a --- /dev/null +++ b/sources/utils/colordialogdoubleclick.h @@ -0,0 +1,50 @@ +/* + 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 COLOR_DIALOG_DOUBLE_CLICK_H +#define COLOR_DIALOG_DOUBLE_CLICK_H + +#include + +class QColorDialog; + +/** + @brief The ColorDialogDoubleClick class + Lets a double-click on one of a QColorDialog's colour swatches (the + basic colours and the custom colours) choose that colour and close + the dialog, as OK does. Qt's dialog only selects the swatch. + + Only Qt's own dialog has swatches to watch. Where the platform shows + its native dialog instead (macOS, some Linux desktops), this does + nothing and the native dialog behaves as it always has. +*/ +class ColorDialogDoubleClick : public QObject +{ + Q_OBJECT + + public: + static void install(QColorDialog *dialog); + + protected: + bool eventFilter(QObject *watched, QEvent *event) override; + + private: + explicit ColorDialogDoubleClick(QColorDialog *dialog); + QColorDialog *m_dialog; +}; + +#endif // COLOR_DIALOG_DOUBLE_CLICK_H From cb5247e07c982b030c53f6e0feef5edf7703649f Mon Sep 17 00:00:00 2001 From: ispyisail Date: Wed, 30 Sep 2026 13:47:45 +1300 Subject: [PATCH 15/15] Place a template under the cursor, and list it as soon as it is saved A template (.qetmak) was previewed and placed offset from the cursor by the position its items had on the folio it was saved from: the preview pixmap kept that offset, and addMacro() added it back before handing the position to fromXml(). A template saved from the middle or the lower right of a folio therefore landed far below and to the right of the click, often off the folio, which reads as "the template does not drop" (forum topic 3190). Anchor both the preview and the placement on the template's own top-left corner instead. fromXml() skips translating for a null position, so a click on the folio origin is nudged by a fraction of a pixel that the grid snap removes. Also: - a template saved with "Create a template" only appeared in the templates tab after reloading the collections or restarting; it is now added to the tab of every open editor when it is saved; - the placement status bar message ended in an untranslated "(Makro-Anker)"; it now reads "x : y" like element placement. Co-Authored-By: Claude Opus 5.5 --- .../elementscollectionwidget.cpp | 12 ++++++++++++ .../elementscollectionwidget.h | 1 + sources/diagramevent/diagrameventaddmacro.cpp | 18 +++++++++++++++--- sources/diagramview.cpp | 4 ++++ sources/qetdiagrameditor.cpp | 11 +++++++++++ sources/qetdiagrameditor.h | 1 + 6 files changed, 44 insertions(+), 3 deletions(-) diff --git a/sources/ElementsCollection/elementscollectionwidget.cpp b/sources/ElementsCollection/elementscollectionwidget.cpp index 8183faaaa..0e9725ad6 100644 --- a/sources/ElementsCollection/elementscollectionwidget.cpp +++ b/sources/ElementsCollection/elementscollectionwidget.cpp @@ -172,6 +172,18 @@ void ElementsCollectionWidget::setCurrentLocation( m_model->indexFromLocation(location)); } +/** + @brief ElementsCollectionWidget::addTemplate + Show a newly saved template in the templates tab, which is otherwise + only read when the collections are loaded or reloaded. + @param location : the saved .qetmak file +*/ +void ElementsCollectionWidget::addTemplate(const ElementsLocation &location) +{ + if (m_macros_model && location.exist()) + m_macros_model->addLocation(location); +} + void ElementsCollectionWidget::leaveEvent(QEvent *event) { if (QETDiagramEditor *qde = QETApp::diagramEditorAncestorOf(this)) diff --git a/sources/ElementsCollection/elementscollectionwidget.h b/sources/ElementsCollection/elementscollectionwidget.h index 7c46a6e51..db6daa7dc 100644 --- a/sources/ElementsCollection/elementscollectionwidget.h +++ b/sources/ElementsCollection/elementscollectionwidget.h @@ -72,6 +72,7 @@ class ElementsCollectionWidget : public QWidget void removeProject (QETProject *project); void highlightUnusedElement(); void setCurrentLocation(const ElementsLocation &location); + void addTemplate(const ElementsLocation &location); QVector rankedSearch(const QString &text, const QModelIndex &within = QModelIndex()); diff --git a/sources/diagramevent/diagrameventaddmacro.cpp b/sources/diagramevent/diagrameventaddmacro.cpp index 6b629cb6d..ecdf7a007 100644 --- a/sources/diagramevent/diagrameventaddmacro.cpp +++ b/sources/diagramevent/diagrameventaddmacro.cpp @@ -88,7 +88,12 @@ m_preview_item(nullptr) dummy_diagram->render(&painter, QRectF(QPointF(0,0), scene_rect.size()), scene_rect); m_preview_item = new QGraphicsPixmapItem(pixmap); - m_preview_item->setOffset(scene_rect.topLeft()); + // Anchor the preview on the template's own top-left + // corner, where addMacro() puts it, not on the origin of + // the folio it was saved from: a template saved from the + // middle of a folio was otherwise shown, and placed, that + // far below and to the right of the cursor. + m_preview_item->setOffset(scene_rect.topLeft() - m_items_top_left); } } @@ -134,7 +139,7 @@ void DiagramEventAddMacro::mouseMoveEvent(QGraphicsSceneMouseEvent *event) m_preview_item->setPos(pos_); if (m_status_bar) { - m_status_bar->showMessage(QString("x %1 : y %2 (Makro-Anker)").arg(QString::number(pos_.x()), QString::number(pos_.y()))); + m_status_bar->showMessage(QString("x %1 : y %2").arg(QString::number(pos_.x()), QString::number(pos_.y()))); } } event->setAccepted(true); @@ -255,7 +260,14 @@ void DiagramEventAddMacro::addMacro(QPointF final_pos) QDomElement cloned_node = diagram_node.cloneNode(true).toElement(); DiagramContent pasted_content; - m_diagram->fromXml(cloned_node, final_pos + m_items_top_left, false, &pasted_content); + // fromXml() puts the template's top-left corner on final_pos, + // under the cursor. It skips that for a null position, which + // would leave the items where they were saved, so a click on + // the folio origin is nudged by less than it snaps away. + if (final_pos.isNull()) { + final_pos = QPointF(0.1, 0.1); + } + m_diagram->fromXml(cloned_node, final_pos, false, &pasted_content); m_diagram->refreshContents(); // Prevent PasteDiagramCommand from erasing labels (BMK) diff --git a/sources/diagramview.cpp b/sources/diagramview.cpp index d74e524b4..100001582 100644 --- a/sources/diagramview.cpp +++ b/sources/diagramview.cpp @@ -2086,6 +2086,10 @@ void DiagramView::createTemplateFromSelection() file.close(); qDebug() << "Template successfully saved to:" << full_path; + // List it right away, not only after the collections are reloaded + for (QETDiagramEditor *qde : QETApp::diagramEditors()) + qde->templateSaved(template_location); + QMessageBox::information(this, tr("Modèle enregistré"), tr("Le modèle a été enregistré avec succès sous :\n%1").arg(full_path)); } else { diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index f08d00519..f826bbc52 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -1781,6 +1781,17 @@ ProjectView *QETDiagramEditor::currentProjectView() const return(nullptr); } +/** + @brief QETDiagramEditor::templateSaved + List a template saved from a folio in this editor's templates tab. + @param location : the saved .qetmak file +*/ +void QETDiagramEditor::templateSaved(const ElementsLocation &location) +{ + if (m_element_collection_widget) + m_element_collection_widget->addTemplate(location); +} + /** @brief QETDiagramEditor::currentProject @return the current edited project. diff --git a/sources/qetdiagrameditor.h b/sources/qetdiagrameditor.h index ba35ea2f3..d8c59e29d 100644 --- a/sources/qetdiagrameditor.h +++ b/sources/qetdiagrameditor.h @@ -75,6 +75,7 @@ class QETDiagramEditor : public QETMainWindow ProjectView *viewForFile (const QString &) const; ProjectView *currentProjectView() const; QETProject *currentProject() const; + void templateSaved(const ElementsLocation &location); bool drawGrid() const; void openBackupFiles (QList backup_files);