Preserve master/slave links when pasting or duplicating a folio

Reviving #659, closed 2026-09-10 purely to clear a review backlog
(#630), not on merit. Rebuilt fresh against current master rather than
merged from the old branch (elementspanelwidget.cpp had drifted enough
that a textual merge risked silently losing content, as it did earlier
in this same session for a different revival). Builds discussion #607.

Cutting/copying a linked group of elements -- a relay coil with its
contacts, a PLC master with its slave I/O elements -- dropped the
master/slave link entirely. Traced end to end: Element::toXml() writes
each partner's uuid into <link_uuid>, Element::fromXml() reads it back
into a deferred, unresolved buffer (tmp_uuids_link), and the only code
that ever resolves that buffer is initLink(QETProject *) -- called
only from Diagram::refreshContents(), itself only called from full
project load and macro-block insertion. Neither DiagramView::paste()
nor ElementsPanelWidget::duplicateDiagram() ever call it, so
tmp_uuids_link is populated correctly and never resolved: the link is
silently dropped. duplicateDiagram() already knew this and worked
around it by calling clearPendingLinks() -- correct to not link back
to a stale source, but it meant folio duplication never preserved a
link either.

Added Element::initLink(const QList<Element *> &candidates) --
resolves against a caller-supplied list instead of a project-wide
search. The scoping is the subtle part: right after the XML round-trip
and before uuids are renewed, a pasted/duplicated element's
tmp_uuids_link still holds its source's original partner uuid, which
at that exact moment still equals the not-yet-renewed uuid of that
partner's own copy, if it was carried along in the same batch.
Resolving only within the batch is what stops a linked pair pasted
together from matching an original element left elsewhere that
happens to still carry that same soon-to-be-replaced uuid. If only one
half of a linked group is in the batch, its entry finds no match and
is dropped -- the same "leave it unlinked" outcome as before.

Wired into PasteDiagramCommand::redo(), before the existing newUuid()
loop and gated by the same first_redo flag. Wired into
duplicateDiagram() the same way, replacing its clearPendingLinks()
call (initLink() clears tmp_uuids_link internally, matched or not).

Verified live -- the original PR's own test plan left both of these
unchecked, so this closes that gap rather than repeating it. Built a
project with a linked PLC master/slave pair (qet-mcp's link_elements),
then drove the real interaction under Xvfb:

  Ctrl+A, Ctrl+C, Ctrl+V:
    originals   95ad58fc <-> e728632c   (unchanged)
    pasted      513e6bf8 <-> 29aa60b4   (linked to each other)

  Right-click folio > "Copier et coller":
    originals   95ad58fc <-> e728632c   (unchanged)
    duplicated  0a33ccb4 <-> 3264fe66   (linked to each other)

Neither copy links back to an original or comes in unlinked. Qt 6.10.2,
ctest 13/13.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-09-23 16:37:45 +12:00
parent 6d8459d647
commit de9b3eae06
4 changed files with 87 additions and 4 deletions
+17 -1
View File
@@ -77,6 +77,23 @@ void PasteDiagramCommand::redo()
{
first_redo = false;
//Resolve a linked master/slave pair pasted together (bugtracker
//#607) before anything below renews their uuids: at this exact
//moment a pasted element's tmp_uuids_link still holds its
//source's original partner uuid, which still equals the
//not-yet-renewed uuid of that partner's own pasted copy if it
//was carried along in the same batch. Scoped to this batch only
//(not a project-wide search), so a pair pasted together links to
//each other and not to an original element left elsewhere that
//happens to still carry that same soon-to-be-replaced uuid. If
//only one half of a linked group was pasted, its link entry
//simply finds no match here and is dropped -- same "leave it
//unlinked" outcome as always.
const QList <Element *> elmts_list = content.m_elements;
for (Element *e : elmts_list) {
e->initLink(elmts_list);
}
//make new uuid for every pasted conductor, because old uuid are
//the uuid of the copied conductor
const QList <Conductor *> all_pasted_conductors = content.conductors();
@@ -85,7 +102,6 @@ void PasteDiagramCommand::redo()
}
//this is the first paste, we do some actions for the new element
const QList <Element *> elmts_list = content.m_elements;
for (Element *e : elmts_list)
{
//make new uuid, because old uuid are the uuid of the copied element