Merge pull request #1208 from Kellermorph/strg-shift-v
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 3m18s

Ctrl+Shift+V pastes at the origin point, Ctrl+V keeps pasting under the cursor
This commit is contained in:
Laurent Trinques
2026-10-01 18:24:03 +02:00
committed by GitHub
4 changed files with 111 additions and 29 deletions
+70 -20
View File
@@ -28,7 +28,9 @@
#include <QApplication>
#include <QClipboard>
#include <QCursor>
#include <QGraphicsSceneMouseEvent>
#include <QGraphicsView>
#include <QKeyEvent>
#include <QStatusBar>
@@ -36,11 +38,18 @@
@brief DiagramEventAddPaste::DiagramEventAddPaste
@param diagram : diagram to paste into
@param start_pos : where the pasted items first appear, in scene
coordinates -- normally the cursor
coordinates -- normally the cursor. Only used for UnderCursor; an
AtOrigin paste stays at the coordinates it was copied from.
@param placement : UnderCursor moves the group under the cursor
(default), AtOrigin leaves it at its original XML position and warps
the OS cursor to the group's origin so baseline and screen match.
*/
DiagramEventAddPaste::DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos) :
DiagramEventAddPaste::DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos,
PastePlacement placement) :
DiagramEventInterface(diagram)
{
m_placement = placement;
//DiagramEventInterface::init() is called by Diagram::setEventInterface
//only when it is replacing an earlier interface, so call it here as
//DiagramEventAddMacro does.
@@ -106,21 +115,31 @@
qRound(p.x() / xGrid) * xGrid,
qRound(p.y() / yGrid) * yGrid);
};
const QPointF grid_origin = snapGrid(start_pos);
const QPointF grid_origin = (m_placement == AtOrigin)
? snapGrid(top_left)
: snapGrid(start_pos);
//Land the pasted content under the cursor immediately, rather than
//leaving it at the copied source's own coordinates: fromXml() above
//loads items at their original position purely because it doesn't
//know the target yet, not because that is where a paste should end
//up. The previous approach instead left items there and warped the
//OS cursor to match -- QCursor::setPos() is silently ignored by
//many window managers and compositors (Wayland in particular), so
//on any of those the warp simply never happened and the paste was
//left wherever it had originally been copied from, which could be
//anywhere on the folio -- exactly the "far from the cursor" bug.
const QPointF initial_delta = grid_origin - snapGrid(top_left);
for (auto *item : movable) {
item->setPos(item->pos() + initial_delta);
if (m_placement == UnderCursor) {
//Land the pasted content under the cursor immediately, rather than
//leaving it at the copied source's own coordinates: fromXml() above
//loads items at their original position purely because it doesn't
//know the target yet, not because that is where a paste should end
//up. The previous approach instead left items there and warped the
//OS cursor to match -- QCursor::setPos() is silently ignored by
//many window managers and compositors (Wayland in particular), so
//on any of those the warp simply never happened and the paste was
//left wherever it had originally been copied from, which could be
//anywhere on the folio -- exactly the "far from the cursor" bug.
const QPointF initial_delta = grid_origin - snapGrid(top_left);
for (auto *item : movable) {
item->setPos(item->pos() + initial_delta);
}
} else {
//AtOrigin: the items keep the coordinates they were copied from,
//so the copy stands exactly where the original stands. No delta is
//applied; what has to move is the cursor -- it is warped to the
//group's grid-snapped origin below, once m_group_origin is set,
//so the baseline and the physical cursor position agree.
}
//Store each item's now-placed position. moveTo() applies a
@@ -138,10 +157,41 @@
conductor->updatePath();
}
//The baseline is the group's grid-snapped origin, so moveTo()
//does not have to capture one from the first mouse movement.
m_initial_cursor = m_group_origin;
m_baseline_captured = true;
//The baseline is the group's grid-snapped origin. For UnderCursor
//the group was just put there, so the baseline is known outright.
//For AtOrigin the group stayed where it was copied from and the
//OS cursor is warped to match instead -- but only a warp that
//verifiably landed may be trusted as the baseline: QCursor::setPos()
//needs pointer focus inside the target window (on Wayland it goes
//through wp_pointer_warp_v1, and is a silent no-op without it), and
//a baseline that assumes a warp which did not happen would move the
//whole group by the warp-sized delta on the first mouse move. So
//when the warp cannot be confirmed, leave m_baseline_captured
//false: moveTo() re-baselines from the first real mouse position
//instead, which costs at most the first movement but never flings
//the group across the folio.
if (m_placement == AtOrigin && !m_diagram->views().isEmpty()) {
if (auto *view = m_diagram->views().at(0)) {
//An origin outside the visible area cannot be warped to
//(the compositor rejects targets outside the window) and
//would leave the paste invisible, so scroll it into view
//first.
if (!view->viewport()->rect().contains(view->mapFromScene(m_group_origin))) {
view->ensureVisible(items_rect, 50, 50);
}
const QPoint view_pos = view->mapFromScene(m_group_origin);
const QPoint global_pos = view->viewport()->mapToGlobal(view_pos);
QCursor::setPos(global_pos);
if ((QCursor::pos() - global_pos).manhattanLength() <= 4) {
m_initial_cursor = m_group_origin;
m_baseline_captured = true;
}
}
}
if (m_placement == UnderCursor) {
m_initial_cursor = m_group_origin;
m_baseline_captured = true;
}
m_diagram->clearSelection();
for (auto *item : movable) {
+24 -7
View File
@@ -56,7 +56,21 @@ class DiagramEventAddPaste : public DiagramEventInterface
| DiagramContent::Tables
| DiagramContent::TerminalStrip;
DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos);
///Where the group starts, relative to what was copied.
enum PastePlacement {
///Move the group so its top left lands at \a start_pos
///(normally the cursor): Ctrl+V, the copy appears under
///the pointer and follows it.
UnderCursor,
///Leave the group at its original XML position and warp
///the OS cursor to the group's origin so the baseline
///matches what is on screen: Ctrl+Shift+V, "paste at the
///origin point".
AtOrigin
};
DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos,
PastePlacement placement = UnderCursor);
~DiagramEventAddPaste() override;
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
@@ -79,14 +93,17 @@ class DiagramEventAddPaste : public DiagramEventInterface
///Each movable item's position relative to the group's top left,
///taken once so repeated moves cannot accumulate rounding drift.
QHash<QGraphicsItem *, QPointF> m_relative_pos;
///Where the group's grid-snapped top left was put when the paste
///started, in scene coordinates -- the cursor, so the copy
///appears under the pointer rather than on top of what was
///copied.
///Where this paste starts: UnderCursor moves the group to
///start_pos, AtOrigin leaves it at its original XML position.
PastePlacement m_placement{UnderCursor};
///Where the group's grid-snapped top left sits when the paste
///started, in scene coordinates -- under the cursor for
///UnderCursor, the copy's own origin for AtOrigin.
QPointF m_group_origin;
///Cursor position (scene coords) the delta-based movement in
///moveTo() measures from. Equal to m_group_origin, since the
///group is placed at the cursor.
///moveTo() measures from. Equal to m_group_origin: either the
///group was put there (UnderCursor) or the cursor was warped
///there (AtOrigin).
QPointF m_initial_cursor;
///Whether m_initial_cursor holds a usable baseline. A flag rather
///than testing m_initial_cursor.isNull(), which cannot tell "not
+16 -2
View File
@@ -359,14 +359,17 @@ void QETDiagramEditor::setUpActions()
m_cut = new QAction(QET::Icons::EditCut, tr("Co&uper"), this);
m_copy = new QAction(QET::Icons::EditCopy, tr("Cop&ier"), this);
m_paste = new QAction(QET::Icons::EditPaste, tr("C&oller"), this);
m_paste_origin = new QAction(QET::Icons::EditPaste, tr("Coller au point d'origine"), this);
ShortcutManager::instance().registerAction(m_cut, "diagrameditor.cut", tr("Éditeur de schémas"), QKeySequence::Cut);
ShortcutManager::instance().registerAction(m_copy, "diagrameditor.copy", tr("Éditeur de schémas"), QKeySequence::Copy);
ShortcutManager::instance().registerAction(m_paste, "diagrameditor.paste", tr("Éditeur de schémas"), QKeySequence::Paste);
ShortcutManager::instance().registerAction(m_paste_origin, "diagrameditor.paste_origin", tr("Éditeur de schémas"), QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_V));
m_cut -> setStatusTip(tr("Transfère les éléments sélectionnés dans le presse-papier", "status bar tip"));
m_copy -> setStatusTip(tr("Copie les éléments sélectionnés dans le presse-papier", "status bar tip"));
m_paste -> setStatusTip(tr("Place les éléments du presse-papier sur le folio", "status bar tip"));
m_paste_origin -> setStatusTip(tr("Place les éléments du presse-papier à leur position d'origine et déplace le curseur vers ce point", "status bar tip"));
connect(m_cut, &QAction::triggered, [this]() {
if (currentDiagramView())
@@ -376,7 +379,10 @@ void QETDiagramEditor::setUpActions()
if (currentDiagramView())
currentDiagramView()->copy();
});
connect(m_paste, &QAction::triggered, [this]() {
//Both paste shortcuts share one starter; they differ only in the
//placement mode handed to DiagramEventAddPaste.
const auto start_paste = [this](DiagramEventAddPaste::PastePlacement placement) {
auto *dv = currentDiagramView();
if (!dv || !dv->diagram()) return;
@@ -399,7 +405,13 @@ void QETDiagramEditor::setUpActions()
const QPointF start_pos = dv->mapToScene(view_pos);
dv->diagram()->setEventInterface(
new DiagramEventAddPaste(dv->diagram(), start_pos));
new DiagramEventAddPaste(dv->diagram(), start_pos, placement));
};
connect(m_paste, &QAction::triggered, [start_paste]() {
start_paste(DiagramEventAddPaste::UnderCursor);
});
connect(m_paste_origin, &QAction::triggered, [start_paste]() {
start_paste(DiagramEventAddPaste::AtOrigin);
});
//Duplicate: copy the selection and place it at a configured,
@@ -1247,6 +1259,7 @@ void QETDiagramEditor::setUpMenu()
menu_edition -> addAction(m_cut);
menu_edition -> addAction(m_copy);
menu_edition -> addAction(m_paste);
menu_edition -> addAction(m_paste_origin);
menu_edition -> addAction(m_duplicate);
menu_edition -> addAction(m_configure_duplicate);
menu_edition -> addAction(m_insert_last_element);
@@ -2578,6 +2591,7 @@ void QETDiagramEditor::slot_updatePasteAction()
// pour coller, il faut un schema ouvert et un schema dans le presse-papier
m_paste -> setEnabled(editable_diagram && Diagram::clipboardMayContainDiagram());
m_paste_origin -> setEnabled(editable_diagram && Diagram::clipboardMayContainDiagram());
}
/**
+1
View File
@@ -220,6 +220,7 @@ class QETDiagramEditor : public QETMainWindow
*undo, ///< Cancel the latest action
*redo, ///< Redo the latest cancelled operation
*m_paste, ///< Paste clipboard content on the current diagram
*m_paste_origin, ///< Same, at the copied position, cursor warped to the origin (Ctrl+Shift+V)
*m_duplicate, ///< Copy selection, offset by the configured step (#991)
*m_configure_duplicate, ///< Reopen the duplicate offset/direction dialog (#991)
*m_auto_conductor, ///< Enable/Disable the use of auto conductor