diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake
index 3fdb11e08..a8654b903 100644
--- a/cmake/qet_compilation_vars.cmake
+++ b/cmake/qet_compilation_vars.cmake
@@ -330,6 +330,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/diagramevent/diagrameventinterface.h
${QET_DIR}/sources/diagramevent/diagrameventaddmacro.cpp
${QET_DIR}/sources/diagramevent/diagrameventaddmacro.h
+ ${QET_DIR}/sources/diagramevent/diagrameventaddpaste.cpp
+ ${QET_DIR}/sources/diagramevent/diagrameventaddpaste.h
${QET_DIR}/sources/dvevent/dveventinterface.cpp
${QET_DIR}/sources/dvevent/dveventinterface.h
diff --git a/sources/diagramevent/diagrameventaddpaste.cpp b/sources/diagramevent/diagrameventaddpaste.cpp
new file mode 100644
index 000000000..b08406b68
--- /dev/null
+++ b/sources/diagramevent/diagrameventaddpaste.cpp
@@ -0,0 +1,241 @@
+/*
+ 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 "diagrameventaddpaste.h"
+
+#include "../diagram.h"
+#include "../diagramcommands.h"
+#include "../qetapp.h"
+#include "../qetdiagrameditor.h"
+#include "../qetgraphicsitem/conductor.h"
+
+#include
+#include
+#include
+#include
+#include
+
+/**
+ @brief DiagramEventAddPaste::DiagramEventAddPaste
+ @param diagram : diagram to paste into
+ @param start_pos : where the pasted items first appear, in scene
+ coordinates -- normally the cursor
+*/
+DiagramEventAddPaste::DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos) :
+ DiagramEventInterface(diagram)
+{
+ //DiagramEventInterface::init() is called by Diagram::setEventInterface
+ //only when it is replacing an earlier interface, so call it here as
+ //DiagramEventAddMacro does.
+ init();
+
+ const QString clipboard_text = QApplication::clipboard()->text();
+ if (clipboard_text.isEmpty()) return;
+
+ QDomDocument document_xml;
+ if (!document_xml.setContent(clipboard_text)) return;
+
+ m_diagram->fromXml(document_xml, Diagram::snapToGrid(start_pos), false, &m_content);
+ if (!m_content.count()) return;
+
+ //Remember where each item sits relative to the group's top left, so a
+ //move is one assignment per item rather than an accumulated delta.
+ QRectF group_rect;
+ const QList movable = m_content.items(MovableItems);
+ for (auto *item : movable) {
+ group_rect = group_rect.united(item->mapToScene(item->boundingRect()).boundingRect());
+ }
+ const QPointF top_left = group_rect.topLeft();
+ for (auto *item : movable) {
+ m_relative_pos.insert(item, item->pos() - top_left);
+ }
+
+ m_diagram->clearSelection();
+ for (auto *item : movable) {
+ item->setSelected(true);
+ }
+
+ if (!m_diagram->views().isEmpty()) {
+ if (const auto qde = QETApp::diagramEditorAncestorOf(m_diagram->views().at(0))) {
+ m_status_bar = qde->statusBar();
+ }
+ }
+ showHint();
+
+ m_running = true;
+}
+
+/**
+ @brief DiagramEventAddPaste::~DiagramEventAddPaste
+ If the placement never finished -- the editor closed, or another tool took
+ over -- the items are still on the folio with nothing on the undo stack to
+ account for them, so take them away.
+*/
+DiagramEventAddPaste::~DiagramEventAddPaste()
+{
+ if (!m_finished && m_diagram) {
+ cancel();
+ }
+ if (m_status_bar) {
+ m_status_bar->clearMessage();
+ }
+}
+
+/**
+ @brief DiagramEventAddPaste::clipboardHasDiagram
+ @return true if the clipboard holds a diagram fragment
+*/
+bool DiagramEventAddPaste::clipboardHasDiagram()
+{
+ return Diagram::clipboardMayContainDiagram();
+}
+
+/**
+ @brief DiagramEventAddPaste::init
+ Suppress the context menu while placing, so a right click can cancel
+ instead of opening a menu over the items being positioned.
+*/
+void DiagramEventAddPaste::init()
+{
+ if (!m_diagram) return;
+ const auto views = m_diagram->views();
+ for (auto *view : views) {
+ view->setContextMenuPolicy(Qt::NoContextMenu);
+ }
+}
+
+void DiagramEventAddPaste::showHint()
+{
+ if (m_status_bar) {
+ m_status_bar->showMessage(
+ tr("Cliquez pour poser le collage, Échap ou clic droit pour annuler",
+ "status bar tip while positioning a paste"));
+ }
+}
+
+/**
+ @brief DiagramEventAddPaste::moveTo
+ Put the group's top left corner at @a scene_pos, snapped to the grid.
+*/
+void DiagramEventAddPaste::moveTo(const QPointF &scene_pos)
+{
+ const QPointF anchor = Diagram::snapToGrid(scene_pos);
+ for (auto it = m_relative_pos.constBegin() ; it != m_relative_pos.constEnd() ; ++it) {
+ if (it.key()) {
+ it.key()->setPos(anchor + it.value());
+ }
+ }
+}
+
+void DiagramEventAddPaste::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
+{
+ if (!m_running) return;
+ moveTo(event->scenePos());
+ event->setAccepted(true);
+}
+
+void DiagramEventAddPaste::mousePressEvent(QGraphicsSceneMouseEvent *event)
+{
+ if (!m_running) return;
+ //Swallowed so the press cannot start a rubber band or drag an item
+ //out of the group; the release is what decides.
+ event->setAccepted(true);
+}
+
+void DiagramEventAddPaste::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
+{
+ if (!m_running) return;
+
+ if (event->button() == Qt::LeftButton) {
+ moveTo(event->scenePos());
+ commit();
+ } else if (event->button() == Qt::RightButton) {
+ cancel();
+ }
+ event->setAccepted(true);
+}
+
+void DiagramEventAddPaste::keyPressEvent(QKeyEvent *event)
+{
+ if (!m_running) return;
+
+ switch (event->key()) {
+ case Qt::Key_Escape:
+ cancel();
+ event->setAccepted(true);
+ break;
+ //Return and Enter drop the paste where it stands, so the whole
+ //operation can be completed without a mouse.
+ case Qt::Key_Return:
+ case Qt::Key_Enter:
+ commit();
+ event->setAccepted(true);
+ break;
+ default:
+ break;
+ }
+}
+
+/**
+ @brief DiagramEventAddPaste::commit
+ Hand the items to the undo stack where they stand.
+
+ PasteDiagramCommand's first redo() does not add the items to the scene --
+ it assumes they are already there, which is what Diagram::fromXml did when
+ this started. So pushing it here adopts them rather than duplicating them.
+*/
+void DiagramEventAddPaste::commit()
+{
+ if (m_finished || !m_diagram) return;
+ m_finished = true;
+ m_running = false;
+
+ m_diagram->undoStack().push(new PasteDiagramCommand(m_diagram, m_content));
+ emit finish();
+}
+
+/**
+ @brief DiagramEventAddPaste::cancel
+ Take the items back off the folio. Nothing was pushed to the undo stack,
+ so there is nothing to undo afterwards.
+*/
+void DiagramEventAddPaste::cancel()
+{
+ if (m_finished || !m_diagram) return;
+ m_finished = true;
+ m_running = false;
+
+ //Conductors first: they hold pointers to the terminals of the
+ //elements below, so removing an element out from under one would
+ //leave it pointing at freed memory for as long as it is still in the
+ //scene.
+ const QList conductors = m_content.conductors(DiagramContent::AnyConductor);
+ for (auto *conductor : conductors) {
+ m_diagram->removeItem(conductor);
+ delete conductor;
+ }
+
+ const QList rest = m_content.items(MovableItems);
+ for (auto *item : rest) {
+ m_diagram->removeItem(item);
+ delete item;
+ }
+
+ m_content.clear();
+ m_relative_pos.clear();
+ emit finish();
+}
diff --git a/sources/diagramevent/diagrameventaddpaste.h b/sources/diagramevent/diagrameventaddpaste.h
new file mode 100644
index 000000000..a200ca04d
--- /dev/null
+++ b/sources/diagramevent/diagrameventaddpaste.h
@@ -0,0 +1,85 @@
+/*
+ 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 DIAGRAMEVENTADDPASTE_H
+#define DIAGRAMEVENTADDPASTE_H
+
+#include "diagrameventinterface.h"
+#include "../diagramcontent.h"
+
+#include
+#include
+
+class QStatusBar;
+
+/**
+ @brief The DiagramEventAddPaste class
+ Paste the clipboard as a placement you can still move.
+
+ The pasted items are added straight away and follow the cursor until a
+ left click drops them; Escape or a right click takes them away again.
+ This is the same interaction as placing a new element, so a paste behaves
+ like every other "put something on the folio" action.
+
+ The items are the real ones from the start, not a preview: Diagram::fromXml
+ creates them, this class moves them, and PasteDiagramCommand is pushed only
+ once they are dropped. That keeps one copy of the paste logic rather than
+ two, and means a cancelled paste leaves nothing on the undo stack.
+*/
+class DiagramEventAddPaste : public DiagramEventInterface
+{
+ Q_OBJECT
+
+ public:
+ ///Items with a position of their own. Conductors are left out
+ ///deliberately: they are drawn from their terminals, so they
+ ///follow when the elements they attach to move.
+ static const int MovableItems =
+ DiagramContent::Elements
+ | DiagramContent::TextFields
+ | DiagramContent::Images
+ | DiagramContent::Shapes
+ | DiagramContent::Tables
+ | DiagramContent::TerminalStrip;
+
+ DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos);
+ ~DiagramEventAddPaste() override;
+
+ void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
+ void mousePressEvent (QGraphicsSceneMouseEvent *event) override;
+ void mouseReleaseEvent (QGraphicsSceneMouseEvent *event) override;
+ void keyPressEvent (QKeyEvent *event) override;
+ void init() override;
+
+ ///@return true if the clipboard holds something this can paste.
+ static bool clipboardHasDiagram();
+
+ private:
+ void moveTo(const QPointF &scene_pos);
+ void commit();
+ void cancel();
+ void showHint();
+
+ DiagramContent m_content;
+ ///Each movable item's position relative to the group's top left,
+ ///taken once so repeated moves cannot accumulate rounding drift.
+ QHash m_relative_pos;
+ QPointer m_status_bar;
+ bool m_finished{false};
+};
+
+#endif // DIAGRAMEVENTADDPASTE_H
diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp
index 48a34c515..75a478e2c 100644
--- a/sources/qetdiagrameditor.cpp
+++ b/sources/qetdiagrameditor.cpp
@@ -29,6 +29,7 @@
#include "diagramevent/diagrameventaddshape.h"
#include "diagramevent/diagrameventaddpath.h"
#include "diagramevent/diagrameventaddtext.h"
+#include "diagramevent/diagrameventaddpaste.h"
#include "diagramview.h"
#include "elementspanelwidget.h"
#include "factory/qetgraphicstablefactory.h"
@@ -349,8 +350,21 @@ void QETDiagramEditor::setUpActions()
currentDiagramView()->copy();
});
connect(m_paste, &QAction::triggered, [this]() {
- if(currentDiagramView())
- currentDiagramView()->paste();
+ auto *dv = currentDiagramView();
+ if (!dv || !dv->diagram()) return;
+
+ //Paste as a placement rather than dropping the items straight
+ //down. Pasting in place put the copy exactly on top of the
+ //original, where it was easy to miss entirely; now it appears
+ //under the cursor and follows it until a click, Return, or Escape
+ //to cancel -- the same interaction as placing a new element.
+ const QPoint view_pos = dv->viewport()->mapFromGlobal(QCursor::pos());
+ const QPointF start_pos = dv->viewport()->rect().contains(view_pos)
+ ? dv->mapToScene(view_pos)
+ : dv->mapToScene(dv->viewport()->rect().center());
+
+ dv->diagram()->setEventInterface(
+ new DiagramEventAddPaste(dv->diagram(), start_pos));
});
//Reset conductor path