diff --git a/sources/diagramevent/diagrameventaddpaste.cpp b/sources/diagramevent/diagrameventaddpaste.cpp index ae408d4b2..e4b1b4ba5 100644 --- a/sources/diagramevent/diagrameventaddpaste.cpp +++ b/sources/diagramevent/diagrameventaddpaste.cpp @@ -28,7 +28,9 @@ #include #include +#include #include +#include #include #include @@ -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) { diff --git a/sources/diagramevent/diagrameventaddpaste.h b/sources/diagramevent/diagrameventaddpaste.h index 2cb7eaf9f..fb56113cc 100644 --- a/sources/diagramevent/diagrameventaddpaste.h +++ b/sources/diagramevent/diagrameventaddpaste.h @@ -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 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 diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index f826bbc52..ef5be8835 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -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, @@ -1239,6 +1251,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); @@ -2566,6 +2579,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()); } /** diff --git a/sources/qetdiagrameditor.h b/sources/qetdiagrameditor.h index d8c59e29d..0345255d4 100644 --- a/sources/qetdiagrameditor.h +++ b/sources/qetdiagrameditor.h @@ -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