From 6ed41bbfd0bcd13e0c21589c5b85a02f72c10f0b Mon Sep 17 00:00:00 2001 From: ispyisail Date: Mon, 28 Sep 2026 22:33:28 +1300 Subject: [PATCH] Click again on a member of a selected group to pick it on its own Clicking an item of a group selects the whole group (#1070). Clicking again on one of its items is meant to select just that item, to edit it on its own -- what discussion #1070 proposed -- but the second click selected the whole group again: Qt left only the clicked item selected on release, and the group completion pulled the others back in. A press on a member of a group that is selected whole now notes that member (ItemGroups::memberToPick(), which also finds the member when the click lands on a symbol's own text). If the click ends without a drag and Qt has left only that member selected, the selection stays so. A drag still moves the whole group; Ctrl+click keeps its meaning; a group of one is not picked from. In the GUI, on two grouped texts: one click then Delete removes both (master and this); click, click again, Delete removes only the clicked text here, both on master; dragging after one click moves both texts by the same amount on both. tst_itemgroups: 5 new checks; removing the whole-group or the group-of-one condition fails one each. ctest 24/24. Co-Authored-By: Claude Opus 5.5 --- sources/diagram.cpp | 31 ++++++++++++++++++++++++ sources/diagram.h | 3 +++ sources/itemgroups.cpp | 41 ++++++++++++++++++++++++++++++++ sources/itemgroups.h | 3 +++ tests/qttest/tst_itemgroups.cpp | 42 +++++++++++++++++++++++++++++++++ 5 files changed, 120 insertions(+) diff --git a/sources/diagram.cpp b/sources/diagram.cpp index 4831b6484..498a82341 100644 --- a/sources/diagram.cpp +++ b/sources/diagram.cpp @@ -437,6 +437,24 @@ void Diagram::mousePressEvent(QGraphicsSceneMouseEvent *event) } rememberSelection(); + //Clicking again on a member of a group that is selected whole picks + //that member out, to edit it on its own (discussion #1070): noted + //here, decided on release, since a drag must still move the group. + //Ctrl keeps its usual meaning. + m_member_to_pick.clear(); + if (event->button() == Qt::LeftButton + && !event->modifiers().testFlag(Qt::ControlModifier)) { + QTransform view_transform; + if (event->widget()) { + if (auto view = qobject_cast(event->widget()->parentWidget())) { + view_transform = view->transform(); + } + } + if (QGraphicsItem *member = ItemGroups::memberToPick( + itemAt(event->scenePos(), view_transform))) { + m_member_to_pick = member->toGraphicsObject(); + } + } QGraphicsScene::mousePressEvent(event); completeGroupSelection(); } @@ -477,6 +495,19 @@ void Diagram::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) } QGraphicsScene::mouseReleaseEvent(event); + + //A click that did not drag, on a member of a group selected whole: + //Qt has left only that member selected, and it stays so. + QGraphicsObject *picked = m_member_to_pick.data(); + m_member_to_pick.clear(); + if (picked + && (event->screenPos() - event->buttonDownScreenPos(Qt::LeftButton)).manhattanLength() + < QApplication::startDragDistance() + && selectedItems() == QList{picked}) { + rememberSelection(); + return; + } + //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(); diff --git a/sources/diagram.h b/sources/diagram.h index 8c2b453d6..83d408835 100644 --- a/sources/diagram.h +++ b/sources/diagram.h @@ -142,6 +142,9 @@ class Diagram : public QGraphicsScene //Selection before the current click, see completeGroupSelection() QList> m_previous_selection; void rememberSelection(); + //Member of a wholly selected group under the current click, which + //the click picks out on its own if it ends without a drag + QPointer m_member_to_pick; bool uuidUsedByOtherDiagram(const QUuid &uuid) const; QUuid derivedUuid(const QDomElement &root, const QString &reason) const; diff --git a/sources/itemgroups.cpp b/sources/itemgroups.cpp index 4e4c7ac9d..60da47593 100644 --- a/sources/itemgroups.cpp +++ b/sources/itemgroups.cpp @@ -62,6 +62,47 @@ QUuid ItemGroups::read(const QDomElement &xml) return QUuid(xml.attribute(QString::fromLatin1(xml_attribute))); } +/** + @return @a item, or its nearest ancestor, that belongs to a group -- a + click on a symbol's own text hits the text, but the symbol is the member + -- or nullptr if none does. +*/ +QGraphicsItem *ItemGroups::groupedItem(QGraphicsItem *item) +{ + for (; item; item = item->parentItem()) { + if (!groupOf(item).isNull()) { + return item; + } + } + return nullptr; +} + +/** + @return the member a click on @a hit may pick out on its own: the grouped + item hit, when every member of its group is selected already. A first + click selects the whole group; a second click, on a member of the group + it selected, is how the user asks for that member alone (discussion + #1070). nullptr when the click is not that. +*/ +QGraphicsItem *ItemGroups::memberToPick(QGraphicsItem *hit) +{ + QGraphicsItem *member = groupedItem(hit); + if (!member || !member->isSelected() || !member->scene()) { + return nullptr; + } + const QUuid group = groupOf(member); + int members = 0; + for (QGraphicsItem *item : member->scene()->items()) { + if (groupOf(item) == group) { + if (!item->isSelected()) { + return nullptr; + } + ++members; + } + } + return members > 1 ? member : nullptr; +} + /** Make the selection of @a scene whole groups again after it changed. A group with a selected member is selected entirely, except when the diff --git a/sources/itemgroups.h b/sources/itemgroups.h index 7e3c944b1..7e785bd8c 100644 --- a/sources/itemgroups.h +++ b/sources/itemgroups.h @@ -52,6 +52,9 @@ namespace ItemGroups void write(QDomElement &xml, const QGraphicsItem *item); QUuid read(const QDomElement &xml); + QGraphicsItem *groupedItem(QGraphicsItem *item); + QGraphicsItem *memberToPick(QGraphicsItem *hit); + bool completeSelection(QGraphicsScene *scene, const QList &previous, bool toggling); diff --git a/tests/qttest/tst_itemgroups.cpp b/tests/qttest/tst_itemgroups.cpp index 079c8c94c..486c8b82b 100644 --- a/tests/qttest/tst_itemgroups.cpp +++ b/tests/qttest/tst_itemgroups.cpp @@ -121,6 +121,48 @@ private slots: b = nullptr; } + // A second click on a member of a group selected whole picks that member + // out; a click on a member of a group not selected whole does not. + void aMemberOfAWholeGroupCanBePicked() + { + select({a, b}); + QCOMPARE(ItemGroups::memberToPick(a), a); + QCOMPARE(ItemGroups::memberToPick(b), b); + } + + void aMemberOfAPartlySelectedGroupIsNotPicked() + { + select({a}); // after one member was picked + QCOMPARE(ItemGroups::memberToPick(a), nullptr); + select({}); + QCOMPARE(ItemGroups::memberToPick(a), nullptr); + } + + void anUngroupedItemIsNotPicked() + { + select({c}); + QCOMPARE(ItemGroups::memberToPick(c), nullptr); + QCOMPARE(ItemGroups::memberToPick(nullptr), nullptr); + } + + void aGroupOfOneIsNotPicked() + { + ItemGroups::setGroup(e, QUuid()); // g2 is now d alone + select({d}); + QCOMPARE(ItemGroups::memberToPick(d), nullptr); + } + + // A click lands on a symbol's own text, not on the symbol: the member is + // the nearest grouped ancestor. + void aClickOnAMembersChildPicksTheMember() + { + auto child = new QGraphicsRectItem(0, 0, 2, 2, a); + QCOMPARE(ItemGroups::groupedItem(child), a); + select({a, b}); + QCOMPARE(ItemGroups::memberToPick(child), a); + QCOMPARE(ItemGroups::groupedItem(c), nullptr); + } + void xmlRoundTrip() { QDomDocument doc;