View Issue Details

IDProjectCategoryView StatusLast Update
0017942ScribusUndo/Redopublic2026-08-31 04:28
Reporterqirat Assigned To 
PrioritynormalSeverityminorReproducibilityalways
Status newResolutionopen 
PlatformLinuxOSFedora WorkstationOS Version44
Product Version1.7.4.svn 
Summary0017942: [PATCH] 'Group/move/ungroup/move' undo issue
DescriptionUndoing 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 InformationMight 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)
PatchYes

Activities

qirat

2026-08-31 04:28

reporter   ~0054385

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())

Issue History

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