Files
qelectrotech-source-mirror/sources/qetgraphicsitem/element.h
T
ispyisail de9b3eae06 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>
2026-09-23 16:37:45 +12:00

358 lines
11 KiB
C++

/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef ELEMENT_H
#define ELEMENT_H
#include "../ElementsCollection/elementslocation.h"
#include "../NameList/nameslist.h"
#include "../autoNum/assignvariables.h"
#include "../diagramcontext.h"
#include "../qet.h"
#include "qetgraphicsitem.h"
#include "../properties/elementdata.h"
#include <QHash>
#include <QPicture>
#include <algorithm>
class QETProject;
class Terminal;
class Conductor;
class DynamicElementTextItem;
class ElementTextItemGroup;
class QUndoCommand;
/**
This is the base class for electrical elements.
*/
class Element : public QetGraphicsItem
{
friend class DiagramEventAddElement;
friend class DynamicElementTextItem;
Q_OBJECT
public:
/**
@brief The kind enum
Used to know the kind of this element
(master, slave, report ect...)
*/
enum kind {
Simple = 1,
NextReport = 2,
PreviousReport = 4,
AllReport = 6,
Master = 8,
Slave = 16,
Terminale = 32,
Thumbnail = 64,
ConductorDefinition = 128};
/**
* @brief The LinkInfo struct
* Stores link data for element connections.
* For slave elements, group_index indicates which contact group
* of the master this slave is assigned to (-1 = no group assigned).
*/
struct LinkInfo {
QUuid uuid;
int group_index = -1; ///< Index into master's slaveContactGroups, -1 = not assigned
LinkInfo() = default;
LinkInfo(const QUuid &u, int gi = -1) : uuid(u), group_index(gi) {}
};
Element(const ElementsLocation &location,
QGraphicsItem * = nullptr,
int *state = nullptr,
Element::kind link_type = Element::Simple);
~Element() override;
private:
bool definitionGeometryMatches(const QDomElement &definition) const;
Element(const Element &);
// attributes
public:
/**
Enable the use of qgraphicsitem_cast
to safely cast a QGraphicsItem into an Element.
@return the QGraphicsItem type
*/
enum { Type = UserType + 1000 };
int type() const override { return Type; }
signals:
void linkedElementChanged(); //This signal is emitted when the linked elements with this element change
void elementInfoChange(
DiagramContext old_info,
DiagramContext new_info);
void textAdded(DynamicElementTextItem *deti);
void textRemoved(DynamicElementTextItem *deti);
void textsGroupAdded(ElementTextItemGroup *group);
void textsGroupAboutToBeRemoved(ElementTextItemGroup *group);
void textAddedToGroup(
DynamicElementTextItem *text,
ElementTextItemGroup *group);
void textRemovedFromGroup(
DynamicElementTextItem *text,
ElementTextItemGroup *group);
public slots:
void updateConductorTexts();
public:
QList<Terminal *> terminals() const;
QList<Conductor *> conductors() const;
QList<QPair<Terminal *,Terminal *>> AlignedFreeTerminals() const;
void clearPendingLinks() {
tmp_uuids_link.clear();
m_group_index_map.clear();
}
//METHODS related to information
DiagramContext elementInformations()const
{return m_data.m_informations;}
virtual void setElementInformations(DiagramContext dc);
ElementData elementData() const;
void setElementData (ElementData data);
/**
* @brief kindInformations
* @deprecated
* use elementData function instead
* @return
*/
DiagramContext kindInformations() const
{return m_kind_informations;}
//@kind_information_ is used to store more information
//about the herited class like contactelement for know
// kind of contact (simple tempo) or number of contact show by the element.
autonum::sequentialNumbers sequenceStruct() const
{return m_autoNum_seq;}
autonum::sequentialNumbers& rSequenceStruct()
{return m_autoNum_seq;}
void setUpFormula(bool code_letter = true, QUndoCommand *parent_undo = nullptr);
void setPrefix(QString);
QString getPrefix() const;
void freezeLabel(bool freeze);
bool isFreezeLabel() const {return m_freeze_label;}
void freezeNewAddedElement();
QString actualLabel();
QString name() const override;
ElementsLocation location() const;
/// Result of Element::reloadPicture()
enum class ReloadPictureResult {
Reloaded, ///< drawing replaced by the current definition
Unavailable, ///< definition missing or unreadable, old drawing kept
GeometryChanged ///< size, hotspot or terminals changed, old drawing kept
};
ReloadPictureResult reloadPicture();
virtual void setHighlighted(bool);
void displayHelpLine(bool b = true);
QSize size() const;
QPixmap pixmap();
QPoint setHotspot(QPoint);
QPoint hotspot() const;
void editProperty() override;
static bool valideXml(QDomElement &);
virtual bool fromXml(
QDomElement &,
QHash<int,
Terminal *> &);
virtual QDomElement toXml(
QDomDocument &,
QHash<Terminal *,
int> &) const;
QUuid uuid() const;
int orientation() const;
//METHODS related to texts
void addDynamicTextItem(DynamicElementTextItem *deti = nullptr);
void removeDynamicTextItem(DynamicElementTextItem *deti);
QList<DynamicElementTextItem *> dynamicTextItems() const;
ElementTextItemGroup *addTextGroup(const QString &name);
void addTextGroup(ElementTextItemGroup *group);
void removeTextGroup(ElementTextItemGroup *group);
ElementTextItemGroup *textGroup(const QString &name) const;
QList<ElementTextItemGroup *> textGroups() const;
bool addTextToGroup(
DynamicElementTextItem *text,
ElementTextItemGroup *group);
bool removeTextFromGroup(
DynamicElementTextItem *text,
ElementTextItemGroup *group);
//METHODS related to linked element
bool isFree() const;
virtual void linkToElement(Element *) {}
virtual void unlinkAllElements() {}
virtual void unlinkElement(Element *) {}
virtual void initLink(QETProject *);
/**
Resolve tmp_uuids_link against a caller-supplied candidate
list instead of a project-wide search (bugtracker #607).
Used right after an XML round-trip (paste, folio
duplication) and before the pasted/duplicated elements'
uuids are renewed: at that moment a copy's tmp_uuids_link
still holds its source's original partner uuid, which
still matches the not-yet-renewed uuid of that partner's
own copy if it was carried along in the same batch.
Resolving only within @p candidates -- not the whole
project -- 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
@p candidates, its entry finds no match and is dropped,
same as initLink(QETProject *) leaving an unresolvable
link unlinked.
*/
void initLink(const QList<Element *> &candidates);
QList<Element *> linkedElements ();
int groupIndexForElement(Element *elmt) const;
void setGroupIndexForElement(Element *elmt, int index);
/**
* @brief linkType
* use elementData function instead
* @return
*/
virtual kind linkType() const {return m_link_type;} // return the linkable type
/**
* @brief linkTypeToString
* use elementData function instead
* @return
*/
QString linkTypeToString() const;
void newUuid() {m_uuid = QUuid::createUuid();} //create new uuid for this element
protected:
void drawAxes(QPainter *, const QStyleOptionGraphicsItem *);
void setSize(int, int);
private:
void drawHighlight(
QPainter *,
const QStyleOptionGraphicsItem *);
bool buildFromXml(const QDomElement &, int * = nullptr);
bool parseElement(const QDomElement &dom);
bool parseInput(const QDomElement &dom_element);
DynamicElementTextItem *parseDynamicText(
const QDomElement &dom_element);
Terminal *parseTerminal(const QDomElement &dom_element);
//Reimplemented from QGraphicsItem
public:
void paint(
QPainter *,
const QStyleOptionGraphicsItem *,
QWidget *) override;
QRectF boundingRect() const override;
protected:
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent(
QGraphicsSceneMouseEvent *event) override;
void hoverEnterEvent(QGraphicsSceneHoverEvent *) override;
void hoverLeaveEvent(QGraphicsSceneHoverEvent *) override;
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
protected:
//ATTRIBUTES related to linked element
QList <Element *> connected_elements;
QList <LinkInfo> tmp_uuids_link;
QHash <Element *, int> m_group_index_map; ///< Maps linked elements to their group index (for slave->master links)
QUuid m_uuid;
kind m_link_type = Element::Simple;
//ATTRIBUTES related to informations
DiagramContext m_kind_informations;
autonum::sequentialNumbers m_autoNum_seq;
bool m_freeze_label = false;
QString m_F_str;
ElementsLocation m_location;
QList <Terminal *> m_terminals;
QPicture m_picture;
QPicture m_low_zoom_picture;
ElementData m_data;
QList<QPointF> m_plc_table_positions; // Positions of plc_table parts in the element definition
void drawPlcTable(QPainter *painter);
public:
/// Positions where the PLC IO table is drawn (from the .elmt file).
QList<QPointF> plcTablePositions() const { return m_plc_table_positions; }
private:
bool m_must_highlight = false;
QSize dimensions;
QPoint hotspot_coord;
bool m_mouse_over = false;
QString m_prefix;
QList <DynamicElementTextItem *> m_dynamic_text_list;
QList <ElementTextItemGroup *> m_texts_group;
};
bool comparPos(const Element * elmt1, const Element * elmt2);
inline bool Element::isFree() const
{
return (connected_elements.isEmpty());
}
/**
Indicate the current orientation of this element
O = 0°
1 = 90°
2 = 180°
3 = 270°
@return the current orientation of this element
*/
inline int Element::orientation() const
{
return(QET::correctAngle(rotation())/90);
}
/**
@brief Element::uuid
@return the uuid of this element
*/
inline QUuid Element::uuid() const
{return m_uuid;}
/**
@brief Element::linkedElements
@return the list of linked elements, the list is sorted by position
*/
inline QList <Element *> Element::linkedElements()
{
std::sort(
connected_elements.begin(),
connected_elements.end(),
comparPos);
return connected_elements;
}
#endif