Add grouping of folio items: Group and Ungroup (#1070)

Symbols, free texts, shapes and pictures can be grouped from the Edit
menu, the selection's context menu or the command search. Clicking one
member selects the group, so moving, copying and deleting act on all of
it; Ctrl+click on a member deselects the group; a rubber band touching
part of a group selects all of it when released. No default shortcut:
Ctrl+G is "jump to element".

A group is not an object in the scene. Each member keeps its place and
carries the group's uuid (QGraphicsItem::data()), saved as a "group"
attribute written only when set: a project without groups saves exactly
as before, and older versions open a grouped one and ignore the groups.
Re-parenting under a QGraphicsItemGroup would have made every member's
position group-relative; ElementTextItemGroup already needs nine special
cases for that.

- Selection is completed on clicks and at the end of a rubber band, not
  on every selectionChanged(): export, search and Tab select items
  themselves and must not have groups pulled back in.
- Project database: group_uuid on element, shape, independent_text and
  image, kept in step by projectDataBase::itemGroupChanged().
- Paste and folio duplication give each source group one new uuid.
- Undo of Ungroup restores each item's exact group.

Builds on #1065 (uuids and database rows for texts, shapes and images).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
This commit is contained in:
ispyisail
2026-09-27 18:40:25 +13:00
parent 44cabbcfe4
commit f7591e0c9d
16 changed files with 788 additions and 7 deletions
+85 -2
View File
@@ -42,7 +42,9 @@
#include "qetinformation.h"
#include "qetproject.h"
#include "diagramsortkeys.h"
#include "itemgroups.h"
#include "textgrid.h"
#include <QGraphicsView>
#include <QTextStream>
#include <algorithm>
#include <climits>
@@ -121,6 +123,7 @@ namespace {
for (T *item : items) {
Entry entry{stack_rank.value(item, INT_MAX), QString(),
item->toXml(document)};
ItemGroups::write(entry.xml, item);
// Only an item the stacking query missed needs a tiebreak.
if (entry.rank == INT_MAX) {
QTextStream stream(&entry.xml_text);
@@ -210,6 +213,7 @@ Diagram::Diagram(QETProject *project) :
connect(&border_and_titleblock,
&BorderTitleBlock::needTitleBlockTemplate,
this, &Diagram::setTitleBlockTemplate);
connect(&border_and_titleblock,
&BorderTitleBlock::informationChanged,
this, &Diagram::titleChanged);
@@ -432,7 +436,9 @@ void Diagram::mousePressEvent(QGraphicsSceneMouseEvent *event)
}
}
rememberSelection();
QGraphicsScene::mousePressEvent(event);
completeGroupSelection();
}
/**
@@ -471,6 +477,9 @@ void Diagram::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
}
QGraphicsScene::mouseReleaseEvent(event);
//A click on an already selected item changes the selection on
//release, not on press (Ctrl toggles it, a plain click keeps only it).
completeGroupSelection();
}
/**
@@ -1245,8 +1254,9 @@ QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) {
if (!list_elements.isEmpty()) {
auto dom_elements = document.createElement(QStringLiteral("elements"));
for (auto elmt : list_elements) {
dom_elements.appendChild(elmt->toXml(document,
table_adr_id));
QDomElement dom_element = elmt->toXml(document, table_adr_id);
ItemGroups::write(dom_element, elmt);
dom_elements.appendChild(dom_element);
// If copy is active we have to undo the changes we have made during creating(filling) 'list_elements'
if(is_copy_command && (elmt->linkType() == Element::Slave || elmt->linkType()&Element::AllReport))
restoreText(elmt);
@@ -1666,6 +1676,7 @@ bool Diagram::fromXml(QDomElement &document,
delete nvel_elmt;
qDebug() << QStringLiteral("Diagram::fromXml() : Le chargement des parametres d'un element a echoue");
} else {
ItemGroups::setGroup(nvel_elmt, ItemGroups::read(element_xml));
added_elements << nvel_elmt;
}
}
@@ -1701,6 +1712,7 @@ bool Diagram::fromXml(QDomElement &document,
IndependentTextItem *iti = new IndependentTextItem();
iti -> fromXml(text_xml);
settle_uuid(iti, text_xml, QStringLiteral("input"), added_texts.size());
ItemGroups::setGroup(iti, ItemGroups::read(text_xml));
addItem(iti);
added_texts << iti;
}
@@ -1713,6 +1725,7 @@ bool Diagram::fromXml(QDomElement &document,
DiagramImageItem *dii = new DiagramImageItem ();
dii -> fromXml(image_xml);
settle_uuid(dii, image_xml, QStringLiteral("image"), added_images.size());
ItemGroups::setGroup(dii, ItemGroups::read(image_xml));
addItem(dii);
added_images << dii;
}
@@ -1725,6 +1738,7 @@ bool Diagram::fromXml(QDomElement &document,
QetShapeItem *dii = new QetShapeItem (QPointF(0,0));
dii -> fromXml(shape_xml);
settle_uuid(dii, shape_xml, QStringLiteral("shape"), added_shapes.size());
ItemGroups::setGroup(dii, ItemGroups::read(shape_xml));
addItem(dii);
added_shapes << dii;
}
@@ -2127,6 +2141,75 @@ void Diagram::selectAllTextFields()
emit selectionChanged();
}
/**
@brief Diagram::setItemGroup
Put @a item in @a group, or take it out of its group if @a group is
null, and keep its row in the project database in step.
@param item
@param group
*/
void Diagram::setItemGroup(QGraphicsItem *item, const QUuid &group)
{
ItemGroups::setGroup(item, group);
if (m_project) {
m_project->dataBase()->itemGroupChanged(item);
}
}
/**
@brief Diagram::completeGroupSelection
Make the selection whole groups again after the user changed it with a
click, see ItemGroups::completeSelection(). Called from the mouse
handlers and, when a rubber band is released, from DiagramView -- not
on every selectionChanged(): code that selects items itself (export,
search, Tab cycling) deselects and reselects one item at a time and
must not have groups pulled back in behind it.
Left alone while a rubber band is being dragged, which reselects exactly
what it covers on every mouse step.
*/
void Diagram::completeGroupSelection()
{
for (QGraphicsView *view : views()) {
if (!view->rubberBandRect().isNull()) {
return;
}
}
QList<QGraphicsItem *> previous;
for (const QPointer<QGraphicsObject> &item : std::as_const(m_previous_selection)) {
if (item) {
previous << item.data();
}
}
//One selectionChanged() for the whole group, not one per member:
//the properties dock rebuilds on each.
blockSignals(true);
const bool changed = ItemGroups::completeSelection(
this, previous,
QApplication::keyboardModifiers().testFlag(Qt::ControlModifier));
blockSignals(false);
if (changed) {
emit selectionChanged();
}
rememberSelection();
}
/**
@brief Diagram::rememberSelection
Keep the current selection, the "before" completeGroupSelection() needs
to tell a member being Ctrl+clicked off.
*/
void Diagram::rememberSelection()
{
m_previous_selection.clear();
for (QGraphicsItem *item : selectedItems()) {
if (QGraphicsObject *object = item->toGraphicsObject()) {
m_previous_selection << object;
}
}
}
/**
@brief Diagram::selectNextItem
Select the next (or, if @a forward is false, the previous) selectable