diff --git a/sources/diagramevent/diagrameventaddpaste.cpp b/sources/diagramevent/diagrameventaddpaste.cpp index 5a853d754..ae408d4b2 100644 --- a/sources/diagramevent/diagrameventaddpaste.cpp +++ b/sources/diagramevent/diagrameventaddpaste.cpp @@ -41,7 +41,6 @@ DiagramEventAddPaste::DiagramEventAddPaste(Diagram *diagram, const QPointF &start_pos) : DiagramEventInterface(diagram) { - Q_UNUSED(start_pos); // items stay at their original XML position //DiagramEventInterface::init() is called by Diagram::setEventInterface //only when it is replacing an earlier interface, so call it here as //DiagramEventAddMacro does. @@ -77,21 +76,26 @@ const QList movable = m_content.items(MovableItems); if (movable.isEmpty()) return; - //Compute the top-left of all items' positions (not bounding - //rects) and snap to grid: this is the point that gets placed - //under the cursor, and the baseline moveTo() measures from. - QPointF top_left; - bool first = true; + //Compute the top-left of all items' actual on-screen bounding + //boxes (not their raw pos()) and snap to grid: this is the point + //that gets placed under the cursor, and the baseline moveTo() + //measures from. mapToScene(boundingRect()) matters here, not + //pos() alone: pos() is the scene location of an item's local + //origin, but for anything with a pivot-centered transform (a + //scaled or rotated image, in particular) that origin can sit far + //from where the item is actually drawn -- pivot + scale*(0 - + //pivot) is nowhere near (0, 0) once scale is well under 1. Using + //pos() here silently pasted content at the right *delta* from a + //point that wasn't actually where the content visually was, + //producing a constant, scale-dependent offset between the cursor + //and the pasted picture. Diagram::fromXml()'s own position + //parameter already gets this right the same way, for the same + //reason. + QRectF items_rect; for (auto *item : movable) { - const QPointF p = item->pos(); - if (first) { - top_left = p; - first = false; - } else { - if (p.x() < top_left.x()) top_left.setX(p.x()); - if (p.y() < top_left.y()) top_left.setY(p.y()); - } + items_rect = items_rect.united(item->mapToScene(item->boundingRect()).boundingRect()); } + const QPointF top_left = items_rect.topLeft(); QSettings settings; const int xGrid = settings.value(QStringLiteral("diagrameditor/Xgrid"), Diagram::xGrid).toInt(); @@ -102,9 +106,24 @@ qRound(p.x() / xGrid) * xGrid, qRound(p.y() / yGrid) * yGrid); }; - const QPointF grid_origin = snapGrid(top_left); + const QPointF grid_origin = snapGrid(start_pos); - //Store each item's original position. moveTo() applies a + //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); + } + + //Store each item's now-placed position. moveTo() applies a //grid-snapped delta from the baseline to these, so items //preserve their layout and move in whole grid steps. for (auto *item : movable) { @@ -134,14 +153,6 @@ if (const auto qde = QETApp::diagramEditorAncestorOf(view)) { m_status_bar = qde->statusBar(); } - //Warp the cursor to the group's grid-snapped origin so - //the actual cursor position matches m_initial_cursor. - //Without this the first mouseMoveEvent computes a large - //delta (cursor is still at the Ctrl+V press location) - //and the items jump on first touch. - const QPoint view_pos = view->mapFromScene(m_initial_cursor); - const QPoint global_pos = view->viewport()->mapToGlobal(view_pos); - QCursor::setPos(global_pos); } } showHint(); diff --git a/sources/diagramview.cpp b/sources/diagramview.cpp index 3c07a5fb3..843b694be 100644 --- a/sources/diagramview.cpp +++ b/sources/diagramview.cpp @@ -584,6 +584,7 @@ void DiagramView::mousePressEvent(QMouseEvent *e) */ void DiagramView::mouseMoveEvent(QMouseEvent *e) { + m_last_mouse_pos = e->pos(); setToolTip(tr("X: %1 Y: %2").arg(e->pos().x()).arg(e->pos().y())); if (m_event_interface && m_event_interface->mouseMoveEvent(e)) return; diff --git a/sources/diagramview.h b/sources/diagramview.h index c35544c58..eb73432cb 100644 --- a/sources/diagramview.h +++ b/sources/diagramview.h @@ -55,6 +55,7 @@ class DiagramView : public PaletteGraphicsView QAction *m_multi_paste = nullptr; QAction *m_create_template = nullptr; QPoint m_paste_here_pos; + QPoint m_last_mouse_pos = QPoint(-1, -1); QPointF m_drag_last_pos; bool m_fresh_focus_in, m_first_activation = true; @@ -71,6 +72,12 @@ class DiagramView : public PaletteGraphicsView void editSelection(); void setEventInterface (DVEventInterface *event_interface); QList contextMenuActions() const; + /// Last mouse position seen by mouseMoveEvent(), in viewport + /// coordinates -- (-1, -1) if the mouse hasn't moved over this + /// view yet. Filled from ordinary Qt mouse events, not a global + /// cursor query (QCursor::pos()/setPos() are silently ignored by + /// several window managers and compositors, Wayland included). + QPoint lastMousePos() const { return m_last_mouse_pos; } protected: void mouseDoubleClickEvent(QMouseEvent *) override; diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index 6db163f87..2cd164c87 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -367,10 +367,18 @@ void QETDiagramEditor::setUpActions() //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->lastMousePos() (an ordinary Qt mouse-move position), not + //QCursor::pos() (a global, OS-level cursor query): several + //window managers and compositors -- Wayland in particular -- + //silently refuse that query, returning a stale or wrong + //position, which is exactly what made the pasted content land + //far from the cursor instead of under it. + const QPoint last_pos = dv->lastMousePos(); + const QPoint view_pos = (last_pos.x() >= 0 && dv->viewport()->rect().contains(last_pos)) + ? last_pos + : dv->viewport()->rect().center(); + const QPointF start_pos = dv->mapToScene(view_pos); dv->diagram()->setEventInterface( new DiagramEventAddPaste(dv->diagram(), start_pos));