Files
qelectrotech-source-mirror/sources/diagram.h
T
Beat Hangartner 7cd04f2a14 Persist the folio uuid, derived deterministically for legacy folios
Diagram::m_uuid was created in the constructor and never written, so a
folio got a new uuid on every load. Inside a running instance that is
enough (the project database keys on it), but nothing outside it could
tell which folio is which: in the file, folios were only identified by
their position.

Motivation

More and more .qet projects live in version control -- a Git repository
on GitHub or GitLab, reviewed through pull requests, sometimes edited
by several people -- or are synchronised through a cloud or key-value
store. A .qet file is plain XML, so in principle it can be diffed,
merged and split up, but only if the same folio can be recognised in
two versions of the file. Today it cannot:

- Inserting, deleting or reordering a folio shifts every following
  <diagram> element. A line-based diff, and GitHub's review view, then
  pair up unrelated folios and show far more change than was made.
- A three-way merge of two branches that both touched the project has
  no way to match "folio 3" on one side with "folio 3" on the other if
  either side reordered folios.
- Any tool that wants to say "folio X changed in this commit", keep
  per-folio history, lock a single folio, or store folios as separate
  objects has nothing stable to key on. The title and the folio number
  are user-editable and not unique.

Element uuids are already persisted and used for cross-folio links, so
the file format already relies on uuids for identity; the folio itself
was the missing piece. A stable folio uuid is the prerequisite for
later work towards better version control support: per-folio diffs and
locks (check-out / check-in), and possibly storing a project as a
directory with one file per folio.

Change

Write the uuid as an attribute of <diagram> when the whole content is
saved, and restore it first thing when the project is loaded, before any
item is created. Older versions ignore the attribute, so files stay
readable in both directions.

Folios without a uuid: why not a random one

The obvious migration -- keep the random uuid created by the
constructor and save it -- conflicts with #754 / #779: saving an
unmodified project must give the same bytes every time. Every example
project predates the attribute, so each load would invent different
uuids and write them out. Measured on the 24 example projects (resaved
3-4x each from the same original, QT_HASH_SEED=0 so that QDom's
attribute order is stable, isolated HOME per run):

  upstream master              23/24 byte-identical
  persist, random uuid          0/24
  persist, derived uuid (this) 23/24

The remaining project, schema_indus.qet, differs only in element uuids,
the known residual #779 leaves for elements; its folio uuid is stable.

This is the same problem #779 solved for conductors by not writing an
invented uuid back at all. That is not an option here: legacy folios
would never get a persistent uuid, which is the whole point of the
change. Instead, a folio without a uuid gets a name-based (version 5)
uuid, derived only from data read from the file:

  QUuid::createUuidV5(<fixed QET folio namespace>,
                      "legacy" + project title
                               + position of the folio in the file
                               + folio title)

- The same input file always yields the same uuids, so resaving an
  unmodified legacy project stays reproducible.
- The uuid is derived once, at load time, and saved from then on. After
  that it is read, never recomputed: renaming, reordering or editing
  the folio later does not change it. Renaming in the same session as
  the migration does not change it either, since it was derived from
  the title as loaded.
- Two people opening the same legacy file on different branches get
  the same uuid for each folio, even if one of them reorders or renames
  folios before saving. With random uuids the two branches would
  disagree about every folio and a later merge could not match them.
- The folio content is deliberately not part of the name: QDom keeps
  attributes in a hash whose iteration order changes between runs, so
  hashing the content would need a canonical form for no real gain.

Folios are only guaranteed unique within their project. Two unrelated
legacy projects with the same title and the same first folio title get
the same uuid for that folio; anything keying folios globally has to
combine the folio uuid with a project identifier. (The project uuid is
not persisted yet; that is a separate change.)

Duplicated uuids

A hand-edited or merged file can contain the same uuid twice, e.g. a
folio copied by duplicating its XML block. Since the uuid is used as a
key, the second folio gets a derived uuid as well ("duplicate" + the
clashing uuid + the same inputs as above), so this case is
reproducible too. Should a derived uuid ever be taken already, which
takes a hand-crafted file, the name is salted with a counter until it
is free.

The namespace uuid is fixed in the code and must never change, or every
legacy folio would get a different uuid.

Tests (Qt 6.4, offscreen, qelectrotech --resave / --set-titleblock)

- 24 example projects, 3-4 resaves each from the same original: results
  above; all folio uuids identical across runs, no duplicates within
  any project.
