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 > > +{ +public: + explicit GroupUngroupGeometryState(const QString& name) : ScItemState > >(name) {} + + QList geometryStandalone; + QList childGroupRel; + QRectF containerRect; + double containerGroupWidth {0.0}; + double containerGroupHeight {0.0}; +}; + void ScribusDoc::restoreGrouping(SimpleState* ss, bool isUndo) { auto* is = dynamic_cast > >*>(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(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* 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(is) : nullptr; + if (gs) + { + const QList& 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 geometryStandalone; int lowestItem = std::numeric_limits::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 > >(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 oldGroupItems = currItem->groupItemList; + // Capture the children's group-relative bookkeeping while they are still + // inside the group - removeFromGroup() below overwrites it. + QList 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 > >(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)