From afa35e094dd225de4ed8af6a91af6735fe6e2e39 Mon Sep 17 00:00:00 2001 From: Beat Hangartner Date: Wed, 7 Oct 2026 06:19:33 +0200 Subject: [PATCH] Undo restores whether a picture's pivot was placed by hand A picture remembers whether its pivot was placed by hand: a hand-placed pivot is saved and kept through a resize, a default one is not saved and recentres after a resize. Two actions changed that flag outside their undo step: - applyCrop() marked the pivot as default after moving it to the centre of the kept region. After Ctrl+Z the pivot was back where the user had put it, but marked as default, so the next save dropped it. - dragging the pivot handle marked it as hand-placed. After Ctrl+Z the pivot was back at the centre but still marked as hand-placed, so a later resize left it at the handle's anchor corner. pivotIsCustom becomes a property, and both actions put it into their undo command next to rawPivot. tst_imagecropundo checks that a project saved after undoing a crop keeps its hand-placed pivot. Co-Authored-By: Claude Fable 5.1 --- sources/qetgraphicsitem/diagramimageitem.cpp | 13 +++++- sources/qetgraphicsitem/diagramimageitem.h | 7 +++ tests/qttest/tst_imagecropundo.cpp | 47 ++++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/sources/qetgraphicsitem/diagramimageitem.cpp b/sources/qetgraphicsitem/diagramimageitem.cpp index 78a64bfb9..e2dc0f5ce 100644 --- a/sources/qetgraphicsitem/diagramimageitem.cpp +++ b/sources/qetgraphicsitem/diagramimageitem.cpp @@ -805,6 +805,7 @@ void DiagramImageItem::handlerMousePressEvent(int index, Qt::KeyboardModifiers m m_vector_index = index; m_original_pos = pos(); m_original_transform = m_transform; + m_original_pivotIsCustom = m_pivotIsCustom; if (m_handleRoles.at(index) == HandleRole::Resize) { @@ -921,7 +922,12 @@ void DiagramImageItem::handlerMouseReleaseEvent(int index) undo = new QUndoCommand(tr("Déplacer le centre de rotation d'une image")); new QPropertyUndoCommand(this, "pos", m_original_pos, pos(), undo); new QPropertyUndoCommand(this, "rawPivot", m_original_transform.pivot, m_transform.pivot, undo); + // dragPivot() marked the pivot as hand-placed; undoing + // the move has to take that back as well. + new QPropertyUndoCommand(this, "pivotIsCustom", m_original_pivotIsCustom, m_pivotIsCustom, undo); } + else + m_pivotIsCustom = m_original_pivotIsCustom; // dragged back to where it was: nothing to undo break; } @@ -1991,7 +1997,7 @@ void DiagramImageItem::crop() simply the whole base. One undo step, which restores the pixmap, the position, the pivot - and the crop rectangle together. + (and whether it was hand-placed) and the crop rectangle together. @return false if nothing was cropped: read-only folio, or a rectangle that is empty, outside the original, or the current one. */ @@ -2032,7 +2038,10 @@ bool DiagramImageItem::applyCrop(const QRect &cropRect) QVariant::fromValue(newSource), undo); new QPropertyUndoCommand(this, "pos", oldPos, newPos, undo); new QPropertyUndoCommand(this, "rawPivot", oldPivot, newOriginPoint, undo); - m_pivotIsCustom = false; + // The pivot goes to the centre of the kept region, so it is no longer + // hand-placed -- until the crop is undone, which brings the old pivot + // back and must bring its flag back with it. + new QPropertyUndoCommand(this, "pivotIsCustom", m_pivotIsCustom, false, undo); diagram()->undoStack().push(undo); return true; } diff --git a/sources/qetgraphicsitem/diagramimageitem.h b/sources/qetgraphicsitem/diagramimageitem.h index ad8a1c366..820c9fd5c 100644 --- a/sources/qetgraphicsitem/diagramimageitem.h +++ b/sources/qetgraphicsitem/diagramimageitem.h @@ -65,6 +65,10 @@ class DiagramImageItem : public QetGraphicsItem { // would silently overwrite that already-correct pos() a second // time. rawPivot exists solely for that one caller. Q_PROPERTY(QPointF rawPivot READ pivot WRITE setPivotRaw NOTIFY transformChanged) + // Whether the pivot was placed by hand: a hand-placed pivot is saved + // and kept through a resize. Changes in the same undo step as the + // pivot itself, so that Ctrl+Z restores both. + Q_PROPERTY(bool pivotIsCustom READ pivotIsCustom WRITE setPivotIsCustom) // constructors, destructor public: @@ -120,6 +124,8 @@ class DiagramImageItem : public QetGraphicsItem { void editProperty() override; void setPixmap(const QPixmap &pixmap); QPixmap pixmap() const { return pixmap_; } + bool pivotIsCustom() const { return m_pivotIsCustom; } + void setPivotIsCustom(bool custom) { m_pivotIsCustom = custom; } QRectF boundingRect() const override; QString name() const override; @@ -259,6 +265,7 @@ class DiagramImageItem : public QetGraphicsItem { int m_vector_index = -1; QPointF m_original_pos; // scene position at the start of a resize/rotate/pivot drag, for Escape-to-cancel ShapeTransform m_original_transform; + bool m_original_pivotIsCustom = false; bool m_deferHandleReposition = false; // see setPivot()'s comment // Optional caption drawn centred under the picture (issue #349). // Empty by default, and then neither saved nor painted, so a picture diff --git a/tests/qttest/tst_imagecropundo.cpp b/tests/qttest/tst_imagecropundo.cpp index cb2e0cc3b..b92d9c0ff 100644 --- a/tests/qttest/tst_imagecropundo.cpp +++ b/tests/qttest/tst_imagecropundo.cpp @@ -33,6 +33,11 @@ shown: before, a project saved after Ctrl+Z still recorded the undone crop, and the picture came back cropped once reopened. Runs the real binary on a script: add a picture, crop it, undo, save; redo, save. + + It also restores whether the pivot was placed by hand: a crop moves + the pivot to the centre of the kept region and marks it as default, + and before, Ctrl+Z brought the hand-placed pivot back without its + mark, so the next save dropped it. */ class tst_imagecropundo : public QObject { @@ -145,6 +150,48 @@ qet.log('PROBE ' + JSON.stringify(r)); for (const char *attribute : {R"(x="10")", R"(y="5")", R"(w="20")", R"(h="10")"}) QVERIFY2(crop.captured(1).contains(QLatin1String(attribute)), attribute); } + + void undoneCropKeepsHandPlacedPivot() + { + // A picture whose pivot was placed by hand: saved as + // with pivotX/pivotY, which no script call writes, so the test + // adds it to a saved project itself. + const QString plain = m_dir.filePath(QStringLiteral("plain.qet")); + const QString pivot = m_dir.filePath(QStringLiteral("pivot.qet")); + QJsonObject r = run(QStringLiteral(R"JS( +var i = qet.addImage(0, '%1', 100, 100); +qet.save('%2'); +qet.log('PROBE ' + JSON.stringify({added: i >= 0})); +)JS").arg(m_dir.filePath(QStringLiteral("pic.png")), plain), + QStringLiteral(QET_EXAMPLES_DIR "/741.qet")); + QVERIFY(r.value("added").toBool()); + QByteArray xml = read(plain); + QCOMPARE(xml.count(""), 1); + xml.replace("", ""); + QFile f(pivot); + QVERIFY(f.open(QIODevice::WriteOnly)); + f.write(xml); + f.close(); + + const QString before = m_dir.filePath(QStringLiteral("before.qet")); + const QString undone = m_dir.filePath(QStringLiteral("pivot-undone.qet")); + r = run(QStringLiteral(R"JS( +var r = {}; +qet.save('%1'); +r.cropped_ok = qet.cropImage(0, 0, 10, 5, 20, 10); +qet.undo(); +qet.save('%2'); +qet.log('PROBE ' + JSON.stringify(r)); +)JS").arg(before, undone), pivot); + QVERIFY(r.value("cropped_ok").toBool()); + // The fixture is read as intended: saved untouched, it keeps the pivot. + const QByteArray saved = read(before); + QVERIFY(saved.contains("pivotX=\"5\"") && saved.contains("pivotY=\"7\"")); + const QByteArray afterUndo = read(undone); + QVERIFY2(afterUndo.contains("pivotX=\"5\"") && afterUndo.contains("pivotY=\"7\""), + "the hand-placed pivot was dropped after undoing the crop"); + } }; QTEST_GUILESS_MAIN(tst_imagecropundo)