From 55c2c0df9d2c35e614bfca31f800eaf35a50e44c Mon Sep 17 00:00:00 2001 From: ispyisail Date: Tue, 15 Sep 2026 17:07:15 +1200 Subject: [PATCH] Paste under the cursor and let it be positioned before it lands Ctrl+V pasted in place, which put the copy exactly on top of the original. Nothing appeared to happen: the only clue was a doubled outline, and the copy had to be dragged off the original to be seen at all. The cursor was ignored entirely. Ctrl+V now starts a placement. The items appear under the cursor and follow it until a left click or Return drops them; Escape or a right click takes them away again. That is the same interaction as placing a new element, so paste behaves like every other way of putting something on a folio, and the copy lands where the user is looking. Implemented as a DiagramEventInterface beside the existing add-element and add-macro tools. The pasted items are the real ones from the start rather than a preview: Diagram::fromXml creates them exactly as before, this class moves them, and PasteDiagramCommand is pushed only once they are dropped. PasteDiagramCommand's first redo() deliberately does not add items to the scene -- it assumes fromXml already did -- so pushing it on commit adopts them rather than duplicating them. One copy of the paste logic, and a cancelled paste leaves nothing on the undo stack. Conductors are not moved directly; they are drawn from their terminals and follow the elements they attach to. On cancel they are removed before the elements, so none is left in the scene holding a pointer to a freed terminal. Verified by counting elements in the saved file rather than by eye: 56 to start, 56 after paste-then-Escape, 57 after paste-then-drop, and 56 again after undo. Save determinism run against this build: pass, no regressions against baseline. Tests 5/5 on Qt 6. Co-Authored-By: Claude Opus 5 --- cmake/qet_compilation_vars.cmake | 2 + sources/diagramevent/diagrameventaddpaste.cpp | 241 ++++++++++++++++++ sources/diagramevent/diagrameventaddpaste.h | 85 ++++++ sources/qetdiagrameditor.cpp | 18 +- 4 files changed, 344 insertions(+), 2 deletions(-) create mode 100644 sources/diagramevent/diagrameventaddpaste.cpp create mode 100644 sources/diagramevent/diagrameventaddpaste.h 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 cf6f7d8fd..c771e38c8 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