- Resaving an already migrated file is byte-identical to the first
  output.
- Renaming a folio in a migrated file keeps its uuid.
- Swapping two <diagram> blocks in a migrated file: each uuid moves
  with its folio.
- Migrating and renaming in the same run (--set-titleblock title=...
  on a legacy file) gives the same uuids as a plain resave.
- A file with a duplicated uuid: the second folio gets a new uuid, the
  same one on every run.
- A migrated file opened with upstream master loads normally; the
  attribute is ignored and dropped on save.

Refs #754, #779

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDyt4txaott5JyPNGQaeVp
2026-09-15 22:09:10 +00:00

459 lines
13 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 DIAGRAM_H
#define DIAGRAM_H
#include "autoNum/numerotationcontext.h"
#include "bordertitleblock.h"
#include "conductorproperties.h"
#include "elementsmover.h"
#include "elementtextsmover.h"
#include "exportproperties.h"
#include "properties/xrefproperties.h"
#include "qgimanager.h"
#include <QHash>
#include <QUuid>
#include <QtWidgets>
#include <QtXml>
class Conductor;
class CustomElement;
class DiagramContent;
class DiagramPosition;
class DiagramTextItem;
class Element;
class ElementsLocation;
class DiagramImageItem;
class DiagramEventInterface;
class DiagramFolioList;
class QETProject;
struct GuideProperties;
/**
@brief The Diagram class
This class represents an electric diagram.
It manages its various child elements,
conductors and texts and handles their graphic rendering.
*/
class Diagram : public QGraphicsScene
{
friend DiagramFolioList;
friend QETProject;
Q_OBJECT
// constructors, destructor
private:
Diagram(QETProject *project);
~Diagram() override;
Diagram(const Diagram &diagram);
// ATTRIBUTES
public:
struct Guide {
enum Orientation { Horizontal, Vertical };
Orientation orientation;
qreal position;
QColor color;
};
/**
@brief The BorderOptions enum
Represents available options when rendering a particular diagram:
EmptyBorder: display border only
TitleBlock: display title block
Columns: display columns
*/
enum BorderOptions { EmptyBorder, TitleBlock, Columns };
/// Represents available option of Numerotation type.
enum NumerotationType { Conductors };
/// Default properties for new conductors
ConductorProperties defaultConductorProperties;
/// Diagram dimensions and title block
BorderTitleBlock border_and_titleblock;
/// abscissa grid step size
static int xGrid;
/// ordinate grid step size
static int yGrid;
/// Key grid x step size
static int xKeyGrid;
/// Key grid y step size
static int yKeyGrid;
/// Key grid fine x step size
static int xKeyGridFine;
/// Key grid fine y step size
static int yKeyGridFine;
/// margin around the diagram
static const qreal margin;
/// background color of diagram
static QColor background_color;
/// Hash containing max values for folio sequential autonums in this diagram
QHash <QString, QStringList> m_elmt_unitfolio_max;
QHash <QString, QStringList> m_elmt_tenfolio_max;
QHash <QString, QStringList> m_elmt_hundredfolio_max;
/// Hash containing max values for folio sequential autonums in this diagram
QHash <QString, QStringList> m_cnd_unitfolio_max;
QHash <QString, QStringList> m_cnd_tenfolio_max;
QHash <QString, QStringList> m_cnd_hundredfolio_max;
private:
QGraphicsLineItem *conductor_setter_;
ElementsMover m_elements_mover;
ElementTextsMover m_element_texts_mover;
QGIManager *qgi_manager_;
QETProject *m_project;
QDomDocument xml_document_;
qreal diagram_qet_version_;
bool draw_grid_;
bool use_border_;
bool draw_guides_;
QList<Diagram::Guide> m_guides_list;
bool draw_terminals_;
bool draw_terminal_names_;
bool draw_colored_conductors_;
QString m_conductors_autonum_name;
DiagramEventInterface *m_event_interface;
bool m_freeze_new_elements;
bool m_freeze_new_conductors_;
QUuid m_uuid = QUuid::createUuid();
bool uuidUsedByOtherDiagram(const QUuid &uuid) const;
QUuid derivedUuid(const QDomElement &root, const QString &reason) const;
// METHODS
protected:
void drawBackground(QPainter *, const QRectF &) override;
void mouseDoubleClickEvent (
QGraphicsSceneMouseEvent *event) override;
void mousePressEvent (QGraphicsSceneMouseEvent *event) override;
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent (
QGraphicsSceneMouseEvent *event) override;
void wheelEvent (QGraphicsSceneWheelEvent *event) override;
void keyPressEvent (QKeyEvent *event) override;
void keyReleaseEvent (QKeyEvent *) override;
bool event(QEvent *event) override;
private:
void selectNextItem(bool forward);
public:
void correctTextPos(Element* elmt);
void restoreText(Element* elmt);
QUuid uuid();
void setEventInterface (DiagramEventInterface *event_interface);
void clearEventInterface();
//methods related to autonum
QString conductorsAutonumName() const;
void setConductorsAutonumName(const QString &name);
static bool clipboardMayContainDiagram();
// methods related to parent project
QETProject *project() const;
int folioIndex() const;
void showMe() {emit showDiagram(this);}
bool isReadOnly() const;
// methods related to conductor creation
void setConductor(bool);
void setConductorStart (QPointF);
void setConductorStop(QPointF);
QList < QSet <Conductor *> > potentials();
// methods related to XML import/export
QDomDocument toXml(bool wholeContent = true, bool is_copy_command = false);
bool initFromXml(QDomElement &,
QPointF = QPointF(),
bool = true,
DiagramContent * = nullptr);
bool fromXml(QDomDocument &,
QPointF = QPointF(),
bool = true,
DiagramContent * = nullptr);
bool fromXml(QDomElement &,
QPointF = QPointF(),
bool = true,
DiagramContent * = nullptr);
void folioSequentialsToXml(QHash<QString,
QStringList>*,
QDomElement *,
const QString&,
const QString&,
QDomDocument *);
void folioSequentialsFromXml(const QDomElement&,
QHash<QString,
QStringList>*,
const QString&,
const QString&,
const QString&,
const QString&);
void refreshContents();
// methods related to graphics items addition/removal on the diagram
virtual void addItem (QGraphicsItem *item);
virtual void removeItem (QGraphicsItem *item);
// methods related to graphics options
ExportProperties applyProperties(const ExportProperties &);
void setDisplayGrid(bool);
bool displayGrid();
void setDisplayGuides(bool);
bool displayGuides();
void updateProjectGuides(const QList<GuideProperties> &guides);
void setUseBorder(bool);
bool useBorder();
void setBorderOptions(BorderOptions);
BorderOptions borderOptions();
DiagramPosition convertPosition(const QPointF &);
static QPointF snapToGrid(const QPointF &p);
bool drawTerminals() const;
void setDrawTerminals(bool);
bool drawTerminalNames() const;
void setDrawTerminalNames(bool);
bool drawColoredConductors() const;
void setDrawColoredConductors(bool);
QString title() const;
bool toPaintDevice(QPaintDevice &, int = -1, int = -1,
Qt::AspectRatioMode = Qt::KeepAspectRatio);
QSize imageSize() const;
bool isEmpty() const;
QList<Element *> elements() const;
QList<Conductor *> conductors() const;
QSet<Conductor *> selectedConductors() const;
DiagramContent content() const;
bool canRotateSelection() const;
ElementsMover &elementsMover();
ElementTextsMover &elementTextsMover();
bool usesElement(const ElementsLocation &);
bool usesTitleBlockTemplate(const QString &);
QUndoStack &undoStack();
QGIManager &qgiManager();
//methods related to element label Update Policy
void freezeElements(bool freeze);
void unfreezeElements();
void setFreezeNewElements(bool);
bool freezeNewElements();
//methods related to conductor label Update Policy
void freezeConductors(bool freeze);
void setFreezeNewConductors(bool);
bool freezeNewConductors();
//methods related to insertion and loading of folio sequential
void insertFolioSeqHash (QHash<QString, QStringList> *hash,
const QString& title,
const QString& seq,
NumerotationContext *nc);
void loadFolioSeqHash (QHash<QString, QStringList> *hash,
const QString& title, const QString& seq,
NumerotationContext *nc);
void changeZValue(QET::DepthOption option);
public slots:
void adjustSceneRect ();
void titleChanged();
void titleBlockTemplateChanged(const QString &);
void titleBlockTemplateRemoved(const QString &,
const QString & = QString());
void setTitleBlockTemplate(const QString &);
void loadElmtFolioSeq();
void loadCndFolioSeq();
// methods related to graphics items selection
void selectAll();
void deselectAll();
void invertSelection();
void selectAllConductors();
void selectAllTextFields();
signals:
void showDiagram (Diagram *);
void usedTitleBlockTemplateChanged(const QString &);
void diagramTitleChanged(Diagram *);
/// Signal emitted when users wish to locate an element
/// from the diagram within elements collection
void findElementRequired(const ElementsLocation &);
void diagramActivated();
void diagramInformationChanged();
};
Q_DECLARE_METATYPE(Diagram *)
/**
@brief Diagram::setConductor
Display or hide the conductor setter,
i.e. a dashed conductor stub which appears
when creating a conductor between two terminals.
@param adding true add conductor ,false remove conductor
*/
inline void Diagram::setConductor(bool adding) {
if (adding) {
if (!conductor_setter_ -> scene()) addItem(conductor_setter_);
} else {
if (conductor_setter_ -> scene()) removeItem(conductor_setter_);
}
}
/**
@brief Diagram::setConductorStart
Set the start point of the conductor setter.
@param start the point (in scene coordinates) which the newly created
conductor should start from.
*/
inline void Diagram::setConductorStart(QPointF start) {
conductor_setter_ -> setLine(QLineF(start, conductor_setter_ -> line().p2()));
}
/**
@brief Diagram::setConductorStop
Set the end point of the conductor setter.
@param end the point (in scene coordinates) upon to which the newly created
conductor should be drawn.
*/
inline void Diagram::setConductorStop(QPointF end) {
conductor_setter_ -> setLine(QLineF(conductor_setter_ -> line().p1(), end));
}
/**
@brief Diagram::setDisplayGrid
Set whether the diagram grid should be drawn.
@param dg true to render the grid, false otherwise.
*/
inline void Diagram::setDisplayGrid(bool dg) {
draw_grid_ = dg;
}
/**
@brief Diagram::displayGrid
@return draw_grid_ true if the grid is drawn, false otherwise.
*/
inline bool Diagram::displayGrid() {
return(draw_grid_);
}
inline void Diagram::setDisplayGuides(bool dg) {
if (draw_guides_ != dg) {
draw_guides_ = dg;
update();
}
}
inline bool Diagram::displayGuides() {
return(draw_guides_);
}
/**
@brief Diagram::setUseBorder
Set whether the diagram border (including rows/columns headers and the title
block) should be rendered along with the diagram. When set to false, the size
of the smallest rectangle containing all items is considered as the diagram
size.
@param ub true to take the border into account, false otherwise
*/
inline void Diagram::setUseBorder(bool ub) {
use_border_ = ub;
}
/**
@brief Diagram::useBorder
@return use_border_ true if the border is rendered and take into account,
false otherwise.
*/
inline bool Diagram::useBorder() {
return(use_border_);
}
/**
@brief Diagram::setBorderOptions
Set the rendering options for the diagram border (including rows/columns
headers and the title block)
@param bo Enabled options ORed together
@see BorderOptions
*/
inline void Diagram::setBorderOptions(Diagram::BorderOptions bo) {
border_and_titleblock.displayBorder(!(bo & EmptyBorder));
border_and_titleblock.displayColumns(bo & Columns);
border_and_titleblock.displayTitleBlock(bo & TitleBlock);
}
/**
@brief Diagram::borderOptions
@return The rendering options for the diagram border
@see setBorderOptions
*/
inline Diagram::BorderOptions Diagram::borderOptions() {
BorderOptions options = EmptyBorder;
if (border_and_titleblock.titleBlockIsDisplayed())
options = (BorderOptions)(options|TitleBlock);
if (border_and_titleblock.columnsAreDisplayed())
options = (BorderOptions)(options|Columns);
return(options);
}
/**
@brief Diagram::qgiManager
@return the diagram graphics item manager
*/
inline QGIManager &Diagram::qgiManager() {
return(*qgi_manager_);
}
/**
@brief Diagram::drawTerminals
@return true if terminals are rendered, false otherwise
*/
inline bool Diagram::drawTerminals() const
{
return(draw_terminals_);
}
/**
@brief Diagram::drawTerminalNames
@return true if terminal names are rendered, false otherwise
*/
inline bool Diagram::drawTerminalNames() const
{
return(draw_terminal_names_);
}
/**
@brief Diagram::drawColoredConductors
@return true if conductors colors are rendered, false otherwise.
*/
inline bool Diagram::drawColoredConductors() const
{
return(draw_colored_conductors_);
}
#endif