Files
qelectrotech-source-mirror/sources/qetproject.h
T
Beat Hangartner 2269a4641b Persist the project uuid, derived from the file content for legacy files
QETProject::m_uuid was created in the constructor and never written, so
a project got a new uuid every time it was opened. Inside a running
instance it is only used to name the SQLite connection, but nothing
outside the instance could tell which project a file belongs to.

Motivation

A .qet file is increasingly handled by tools outside QElectroTech: Git
repositories on GitHub or GitLab, cloud storage, key-value stores,
per-project locks. All of them need a stable key for "this project":

- The file name and path are not stable: files get renamed, moved,
  checked out in different places.
- The project title is user-editable and not unique.
- Folio uuids (persisted separately) are only unique within their
  project; keying folios globally needs a project identifier as well,
  e.g. projects/{projectUuid}/folios/{folioUuid}.

Change

Write the uuid as an attribute of <project> and restore it in
QETProject::openFile(), right after parsing and before the project is
built from the XML. Older versions ignore the attribute, so files stay
readable in both directions.

The project database is not affected: it takes its connection name
from the uuid created at construction (m_uuid is declared before
m_data_base), before the file is read. Two open projects carrying the
same persisted uuid therefore still get distinct connections.

Files without a uuid: why not a random one

Keeping the random uuid created by the constructor and saving it
conflicts with #754 / #779: saving an unmodified project must give the
same bytes every time. Every example project predates the attribute.
Measured on the 24 example projects (resaved 3x 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 project uuid is
stable.

Instead, a project file without a uuid gets a name-based (version 5)
uuid derived from the raw content of the file:

  QUuid::createUuidV5(<fixed QET project namespace>,
                      "qet-project-legacy\n" + file content without CR)

- The same file always yields the same uuid, so resaving an unmodified
  legacy project stays reproducible.
- Different projects practically never share a uuid, because any
  difference in content gives a different one. This is unlike folios,
  where only data such as title and position could be used; the raw
  file bytes are stable input for the whole project.
- Carriage returns are dropped before hashing. QFile's Text mode already
  strips them on Windows but not elsewhere, and git's autocrlf can
  change them on checkout; either way the uuid is the same on every
  platform.
- The uuid is derived once, at load time, and saved from then on. After
  that it is read, never recomputed: renaming the project, editing it
  or changing it in the same session as the migration does not change
  it.
- Two people opening the same legacy file on different branches get the
  same project uuid.

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

Known limitations, open for discussion

- Copies share the uuid. Two byte-identical legacy files get the same
  uuid (examples/cablage-eclairages_sikli-v5.qet and
  câblage-éclairages-sikli-v5.qet are such a pair), and so does a
  migrated file copied in the file manager or saved with "Save as".
  That is what identity means for a copy, and the same happens with Git,
  but a tool that treats the uuid as globally unique has to cope with
  it. Regenerating the uuid on "Save as" could be a follow-up, if that
  is the preferred behaviour.
- A legacy file that differs from another only in formatting (e.g.
  re-indented) gets a different uuid. The two sides of a merge only
  agree if they started from the same bytes, which is the normal case.

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

- 24 example projects, 3 resaves each from the same original: results
  above; the project uuid is identical across runs. All 24 uuids are
  distinct, except the byte-identical pair mentioned above.
- Resaving an already migrated file is byte-identical to the first
  output.
- The same legacy file with CRLF line endings gets the same uuid as
  with LF.
- Changing the project title in a migrated file keeps its uuid.
- Migrating and modifying in the same run (--set-titleblock on a legacy
  file) gives the same uuid as a plain resave.
- Re-indenting a legacy file gives a different uuid (expected).
- A migrated file opened with upstream master loads normally; the
  attribute is ignored and dropped on save.
- --info on a migrated file still works.

Refs #754, #779

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

341 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 QET_PROJECT_H
#define QET_PROJECT_H
#include "ElementsCollection/elementslocation.h"
#include "NameList/nameslist.h"
#include "project/projectpropertieshandler.h"
#include "borderproperties.h"
#include "conductorproperties.h"
#include "dataBase/projectdatabase.h"
#include "properties/reportproperties.h"
#include "properties/xrefproperties.h"
#include "titleblock/templatescollection.h"
#include "titleblockproperties.h"
#include "diagram.h"
#ifdef BUILD_WITHOUT_KF
# include "ui/nokde/kautosavefile.h"
#else
# include <KAutoSaveFile>
#endif
#include <QHash>
#include <QFuture>
class Diagram;
class ElementsLocation;
class QETResult;
class TitleBlockTemplate;
class MoveTitleBlockTemplatesHandler;
class NumerotationContext;
class QUndoStack;
class XmlElementCollection;
class QTimer;
class TerminalStrip;
#include <QColor>
struct GuideProperties {
int orientation; // 0 = Horizontal, 1 = Vertical
qreal position;
QColor color;
bool operator==(const GuideProperties &other) const {
return orientation == other.orientation &&
position == other.position &&
color == other.color;
}
bool operator!=(const GuideProperties &other) const {
return !(*this == other);
}
};
/**
This class represents a QET project. Typically saved as a .qet file, it
consists in an XML document grouping 0 to n diagrams and embedding an elements
collection. This collection enables users to export diagrams on remote
machines without wondering whether required elements are available to them.
*/
class QETProject : public QObject
{
friend class AddDiagramCommand;
friend class RemoveDiagramCommand;
Q_OBJECT
public :
//This enum lists possible states for a particular project.
enum ProjectState {
Ok = 0, /// no error
FileOpenFailed = 1, /// file opening failed
XmlParsingFailed = 2, /// XML parsing failed
ProjectParsingRunning = 3, /// the XML content is currently being processed
ProjectParsingFailed = 4, /// the parsing of the XML content failed
FileOpenDiscard = 5 /// the user cancelled the file opening
};
Q_PROPERTY(bool autoConductor READ autoConductor WRITE setAutoConductor)
Q_PROPERTY(bool autoBreakConductor READ autoBreakConductor WRITE setAutoBreakConductor)
// constructors, destructor
public:
QETProject (QObject *parent = nullptr);
QETProject (const QString &path, QObject * = nullptr);
QETProject (KAutoSaveFile *backup, QObject *parent=nullptr);
~QETProject() override;
private:
QETProject(const QETProject &);
// methods
public:
ProjectPropertiesHandler& projectPropertiesHandler();
projectDataBase *dataBase();
QUuid uuid() const;
ProjectState state() const;
QList<Diagram *> diagrams() const;
int folioIndex(const Diagram *) const;
XmlElementCollection *embeddedElementCollection()const;
TitleBlockTemplatesProjectCollection *embeddedTitleBlockTemplatesCollection();
QString filePath();
void setFilePath(const QString &);
QString currentDir() const;
QString pathNameTitle() const;
QString title() const;
QVersionNumber declaredQElectroTechVersion();
void setTitle(const QString &);
/// Enable/disable the asynchronous crash-recovery backup for all
/// projects. Disabled by the headless CLI: the backup write runs on a
/// background thread referencing the project, and a short-lived CLI
/// process can destroy the project before the write finishes (crash).
static void setBackupEnabled(bool enabled);
///DEFAULT PROPERTIES
BorderProperties defaultBorderProperties() const;
void setDefaultBorderProperties(const BorderProperties &);
QList<GuideProperties> defaultGuides() const;
void setDefaultGuides(const QList<GuideProperties> &guides);
TitleBlockProperties defaultTitleBlockProperties() const;
void setDefaultTitleBlockProperties(const TitleBlockProperties &);
ConductorProperties defaultConductorProperties() const;
void setDefaultConductorProperties(const ConductorProperties &);
QString defaultReportProperties() const;
void setDefaultReportProperties (const QString &properties);
XRefProperties defaultXRefProperties (const QString &type) const {return m_default_xref_properties[type];}
QHash <QString, XRefProperties> defaultXRefProperties() const {return m_default_xref_properties;}
void setDefaultXRefProperties(const QString& type, const XRefProperties &properties);
void setDefaultXRefProperties(QHash <QString, XRefProperties> hash);
QHash <QString, NumerotationContext> conductorAutoNum() const;
QHash <QString, NumerotationContext> elementAutoNum() const;
QHash <QString, NumerotationContext> folioAutoNum() const;
void addConductorAutoNum (const QString& key, const NumerotationContext& context);
void addElementAutoNum (const QString& key, const NumerotationContext& context);
void addFolioAutoNum (const QString& key, const NumerotationContext& context);
void removeConductorAutoNum (const QString& key);
void removeElementAutoNum (const QString& key);
void removeFolioAutoNum (const QString& key);
NumerotationContext conductorAutoNum(const QString &key) const;
NumerotationContext folioAutoNum(const QString &key) const;
NumerotationContext elementAutoNum(const QString &key);
QString conductorAutoNumFormula(const QString& key) const; //returns Formula
QString conductorCurrentAutoNum() const;
void setCurrentConductorAutoNum(QString autoNum);
QString elementAutoNumFormula(const QString& key) const;
QString elementAutoNumCurrentFormula() const;
QString elementCurrentAutoNum() const;
void setCurrrentElementAutonum(QString autoNum);
//Element
void freezeExistentElementLabel(bool freeze, int from, int to);
void freezeNewElementLabel(bool freeze, int from, int to);
bool isFreezeNewElements();
void setFreezeNewElements(bool);
//Conductor
void freezeExistentConductorLabel(bool freeze, int from, int to);
void freezeNewConductorLabel(bool freeze, int from, int to);
bool isFreezeNewConductors();
void setFreezeNewConductors(bool);
bool autoConductor () const;
bool autoBreakConductor () const;
bool autoElement () const;
bool autoFolio () const;
void setAutoConductor (bool ac);
void setAutoBreakConductor (bool abc);
void setAutoElement (bool ae);
void autoFolioNumberingNewFolios ();
void autoFolioNumberingSelectedFolios(int, int, const QString&);
QDomDocument toXml();
bool close();
QETResult write();
bool isReadOnly() const;
void setReadOnly(bool);
bool isEmpty() const;
ElementsLocation importElement(ElementsLocation &location);
QString integrateTitleBlockTemplate(const TitleBlockTemplateLocation &, MoveTitleBlockTemplatesHandler *handler);
bool usesElement(const ElementsLocation &) const;
QList <ElementsLocation> unusedElements() const;
bool usesTitleBlockTemplate(const TitleBlockTemplateLocation &);
bool projectWasModified();
bool projectOptionsWereModified();
DiagramContext projectProperties();
void setProjectProperties(const DiagramContext &);
QUndoStack* undoStack() {return m_undo_stack;}
QVector<TerminalStrip *> terminalStrip() const;
TerminalStrip * newTerminalStrip(QString installation = QString(), QString location = QString(), QString name = QString());
bool addTerminalStrip(TerminalStrip *strip);
bool removeTerminalStrip(TerminalStrip *strip);
public slots:
Diagram *addNewDiagram(int pos = -1);
void removeDiagram(Diagram *);
void diagramOrderChanged(int, int);
void setModified(bool);
signals:
void projectFilePathChanged(QETProject *, const QString &);
void projectTitleChanged(QETProject *, const QString &);
void projectInformationsChanged(QETProject *);
void diagramAdded(QETProject *, Diagram *);
void diagramRemoved(QETProject *, Diagram *);
void projectModified(QETProject *, bool);
void projectDiagramsOrderChanged(QETProject *, int, int);
void diagramUsedTemplate(TitleBlockTemplatesCollection *, const QString &);
void readOnlyChanged(QETProject *, bool);
void reportPropertiesChanged(const QString &old_str, const QString &new_str);
void XRefPropertiesChanged ();
void addAutoNumDiagram();
void elementAutoNumAdded(QString name);
void elementAutoNumRemoved(QString name);
void conductorAutoNumAdded();
void conductorAutoNumRemoved();
void folioAutoNumAdded();
/// A numerotation context's *values* changed -- as happens every
/// time an element or conductor consumes the next number, not
/// only when a rule is added or removed. Deliberately separate
/// from the *Added/*Removed signals above, which make listeners
/// rebuild their rule lists; this one just says "re-read me".
void autoNumContextUpdated();
void folioAutoNumRemoved();
void defaultTitleBlockPropertiesChanged();
void conductorAutoNumChanged();
private slots:
void updateDiagramsFolioData();
void updateDiagramsTitleBlockTemplate(TitleBlockTemplatesCollection *, const QString &);
void removeDiagramsTitleBlockTemplate(TitleBlockTemplatesCollection *, const QString &);
void usedTitleBlockTemplateChanged(const QString &);
void undoStackChanged (bool a) {if (!a) setModified(true);}
private:
void readProjectXml(QDomDocument &xml_project);
void readDiagramsXml(QDomDocument &xml_project);
void readElementsCollectionXml(QDomDocument &xml_project);
void readProjectPropertiesXml(QDomDocument &xml_project);
void readDefaultPropertiesXml(QDomDocument &xml_project);
void readTerminalStripXml(const QDomDocument &xml_project);
void readUsageXml(QDomDocument &xml_project);
void writeProjectPropertiesXml(QDomElement &);
void writeDefaultPropertiesXml(QDomElement &);
void writeUsageXml(QDomElement &);
void addDiagram(Diagram *diagram, int pos = -1);
void detachDiagram(Diagram *diagram);
void writeBackup();
void init();
ProjectState openFile(QFile *file);
static QUuid derivedUuid(const QByteArray &content);
void refresh();
// attributes
private:
/// When false, writeBackup() is a no-op (set by the headless CLI)
static bool m_backup_enabled;
/// File path this project is saved to
QString m_file_path;
/// Current state of the project
ProjectState m_state;
/// Diagrams carried by the project
QList<Diagram *> m_diagrams_list;
/// Project title
QString project_title_;
/// QElectroTech version declared in the XML document at opening time
QVersionNumber m_project_qet_version;
/// Whether options were modified
bool m_modified = false;
/// Whether the project is read only
bool m_read_only = false;
/// Filepath for which this project is considered read only
QString read_only_file_path_;
/// Default dimensions and properties for new diagrams created within the project
BorderProperties default_border_properties_ = BorderProperties::defaultProperties();
/// Default guides for new diagrams created within the project
QList<GuideProperties> m_default_guides;
/// Default conductor properties for new diagrams created within the project
ConductorProperties default_conductor_properties_ = ConductorProperties::defaultProperties();
/// Default title block properties for new diagrams created within the project
TitleBlockProperties default_titleblock_properties_;
/// Default report properties
QString m_default_report_properties = ReportProperties::defaultProperties();
/// Default xref properties
QHash <QString, XRefProperties> m_default_xref_properties = XRefProperties::defaultProperties();
/// Embedded title block templates collection
TitleBlockTemplatesProjectCollection m_titleblocks_collection;
/// project-wide variables that will be made available to child diagrams
DiagramContext m_project_properties;
/// undo stack for this project
QUndoStack *m_undo_stack;
/// Conductor auto numerotation
QHash <QString, NumerotationContext> m_conductor_autonum;//Title and NumContext hash
QString m_current_conductor_autonum;
/// Folio auto numbering
QHash <QString, NumerotationContext> m_folio_autonum;
/// Element Auto Numbering
QHash <QString, NumerotationContext> m_element_autonum; //Title and NumContext hash
QString m_current_element_autonum;
bool m_auto_conductor = true;
bool m_auto_break_conductor = false;
XmlElementCollection *m_elements_collection = nullptr;
bool m_freeze_new_elements = false;
bool m_freeze_new_conductors = false;
QTimer m_save_backup_timer,
m_autosave_timer;
QFuture<bool> m_backup_future;
KAutoSaveFile m_backup_file;
QUuid m_uuid = QUuid::createUuid();
projectDataBase m_data_base;
QVector<TerminalStrip *> m_terminal_strip_vector;
ProjectPropertiesHandler m_project_properties_handler;
};
Q_DECLARE_METATYPE(QETProject *)
#endif