Merge remote-tracking branch 'upstream/master' into feat/lasting-wire-uuid

# Conflicts:
#	tests/qttest/CMakeLists.txt
This commit is contained in:
ispyisail
2026-09-29 06:34:16 +13:00
11 changed files with 444 additions and 13 deletions
+31
View File
@@ -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<QGraphicsView *>(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<QGraphicsItem *>{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();
+3
View File
@@ -142,6 +142,9 @@ class Diagram : public QGraphicsScene
//Selection before the current click, see completeGroupSelection()
QList<QPointer<QGraphicsObject>> 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<QGraphicsObject> m_member_to_pick;
bool uuidUsedByOtherDiagram(const QUuid &uuid) const;
QUuid derivedUuid(const QDomElement &root, const QString &reason) const;
+25 -11
View File
@@ -144,6 +144,27 @@ bool DiagramContext::operator!=(const DiagramContext &dc) const
return(!(*this == dc));
}
namespace {
/**
The value as it is saved, and so as it is read back: stray leading and
trailing whitespace around real content trimmed, but not a value that IS
whitespace -- unconditionally trimming an all-whitespace string collapses
it to "", which is silently indistinguishable from a value that was never
set. A title-block custom variable set to a single space -- a workaround
for #973, where an unset variable renders as its own literal placeholder
-- would otherwise vanish on the very next save.
Applied when reading as well as when writing, so that what is in memory
after a load is what the next save writes: a label shown from an
untrimmed value would otherwise keep its spaces on screen and in its
displayed copy until the project was saved and opened again, and a
just-saved project would change on its second save.
*/
QString storedValue(const QString &raw)
{
return raw.trimmed().isEmpty() ? raw : raw.trimmed();
}
} // namespace
/**
Export this context properties under the \a e XML element, using tags
named \a tag_name (defaults to "property").
@@ -161,15 +182,7 @@ void DiagramContext::toXml(QDomElement &e, const QString &tag_name) const
property.removeAttribute("name");
property.setAttribute("show", m_content_show[key]);
property.setAttribute("name", key);
// Trim stray leading/trailing whitespace around real content, but
// not a value that IS whitespace: unconditionally trimming an
// all-whitespace string collapses it to "", which is silently
// indistinguishable from a value that was never set. A title-block
// custom variable set to a single space -- a workaround for #973,
// where an unset variable renders as its own literal placeholder --
// would otherwise vanish on the very next save.
const QString stored = raw.trimmed().isEmpty() ? raw : raw.trimmed();
QDomText value = e.ownerDocument().createTextNode(stored);
QDomText value = e.ownerDocument().createTextNode(storedValue(raw));
property.appendChild(value);
e.appendChild(property);
}
@@ -182,7 +195,7 @@ void DiagramContext::toXml(QDomElement &e, const QString &tag_name) const
void DiagramContext::fromXml(const QDomElement &e, const QString &tag_name) {
foreach (QDomElement property, QET::findInDomElement(e, tag_name)) {
if (!property.hasAttribute("name")) continue;
addValue(property.attribute("name"), QVariant(property.text()));
addValue(property.attribute("name"), QVariant(storedValue(property.text())));
m_content_show.insert(property.attribute("name"), property.attribute("show", "1").toInt());
}
}
@@ -198,7 +211,8 @@ void DiagramContext::fromXml(const pugi::xml_node &dom_element, const QString &t
{
for(auto node = dom_element.child(tag_name.toStdString().c_str()) ; node ; node = node.next_sibling(tag_name.toStdString().c_str()))
{
addValue(node.attribute("name").as_string(), QVariant(node.text().as_string()));
addValue(node.attribute("name").as_string(),
QVariant(storedValue(QString::fromUtf8(node.text().as_string()))));
m_content_show.insert(node.attribute("name").as_string(), node.attribute("show").empty()? 1 : node.attribute("show").as_int());
}
}
+72
View File
@@ -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
@@ -122,3 +163,34 @@ bool ItemGroups::completeSelection(QGraphicsScene *scene,
}
return changed;
}
/**
@return the group @a selected is exactly, whole -- every item in it
belongs to that group and every member of the group is in it -- or a null
uuid. Rotating such a selection turns the group as one piece rather than
each member in place (discussion #1070). A member picked out on its own
is not a whole group, and turns in place.
@param selected : the selected items that can be members (the caller
leaves out wires, which follow their symbols)
*/
QUuid ItemGroups::soleWholeGroup(const QList<QGraphicsItem *> &selected)
{
if (selected.isEmpty() || !selected.first()->scene()) {
return QUuid();
}
const QUuid group = groupOf(selected.first());
if (group.isNull()) {
return QUuid();
}
for (QGraphicsItem *item : selected) {
if (groupOf(item) != group) {
return QUuid();
}
}
for (QGraphicsItem *item : selected.first()->scene()->items()) {
if (groupOf(item) == group && !item->isSelected()) {
return QUuid();
}
}
return group;
}
+5
View File
@@ -52,9 +52,14 @@ 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<QGraphicsItem *> &previous,
bool toggling);
QUuid soleWholeGroup(const QList<QGraphicsItem *> &selected);
}
#endif // ITEMGROUPS_H
+12 -1
View File
@@ -25,6 +25,7 @@
#include "ElementsCollection/elementpickerpopup.h"
#include "shortcutbarsettings.h"
#include "qetgraphicsitem/conductor.h"
#include "itemgroups.h"
#include "commandsearchpopup.h"
#include "QWidgetAnimation/qwidgetanimation.h"
#include "autoNum/ui/autonumberingdockwidget.h"
@@ -2078,7 +2079,17 @@ void QETDiagramEditor::selectionGroupTriggered(QAction *action)
}
else if (value == "rotate_selection")
{
RotateSelectionCommand *c = new RotateSelectionCommand(diagram);
//A selection that is exactly one whole group turns as one piece,
//as "Pivoter le groupe" does, rather than each member in place
//(discussion #1070). Wires follow their symbols either way.
QList<QGraphicsItem *> members;
for (QGraphicsItem *item : diagram->selectedItems()) {
if (item->type() != Conductor::Type) {
members << item;
}
}
const bool whole_group = !ItemGroups::soleWholeGroup(members).isNull();
RotateSelectionCommand *c = new RotateSelectionCommand(diagram, 90, nullptr, whole_group);
if(c->isValid())
diagram->undoStack().push(c);
}
+5 -1
View File
@@ -1033,7 +1033,11 @@ QDomElement Element::toXml(
QDomElement infos =
document.createElement(QStringLiteral("elementInformations"));
m_data.m_informations.toXml(infos, QStringLiteral("elementInformation"));
element.appendChild(infos);
//toXml() skips empty values: an element whose information is
//all empty would otherwise be written an empty block, which the
//next load reads as no information and the next save drops.
if (infos.hasChildNodes())
element.appendChild(infos);
}
//Save override properties (For now, only used when the element is a terminal)