From 8a72230916c6a2a9843f78e3432c4dbaac97fe54 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 2 Aug 2026 23:06:12 +1200 Subject: [PATCH 1/2] Add "Export to SVG" to the element editor Implements discussion #605. The diagram editor can already export a folio to SVG; the element editor, where a single .elmt symbol is drawn, had no export capability at all -- confirmed by grepping its header for "export" before starting: nothing. ## Renders the live scene, not ElementPictureFactory's cache The discussion proposed sourcing this from ElementPictureFactory's cached per-element QPicture (m_pictures_H), the one used for the elements-panel preview icons. Checked that cache's actual invalidation before building on it: nothing in the editor ever tells it to drop an entry on edit, and it is keyed by the element's on-disk uuid. So for any element already previewed once in the panel, exporting from the cache would silently produce stale content after any edit; for a brand-new, never-saved element, no entry would exist at all. Neither is acceptable for a File > Export action a user expects to reflect what's on screen right now. Renders ElementScene directly instead, the same way ExportDialog::generateSvg() already renders the live Diagram for the diagram editor's own SVG export: no new drawing logic, only a new playback target (QSvgGenerator instead of the screen), sized to the element's own content bounds via the existing elementSceneGeometricRect() helper. ## Hotspot cross excluded from the export ElementScene::drawForeground() draws the red origin/hotspot cross on every render() call, unconditionally -- it's an editing aid, not part of the element being drawn, and diagram editor's SVG export has no equivalent problem since Diagram doesn't draw one. Added a settable hotspotVisible flag, defaulting to true (the existing editing view is completely unaffected) and turned off only for the duration of the export render() call. ## Verified end-to-end via a real Xvfb session, not just a build Opened a real shipped element (en_60617_05_06_04.elmt, "Phototransistor"), exported it, and rendered the resulting SVG back to a bitmap with a small QSvgRenderer-based harness -- pixel-identical in shape to the element as shown in the editor. Confirmed the file is valid XML and contains no red/#ff0000 stroke (the hotspot cross did not leak in). Then the case the whole "render live, not cached" decision was about: opened the same element, drew a new line with the line tool, and exported again *without saving*. The new line is present in the exported SVG. git status on the source .elmt file after both exports shows it completely untouched -- the export is read-only and reflects live, unsaved editor state, exactly the property a cache-based implementation would have gotten wrong. Built clean, no new warnings. --- sources/editor/elementscene.cpp | 4 ++ sources/editor/elementscene.h | 10 ++++ sources/editor/ui/qetelementeditor.cpp | 80 ++++++++++++++++++++++++++ sources/editor/ui/qetelementeditor.h | 1 + sources/editor/ui/qetelementeditor.ui | 10 ++++ 5 files changed, 105 insertions(+) diff --git a/sources/editor/elementscene.cpp b/sources/editor/elementscene.cpp index 393d280b9..a2c8146d3 100644 --- a/sources/editor/elementscene.cpp +++ b/sources/editor/elementscene.cpp @@ -317,6 +317,10 @@ void ElementScene::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) */ void ElementScene::drawForeground(QPainter *p, const QRectF &) { + if (!m_hotspot_visible) { + return; + } + p -> save(); // desactive tout antialiasing, sauf pour le texte diff --git a/sources/editor/elementscene.h b/sources/editor/elementscene.h index 97ec4812a..a53afff6f 100644 --- a/sources/editor/elementscene.h +++ b/sources/editor/elementscene.h @@ -90,6 +90,8 @@ class ElementScene : public QGraphicsScene m_y_grid; QPointer m_single_selected_item; + + bool m_hotspot_visible = true; // methods public: @@ -132,6 +134,14 @@ class ElementScene : public QGraphicsScene QETElementEditor* editor() const; void addItems(QVector items); void removeItems(QVector items); + + /// Whether drawForeground() draws the red hotspot cross. On by + /// default (the normal editing view); turned off around a + /// render() call meant to capture the element's actual content + /// only, e.g. exporting to SVG -- the cross is an editing aid, + /// not part of the drawn symbol. + void setHotspotVisible(bool visible) {m_hotspot_visible = visible;} + bool hotspotVisible() const {return m_hotspot_visible;} protected: void mouseMoveEvent (QGraphicsSceneMouseEvent *) override; diff --git a/sources/editor/ui/qetelementeditor.cpp b/sources/editor/ui/qetelementeditor.cpp index ba8d80f19..04ffa60b6 100644 --- a/sources/editor/ui/qetelementeditor.cpp +++ b/sources/editor/ui/qetelementeditor.cpp @@ -53,6 +53,8 @@ #include #include +#include +#include /** * @brief QETElementEditor::QETElementEditor @@ -1377,6 +1379,84 @@ bool QETElementEditor::on_m_save_as_file_action_triggered() return false; } +/** + @brief QETElementEditor::on_m_export_svg_action_triggered + Export the element currently open in this editor to a standalone SVG + file. + + Renders the live ElementScene directly, the same way + ExportDialog::generateSvg() renders the live Diagram for the diagram + editor's own SVG export -- not ElementPictureFactory's cached picture. + That cache is keyed by the element's saved-to-disk uuid and is never + invalidated on edit (nothing in this editor ever tells it to), so it + would silently export stale content for any element already previewed + once in the elements panel, and nothing at all for one that has never + been saved. Rendering the scene directly has neither problem and + always reflects exactly what is currently on screen, saved or not. + @return true if the file was written +*/ +bool QETElementEditor::on_m_export_svg_action_triggered() +{ + QString fn = QFileDialog::getSaveFileName( + this, + tr("Exporter en SVG", "dialog title"), + m_file_name.isEmpty() ? QETApp::customElementsDir() : QDir(m_file_name).absolutePath(), + tr("Image SVG (*.svg)", "filetypes allowed when exporting an element to SVG")); + + if (fn.isEmpty()) { + return false; + } + if (!fn.endsWith(".svg", Qt::CaseInsensitive)) { + fn += ".svg"; + } + + QFile file(fn); + if (!file.open(QIODevice::WriteOnly)) { + QMessageBox::critical(this, tr("Échec de l'export"), + tr("Impossible d'écrire dans le fichier « %1 ».").arg(fn)); + return false; + } + + //Margin-less bounding rect of the element's own drawn content + //(lines, rects, terminals, text...), excluding the origin cross + //and other editor-only decoration -- see + //ElementScene::elementSceneGeometricRect()'s own doc comment. + //Falls back to itemsBoundingRect() for the rare element made up + //only of item types that helper deliberately excludes. + QRectF source_rect = m_elmt_scene->elementSceneGeometricRect(); + if (source_rect.isEmpty()) { + source_rect = m_elmt_scene->itemsBoundingRect(); + } + constexpr qreal margin = 5.0; + source_rect.adjust(-margin, -margin, margin, margin); + + QSize target_size = source_rect.size().toSize(); + if (target_size.isEmpty()) { + target_size = QSize(1, 1); + } + + QSvgGenerator svg_engine; + svg_engine.setSize(target_size); + svg_engine.setViewBox(QRect(QPoint(0, 0), target_size)); + svg_engine.setOutputDevice(&file); + + QPainter svg_painter(&svg_engine); + svg_painter.setRenderHint(QPainter::Antialiasing, true); + svg_painter.setRenderHint(QPainter::TextAntialiasing, true); + + //The hotspot cross is ElementScene::drawForeground()'s editing aid, + //drawn unconditionally on every render() call including this one + //unless told not to -- it is not part of the element being + //exported. + m_elmt_scene->setHotspotVisible(false); + m_elmt_scene->render(&svg_painter, QRectF(QPointF(0, 0), target_size), source_rect); + m_elmt_scene->setHotspotVisible(true); + + svg_painter.end(); + + return true; +} + void QETElementEditor::on_m_reload_action_triggered() { //If user already edit the element, ask confirmation to reload diff --git a/sources/editor/ui/qetelementeditor.h b/sources/editor/ui/qetelementeditor.h index 160986d82..579d9bab4 100644 --- a/sources/editor/ui/qetelementeditor.h +++ b/sources/editor/ui/qetelementeditor.h @@ -87,6 +87,7 @@ class QETElementEditor : public QMainWindow void on_m_open_action_triggered(); void on_m_open_from_file_action_triggered(); bool on_m_save_as_file_action_triggered(); + bool on_m_export_svg_action_triggered(); void on_m_reload_action_triggered(); void on_m_quit_action_triggered(); void on_m_deselect_all_action_triggered(); diff --git a/sources/editor/ui/qetelementeditor.ui b/sources/editor/ui/qetelementeditor.ui index 90cf36118..65a55aa8b 100644 --- a/sources/editor/ui/qetelementeditor.ui +++ b/sources/editor/ui/qetelementeditor.ui @@ -40,6 +40,7 @@ + @@ -262,6 +263,15 @@ Enregistrer dans un fichier + + + + :/ico/22x22/document-export.png:/ico/22x22/document-export.png + + + Exporter en SVG + + From eb520ac3f8927ce70927310753c0e13922ae2331 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Mon, 3 Aug 2026 08:31:17 +1200 Subject: [PATCH 2/2] Pre-fill SVG export filename with the element's own name Suggests the element's filename (without its .elmt extension) as the default save name when exporting to SVG, instead of only defaulting to the customElementsDir with no filename. Addresses plc-user's review suggestion on PR #637. --- sources/editor/ui/qetelementeditor.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/sources/editor/ui/qetelementeditor.cpp b/sources/editor/ui/qetelementeditor.cpp index 04ffa60b6..9a4eb2ffb 100644 --- a/sources/editor/ui/qetelementeditor.cpp +++ b/sources/editor/ui/qetelementeditor.cpp @@ -1397,10 +1397,20 @@ bool QETElementEditor::on_m_save_as_file_action_triggered() */ bool QETElementEditor::on_m_export_svg_action_triggered() { + //Suggest the element's own filename (without its .elmt extension) as + //the default export name, per plc-user's review on #637 -- the + //directory-only default below is unchanged for an element that has + //never been saved, since there is no filename to derive one from. + QString suggested_path = QETApp::customElementsDir(); + if (!m_file_name.isEmpty()) { + QFileInfo file_info(m_file_name); + suggested_path = QDir(file_info.absolutePath()).filePath(file_info.completeBaseName()); + } + QString fn = QFileDialog::getSaveFileName( this, tr("Exporter en SVG", "dialog title"), - m_file_name.isEmpty() ? QETApp::customElementsDir() : QDir(m_file_name).absolutePath(), + suggested_path, tr("Image SVG (*.svg)", "filetypes allowed when exporting an element to SVG")); if (fn.isEmpty()) {