View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0017942 | Scribus | Undo/Redo | public | 2026-08-24 14:57 | 2026-08-31 04:28 |
| Reporter | qirat | Assigned To | |||
| Priority | normal | Severity | minor | Reproducibility | always |
| Status | new | Resolution | open | ||
| Platform | Linux | OS | Fedora Workstation | OS Version | 44 |
| Product Version | 1.7.4.svn | ||||
| Summary | 0017942: [PATCH] 'Group/move/ungroup/move' undo issue | ||||
| Description | Undoing a group after moving causes undo/redo issue | ||||
| Steps To Reproduce | - Draw 2 rectangles - Group them - Move the group - Ungroup it - Move one of the (now ungrouped) rectangle) - Press Ctrl+Z (it jumps to a step that was not even there when you performed the actions) | ||||
| Additional Information | Might be related to 0012951 | ||||
| Tags | #please_test | ||||
| Attached Files | reundo1_group-ungroup-geometry-snapshot_v1.63.patch (9,914 bytes)
Index: scribus/scribusdoc.cpp
===================================================================
--- scribus/scribusdoc.cpp (revision 27773)
+++ scribus/scribusdoc.cpp (working copy)
@@ -2115,6 +2115,33 @@
setCurrentPage(oldPage);
}
+/**
+ * @brief Undo state for Group/Ungroup actions, carrying a geometry snapshot.
+ *
+ * Group children live in the container's unscaled coordinate space:
+ * PageItem_Group::DrawObj_Item() scales once by width/groupWidth, then
+ * translates each child by gXpos/gYpos and draws it at its own unscaled size.
+ * These values are only consistent as a set, so undo/redo restores them
+ * verbatim rather than deriving any of them from current absolute geometry.
+ *
+ * containerRect/containerGroupWidth/containerGroupHeight hold the container's
+ * geometry and unscaled reference size; childGroupRel holds each child's
+ * gXpos/gYpos/gWidth/gHeight; geometryStandalone holds each child's absolute
+ * geometry outside the group, used for the ungroup direction. Child order
+ * matches the item list's child entries, container excluded.
+ */
+class GroupUngroupGeometryState : public ScItemState<QList<QPointer<PageItem> > >
+{
+public:
+ explicit GroupUngroupGeometryState(const QString& name) : ScItemState<QList<QPointer<PageItem> > >(name) {}
+
+ QList<QRectF> geometryStandalone;
+ QList<QRectF> childGroupRel;
+ QRectF containerRect;
+ double containerGroupWidth {0.0};
+ double containerGroupHeight {0.0};
+};
+
void ScribusDoc::restoreGrouping(SimpleState* ss, bool isUndo)
{
auto* is = dynamic_cast<ScItemState<QList<QPointer<PageItem> > >*>(ss);
@@ -2138,15 +2165,99 @@
}
else
{
- for (int i = 0; i < select.size() - 1; ++i)
- tempSelect.addItem(select.at(i));
- tempSelect.getGroupRect(&x, &y, &w, &h);
PageItem_Group* oldGroupItem = select.last()->asGroupFrame();
- PageItem* newGroupItem = itemSelection_GroupObjects(false, false, &tempSelect, oldGroupItem);
- select.removeLast();
- select.append(newGroupItem);
- is->setItem(select);
+ auto* gsCont = dynamic_cast<GroupUngroupGeometryState*>(is);
+ bool haveSnapshot = (oldGroupItem && gsCont && !gsCont->childGroupRel.isEmpty()
+ && gsCont->containerGroupWidth > 0.0 && gsCont->containerGroupHeight > 0.0
+ && gsCont->containerRect.isValid());
+ if (!haveSnapshot)
+ {
+ for (int i = 0; i < select.size() - 1; ++i)
+ tempSelect.addItem(select.at(i));
+ tempSelect.getGroupRect(&x, &y, &w, &h);
+ PageItem* newGroupItem = itemSelection_GroupObjects(false, false, &tempSelect, oldGroupItem);
+ select.removeLast();
+ select.append(newGroupItem);
+ is->setItem(select);
+ }
+ else
+ {
+ // Reattach the children and restore their group-relative bookkeeping
+ // verbatim. Child sizes are left alone: the sibling states in this
+ // same undo transaction restore those, including their paths.
+ QList<PageItem*>* targetList = Items;
+ if (oldGroupItem->isGroupChild() && oldGroupItem->Parent && oldGroupItem->Parent->asGroupFrame())
+ targetList = &oldGroupItem->Parent->asGroupFrame()->groupItemList;
+
+ int insertIndex = targetList->size();
+ for (int i = 0; i < select.size() - 1; ++i)
+ {
+ int childIndex = targetList->indexOf(select.at(i));
+ if (childIndex >= 0)
+ insertIndex = qMin(insertIndex, childIndex);
+ }
+
+ oldGroupItem->groupItemList.clear();
+ for (int i = 0; i < select.size() - 1; ++i)
+ {
+ PageItem* child = select.at(i);
+ if (!child)
+ continue;
+ targetList->removeAll(child);
+ child->Parent = oldGroupItem;
+ oldGroupItem->groupItemList.append(child);
+ const QRectF& gr = gsCont->childGroupRel.at(qMin(i, gsCont->childGroupRel.size() - 1));
+ child->gXpos = gr.x();
+ child->gYpos = gr.y();
+ child->gWidth = gr.width();
+ child->gHeight = gr.height();
+ }
+
+ oldGroupItem->setXYPos(gsCont->containerRect.x(), gsCont->containerRect.y(), true);
+ oldGroupItem->setWidthHeight(gsCont->containerRect.width(), gsCont->containerRect.height(), true);
+ oldGroupItem->groupWidth = gsCont->containerGroupWidth;
+ oldGroupItem->groupHeight = gsCont->containerGroupHeight;
+ oldGroupItem->oldXpos = oldGroupItem->xPos();
+ oldGroupItem->oldYpos = oldGroupItem->yPos();
+ oldGroupItem->oldWidth = oldGroupItem->width();
+ oldGroupItem->oldHeight = oldGroupItem->height();
+ oldGroupItem->Clip = flattenPath(oldGroupItem->PoLine, oldGroupItem->Segments);
+
+ targetList->insert(qMin(insertIndex, targetList->size()), oldGroupItem);
+ oldGroupItem->OwnPage = OnPage(oldGroupItem);
+ oldGroupItem->adjustXYPosition();
+
+ x = oldGroupItem->xPos();
+ y = oldGroupItem->yPos();
+ w = oldGroupItem->width();
+ h = oldGroupItem->height();
+
+ select.removeLast();
+ select.append(oldGroupItem);
+ is->setItem(select);
+ }
}
+
+ // The ungroup direction runs the live ungroup code, which derives each
+ // child's absolute geometry from the container's current transform. Replay
+ // the recorded standalone geometry so the result matches the original.
+ auto* gs = isUndo ? dynamic_cast<GroupUngroupGeometryState*>(is) : nullptr;
+ if (gs)
+ {
+ const QList<QRectF>& target = gs->geometryStandalone;
+ int childCount = qMin(target.size(), select.size() - 1);
+ for (int i = 0; i < childCount; ++i)
+ {
+ PageItem* itm = select.at(i);
+ if (!itm)
+ continue;
+ const QRectF& r = target.at(i);
+ moveItem(r.x() - itm->xPos(), r.y() - itm->yPos(), itm);
+ if ((qAbs(itm->width() - r.width()) > 1e-6) || (qAbs(itm->height() - r.height()) > 1e-6))
+ sizeItem(r.width(), r.height(), itm, false, true, true);
+ }
+ }
+
QRectF rect(x, y , w, h);
regionsChanged()->update(rect.adjusted(-10, -10, 20, 20));
m_Selection->delaySignalsOff();
@@ -16020,10 +16131,12 @@
tempSelection.addItems(selectedItems);
tempSelection.getVisualGroupRect(&x, &y, &w, &h);
+ QList<QRectF> geometryStandalone;
int lowestItem = std::numeric_limits<int>::max();
for (int i = 0; i < selectedItemCount; ++i)
{
currItem = selectedItems.at(i);
+ geometryStandalone << QRectF(currItem->xPos(), currItem->yPos(), currItem->width(), currItem->height());
currItem->gXpos = currItem->xPos() - x;
currItem->gYpos = currItem->yPos() - y;
currItem->gWidth = w;
@@ -16065,6 +16178,14 @@
groupItem->groupWidth = gw;
groupItem->groupHeight = gh;
groupItem->m_layerID = objectsLayer;
+ // Keep the container's own bounding box in step with its children: a fresh
+ // container gets this from itemAdd(), a reused one does not.
+ groupItem->setXYPos(gx, gy, true);
+ groupItem->setWidthHeight(gw, gh, true);
+ groupItem->oldXpos = groupItem->xPos();
+ groupItem->oldYpos = groupItem->yPos();
+ groupItem->oldWidth = groupItem->width();
+ groupItem->oldHeight = groupItem->height();
m_undoManager->setUndoEnabled(true);
for (int i = 0; i < selectedItemCount; ++i)
{
@@ -16077,8 +16198,17 @@
if (UndoManager::undoEnabled())
{
- auto *is = new ScItemState<QList<QPointer<PageItem> > >(UndoManager::Group);
+ auto *is = new GroupUngroupGeometryState(UndoManager::Group);
is->set("GROUP");
+ is->geometryStandalone = geometryStandalone;
+ is->containerGroupWidth = groupItem->groupWidth;
+ is->containerGroupHeight = groupItem->groupHeight;
+ is->containerRect = QRectF(groupItem->xPos(), groupItem->yPos(), groupItem->width(), groupItem->height());
+ for (int i = 0; i < selectedItemCount; ++i)
+ {
+ currItem = selectedItems.at(i);
+ is->childGroupRel << QRectF(currItem->gXpos, currItem->gYpos, currItem->gWidth, currItem->gHeight);
+ }
tempSelection.addItem(groupItem);
is->setItem(tempSelection.selectionList());
m_undoManager->action(this, is);
@@ -16145,6 +16275,15 @@
list->removeAt(d);
itemSelection->removeItem(currItem);
QList<PageItem*> oldGroupItems = currItem->groupItemList;
+ // Capture the children's group-relative bookkeeping while they are still
+ // inside the group - removeFromGroup() below overwrites it.
+ QList<QRectF> childGroupRel;
+ QRectF containerRect(currItem->xPos(), currItem->yPos(), currItem->width(), currItem->height());
+ for (int gi = 0; gi < oldGroupItems.count(); ++gi)
+ {
+ PageItem* gRelItem = oldGroupItems.at(gi);
+ childGroupRel << QRectF(gRelItem->gXpos, gRelItem->gYpos, gRelItem->gWidth, gRelItem->gHeight);
+ }
int gcount = currItem->groupItemList.count();
for (int j = 0; j < gcount; j++)
{
@@ -16165,8 +16304,19 @@
}
if (UndoManager::undoEnabled())
{
- auto *is = new ScItemState<QList<QPointer<PageItem> > >(UndoManager::Ungroup);
+ auto *is = new GroupUngroupGeometryState(UndoManager::Ungroup);
is->set("UNGROUP");
+ is->containerGroupWidth = currItem->groupWidth;
+ is->containerGroupHeight = currItem->groupHeight;
+ is->containerRect = containerRect;
+ is->childGroupRel = childGroupRel;
+ // removeFromGroup() has already converted each child to absolute
+ // coordinates, so these are its true standalone values.
+ for (int gi = 0; gi < oldGroupItems.count(); ++gi)
+ {
+ PageItem* gAfterItem = oldGroupItems.at(gi);
+ is->geometryStandalone << QRectF(gAfterItem->xPos(), gAfterItem->yPos(), gAfterItem->width(), gAfterItem->height());
+ }
Selection tempSelection(this, false);
tempSelection.addItems(oldGroupItems);
tempSelection.addItem(currItem);
@@ -16365,6 +16515,15 @@
else
item->Clip = flattenPath(item->PoLine, item->Segments);
setRedrawBounding(item);
+
+ // setXYPos()/sizeItem() bypass checkChanges(), leaving oldXpos/oldYpos/
+ // oldWidth/oldHeight - the baseline the next move or resize compares
+ // against - unsynced. Resync so the item's next edit records a correct
+ // "old" value.
+ item->oldXpos = item->xPos();
+ item->oldYpos = item->yPos();
+ item->oldWidth = item->width();
+ item->oldHeight = item->height();
}
void ScribusDoc::rescaleGroup(PageItem* group, double scale)
| ||||
| Patch | Yes | ||||
|
|
Here is a new minimal one. #Cause: `removeFromGroup()` rewrites child geometry into standalone coordinates while bypassing normal `PageItem` change tracking, leaving cached `old*` values stale. #Failure mode: the next move/resize undo state could record pre-ungroup geometry and restore the item to the wrong position/size. #Fix: resynchronize the child’s cached undo/change baseline after ungroup/reparenting has finalized its geometry. #Why here: corrects the stale state at its source without changing generic Move undo or introducing a broader Group/Ungroup snapshot mechanism. #Scope: generic group/ungroup path; applies to ordinary items and imported vector groups alike. ungroup-undo-baseline-r27798-v1.0.patch (984 bytes)
Index: scribus/scribusdoc.cpp =================================================================== --- scribus/scribusdoc.cpp (revision 27798) +++ scribus/scribusdoc.cpp (working copy) @@ -16161,6 +16161,21 @@ Items->insert(d, gItem); gItem->OwnPage = OnPage(gItem); } + + // Ungrouping converts the item to its new coordinate space while normal + // PageItem change tracking is bypassed. Sync the undo baselines only + // after the item has reached its final parent/page. + gItem->oldXpos = gItem->xPos(); + gItem->oldYpos = gItem->yPos(); + gItem->oldWidth = gItem->width(); + gItem->oldHeight = gItem->height(); + gItem->oldRot = gItem->rotation(); + gItem->oldOwnPage = gItem->OwnPage; + gItem->oldLocalScX = gItem->imageXScale(); + gItem->oldLocalScY = gItem->imageYScale(); + gItem->oldLocalX = gItem->imageXOffset(); + gItem->oldLocalY = gItem->imageYOffset(); + itemSelection->addItem(gItem); } if (UndoManager::undoEnabled()) |
| Date Modified | Username | Field | Change |
|---|---|---|---|
| 2026-08-24 14:57 | qirat | New Issue | |
| 2026-08-24 14:57 | qirat | File Added: reundo1_group-ungroup-geometry-snapshot_v1.63.patch | |
| 2026-08-24 14:57 | qirat | Tag Attached: #please_test | |
| 2026-08-31 04:28 | qirat | Note Added: 0054385 | |
| 2026-08-31 04:28 | qirat | File Added: ungroup-undo-baseline-r27798-v1.0.patch |