mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-14 09:14:14 +02:00
Compare commits
25 Commits
resolve-1461
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 33a731b56f | |||
| 373a705bbb | |||
| af142aba90 | |||
| 8cda53f4ce | |||
| 4e8af81fc6 | |||
| 9920c13bd9 | |||
| 1768518b44 | |||
| da6eb83035 | |||
| 6751fc6f19 | |||
| 2bf73585ba | |||
| f65b24250c | |||
| 590bd6e532 | |||
| 4c3d87e3f7 | |||
| 5975b75766 | |||
| 408ef5114d | |||
| 8d118faf36 | |||
| 8b680af938 | |||
| 7c2a09f2bc | |||
| 132eefb8b8 | |||
| 00e200b504 | |||
| 1c4618c8dd | |||
| 1ad3c8e2b2 | |||
| aa503aec81 | |||
| c240e4e6f7 | |||
| 561ddc29bf |
@@ -226,9 +226,12 @@ void SearchAndReplaceWorker::replaceConductor(QList<Conductor *> list)
|
|||||||
project_->undoStack()->beginMacro(QObject::tr("Search / replace conductors properties."));
|
project_->undoStack()->beginMacro(QObject::tr("Search / replace conductors properties."));
|
||||||
for (Conductor *c : list)
|
for (Conductor *c : list)
|
||||||
{
|
{
|
||||||
ConductorProperties cp = applyChange(c->properties(), m_conductor_properties);
|
//Captured before the pushes below: QPropertyUndoCommand applies
|
||||||
|
//on push, so reading it afterwards would give back @a cp.
|
||||||
|
const ConductorProperties old_properties = c->properties();
|
||||||
|
ConductorProperties cp = applyChange(old_properties, m_conductor_properties);
|
||||||
|
|
||||||
if (cp != c->properties())
|
if (cp != old_properties)
|
||||||
{
|
{
|
||||||
QSet <Conductor *> conductors_list = c->relatedPotentialConductors(true);
|
QSet <Conductor *> conductors_list = c->relatedPotentialConductors(true);
|
||||||
conductors_list << c;
|
conductors_list << c;
|
||||||
@@ -239,6 +242,26 @@ void SearchAndReplaceWorker::replaceConductor(QList<Conductor *> list)
|
|||||||
new_value.setValue(cp);
|
new_value.setValue(cp);
|
||||||
project_->undoStack()->push(new QPropertyUndoCommand(cc, "properties", old_value, new_value));
|
project_->undoStack()->push(new QPropertyUndoCommand(cc, "properties", old_value, new_value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//The other side of a function separated terminal element
|
||||||
|
//only receives the voltage / protocol, and only when this
|
||||||
|
//replacement really changed it: the comparison is this
|
||||||
|
//replacement's own before / after (@a old_properties vs
|
||||||
|
//@a cp), never the far wire's own value, which would make
|
||||||
|
//every replacement overwrite it (@see
|
||||||
|
//Conductor::functionSeparatedConductors).
|
||||||
|
for (Conductor *cc : c->functionSeparatedConductors(true))
|
||||||
|
{
|
||||||
|
ConductorProperties separated_cp = cc->properties();
|
||||||
|
separated_cp.applyTensionProtocolChange(old_properties, cp);
|
||||||
|
if (separated_cp == cc->properties())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
QVariant old_value, new_value;
|
||||||
|
old_value.setValue(cc->properties());
|
||||||
|
new_value.setValue(separated_cp);
|
||||||
|
project_->undoStack()->push(new QPropertyUndoCommand(cc, "properties", old_value, new_value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
project_->undoStack()->endMacro();
|
project_->undoStack()->endMacro();
|
||||||
@@ -351,6 +374,25 @@ void SearchAndReplaceWorker::replaceAdvanced(
|
|||||||
new_value.setValue(new_properties);
|
new_value.setValue(new_properties);
|
||||||
project_->undoStack()->push(new QPropertyUndoCommand(c, "properties", old_value, new_value));
|
project_->undoStack()->push(new QPropertyUndoCommand(c, "properties", old_value, new_value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//The other side of a function separated terminal element
|
||||||
|
//only receives the voltage / protocol, and only when
|
||||||
|
//this replacement really changed it: the comparison is
|
||||||
|
//this replacement's own before / after, never the far
|
||||||
|
//wire's own value (@see
|
||||||
|
//Conductor::functionSeparatedConductors).
|
||||||
|
for (Conductor *c : conductor->functionSeparatedConductors(true))
|
||||||
|
{
|
||||||
|
ConductorProperties separated_properties = c->properties();
|
||||||
|
separated_properties.applyTensionProtocolChange(old_properties, new_properties);
|
||||||
|
if (separated_properties == c->properties())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
QVariant old_value, new_value;
|
||||||
|
old_value.setValue(c->properties());
|
||||||
|
new_value.setValue(separated_properties);
|
||||||
|
project_->undoStack()->push(new QPropertyUndoCommand(c, "properties", old_value, new_value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -321,8 +321,8 @@ TitleBlockProperties BorderTitleBlock::localTitleBlock() const
|
|||||||
/**
|
/**
|
||||||
@brief BorderTitleBlock::current
|
@brief BorderTitleBlock::current
|
||||||
@return the properties of the titleblock: for a folio placed in a
|
@return the properties of the titleblock: for a folio placed in a
|
||||||
project the project's store holds them (DB-ACCESSORS-PLAN.md stage
|
project the project's store holds them, for any other this object's
|
||||||
4.4), for any other this object's own copy.
|
own copy.
|
||||||
*/
|
*/
|
||||||
TitleBlockProperties BorderTitleBlock::current() const
|
TitleBlockProperties BorderTitleBlock::current() const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -110,8 +110,8 @@ class BorderTitleBlock : public QObject
|
|||||||
QRectF insideBorderRect() const;
|
QRectF insideBorderRect() const;
|
||||||
|
|
||||||
// methods to get title block basic data. A folio placed in a
|
// methods to get title block basic data. A folio placed in a
|
||||||
// project reads them from the project's store
|
// project reads them from the project's store, any other from its
|
||||||
// (DB-ACCESSORS-PLAN.md stage 4.4), any other from its own copy.
|
// own copy.
|
||||||
/// @return the value of the title block "Author" field
|
/// @return the value of the title block "Author" field
|
||||||
QString author() const { const TitleBlockProperties *s = stored(); return(s ? s->author : btb_author_); }
|
QString author() const { const TitleBlockProperties *s = stored(); return(s ? s->author : btb_author_); }
|
||||||
/// @return the value of the title block "Date" field
|
/// @return the value of the title block "Date" field
|
||||||
|
|||||||
@@ -283,7 +283,8 @@ int exportPdf(QETProject &project, const QString &output,
|
|||||||
}
|
}
|
||||||
|
|
||||||
int exportImages(QETProject &project, const QString &format,
|
int exportImages(QETProject &project, const QString &format,
|
||||||
const QString &out_dir, bool showTerminals = false)
|
const QString &out_dir, bool showTerminals = false,
|
||||||
|
bool svgSmallPictures = false)
|
||||||
{
|
{
|
||||||
const QList<Diagram *> diagrams = project.diagrams();
|
const QList<Diagram *> diagrams = project.diagrams();
|
||||||
if (diagrams.isEmpty()) {
|
if (diagrams.isEmpty()) {
|
||||||
@@ -306,8 +307,10 @@ int exportImages(QETProject &project, const QString &format,
|
|||||||
gen.setViewBox(QRect(0, 0, r.width(), r.height()));
|
gen.setViewBox(QRect(0, 0, r.width(), r.height()));
|
||||||
gen.setTitle(diagram->title());
|
gen.setTitle(diagram->title());
|
||||||
QPainter painter(&gen);
|
QPainter painter(&gen);
|
||||||
|
diagram->setSmallPictures(svgSmallPictures);
|
||||||
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()),
|
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()),
|
||||||
showTerminals);
|
showTerminals);
|
||||||
|
diagram->setSmallPictures(false);
|
||||||
painter.end();
|
painter.end();
|
||||||
} else { // png
|
} else { // png
|
||||||
QImage image(r.size(), QImage::Format_ARGB32);
|
QImage image(r.size(), QImage::Format_ARGB32);
|
||||||
@@ -1014,6 +1017,7 @@ int run(const QStringList &args)
|
|||||||
const bool showTerminals = filtered.removeAll("--show-terminals") > 0;
|
const bool showTerminals = filtered.removeAll("--show-terminals") > 0;
|
||||||
const bool dxfBlocks = filtered.removeAll("--dxf-blocks") > 0;
|
const bool dxfBlocks = filtered.removeAll("--dxf-blocks") > 0;
|
||||||
const bool dxfAttributes = filtered.removeAll("--dxf-attributes") > 0;
|
const bool dxfAttributes = filtered.removeAll("--dxf-attributes") > 0;
|
||||||
|
const bool svgSmallPictures = filtered.removeAll("--svg-small-pictures") > 0;
|
||||||
// --no-slaves and --no-junctions leave rows out of --export-bom.
|
// --no-slaves and --no-junctions leave rows out of --export-bom.
|
||||||
const bool includeSlaves = filtered.removeAll("--no-slaves") == 0;
|
const bool includeSlaves = filtered.removeAll("--no-slaves") == 0;
|
||||||
const bool includeJunctions = filtered.removeAll("--no-junctions") == 0;
|
const bool includeJunctions = filtered.removeAll("--no-junctions") == 0;
|
||||||
@@ -1085,7 +1089,8 @@ int run(const QStringList &args)
|
|||||||
return resaveProject(project, output);
|
return resaveProject(project, output);
|
||||||
if (format == "settb")
|
if (format == "settb")
|
||||||
return setTitleBlock(project, output, rest.mid(2));
|
return setTitleBlock(project, output, rest.mid(2));
|
||||||
return exportImages(project, format, output, showTerminals);
|
return exportImages(project, format, output, showTerminals,
|
||||||
|
svgSmallPictures);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace CLIExport
|
} // namespace CLIExport
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ namespace CLIExport {
|
|||||||
Usage:
|
Usage:
|
||||||
qelectrotech --export-pdf <project.qet> <output.pdf> [--show-terminals]
|
qelectrotech --export-pdf <project.qet> <output.pdf> [--show-terminals]
|
||||||
qelectrotech --export-png <project.qet> <output_dir> [--show-terminals]
|
qelectrotech --export-png <project.qet> <output_dir> [--show-terminals]
|
||||||
qelectrotech --export-svg <project.qet> <output_dir> [--show-terminals]
|
qelectrotech --export-svg <project.qet> <output_dir> [--show-terminals] [--svg-small-pictures]
|
||||||
qelectrotech --export-dxf <project.qet> <output_dir> [--show-terminals] [--dxf-blocks] [--dxf-attributes]
|
qelectrotech --export-dxf <project.qet> <output_dir> [--show-terminals] [--dxf-blocks] [--dxf-attributes]
|
||||||
qelectrotech --export-cables <project.qet> <output.csv>
|
qelectrotech --export-cables <project.qet> <output.csv>
|
||||||
qelectrotech --export-wires <project.qet> <output.csv>
|
qelectrotech --export-wires <project.qet> <output.csv>
|
||||||
@@ -71,6 +71,9 @@ namespace CLIExport {
|
|||||||
--dxf-attributes: as --dxf-blocks, and a symbol's texts (its label,
|
--dxf-attributes: as --dxf-blocks, and a symbol's texts (its label,
|
||||||
function...) are attributes of its INSERT. LibreCAD does not
|
function...) are attributes of its INSERT. LibreCAD does not
|
||||||
show attributes.
|
show attributes.
|
||||||
|
--svg-small-pictures: in an SVG, each picture at most four times the
|
||||||
|
size it is drawn at instead of its full resolution: a much
|
||||||
|
smaller file, still sharp when printed.
|
||||||
cables: wiring list (one row per conductor) as CSV.
|
cables: wiring list (one row per conductor) as CSV.
|
||||||
wires: list of distinct wire numbers as CSV.
|
wires: list of distinct wire numbers as CSV.
|
||||||
bom: bill of materials (one row per element) as CSV.
|
bom: bill of materials (one row per element) as CSV.
|
||||||
|
|||||||
@@ -62,33 +62,80 @@ namespace ConductorMultiEdit
|
|||||||
@brief targets
|
@brief targets
|
||||||
@param selected : the edited conductors
|
@param selected : the edited conductors
|
||||||
@param apply_all : true to add every conductor of their potentials
|
@param apply_all : true to add every conductor of their potentials
|
||||||
|
@param function_separated : set to the conductors that share the
|
||||||
|
potential of an edited one only by crossing a function separated
|
||||||
|
terminal element -- conductors undo() then gives the voltage /
|
||||||
|
protocol only, and no other property. nullptr, the default, leaves
|
||||||
|
those conductors out of the returned list entirely.
|
||||||
|
|
||||||
|
A conductor the user picked themselves is never part of it: the
|
||||||
|
selection is the edit reaching that conductor, so it takes the
|
||||||
|
full edit however remote it is from the other selected ones --
|
||||||
|
picking a wire on each side of a separation and editing them is
|
||||||
|
exactly that. Nor is a conductor that belongs to a potential, on
|
||||||
|
any path, however far that potential is reached.
|
||||||
@return each conductor an edit reaches, once
|
@return each conductor an edit reaches, once
|
||||||
*/
|
*/
|
||||||
template <typename T>
|
template <typename T>
|
||||||
QList<T *> targets(const QList<T *> &selected, bool apply_all)
|
QList<T *> targets(const QList<T *> &selected, bool apply_all,
|
||||||
|
QSet<T *> *function_separated = nullptr)
|
||||||
{
|
{
|
||||||
QList<T *> list;
|
QList<T *> list;
|
||||||
QSet<T *> seen;
|
QSet<T *> seen;
|
||||||
|
//The explicitly picked ones, so a separation met while walking
|
||||||
|
//the list cannot demote a wire the user asked to edit.
|
||||||
|
QSet<T *> chosen;
|
||||||
|
for (T *conductor : selected)
|
||||||
|
if (conductor)
|
||||||
|
chosen.insert(conductor);
|
||||||
|
//What the potentials reach, and what only a function separation
|
||||||
|
//reaches. A conductor can be in both -- wired normally on one
|
||||||
|
//path and across a separation on another -- and then belongs to
|
||||||
|
//the potential.
|
||||||
|
QSet<T *> potential_reached, separated_reached;
|
||||||
|
|
||||||
for (T *conductor : selected)
|
for (T *conductor : selected)
|
||||||
{
|
{
|
||||||
//Already reached through an earlier conductor's potential:
|
if (!conductor)
|
||||||
//its own potential is the same one.
|
|
||||||
if (!conductor || seen.contains(conductor))
|
|
||||||
continue;
|
continue;
|
||||||
seen.insert(conductor);
|
//Already reached through an earlier conductor's potential or
|
||||||
list << conductor;
|
//separation: its own potential adds nothing new to walk over
|
||||||
if (!apply_all)
|
//-- unless the user picked it, because a separation may have
|
||||||
|
//put it in the list before its potential was ever reached.
|
||||||
|
const bool fresh = !seen.contains(conductor);
|
||||||
|
if (fresh)
|
||||||
|
{
|
||||||
|
seen.insert(conductor);
|
||||||
|
list << conductor;
|
||||||
|
}
|
||||||
|
if (!apply_all || (!fresh && !chosen.contains(conductor)))
|
||||||
continue;
|
continue;
|
||||||
const QSet<T *> potential = conductor->relatedPotentialConductors();
|
const QSet<T *> potential = conductor->relatedPotentialConductors();
|
||||||
for (T *other : potential)
|
for (T *other : potential)
|
||||||
{
|
{
|
||||||
|
potential_reached.insert(other);
|
||||||
if (!seen.contains(other))
|
if (!seen.contains(other))
|
||||||
{
|
{
|
||||||
seen.insert(other);
|
seen.insert(other);
|
||||||
list << other;
|
list << other;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (function_separated)
|
||||||
|
{
|
||||||
|
const QSet<T *> separated = conductor->functionSeparatedConductors();
|
||||||
|
for (T *other : separated)
|
||||||
|
{
|
||||||
|
separated_reached.insert(other);
|
||||||
|
if (!seen.contains(other))
|
||||||
|
{
|
||||||
|
seen.insert(other);
|
||||||
|
list << other;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (function_separated)
|
||||||
|
*function_separated = separated_reached - potential_reached - chosen;
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,13 +144,18 @@ namespace ConductorMultiEdit
|
|||||||
@param targets : the conductors to change, see targets()
|
@param targets : the conductors to change, see targets()
|
||||||
@param shown : the properties as the panel showed them
|
@param shown : the properties as the panel showed them
|
||||||
@param edited : the properties after the user's edit
|
@param edited : the properties after the user's edit
|
||||||
|
@param function_separated : the targets of targets() that only a
|
||||||
|
function separated terminal element links to the edited
|
||||||
|
conductors: they get the voltage / protocol and nothing else
|
||||||
|
(@see ConductorProperties::applyTensionProtocolChange)
|
||||||
@return one undo step giving each target the fields that differ
|
@return one undo step giving each target the fields that differ
|
||||||
between shown and edited, or nullptr if no conductor changes
|
between shown and edited, or nullptr if no conductor changes
|
||||||
*/
|
*/
|
||||||
template <typename T>
|
template <typename T>
|
||||||
QUndoCommand *undo(const QList<T *> &targets,
|
QUndoCommand *undo(const QList<T *> &targets,
|
||||||
const ConductorProperties &shown,
|
const ConductorProperties &shown,
|
||||||
const ConductorProperties &edited)
|
const ConductorProperties &edited,
|
||||||
|
const QSet<T *> &function_separated = QSet<T *>())
|
||||||
{
|
{
|
||||||
if (edited == shown)
|
if (edited == shown)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -114,7 +166,10 @@ namespace ConductorMultiEdit
|
|||||||
{
|
{
|
||||||
const ConductorProperties old_properties = conductor->properties();
|
const ConductorProperties old_properties = conductor->properties();
|
||||||
ConductorProperties properties = old_properties;
|
ConductorProperties properties = old_properties;
|
||||||
properties.applyChanges(shown, edited);
|
if (function_separated.contains(conductor))
|
||||||
|
properties.applyTensionProtocolChange(shown, edited);
|
||||||
|
else
|
||||||
|
properties.applyChanges(shown, edited);
|
||||||
if (properties == old_properties)
|
if (properties == old_properties)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
|||||||
@@ -851,6 +851,32 @@ void ConductorProperties::applyChanges(const ConductorProperties &before,
|
|||||||
singleLineProperties.setPhasesCount(slp_after.phasesCount());
|
singleLineProperties.setPhasesCount(slp_after.phasesCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief ConductorProperties::applyTensionProtocolChange
|
||||||
|
Copy into this the voltage / protocol when it differs between before
|
||||||
|
and after, and leave every other attribute as it is. This is the one
|
||||||
|
property a function separated terminal element lets through
|
||||||
|
(@see Conductor::functionSeparatedConductors): the voltage /
|
||||||
|
protocol describes the electrical node itself, while the text, the
|
||||||
|
function, the wire color... describe one side of the separation and
|
||||||
|
stay local to it.
|
||||||
|
|
||||||
|
Both arguments describe one edit -- what was shown to the user, and
|
||||||
|
what the user made of it -- so that only an edit which really
|
||||||
|
changed the voltage / protocol passes it on. Passing the far wire's
|
||||||
|
own properties as @a before instead would make every edit overwrite
|
||||||
|
its voltage: the two sides differ on that field just as often as
|
||||||
|
not, so the comparison would hold no matter what was edited.
|
||||||
|
@param before : the properties as they were shown to the user
|
||||||
|
@param after : the properties after the user's edit
|
||||||
|
*/
|
||||||
|
void ConductorProperties::applyTensionProtocolChange(const ConductorProperties &before,
|
||||||
|
const ConductorProperties &after)
|
||||||
|
{
|
||||||
|
if (before.m_tension_protocol != after.m_tension_protocol)
|
||||||
|
m_tension_protocol = after.m_tension_protocol;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief ConductorProperties::defaultProperties
|
@brief ConductorProperties::defaultProperties
|
||||||
@return the default properties stored in the setting file
|
@return the default properties stored in the setting file
|
||||||
|
|||||||
@@ -139,6 +139,8 @@ class ConductorProperties
|
|||||||
void applyForEqualAttributes(QList<ConductorProperties> list);
|
void applyForEqualAttributes(QList<ConductorProperties> list);
|
||||||
void applyChanges(const ConductorProperties &before,
|
void applyChanges(const ConductorProperties &before,
|
||||||
const ConductorProperties &after);
|
const ConductorProperties &after);
|
||||||
|
void applyTensionProtocolChange(const ConductorProperties &before,
|
||||||
|
const ConductorProperties &after);
|
||||||
|
|
||||||
static ConductorProperties defaultProperties();
|
static ConductorProperties defaultProperties();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
# The `.qetz` project file, format 3
|
||||||
|
|
||||||
|
A `.qetz` is a QElectroTech project saved as a zip archive: the same project
|
||||||
|
a `.qet` holds, split into files, with the engineering data in an SQLite
|
||||||
|
database. This document describes format 3. `tst_qetcontainer` checks that
|
||||||
|
the schema below is the one QElectroTech writes.
|
||||||
|
|
||||||
|
The code is `qetcontainer.cpp` (the zip and its files), `qetcontainerdb.cpp`
|
||||||
|
(the database) and `qetzip.cpp` (reading and writing zip archives).
|
||||||
|
|
||||||
|
## 1. Principles
|
||||||
|
|
||||||
|
- **One writer.** A `.qet` and a `.qetz` are written from the same project
|
||||||
|
document. Saving a `.qetz` splits that document into files and a database;
|
||||||
|
opening one puts it back together, and the program reads it exactly as a
|
||||||
|
`.qet`. Opening a `.qetz` and saving it as `.qet` gives the file a plain
|
||||||
|
`.qet` save gives.
|
||||||
|
- **Each fact once.** A value moved into the database is taken out of the
|
||||||
|
folio file. Nothing is stored in both.
|
||||||
|
- **What a thing is, against where it is drawn.** The database holds the
|
||||||
|
engineering data; the folio files hold the drawing:
|
||||||
|
|
||||||
|
| | database | folio file |
|
||||||
|
|---|---|---|
|
||||||
|
| Symbol | its information (label, comment, …), prefix, links | which symbol it is (`type`), position, rotation |
|
||||||
|
| Label shown on a folio | the value | the text item: position, font, rotation |
|
||||||
|
| Wire | its ends, number, function, voltage, colour, section | its path, the number's text position and font |
|
||||||
|
| Folio | title-block fields and custom variables | everything drawn on it |
|
||||||
|
| Terminal strip | its terminals, their order and bridges | the strip drawn on a folio |
|
||||||
|
|
||||||
|
Cables are kept in `project.xml` in this format.
|
||||||
|
- **Nothing is lost.** Anything the database does not model stays in the
|
||||||
|
XML as it was. A block is moved only if it can be rebuilt exactly.
|
||||||
|
|
||||||
|
## 2. The archive
|
||||||
|
|
||||||
|
| Entry | Content |
|
||||||
|
|---|---|
|
||||||
|
| `mimetype` | `application/x-qelectrotech-project+zip`, the first entry, stored uncompressed |
|
||||||
|
| `manifest.xml` | `<qetz format="3" min-reader="3" writer="QElectroTech …"/>` |
|
||||||
|
| `project.xml` | the project document, each part replaced by `<qetz-part file="…"/>` |
|
||||||
|
| `project.sqlite` | the engineering data (section 4) |
|
||||||
|
| `folios/<uuid>.xml` | one `<diagram>` per folio, named by the folio's uuid (or its position when it has none) |
|
||||||
|
| `elements/<categories>/<name>` | the project's symbol definitions, as in the symbol collection |
|
||||||
|
| `titleblocks/<name>.titleblock` | the project's title-block templates |
|
||||||
|
| `images/<sha256>.png` | each folio picture once, as a file instead of base64 text (`.bin` if it is not a PNG) |
|
||||||
|
|
||||||
|
Names that would collide get `~2`, `~3`… before the extension. A picture
|
||||||
|
in a folio file keeps the name of its file in the attribute `qetz-file`.
|
||||||
|
|
||||||
|
A reader refuses the whole archive if an entry is encrypted, uses a
|
||||||
|
compression method it cannot read, has a name that is absolute, holds `..`,
|
||||||
|
`\` or `:`, or repeats another entry's name, or if the archive holds more
|
||||||
|
than 100 000 entries or more than 2 GiB once unpacked. Entries the format
|
||||||
|
does not name are ignored.
|
||||||
|
|
||||||
|
## 3. Versions
|
||||||
|
|
||||||
|
`manifest.xml` gives two numbers: `format`, the format written, and
|
||||||
|
`min-reader`, the oldest format allowed to read the file.
|
||||||
|
|
||||||
|
- A reader opens a file whose `min-reader` is not above its own format.
|
||||||
|
- If `format` is above its own, the project opens **read-only**, with a
|
||||||
|
message: saving it could drop what that reader does not know. *Save As*
|
||||||
|
writes a copy in the reader's own format.
|
||||||
|
- If `min-reader` is above its own format, the file is refused with a
|
||||||
|
message.
|
||||||
|
|
||||||
|
The database repeats the format in `meta` (key `format`) and in
|
||||||
|
`PRAGMA user_version`.
|
||||||
|
|
||||||
|
**Rules for a new format:**
|
||||||
|
|
||||||
|
1. New data goes in a new column or table. An older reader skips both.
|
||||||
|
2. A change an older reader would misread raises `min-reader`.
|
||||||
|
3. Every format keeps a saved file in `tests/qttest/fixtures` that must
|
||||||
|
keep opening.
|
||||||
|
|
||||||
|
| Format | Written by | Reading |
|
||||||
|
|---|---|---|
|
||||||
|
| 2 | QElectroTech 0.200.1 development builds (#1462) | read: rows keyed by position (`f1`, `e1`, `c1`) where an item had no uuid of its own, every value as text |
|
||||||
|
| 3 | this document | read and written |
|
||||||
|
|
||||||
|
## 4. The database
|
||||||
|
|
||||||
|
### 4.1 Schema
|
||||||
|
|
||||||
|
<!-- schema: tst_qetcontainer checks it against the database written -->
|
||||||
|
```sql
|
||||||
|
CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT);
|
||||||
|
CREATE TABLE project_property (ord INTEGER PRIMARY KEY, name TEXT, value TEXT, show INTEGER, attrs TEXT);
|
||||||
|
CREATE TABLE folio (uuid TEXT PRIMARY KEY, pos INTEGER NOT NULL UNIQUE,
|
||||||
|
"title" TEXT, "author" TEXT, "date" TEXT, "folio" TEXT, "indexrev" TEXT, "filename" TEXT,
|
||||||
|
"locmach" TEXT, "plant" TEXT, "auto_page_num" TEXT, props_attrs TEXT);
|
||||||
|
CREATE TABLE folio_property (folio_uuid TEXT NOT NULL REFERENCES folio, ord INTEGER NOT NULL,
|
||||||
|
name TEXT, value TEXT, show INTEGER, attrs TEXT, PRIMARY KEY (folio_uuid, ord));
|
||||||
|
CREATE TABLE element (uuid TEXT PRIMARY KEY, folio_uuid TEXT NOT NULL REFERENCES folio,
|
||||||
|
"prefix" TEXT, "freezeLabel" INTEGER, info_attrs TEXT, links_attrs TEXT);
|
||||||
|
CREATE TABLE element_info (element_uuid TEXT NOT NULL REFERENCES element, ord INTEGER NOT NULL,
|
||||||
|
name TEXT, value TEXT, show INTEGER, attrs TEXT, PRIMARY KEY (element_uuid, ord));
|
||||||
|
CREATE TABLE link (element_uuid TEXT NOT NULL REFERENCES element, ord INTEGER NOT NULL,
|
||||||
|
linked_uuid TEXT, attrs TEXT, PRIMARY KEY (element_uuid, ord));
|
||||||
|
CREATE TABLE conductor (uuid TEXT PRIMARY KEY, folio_uuid TEXT NOT NULL REFERENCES folio,
|
||||||
|
"element1" TEXT, "terminal1" TEXT, "element2" TEXT, "terminal2" TEXT, "num" TEXT,
|
||||||
|
"formula" TEXT, "function" TEXT, "tension_protocol" TEXT, "conductor_color" TEXT,
|
||||||
|
"conductor_section" TEXT, "cable" TEXT, "bus" TEXT, "freezeLabel" INTEGER);
|
||||||
|
CREATE TABLE terminal_strip (uuid TEXT PRIMARY KEY, ord INTEGER NOT NULL UNIQUE,
|
||||||
|
installation TEXT, location TEXT, name TEXT, comment TEXT, description TEXT);
|
||||||
|
CREATE TABLE strip_terminal (strip_uuid TEXT NOT NULL REFERENCES terminal_strip,
|
||||||
|
pos INTEGER NOT NULL, level INTEGER NOT NULL, element_uuid TEXT,
|
||||||
|
PRIMARY KEY (strip_uuid, pos, level));
|
||||||
|
CREATE TABLE strip_bridge (uuid TEXT PRIMARY KEY, strip_uuid TEXT NOT NULL REFERENCES terminal_strip,
|
||||||
|
ord INTEGER NOT NULL, color TEXT);
|
||||||
|
CREATE TABLE strip_bridge_terminal (bridge_uuid TEXT NOT NULL REFERENCES strip_bridge,
|
||||||
|
ord INTEGER NOT NULL, element_uuid TEXT, PRIMARY KEY (bridge_uuid, ord));
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 What each table holds
|
||||||
|
|
||||||
|
| Table | One row per | Comes from |
|
||||||
|
|---|---|---|
|
||||||
|
| `meta` | setting of the file | `format`; `props_attrs` (attributes of the project's `<properties>`); `terminal_strips` = `1` when the strips are in the database |
|
||||||
|
| `project_property` | project property (project-wide variable) | `<project><properties><property>` |
|
||||||
|
| `folio` | folio, in folio order (`pos` from 1) | the title-block attributes of `<diagram>` |
|
||||||
|
| `folio_property` | custom title-block variable of a folio | `<diagram><properties><property>` |
|
||||||
|
| `element` | symbol placed on a folio | `<element>`: its label prefix and frozen-label flag |
|
||||||
|
| `element_info` | piece of symbol information, in order | `<elementInformations><elementInformation>` |
|
||||||
|
| `link` | cross-reference link of a symbol, in order | `<links_uuids><link_uuid>` |
|
||||||
|
| `conductor` | wire | `<conductor>`: its two ends (symbol uuid and terminal), number, formula, function, voltage/protocol, colour, section, cable, bus, frozen-label flag |
|
||||||
|
| `terminal_strip`, `strip_terminal`, `strip_bridge`, `strip_bridge_terminal` | terminal strip, terminal, bridge, bridged terminal | `<terminal_strips>` |
|
||||||
|
|
||||||
|
A value is NULL when the XML has no such attribute.
|
||||||
|
|
||||||
|
- `freezeLabel` is 1 for `true`, 0 for `false`. Any other text stays as an
|
||||||
|
attribute in the folio file.
|
||||||
|
- `show` is 0 or 1. Any other text goes in `attrs`.
|
||||||
|
- `attrs`, `props_attrs`, `info_attrs` and `links_attrs` are JSON objects of
|
||||||
|
the attributes that have no column, and the entry's tag when it is not the
|
||||||
|
usual one. In a project QElectroTech saves they are all NULL. They hold
|
||||||
|
something only for older or hand-edited files. Whitespace between
|
||||||
|
entries, which only a hand-edited file has, is kept in them as
|
||||||
|
`qetz-ws`.
|
||||||
|
|
||||||
|
### 4.3 Keys
|
||||||
|
|
||||||
|
Every row is keyed by its item's **uuid**, which stays in the folio file:
|
||||||
|
that is how a folio file's item finds its row. Folios, symbols and wires
|
||||||
|
are keyed this way.
|
||||||
|
|
||||||
|
An item with no uuid of its own (none, or one another item of the same kind
|
||||||
|
shares) gets a key made from the item itself: a version 5 uuid in the
|
||||||
|
namespace `{3f0c6c1e-5b8e-4b8a-9d55-6b2f5f0f9a41}` from its XML text, how
|
||||||
|
many identical items came before it, and a counter that avoids taken keys.
|
||||||
|
The key is the same on every save and never depends on where the item is.
|
||||||
|
It is written on the item as `qetz-key` (a symbol or wire) or `qetz-folio`
|
||||||
|
(a folio).
|
||||||
|
|
||||||
|
### 4.4 Markers
|
||||||
|
|
||||||
|
A block moved into the database leaves `<qetz-db block="…"/>` where it was:
|
||||||
|
`properties`, `elementInformations`, `links_uuids` or `terminal_strips`.
|
||||||
|
Reading puts the block back where its marker is; a block without a marker
|
||||||
|
was never moved.
|
||||||
|
|
||||||
|
Only a block that can be rebuilt exactly is moved: entries of the expected
|
||||||
|
tag with plain text values. Terminal strips are moved only if their rows
|
||||||
|
rebuild the same XML. Anything else stays in the folio file or
|
||||||
|
`project.xml` unchanged.
|
||||||
|
|
||||||
|
## 5. Reading a file safely
|
||||||
|
|
||||||
|
A `.qetz` is untrusted. A reader:
|
||||||
|
|
||||||
|
- runs nothing from the database's schema (`PRAGMA trusted_schema = OFF`,
|
||||||
|
read-only) and refuses a database where any table of section 4.1 is not a
|
||||||
|
plain table (a view, say);
|
||||||
|
- refuses the file, with a message saying what is wrong, if the database
|
||||||
|
and the folio files disagree: an item whose row is missing or on another
|
||||||
|
folio, an item twice, a row no item has, or information, links or
|
||||||
|
properties of a symbol or folio that does not exist.
|
||||||
|
|
||||||
|
A damaged archive is caught by the zip format's own checksums.
|
||||||
@@ -153,15 +153,23 @@ bool QetContainer::isZip(const QByteArray &content)
|
|||||||
/**
|
/**
|
||||||
@brief QetContainer::readDocument
|
@brief QetContainer::readDocument
|
||||||
The project document held in @p content: unzipped and joined for a
|
The project document held in @p content: unzipped and joined for a
|
||||||
.qetz, parsed for a .qet.
|
.qetz, parsed for a .qet. @p format, if any: the .qetz's format, 0
|
||||||
|
for a .qet.
|
||||||
*/
|
*/
|
||||||
bool QetContainer::readDocument(const QByteArray &content, QDomDocument *document, QString *error)
|
bool QetContainer::readDocument(const QByteArray &content, QDomDocument *document,
|
||||||
|
QString *error, Pictures *pictures, int *format)
|
||||||
{
|
{
|
||||||
|
if (format) *format = 0;
|
||||||
if (!isZip(content))
|
if (!isZip(content))
|
||||||
return parse(content, document, error);
|
return parse(content, document, error);
|
||||||
QList<QetZip::Entry> entries;
|
QList<QetZip::Entry> entries;
|
||||||
return QetZip::fromBytes(content, &entries, error)
|
return QetZip::fromBytes(content, &entries, error)
|
||||||
&& join(entries, document, error);
|
&& join(entries, document, error, pictures, format);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString QetContainer::pictureFile(const QDomElement &image)
|
||||||
|
{
|
||||||
|
return image.attribute(ImageFileAttribute);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -264,8 +272,13 @@ QList<QetZip::Entry> QetContainer::split(const QDomDocument &project, const QStr
|
|||||||
Refused, with @p error set: not a .qetz, made for a later QElectroTech
|
Refused, with @p error set: not a .qetz, made for a later QElectroTech
|
||||||
(the manifest's min-reader), or a part missing or unreadable. Entries
|
(the manifest's min-reader), or a part missing or unreadable. Entries
|
||||||
nothing names are ignored, so a later format may add some.
|
nothing names are ignored, so a later format may add some.
|
||||||
|
With @p pictures, the pictures stay out of the document: see the
|
||||||
|
namespace.
|
||||||
|
@p format, if any: the format the file says it is, above Format for a
|
||||||
|
later one this may read (its min-reader allows it)
|
||||||
*/
|
*/
|
||||||
bool QetContainer::join(const QList<QetZip::Entry> &entries, QDomDocument *project, QString *error)
|
bool QetContainer::join(const QList<QetZip::Entry> &entries, QDomDocument *project, QString *error,
|
||||||
|
Pictures *pictures, int *format)
|
||||||
{
|
{
|
||||||
QHash<QString, QByteArray> files;
|
QHash<QString, QByteArray> files;
|
||||||
for (const QetZip::Entry &entry : entries)
|
for (const QetZip::Entry &entry : entries)
|
||||||
@@ -286,6 +299,8 @@ bool QetContainer::join(const QList<QetZip::Entry> &entries, QDomDocument *proje
|
|||||||
.arg(Format));
|
.arg(Format));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (format)
|
||||||
|
*format = manifest.documentElement().attribute(QStringLiteral("format")).toInt();
|
||||||
|
|
||||||
if (!parse(files.value(QStringLiteral("project.xml")), project, error))
|
if (!parse(files.value(QStringLiteral("project.xml")), project, error))
|
||||||
return false;
|
return false;
|
||||||
@@ -310,7 +325,8 @@ bool QetContainer::join(const QList<QetZip::Entry> &entries, QDomDocument *proje
|
|||||||
part.parentNode().replaceChild(project->importNode(document.documentElement(), true), part);
|
part.parentNode().replaceChild(project->importNode(document.documentElement(), true), part);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Pictures back to base64, as the image's first child
|
//Pictures back to base64, as the image's first child, or into
|
||||||
|
//pictures
|
||||||
const QDomNodeList images = project->elementsByTagName(QStringLiteral("image"));
|
const QDomNodeList images = project->elementsByTagName(QStringLiteral("image"));
|
||||||
for (int i = 0 ; i < images.size() ; ++i)
|
for (int i = 0 ; i < images.size() ; ++i)
|
||||||
{
|
{
|
||||||
@@ -321,6 +337,10 @@ bool QetContainer::join(const QList<QetZip::Entry> &entries, QDomDocument *proje
|
|||||||
setError(error, QStringLiteral("picture \"%1\" is missing").arg(file));
|
setError(error, QStringLiteral("picture \"%1\" is missing").arg(file));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (pictures) {
|
||||||
|
pictures->insert(file, files.value(file));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
image.removeAttribute(ImageFileAttribute);
|
image.removeAttribute(ImageFileAttribute);
|
||||||
image.insertBefore(project->createTextNode(QString::fromLatin1(files.value(file).toBase64())),
|
image.insertBefore(project->createTextNode(QString::fromLatin1(files.value(file).toBase64())),
|
||||||
image.firstChild());
|
image.firstChild());
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
#include "qetzip.h"
|
#include "qetzip.h"
|
||||||
|
|
||||||
#include <QDomDocument>
|
#include <QDomDocument>
|
||||||
|
#include <QHash>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief The QetContainer namespace
|
@brief The QetContainer namespace
|
||||||
@@ -31,7 +32,7 @@
|
|||||||
manifest.xml format and oldest format allowed to read it
|
manifest.xml format and oldest format allowed to read it
|
||||||
project.xml the project, each part replaced by
|
project.xml the project, each part replaced by
|
||||||
<qetz-part file="..."/>
|
<qetz-part file="..."/>
|
||||||
project.sqlite the engineering data (format 2,
|
project.sqlite the engineering data (format 2 on,
|
||||||
QetContainerDb): title-block fields, symbol
|
QetContainerDb): title-block fields, symbol
|
||||||
information, links, wires
|
information, links, wires
|
||||||
folios/<uuid>.xml one per folio
|
folios/<uuid>.xml one per folio
|
||||||
@@ -41,26 +42,45 @@
|
|||||||
|
|
||||||
join(split(d)) is d. Nothing here knows what the parts mean: the
|
join(split(d)) is d. Nothing here knows what the parts mean: the
|
||||||
project is read and written by the same code as a .qet file.
|
project is read and written by the same code as a .qet file.
|
||||||
|
|
||||||
|
Given a Pictures map, join() leaves the pictures out of the document:
|
||||||
|
each <image> keeps the name of its file, pictureFile(), and the bytes
|
||||||
|
go in the map. Turning them back into base64 text costs seconds on a
|
||||||
|
project with large pictures, and the folio would only decode it again.
|
||||||
|
|
||||||
|
The format is written down in QETZ-FORMAT.md, beside this file.
|
||||||
*/
|
*/
|
||||||
namespace QetContainer
|
namespace QetContainer
|
||||||
{
|
{
|
||||||
extern const char MimeType[];
|
extern const char MimeType[];
|
||||||
|
/// Picture bytes by file name, see join()
|
||||||
|
using Pictures = QHash<QString, QByteArray>;
|
||||||
/// The format written; files of a later format are refused by join()
|
/// The format written; files of a later format are refused by join()
|
||||||
/// unless their min-reader allows this one.
|
/// unless their min-reader allows this one.
|
||||||
constexpr int Format = 2;
|
constexpr int Format = 3;
|
||||||
|
/// 3: the database's rows keyed by the items' uuids, true/false and
|
||||||
|
/// show flags as 1/0, its PRAGMA user_version the format, the symbols' type left
|
||||||
|
/// in the folio files, no column for where a block sat. Format 2 is
|
||||||
|
/// still read; a later format whose min-reader allows this one is
|
||||||
|
/// read too, and said so through join()'s @p format.
|
||||||
|
|
||||||
QList<QetZip::Entry> split(const QDomDocument &project,
|
QList<QetZip::Entry> split(const QDomDocument &project,
|
||||||
const QString &writer = QString(),
|
const QString &writer = QString(),
|
||||||
QString *error = nullptr);
|
QString *error = nullptr);
|
||||||
bool join(const QList<QetZip::Entry> &entries, QDomDocument *project,
|
bool join(const QList<QetZip::Entry> &entries, QDomDocument *project,
|
||||||
QString *error = nullptr);
|
QString *error = nullptr, Pictures *pictures = nullptr,
|
||||||
|
int *format = nullptr);
|
||||||
|
/// The file join() left the picture of @p image in, or an empty string
|
||||||
|
QString pictureFile(const QDomElement &image);
|
||||||
|
|
||||||
/// Parse @p xml as a .qet is read (QETProject::openFile)
|
/// Parse @p xml as a .qet is read (QETProject::openFile)
|
||||||
bool parse(const QByteArray &xml, QDomDocument *document, QString *error = nullptr);
|
bool parse(const QByteArray &xml, QDomDocument *document, QString *error = nullptr);
|
||||||
/// true if @p content is a zip, so a .qetz rather than a .qet
|
/// true if @p content is a zip, so a .qetz rather than a .qet
|
||||||
bool isZip(const QByteArray &content);
|
bool isZip(const QByteArray &content);
|
||||||
/// The project document in @p content, a .qet or a .qetz
|
/// The project document in @p content, a .qet or a .qetz
|
||||||
bool readDocument(const QByteArray &content, QDomDocument *document, QString *error = nullptr);
|
bool readDocument(const QByteArray &content, QDomDocument *document,
|
||||||
|
QString *error = nullptr, Pictures *pictures = nullptr,
|
||||||
|
int *format = nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // QETCONTAINER_H
|
#endif // QETCONTAINER_H
|
||||||
|
|||||||
@@ -17,11 +17,15 @@
|
|||||||
*/
|
*/
|
||||||
#include "qetcontainerdb.h"
|
#include "qetcontainerdb.h"
|
||||||
|
|
||||||
|
#include "qetcontainer.h"
|
||||||
|
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
|
#include <QTextStream>
|
||||||
|
#include <QUuid>
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
|
||||||
#include <sqlite3.h>
|
#include <sqlite3.h>
|
||||||
@@ -54,43 +58,81 @@ bool replaceMarker(QDomElement parent, const QDomElement &block)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//true if @p parent holds the marker of a block @p tag moved out: where
|
||||||
|
//the block goes back, and that it was there at all
|
||||||
|
bool hasMarker(const QDomElement &parent, const QString &tag)
|
||||||
|
{
|
||||||
|
for (QDomElement m = parent.firstChildElement(MarkerTag) ; !m.isNull() ; m = m.nextSiblingElement(MarkerTag))
|
||||||
|
if (m.attribute(MarkerBlock) == tag) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const QStringList FolioData = {
|
const QStringList FolioData = {
|
||||||
QStringLiteral("title"), QStringLiteral("author"), QStringLiteral("date"),
|
QStringLiteral("title"), QStringLiteral("author"), QStringLiteral("date"),
|
||||||
QStringLiteral("folio"), QStringLiteral("indexrev"), QStringLiteral("filename"),
|
QStringLiteral("folio"), QStringLiteral("indexrev"), QStringLiteral("filename"),
|
||||||
QStringLiteral("locmach"), QStringLiteral("plant"), QStringLiteral("auto_page_num")};
|
QStringLiteral("locmach"), QStringLiteral("plant"), QStringLiteral("auto_page_num")};
|
||||||
|
//From format 3 on an item's uuid is its row's key, not a column of its
|
||||||
|
//own: it stays in the folio file, where it says which row is the item's.
|
||||||
|
//So does a symbol's type, the symbol it is: a folio file read alone
|
||||||
|
//still says what is drawn where
|
||||||
const QStringList ElementData = {
|
const QStringList ElementData = {
|
||||||
QStringLiteral("uuid"), QStringLiteral("type"), QStringLiteral("prefix"),
|
QStringLiteral("prefix"), QStringLiteral("freezeLabel")};
|
||||||
QStringLiteral("freezeLabel")};
|
//Format 2 also moved the type
|
||||||
|
const QStringList ElementDataV2 = {
|
||||||
|
QStringLiteral("uuid"), QStringLiteral("type"), QStringLiteral("prefix"), QStringLiteral("freezeLabel")};
|
||||||
const QStringList ConductorData = {
|
const QStringList ConductorData = {
|
||||||
QStringLiteral("uuid"), QStringLiteral("element1"), QStringLiteral("terminal1"),
|
QStringLiteral("element1"), QStringLiteral("terminal1"),
|
||||||
QStringLiteral("element2"), QStringLiteral("terminal2"), QStringLiteral("num"),
|
QStringLiteral("element2"), QStringLiteral("terminal2"), QStringLiteral("num"),
|
||||||
QStringLiteral("formula"), QStringLiteral("function"), QStringLiteral("tension_protocol"),
|
QStringLiteral("formula"), QStringLiteral("function"), QStringLiteral("tension_protocol"),
|
||||||
QStringLiteral("conductor_color"), QStringLiteral("conductor_section"),
|
QStringLiteral("conductor_color"), QStringLiteral("conductor_section"),
|
||||||
QStringLiteral("cable"), QStringLiteral("bus"), QStringLiteral("freezeLabel")};
|
QStringLiteral("cable"), QStringLiteral("bus"), QStringLiteral("freezeLabel")};
|
||||||
|
|
||||||
|
//Attributes held as 0 or 1 (format 3 on). A value QElectroTech does not
|
||||||
|
//write, "yes" say, stays in the folio file as it was.
|
||||||
|
const QStringList BooleanData = {QStringLiteral("freezeLabel")};
|
||||||
|
|
||||||
QString columns(const QStringList &names)
|
QString columns(const QStringList &names)
|
||||||
{
|
{
|
||||||
QStringList quoted;
|
QStringList quoted;
|
||||||
for (const QString &n : names) quoted << QStringLiteral("\"%1\" TEXT").arg(n);
|
for (const QString &n : names)
|
||||||
|
quoted << QStringLiteral("\"%1\" %2").arg(n, BooleanData.contains(n) ? QStringLiteral("INTEGER")
|
||||||
|
: QStringLiteral("TEXT"));
|
||||||
return quoted.join(QStringLiteral(", "));
|
return quoted.join(QStringLiteral(", "));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//@p text as attribute @p name is stored, or invalid if it cannot be
|
||||||
|
QVariant stored(const QString &name, const QString &text)
|
||||||
|
{
|
||||||
|
if (!BooleanData.contains(name)) return text;
|
||||||
|
if (text == QLatin1String("true")) return 1;
|
||||||
|
if (text == QLatin1String("false")) return 0;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
//A stored value as attribute @p name writes it
|
||||||
|
QString attributeText(const QString &name, const QVariant &value)
|
||||||
|
{
|
||||||
|
if (BooleanData.contains(name) && value.typeId() == QMetaType::Int)
|
||||||
|
return value.toInt() ? QStringLiteral("true") : QStringLiteral("false");
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
QString schema()
|
QString schema()
|
||||||
{
|
{
|
||||||
return QStringLiteral(
|
return QStringLiteral(
|
||||||
"CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT);"
|
"CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT);"
|
||||||
"CREATE TABLE project_property (ord INTEGER PRIMARY KEY, name TEXT, value TEXT, attrs TEXT);"
|
"CREATE TABLE project_property (ord INTEGER PRIMARY KEY, name TEXT, value TEXT, show INTEGER, attrs TEXT);"
|
||||||
"CREATE TABLE folio (id TEXT PRIMARY KEY, pos INTEGER NOT NULL UNIQUE, %1,"
|
"CREATE TABLE folio (uuid TEXT PRIMARY KEY, pos INTEGER NOT NULL UNIQUE, %1,"
|
||||||
" props_at INTEGER, props_attrs TEXT);"
|
" props_attrs TEXT);"
|
||||||
"CREATE TABLE folio_property (folio_id TEXT NOT NULL REFERENCES folio, ord INTEGER NOT NULL,"
|
"CREATE TABLE folio_property (folio_uuid TEXT NOT NULL REFERENCES folio, ord INTEGER NOT NULL,"
|
||||||
" name TEXT, value TEXT, attrs TEXT, PRIMARY KEY (folio_id, ord));"
|
" name TEXT, value TEXT, show INTEGER, attrs TEXT, PRIMARY KEY (folio_uuid, ord));"
|
||||||
"CREATE TABLE element (key TEXT PRIMARY KEY, folio_id TEXT NOT NULL REFERENCES folio, %2,"
|
"CREATE TABLE element (uuid TEXT PRIMARY KEY, folio_uuid TEXT NOT NULL REFERENCES folio, %2,"
|
||||||
" info_at INTEGER, info_attrs TEXT, links_at INTEGER, links_attrs TEXT);"
|
" info_attrs TEXT, links_attrs TEXT);"
|
||||||
"CREATE TABLE element_info (element_key TEXT NOT NULL REFERENCES element, ord INTEGER NOT NULL,"
|
"CREATE TABLE element_info (element_uuid TEXT NOT NULL REFERENCES element, ord INTEGER NOT NULL,"
|
||||||
" name TEXT, value TEXT, attrs TEXT, PRIMARY KEY (element_key, ord));"
|
" name TEXT, value TEXT, show INTEGER, attrs TEXT, PRIMARY KEY (element_uuid, ord));"
|
||||||
"CREATE TABLE link (element_key TEXT NOT NULL REFERENCES element, ord INTEGER NOT NULL,"
|
"CREATE TABLE link (element_uuid TEXT NOT NULL REFERENCES element, ord INTEGER NOT NULL,"
|
||||||
" linked_uuid TEXT, attrs TEXT, PRIMARY KEY (element_key, ord));"
|
" linked_uuid TEXT, attrs TEXT, PRIMARY KEY (element_uuid, ord));"
|
||||||
"CREATE TABLE conductor (key TEXT PRIMARY KEY, folio_id TEXT NOT NULL REFERENCES folio, %3);"
|
"CREATE TABLE conductor (uuid TEXT PRIMARY KEY, folio_uuid TEXT NOT NULL REFERENCES folio, %3);"
|
||||||
"CREATE TABLE terminal_strip (uuid TEXT PRIMARY KEY, ord INTEGER NOT NULL UNIQUE,"
|
"CREATE TABLE terminal_strip (uuid TEXT PRIMARY KEY, ord INTEGER NOT NULL UNIQUE,"
|
||||||
" installation TEXT, location TEXT, name TEXT, comment TEXT, description TEXT);"
|
" installation TEXT, location TEXT, name TEXT, comment TEXT, description TEXT);"
|
||||||
"CREATE TABLE strip_terminal (strip_uuid TEXT NOT NULL REFERENCES terminal_strip,"
|
"CREATE TABLE strip_terminal (strip_uuid TEXT NOT NULL REFERENCES terminal_strip,"
|
||||||
@@ -165,15 +207,16 @@ QString placeholders(int n)
|
|||||||
return q.join(QLatin1Char(','));
|
return q.join(QLatin1Char(','));
|
||||||
}
|
}
|
||||||
|
|
||||||
//The attributes of @p e but @p skip, and its tag, as JSON
|
//The attributes of @p e but @p skip as JSON, and its tag if it is not
|
||||||
QVariant attributesJson(const QDomElement &e, const QString &skip = QString(), bool with_tag = false)
|
//@p usual_tag
|
||||||
|
QVariant attributesJson(const QDomElement &e, const QStringList &skip, const QString &usual_tag)
|
||||||
{
|
{
|
||||||
QJsonObject o;
|
QJsonObject o;
|
||||||
if (with_tag) o.insert(QStringLiteral("tag"), e.tagName());
|
if (e.tagName() != usual_tag) o.insert(QStringLiteral("tag"), e.tagName());
|
||||||
const QDomNamedNodeMap map = e.attributes();
|
const QDomNamedNodeMap map = e.attributes();
|
||||||
for (int i = 0 ; i < map.count() ; ++i) {
|
for (int i = 0 ; i < map.count() ; ++i) {
|
||||||
const QDomAttr a = map.item(i).toAttr();
|
const QDomAttr a = map.item(i).toAttr();
|
||||||
if (a.name() != skip) o.insert(a.name(), a.value());
|
if (!skip.contains(a.name())) o.insert(a.name(), a.value());
|
||||||
}
|
}
|
||||||
if (o.isEmpty()) return QVariant();
|
if (o.isEmpty()) return QVariant();
|
||||||
return QString::fromUtf8(QJsonDocument(o).toJson(QJsonDocument::Compact));
|
return QString::fromUtf8(QJsonDocument(o).toJson(QJsonDocument::Compact));
|
||||||
@@ -257,21 +300,15 @@ bool listBlock(const QDomElement &block, const QString &entry)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
int childIndex(const QDomNode &child)
|
//The attributes @p names of @p e, taken out of it; one that cannot be
|
||||||
{
|
//stored stays, its value null
|
||||||
int i = 0;
|
|
||||||
for (QDomNode n = child.parentNode().firstChild() ; !n.isNull() ; n = n.nextSibling(), ++i)
|
|
||||||
if (n == child) return i;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
QVariantList take(QDomElement e, const QStringList &names)
|
QVariantList take(QDomElement e, const QStringList &names)
|
||||||
{
|
{
|
||||||
QVariantList values;
|
QVariantList values;
|
||||||
for (const QString &n : names) {
|
for (const QString &n : names) {
|
||||||
values << (e.hasAttribute(n) ? QVariant(e.attribute(n)) : QVariant());
|
const QVariant v = e.hasAttribute(n) ? stored(n, e.attribute(n)) : QVariant();
|
||||||
e.removeAttribute(n);
|
values << (v.isValid() ? v : QVariant());
|
||||||
|
if (v.isValid()) e.removeAttribute(n);
|
||||||
}
|
}
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
@@ -279,7 +316,8 @@ QVariantList take(QDomElement e, const QStringList &names)
|
|||||||
void put(QDomElement e, const QStringList &names, const QVariantList &values, int from)
|
void put(QDomElement e, const QStringList &names, const QVariantList &values, int from)
|
||||||
{
|
{
|
||||||
for (int i = 0 ; i < names.size() ; ++i)
|
for (int i = 0 ; i < names.size() ; ++i)
|
||||||
if (!values.at(from + i).isNull()) e.setAttribute(names.at(i), values.at(from + i).toString());
|
if (!values.at(from + i).isNull())
|
||||||
|
e.setAttribute(names.at(i), attributeText(names.at(i), values.at(from + i)));
|
||||||
}
|
}
|
||||||
|
|
||||||
//Same tags, attributes, text and child order
|
//Same tags, attributes, text and child order
|
||||||
@@ -417,18 +455,55 @@ QList<QDomElement> children(const QDomElement &parent, const QString &tag)
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
//uuid when present and unique among @p items, else prefix + count
|
//A list entry -- a <property>, an <elementInformation> -- as its row:
|
||||||
QStringList keys(const QList<QDomElement> &items, const QString &prefix)
|
//its name, value, show flag (0 or 1, else left in the JSON) and any
|
||||||
|
//other attribute as JSON, its tag too if it is not @p usual_tag
|
||||||
|
QVariantList entryRow(const QDomElement &e, const QString &usual_tag)
|
||||||
{
|
{
|
||||||
|
QVariant value;
|
||||||
|
simpleValue(e, &value);
|
||||||
|
const QString show = e.attribute(QStringLiteral("show"));
|
||||||
|
const bool show_stored = show == QLatin1String("0") || show == QLatin1String("1");
|
||||||
|
QStringList kept{QStringLiteral("name")};
|
||||||
|
if (show_stored) kept << QStringLiteral("show");
|
||||||
|
return {e.hasAttribute(QStringLiteral("name")) ? QVariant(e.attribute(QStringLiteral("name"))) : QVariant(),
|
||||||
|
value, show_stored ? QVariant(show.toInt()) : QVariant(), attributesJson(e, kept, usual_tag)};
|
||||||
|
}
|
||||||
|
|
||||||
|
//The key of each of @p items: its uuid, when it has one no other of
|
||||||
|
//@p items has, so the uuid the folio file already holds says which row
|
||||||
|
//is the item's (@p own true). Else (F100: symbols copied with their
|
||||||
|
//uuid) a uuid made from the item itself -- the same on every save,
|
||||||
|
//never from where the item is -- kept in the folio file as qetz-key.
|
||||||
|
QStringList keys(const QList<QDomElement> &items, QList<bool> *own)
|
||||||
|
{
|
||||||
|
static const QUuid key_namespace(QStringLiteral("{3f0c6c1e-5b8e-4b8a-9d55-6b2f5f0f9a41}"));
|
||||||
QHash<QString, int> seen;
|
QHash<QString, int> seen;
|
||||||
for (const QDomElement &e : items)
|
for (const QDomElement &e : items)
|
||||||
if (e.hasAttribute(QStringLiteral("uuid"))) ++seen[e.attribute(QStringLiteral("uuid"))];
|
if (!QUuid(e.attribute(QStringLiteral("uuid"))).isNull()) ++seen[e.attribute(QStringLiteral("uuid"))];
|
||||||
|
QSet<QString> taken(seen.keyBegin(), seen.keyEnd());
|
||||||
|
QHash<QString, int> twins;
|
||||||
QStringList out;
|
QStringList out;
|
||||||
int made_up = 0;
|
|
||||||
for (const QDomElement &e : items) {
|
for (const QDomElement &e : items) {
|
||||||
const QString uuid = e.attribute(QStringLiteral("uuid"));
|
const QString uuid = e.attribute(QStringLiteral("uuid"));
|
||||||
out << ((!uuid.isEmpty() && seen.value(uuid) == 1)
|
if (seen.value(uuid) == 1) {
|
||||||
? uuid : prefix + QString::number(++made_up));
|
out << uuid;
|
||||||
|
*own << true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QString text;
|
||||||
|
QTextStream stream(&text);
|
||||||
|
e.save(stream, -1);
|
||||||
|
stream.flush();
|
||||||
|
//Items alike to the byte: how many came before tells them apart
|
||||||
|
const int twin = twins[text]++;
|
||||||
|
QString key;
|
||||||
|
for (int salt = 0 ; key.isEmpty() || taken.contains(key) ; ++salt)
|
||||||
|
key = QUuid::createUuidV5(key_namespace, QStringLiteral("%1\n%2\n%3").arg(text).arg(twin).arg(salt))
|
||||||
|
.toString();
|
||||||
|
taken.insert(key);
|
||||||
|
out << key;
|
||||||
|
*own << false;
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -450,43 +525,31 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error)
|
|||||||
db.exec(QStringLiteral("BEGIN"));
|
db.exec(QStringLiteral("BEGIN"));
|
||||||
|
|
||||||
Statement meta(db.m_db, QStringLiteral("INSERT INTO meta VALUES (?,?)"));
|
Statement meta(db.m_db, QStringLiteral("INSERT INTO meta VALUES (?,?)"));
|
||||||
Statement project_property(db.m_db, QStringLiteral("INSERT INTO project_property VALUES (?,?,?,?)"));
|
Statement project_property(db.m_db, QStringLiteral("INSERT INTO project_property VALUES (?,?,?,?,?)"));
|
||||||
Statement folio(db.m_db, QStringLiteral("INSERT INTO folio VALUES (?,?,%1,?,?)").arg(placeholders(FolioData.size())));
|
Statement folio(db.m_db, QStringLiteral("INSERT INTO folio VALUES (?,?,%1,?)").arg(placeholders(FolioData.size())));
|
||||||
Statement folio_property(db.m_db, QStringLiteral("INSERT INTO folio_property VALUES (?,?,?,?,?)"));
|
Statement folio_property(db.m_db, QStringLiteral("INSERT INTO folio_property VALUES (?,?,?,?,?,?)"));
|
||||||
Statement element(db.m_db, QStringLiteral("INSERT INTO element VALUES (?,?,%1,?,?,?,?)").arg(placeholders(ElementData.size())));
|
Statement element(db.m_db, QStringLiteral("INSERT INTO element VALUES (?,?,%1,?,?)").arg(placeholders(ElementData.size())));
|
||||||
Statement element_info(db.m_db, QStringLiteral("INSERT INTO element_info VALUES (?,?,?,?,?)"));
|
Statement element_info(db.m_db, QStringLiteral("INSERT INTO element_info VALUES (?,?,?,?,?,?)"));
|
||||||
Statement link(db.m_db, QStringLiteral("INSERT INTO link VALUES (?,?,?,?)"));
|
Statement link(db.m_db, QStringLiteral("INSERT INTO link VALUES (?,?,?,?)"));
|
||||||
Statement conductor(db.m_db, QStringLiteral("INSERT INTO conductor VALUES (?,?,%1)").arg(placeholders(ConductorData.size())));
|
Statement conductor(db.m_db, QStringLiteral("INSERT INTO conductor VALUES (?,?,%1)").arg(placeholders(ConductorData.size())));
|
||||||
bool ok = meta.ok() && project_property.ok() && folio.ok() && folio_property.ok()
|
bool ok = meta.ok() && project_property.ok() && folio.ok() && folio_property.ok()
|
||||||
&& element.ok() && element_info.ok() && link.ok() && conductor.ok();
|
&& element.ok() && element_info.ok() && link.ok() && conductor.ok();
|
||||||
|
|
||||||
ok = ok && meta.run({QStringLiteral("format"), QStringLiteral("2")});
|
ok = ok && meta.run({QStringLiteral("format"), QString::number(QetContainer::Format)})
|
||||||
|
&& db.exec(QStringLiteral("PRAGMA user_version = %1").arg(QetContainer::Format));
|
||||||
|
|
||||||
//<properties> of a block, as rows of @p rows (with @p prefix values)
|
//<properties> of a block, as rows of @p rows (with @p prefix values)
|
||||||
auto moveProperties = [&](QDomElement block, Statement &rows, const QVariantList &prefix) {
|
auto moveProperties = [&](QDomElement block, Statement &rows, const QVariantList &prefix) {
|
||||||
int ord = 0;
|
int ord = 0;
|
||||||
for (QDomElement p : children(block, QStringLiteral("property"))) {
|
for (QDomElement p : children(block, QStringLiteral("property")))
|
||||||
QVariant value;
|
ok = ok && rows.run(QVariantList(prefix) << ord++ << entryRow(p, QStringLiteral("property")));
|
||||||
simpleValue(p, &value);
|
|
||||||
ok = ok && rows.run(QVariantList(prefix) << ord++ << (p.hasAttribute(QStringLiteral("name"))
|
|
||||||
? QVariant(p.attribute(QStringLiteral("name"))) : QVariant())
|
|
||||||
<< value << attributesJson(p, QStringLiteral("name"), true));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
//Project properties
|
//Project properties
|
||||||
const QDomElement project_props = project.firstChildElement(QStringLiteral("properties"));
|
const QDomElement project_props = project.firstChildElement(QStringLiteral("properties"));
|
||||||
if (!project_props.isNull() && listBlock(project_props, QStringLiteral("property"))) {
|
if (!project_props.isNull() && listBlock(project_props, QStringLiteral("property"))) {
|
||||||
ok = ok && meta.run({QStringLiteral("props_at"), QString::number(childIndex(project_props))});
|
|
||||||
ok = ok && meta.run({QStringLiteral("props_attrs"), blockJson(project_props)});
|
ok = ok && meta.run({QStringLiteral("props_attrs"), blockJson(project_props)});
|
||||||
int ord = 0;
|
moveProperties(project_props, project_property, {});
|
||||||
for (QDomElement p : children(project_props, QStringLiteral("property"))) {
|
|
||||||
QVariant value;
|
|
||||||
simpleValue(p, &value);
|
|
||||||
ok = ok && project_property.run({ord++, p.hasAttribute(QStringLiteral("name"))
|
|
||||||
? QVariant(p.attribute(QStringLiteral("name"))) : QVariant(),
|
|
||||||
value, attributesJson(p, QStringLiteral("name"), true)});
|
|
||||||
}
|
|
||||||
leaveMarker(project_props);
|
leaveMarker(project_props);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,72 +561,69 @@ QByteArray QetContainerDb::extract(QDomElement project, QString *error)
|
|||||||
for (const QDomElement &list : children(d, QStringLiteral("conductors")))
|
for (const QDomElement &list : children(d, QStringLiteral("conductors")))
|
||||||
all_conductors += children(list, QStringLiteral("conductor"));
|
all_conductors += children(list, QStringLiteral("conductor"));
|
||||||
}
|
}
|
||||||
const QStringList element_keys = keys(all_elements, QStringLiteral("e"));
|
QList<bool> folio_own, element_own, conductor_own;
|
||||||
const QStringList conductor_keys = keys(all_conductors, QStringLiteral("c"));
|
const QStringList folio_keys = keys(diagrams, &folio_own);
|
||||||
|
const QStringList element_keys = keys(all_elements, &element_own);
|
||||||
|
const QStringList conductor_keys = keys(all_conductors, &conductor_own);
|
||||||
int e_index = 0, c_index = 0;
|
int e_index = 0, c_index = 0;
|
||||||
|
|
||||||
int pos = 0;
|
int pos = 0;
|
||||||
for (QDomElement d : diagrams)
|
for (QDomElement d : diagrams)
|
||||||
{
|
{
|
||||||
const QString id = QStringLiteral("f%1").arg(++pos);
|
const QString id = folio_keys.at(pos);
|
||||||
|
const bool own_id = folio_own.at(pos);
|
||||||
|
++pos;
|
||||||
QVariantList row{id, pos};
|
QVariantList row{id, pos};
|
||||||
row += take(d, FolioData);
|
row += take(d, FolioData);
|
||||||
QDomElement props = d.firstChildElement(QStringLiteral("properties"));
|
QDomElement props = d.firstChildElement(QStringLiteral("properties"));
|
||||||
const bool move_props = !props.isNull() && listBlock(props, QStringLiteral("property"));
|
const bool move_props = !props.isNull() && listBlock(props, QStringLiteral("property"));
|
||||||
row << (move_props ? QVariant(childIndex(props)) : QVariant())
|
row << (move_props ? blockJson(props) : QVariant());
|
||||||
<< (move_props ? blockJson(props) : QVariant());
|
|
||||||
ok = ok && folio.run(row);
|
ok = ok && folio.run(row);
|
||||||
if (move_props) {
|
if (move_props) {
|
||||||
moveProperties(props, folio_property, {id});
|
moveProperties(props, folio_property, {id});
|
||||||
leaveMarker(props);
|
leaveMarker(props);
|
||||||
}
|
}
|
||||||
d.setAttribute(FolioAttribute, id);
|
if (!own_id) d.setAttribute(FolioAttribute, id);
|
||||||
|
|
||||||
for (const QDomElement &list : children(d, QStringLiteral("elements"))) {
|
for (const QDomElement &list : children(d, QStringLiteral("elements"))) {
|
||||||
for (QDomElement e : children(list, QStringLiteral("element"))) {
|
for (QDomElement e : children(list, QStringLiteral("element"))) {
|
||||||
const QString key = element_keys.at(e_index++);
|
const QString key = element_keys.at(e_index);
|
||||||
|
const bool own_key = element_own.at(e_index++);
|
||||||
QVariantList erow{key, id};
|
QVariantList erow{key, id};
|
||||||
erow += take(e, ElementData);
|
erow += take(e, ElementData);
|
||||||
QDomElement info = e.firstChildElement(QStringLiteral("elementInformations"));
|
QDomElement info = e.firstChildElement(QStringLiteral("elementInformations"));
|
||||||
QDomElement links = e.firstChildElement(QStringLiteral("links_uuids"));
|
QDomElement links = e.firstChildElement(QStringLiteral("links_uuids"));
|
||||||
if (!info.isNull() && !listBlock(info, QStringLiteral("elementInformation"))) info = QDomElement();
|
if (!info.isNull() && !listBlock(info, QStringLiteral("elementInformation"))) info = QDomElement();
|
||||||
if (!links.isNull() && !listBlock(links, QStringLiteral("link_uuid"))) links = QDomElement();
|
if (!links.isNull() && !listBlock(links, QStringLiteral("link_uuid"))) links = QDomElement();
|
||||||
const int info_at = info.isNull() ? -1 : childIndex(info);
|
erow << (info.isNull() ? QVariant() : blockJson(info))
|
||||||
const int links_at = links.isNull() ? -1 : childIndex(links);
|
<< (links.isNull() ? QVariant() : blockJson(links));
|
||||||
erow << (info_at < 0 ? QVariant() : QVariant(info_at))
|
|
||||||
<< (info_at < 0 ? QVariant() : blockJson(info))
|
|
||||||
<< (links_at < 0 ? QVariant() : QVariant(links_at))
|
|
||||||
<< (links_at < 0 ? QVariant() : blockJson(links));
|
|
||||||
ok = ok && element.run(erow);
|
ok = ok && element.run(erow);
|
||||||
if (info_at >= 0) {
|
if (!info.isNull()) {
|
||||||
int ord = 0;
|
int ord = 0;
|
||||||
for (QDomElement i : children(info, QStringLiteral("elementInformation"))) {
|
for (QDomElement i : children(info, QStringLiteral("elementInformation")))
|
||||||
QVariant value;
|
ok = ok && element_info.run(QVariantList{key, ord++}
|
||||||
simpleValue(i, &value);
|
<< entryRow(i, QStringLiteral("elementInformation")));
|
||||||
ok = ok && element_info.run({key, ord++, i.hasAttribute(QStringLiteral("name"))
|
|
||||||
? QVariant(i.attribute(QStringLiteral("name"))) : QVariant(),
|
|
||||||
value, attributesJson(i, QStringLiteral("name"), true)});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (links_at >= 0) {
|
if (!links.isNull()) {
|
||||||
int ord = 0;
|
int ord = 0;
|
||||||
for (QDomElement l : children(links, QStringLiteral("link_uuid")))
|
for (QDomElement l : children(links, QStringLiteral("link_uuid")))
|
||||||
ok = ok && link.run({key, ord++, l.hasAttribute(QStringLiteral("uuid"))
|
ok = ok && link.run({key, ord++, l.hasAttribute(QStringLiteral("uuid"))
|
||||||
? QVariant(l.attribute(QStringLiteral("uuid"))) : QVariant(),
|
? QVariant(l.attribute(QStringLiteral("uuid"))) : QVariant(),
|
||||||
attributesJson(l, QStringLiteral("uuid"), true)});
|
attributesJson(l, {QStringLiteral("uuid")}, QStringLiteral("link_uuid"))});
|
||||||
}
|
}
|
||||||
if (info_at >= 0) leaveMarker(info);
|
if (!info.isNull()) leaveMarker(info);
|
||||||
if (links_at >= 0) leaveMarker(links);
|
if (!links.isNull()) leaveMarker(links);
|
||||||
e.setAttribute(KeyAttribute, key);
|
if (!own_key) e.setAttribute(KeyAttribute, key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const QDomElement &list : children(d, QStringLiteral("conductors"))) {
|
for (const QDomElement &list : children(d, QStringLiteral("conductors"))) {
|
||||||
for (QDomElement c : children(list, QStringLiteral("conductor"))) {
|
for (QDomElement c : children(list, QStringLiteral("conductor"))) {
|
||||||
const QString key = conductor_keys.at(c_index++);
|
const QString key = conductor_keys.at(c_index);
|
||||||
|
const bool own_key = conductor_own.at(c_index++);
|
||||||
QVariantList crow{key, id};
|
QVariantList crow{key, id};
|
||||||
crow += take(c, ConductorData);
|
crow += take(c, ConductorData);
|
||||||
ok = ok && conductor.run(crow);
|
ok = ok && conductor.run(crow);
|
||||||
c.setAttribute(KeyAttribute, key);
|
if (!own_key) c.setAttribute(KeyAttribute, key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -637,10 +697,11 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto entry = [&document](const QVariant &name_attr, const QString &name, const QVariant &value,
|
auto entry = [&document](const QVariant &name_attr, const QString &name, const QVariant &value,
|
||||||
const QVariant &attrs, const QString &default_tag) {
|
const QVariant &attrs, const QString &default_tag, const QVariant &show = QVariant()) {
|
||||||
const QJsonObject o = QJsonDocument::fromJson(attrs.toString().toUtf8()).object();
|
const QJsonObject o = QJsonDocument::fromJson(attrs.toString().toUtf8()).object();
|
||||||
QDomElement e = document.createElement(o.value(QStringLiteral("tag")).toString(default_tag));
|
QDomElement e = document.createElement(o.value(QStringLiteral("tag")).toString(default_tag));
|
||||||
if (!name_attr.isNull()) e.setAttribute(name, name_attr.toString());
|
if (!name_attr.isNull()) e.setAttribute(name, name_attr.toString());
|
||||||
|
if (!show.isNull()) e.setAttribute(QStringLiteral("show"), show.toString());
|
||||||
setAttributesJson(e, attrs);
|
setAttributesJson(e, attrs);
|
||||||
if (!value.isNull()) e.appendChild(document.createTextNode(value.toString()));
|
if (!value.isNull()) e.appendChild(document.createTextNode(value.toString()));
|
||||||
return e;
|
return e;
|
||||||
@@ -652,13 +713,47 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS
|
|||||||
Statement s(db.m_db, QStringLiteral("SELECT key, value FROM meta"));
|
Statement s(db.m_db, QStringLiteral("SELECT key, value FROM meta"));
|
||||||
while (s.next()) meta.insert(s.value(0).toString(), s.value(1));
|
while (s.next()) meta.insert(s.value(0).toString(), s.value(1));
|
||||||
}
|
}
|
||||||
if (meta.contains(QStringLiteral("props_at"))) {
|
//Format 2 (QElectroTech 0.200.1 development builds) keyed its rows
|
||||||
|
//apart from the items' uuids: "f1", "e1", "c1" where an item had
|
||||||
|
//none of its own, and every item's key in the folio file
|
||||||
|
const int format = meta.value(QStringLiteral("format")).toString().toInt();
|
||||||
|
//A later format is read when the manifest's min-reader allows it
|
||||||
|
//(QetContainer::join()): its writer kept what this reads
|
||||||
|
if (format < 2) {
|
||||||
|
setError(error, QStringLiteral("the project database is of format %1, this reads format 2 on")
|
||||||
|
.arg(meta.value(QStringLiteral("format")).toString()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const bool v2 = format == 2;
|
||||||
|
const QString folio_key = v2 ? QStringLiteral("id") : QStringLiteral("uuid");
|
||||||
|
const QString folio_ref = v2 ? QStringLiteral("folio_id") : QStringLiteral("folio_uuid");
|
||||||
|
const QString item_key = v2 ? QStringLiteral("key") : QStringLiteral("uuid");
|
||||||
|
const QString element_ref = v2 ? QStringLiteral("element_key") : QStringLiteral("element_uuid");
|
||||||
|
const QString show_column = v2 ? QStringLiteral("NULL") : QStringLiteral("show");
|
||||||
|
const QStringList element_data = v2 ? ElementDataV2 : ElementData;
|
||||||
|
const QStringList conductor_data = v2 ? QStringList{QStringLiteral("uuid")} + ConductorData : ConductorData;
|
||||||
|
auto quoted = [](const QStringList &names) {
|
||||||
|
return QStringLiteral("\"") + names.join(QStringLiteral("\", \"")) + QStringLiteral("\"");
|
||||||
|
};
|
||||||
|
//An item's key: the one it was given, else (format 3) its uuid
|
||||||
|
auto keyOf = [v2](QDomElement e, const QString &attribute) {
|
||||||
|
if (e.hasAttribute(attribute)) {
|
||||||
|
const QString key = e.attribute(attribute);
|
||||||
|
e.removeAttribute(attribute);
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
return v2 ? QString() : e.attribute(QStringLiteral("uuid"));
|
||||||
|
};
|
||||||
|
//A moved block goes back where its marker is: the marker says it
|
||||||
|
//was there
|
||||||
|
if (hasMarker(project, QStringLiteral("properties"))) {
|
||||||
QDomElement props = document.createElement(QStringLiteral("properties"));
|
QDomElement props = document.createElement(QStringLiteral("properties"));
|
||||||
setAttributesJson(props, meta.value(QStringLiteral("props_attrs")));
|
setAttributesJson(props, meta.value(QStringLiteral("props_attrs")));
|
||||||
Statement s(db.m_db, QStringLiteral("SELECT name, value, attrs FROM project_property ORDER BY ord"));
|
Statement s(db.m_db, QStringLiteral("SELECT name, value, attrs, %1 FROM project_property ORDER BY ord")
|
||||||
|
.arg(show_column));
|
||||||
while (s.next())
|
while (s.next())
|
||||||
props.appendChild(entry(s.value(0), QStringLiteral("name"), s.value(1), s.value(2),
|
props.appendChild(entry(s.value(0), QStringLiteral("name"), s.value(1), s.value(2),
|
||||||
QStringLiteral("property")));
|
QStringLiteral("property"), s.value(3)));
|
||||||
restoreWhitespace(props, meta.value(QStringLiteral("props_attrs")));
|
restoreWhitespace(props, meta.value(QStringLiteral("props_attrs")));
|
||||||
if (!replaceMarker(project, props)) {
|
if (!replaceMarker(project, props)) {
|
||||||
setError(error, QStringLiteral("the project's properties have no place"));
|
setError(error, QStringLiteral("the project's properties have no place"));
|
||||||
@@ -686,27 +781,39 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Every row must be an item's, once, on the folio the item is on:
|
||||||
|
//nothing enforces it in the file, which is untrusted
|
||||||
|
QSet<QString> folios_used, elements_used, conductors_used;
|
||||||
|
auto use = [error](QSet<QString> &used, const QString &key, const QString &what) {
|
||||||
|
if (used.contains(key)) {
|
||||||
|
setError(error, QStringLiteral("%1 %2 is in the folios twice").arg(what, key));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
used.insert(key);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
for (QDomElement d : children(project, QStringLiteral("diagram")))
|
for (QDomElement d : children(project, QStringLiteral("diagram")))
|
||||||
{
|
{
|
||||||
const QString id = d.attribute(FolioAttribute);
|
const QString id = keyOf(d, FolioAttribute);
|
||||||
if (id.isEmpty()) continue;
|
if (id.isEmpty()) continue;
|
||||||
d.removeAttribute(FolioAttribute);
|
if (!use(folios_used, id, QStringLiteral("folio"))) return false;
|
||||||
Statement f(db.m_db, QStringLiteral("SELECT %1, props_at, props_attrs FROM folio WHERE id = ?")
|
Statement f(db.m_db, QStringLiteral("SELECT %1, props_attrs FROM folio WHERE %2 = ?")
|
||||||
.arg(QStringLiteral("\"") + FolioData.join(QStringLiteral("\", \"")) + QStringLiteral("\"")));
|
.arg(quoted(FolioData), folio_key));
|
||||||
f.bind({id});
|
f.bind({id});
|
||||||
if (!f.next()) { setError(error, QStringLiteral("folio %1 is not in the database").arg(id)); return false; }
|
if (!f.next()) { setError(error, QStringLiteral("folio %1 is not in the database").arg(id)); return false; }
|
||||||
QVariantList values;
|
QVariantList values;
|
||||||
for (int i = 0 ; i < FolioData.size() + 2 ; ++i) values << f.value(i);
|
for (int i = 0 ; i < FolioData.size() + 1 ; ++i) values << f.value(i);
|
||||||
put(d, FolioData, values, 0);
|
put(d, FolioData, values, 0);
|
||||||
if (!values.at(FolioData.size()).isNull()) {
|
if (hasMarker(d, QStringLiteral("properties"))) {
|
||||||
QDomElement props = document.createElement(QStringLiteral("properties"));
|
QDomElement props = document.createElement(QStringLiteral("properties"));
|
||||||
setAttributesJson(props, values.at(FolioData.size() + 1));
|
setAttributesJson(props, values.at(FolioData.size()));
|
||||||
Statement p(db.m_db, QStringLiteral("SELECT name, value, attrs FROM folio_property WHERE folio_id = ? ORDER BY ord"));
|
Statement p(db.m_db, QStringLiteral("SELECT name, value, attrs, %1 FROM folio_property WHERE %2 = ? ORDER BY ord")
|
||||||
|
.arg(show_column, folio_ref));
|
||||||
p.bind({id});
|
p.bind({id});
|
||||||
while (p.next())
|
while (p.next())
|
||||||
props.appendChild(entry(p.value(0), QStringLiteral("name"), p.value(1), p.value(2),
|
props.appendChild(entry(p.value(0), QStringLiteral("name"), p.value(1), p.value(2),
|
||||||
QStringLiteral("property")));
|
QStringLiteral("property"), p.value(3)));
|
||||||
restoreWhitespace(props, values.at(FolioData.size() + 1));
|
restoreWhitespace(props, values.at(FolioData.size()));
|
||||||
if (!replaceMarker(d, props)) {
|
if (!replaceMarker(d, props)) {
|
||||||
setError(error, QStringLiteral("folio %1's properties have no place").arg(id));
|
setError(error, QStringLiteral("folio %1's properties have no place").arg(id));
|
||||||
return false;
|
return false;
|
||||||
@@ -715,37 +822,42 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS
|
|||||||
|
|
||||||
for (const QDomElement &list : children(d, QStringLiteral("elements"))) {
|
for (const QDomElement &list : children(d, QStringLiteral("elements"))) {
|
||||||
for (QDomElement e : children(list, QStringLiteral("element"))) {
|
for (QDomElement e : children(list, QStringLiteral("element"))) {
|
||||||
if (!e.hasAttribute(KeyAttribute)) continue;
|
const QString key = keyOf(e, KeyAttribute);
|
||||||
const QString key = e.attribute(KeyAttribute);
|
if (key.isEmpty()) continue;
|
||||||
e.removeAttribute(KeyAttribute);
|
if (!use(elements_used, key, QStringLiteral("symbol"))) return false;
|
||||||
Statement s(db.m_db, QStringLiteral("SELECT %1, info_at, info_attrs, links_at, links_attrs FROM element WHERE key = ?")
|
Statement s(db.m_db, QStringLiteral("SELECT %1, info_attrs, links_attrs, %2 FROM element WHERE %3 = ?")
|
||||||
.arg(QStringLiteral("\"") + ElementData.join(QStringLiteral("\", \"")) + QStringLiteral("\"")));
|
.arg(quoted(element_data), folio_ref, item_key));
|
||||||
s.bind({key});
|
s.bind({key});
|
||||||
if (!s.next()) { setError(error, QStringLiteral("symbol %1 is not in the database").arg(key)); return false; }
|
if (!s.next()) { setError(error, QStringLiteral("symbol %1 is not in the database").arg(key)); return false; }
|
||||||
|
if (s.value(element_data.size() + 2).toString() != id) {
|
||||||
|
setError(error, QStringLiteral("symbol %1 is on folio %2, the database says another").arg(key, id));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
QVariantList v;
|
QVariantList v;
|
||||||
for (int i = 0 ; i < ElementData.size() + 4 ; ++i) v << s.value(i);
|
for (int i = 0 ; i < element_data.size() + 2 ; ++i) v << s.value(i);
|
||||||
put(e, ElementData, v, 0);
|
put(e, element_data, v, 0);
|
||||||
const int n = ElementData.size();
|
const int n = element_data.size();
|
||||||
QDomElement info, links;
|
QDomElement info, links;
|
||||||
if (!v.at(n).isNull()) {
|
if (hasMarker(e, QStringLiteral("elementInformations"))) {
|
||||||
info = document.createElement(QStringLiteral("elementInformations"));
|
info = document.createElement(QStringLiteral("elementInformations"));
|
||||||
setAttributesJson(info, v.at(n + 1));
|
setAttributesJson(info, v.at(n));
|
||||||
Statement i(db.m_db, QStringLiteral("SELECT name, value, attrs FROM element_info WHERE element_key = ? ORDER BY ord"));
|
Statement i(db.m_db, QStringLiteral("SELECT name, value, attrs, %1 FROM element_info WHERE %2 = ? ORDER BY ord")
|
||||||
|
.arg(show_column, element_ref));
|
||||||
i.bind({key});
|
i.bind({key});
|
||||||
while (i.next())
|
while (i.next())
|
||||||
info.appendChild(entry(i.value(0), QStringLiteral("name"), i.value(1), i.value(2),
|
info.appendChild(entry(i.value(0), QStringLiteral("name"), i.value(1), i.value(2),
|
||||||
QStringLiteral("elementInformation")));
|
QStringLiteral("elementInformation"), i.value(3)));
|
||||||
restoreWhitespace(info, v.at(n + 1));
|
restoreWhitespace(info, v.at(n));
|
||||||
}
|
}
|
||||||
if (!v.at(n + 2).isNull()) {
|
if (hasMarker(e, QStringLiteral("links_uuids"))) {
|
||||||
links = document.createElement(QStringLiteral("links_uuids"));
|
links = document.createElement(QStringLiteral("links_uuids"));
|
||||||
setAttributesJson(links, v.at(n + 3));
|
setAttributesJson(links, v.at(n + 1));
|
||||||
Statement l(db.m_db, QStringLiteral("SELECT linked_uuid, attrs FROM link WHERE element_key = ? ORDER BY ord"));
|
Statement l(db.m_db, QStringLiteral("SELECT linked_uuid, attrs FROM link WHERE %1 = ? ORDER BY ord").arg(element_ref));
|
||||||
l.bind({key});
|
l.bind({key});
|
||||||
while (l.next())
|
while (l.next())
|
||||||
links.appendChild(entry(l.value(0), QStringLiteral("uuid"), QVariant(), l.value(1),
|
links.appendChild(entry(l.value(0), QStringLiteral("uuid"), QVariant(), l.value(1),
|
||||||
QStringLiteral("link_uuid")));
|
QStringLiteral("link_uuid")));
|
||||||
restoreWhitespace(links, v.at(n + 3));
|
restoreWhitespace(links, v.at(n + 1));
|
||||||
}
|
}
|
||||||
if ((!info.isNull() && !replaceMarker(e, info))
|
if ((!info.isNull() && !replaceMarker(e, info))
|
||||||
|| (!links.isNull() && !replaceMarker(e, links))) {
|
|| (!links.isNull() && !replaceMarker(e, links))) {
|
||||||
@@ -756,18 +868,47 @@ bool QetContainerDb::restore(QDomElement project, const QByteArray &database, QS
|
|||||||
}
|
}
|
||||||
for (const QDomElement &list : children(d, QStringLiteral("conductors"))) {
|
for (const QDomElement &list : children(d, QStringLiteral("conductors"))) {
|
||||||
for (QDomElement c : children(list, QStringLiteral("conductor"))) {
|
for (QDomElement c : children(list, QStringLiteral("conductor"))) {
|
||||||
if (!c.hasAttribute(KeyAttribute)) continue;
|
const QString key = keyOf(c, KeyAttribute);
|
||||||
const QString key = c.attribute(KeyAttribute);
|
if (key.isEmpty()) continue;
|
||||||
c.removeAttribute(KeyAttribute);
|
if (!use(conductors_used, key, QStringLiteral("wire"))) return false;
|
||||||
Statement s(db.m_db, QStringLiteral("SELECT %1 FROM conductor WHERE key = ?")
|
Statement s(db.m_db, QStringLiteral("SELECT %1, %2 FROM conductor WHERE %3 = ?")
|
||||||
.arg(QStringLiteral("\"") + ConductorData.join(QStringLiteral("\", \"")) + QStringLiteral("\"")));
|
.arg(quoted(conductor_data), folio_ref, item_key));
|
||||||
s.bind({key});
|
s.bind({key});
|
||||||
if (!s.next()) { setError(error, QStringLiteral("wire %1 is not in the database").arg(key)); return false; }
|
if (!s.next()) { setError(error, QStringLiteral("wire %1 is not in the database").arg(key)); return false; }
|
||||||
|
if (s.value(conductor_data.size()).toString() != id) {
|
||||||
|
setError(error, QStringLiteral("wire %1 is on folio %2, the database says another").arg(key, id));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
QVariantList v;
|
QVariantList v;
|
||||||
for (int i = 0 ; i < ConductorData.size() ; ++i) v << s.value(i);
|
for (int i = 0 ; i < conductor_data.size() ; ++i) v << s.value(i);
|
||||||
put(c, ConductorData, v, 0);
|
put(c, conductor_data, v, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//No row left over: each table's rows, counted, are the items read,
|
||||||
|
//and every entry belongs to a row
|
||||||
|
auto count = [&db](const QString &sql) {
|
||||||
|
Statement s(db.m_db, sql);
|
||||||
|
return s.next() ? s.value(0).toInt() : -1;
|
||||||
|
};
|
||||||
|
const struct { QString sql; int expected; QString what; } checks[] = {
|
||||||
|
{QStringLiteral("SELECT count(*) FROM folio"), int(folios_used.size()), QStringLiteral("folio rows")},
|
||||||
|
{QStringLiteral("SELECT count(*) FROM element"), int(elements_used.size()), QStringLiteral("symbol rows")},
|
||||||
|
{QStringLiteral("SELECT count(*) FROM conductor"), int(conductors_used.size()), QStringLiteral("wire rows")},
|
||||||
|
{QStringLiteral("SELECT count(*) FROM folio_property WHERE %1 NOT IN (SELECT %2 FROM folio)")
|
||||||
|
.arg(folio_ref, folio_key), 0, QStringLiteral("folio property rows of no folio")},
|
||||||
|
{QStringLiteral("SELECT count(*) FROM element_info WHERE %1 NOT IN (SELECT %2 FROM element)")
|
||||||
|
.arg(element_ref, item_key), 0, QStringLiteral("information rows of no symbol")},
|
||||||
|
{QStringLiteral("SELECT count(*) FROM link WHERE %1 NOT IN (SELECT %2 FROM element)")
|
||||||
|
.arg(element_ref, item_key), 0, QStringLiteral("link rows of no symbol")}};
|
||||||
|
for (const auto &check : checks) {
|
||||||
|
const int found = count(check.sql);
|
||||||
|
if (found != check.expected) {
|
||||||
|
setError(error, QStringLiteral("the project database does not match its folios: %1 %2, %3 expected")
|
||||||
|
.arg(found).arg(check.what).arg(check.expected));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,10 @@
|
|||||||
drawn. extract() takes them out of the document into a database,
|
drawn. extract() takes them out of the document into a database,
|
||||||
restore() puts them back, so that restore(extract(d)) is d.
|
restore() puts them back, so that restore(extract(d)) is d.
|
||||||
|
|
||||||
Each moved item leaves its key in the document (qetz-key, qetz-folio):
|
A row's key is its item's uuid, which stays in the folio file. An item
|
||||||
its uuid where that is unique, else a made-up one. A block of a shape
|
without a uuid of its own -- none, or one another item shares -- gets a
|
||||||
|
uuid made from what it holds, left in the document as qetz-key or
|
||||||
|
qetz-folio. A block of a shape
|
||||||
this does not model -- a value holding elements, say -- stays in the
|
this does not model -- a value holding elements, say -- stays in the
|
||||||
document, so nothing can be lost by moving.
|
document, so nothing can be lost by moving.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QSaveFile>
|
#include <QSaveFile>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
|
#include <QtEndian>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
void setError(QString *error, const QString &text)
|
void setError(QString *error, const QString &text)
|
||||||
@@ -44,6 +45,42 @@ bool safeName(const QString &name)
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Built with MINIZ_NO_TIME, miniz leaves each entry's DOS date at 0,
|
||||||
|
//day 0 of month 0, which is no date: unzip shows 30 Nov 1979, other
|
||||||
|
//tools 13 Dec 1901. Give every entry 1 Jan 1980 00:00 instead, the
|
||||||
|
//first valid DOS date, so the bytes still do not depend on the time.
|
||||||
|
constexpr quint16 EntryDosDate = (1 << 5) | 1;
|
||||||
|
|
||||||
|
bool setEntryDates(QByteArray *bytes)
|
||||||
|
{
|
||||||
|
char *data = bytes->data();
|
||||||
|
mz_zip_archive zip;
|
||||||
|
mz_zip_zero_struct(&zip);
|
||||||
|
if (!mz_zip_reader_init_mem(&zip, data, size_t(bytes->size()), 0)) return false;
|
||||||
|
|
||||||
|
bool ok = true;
|
||||||
|
const mz_uint count = mz_zip_reader_get_num_files(&zip);
|
||||||
|
for (mz_uint i = 0 ; i < count && ok ; ++i)
|
||||||
|
{
|
||||||
|
mz_zip_archive_file_stat stat;
|
||||||
|
if (!mz_zip_reader_file_stat(&zip, i, &stat)) {
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
//date at offset 12 of the local header, 14 of the central one
|
||||||
|
const quint64 local = stat.m_local_header_ofs + 12;
|
||||||
|
const quint64 central = zip.m_central_directory_file_ofs + stat.m_central_dir_ofs + 14;
|
||||||
|
if (local + 2 > quint64(bytes->size()) || central + 2 > quint64(bytes->size())) {
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
qToLittleEndian<quint16>(EntryDosDate, data + local);
|
||||||
|
qToLittleEndian<quint16>(EntryDosDate, data + central);
|
||||||
|
}
|
||||||
|
mz_zip_reader_end(&zip);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,8 +124,12 @@ QByteArray QetZip::toBytes(const QList<Entry> &entries, QString *error)
|
|||||||
mz_zip_writer_end(&zip);
|
mz_zip_writer_end(&zip);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const QByteArray bytes(static_cast<const char *>(buffer), qsizetype(size));
|
QByteArray bytes(static_cast<const char *>(buffer), qsizetype(size));
|
||||||
mz_zip_writer_end(&zip); //frees buffer
|
mz_zip_writer_end(&zip); //frees buffer
|
||||||
|
if (!setEntryDates(&bytes)) {
|
||||||
|
setError(error, QStringLiteral("cannot date the zip entries"));
|
||||||
|
return {};
|
||||||
|
}
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,8 @@
|
|||||||
megabytes, not gigabytes -- and written through QSaveFile, so a failed
|
megabytes, not gigabytes -- and written through QSaveFile, so a failed
|
||||||
save leaves the previous file as it was.
|
save leaves the previous file as it was.
|
||||||
|
|
||||||
Entries carry no timestamp: saving the same project twice gives the
|
Every entry is dated 1 Jan 1980 00:00, not the time of saving: saving
|
||||||
same bytes.
|
the same project twice gives the same bytes.
|
||||||
*/
|
*/
|
||||||
namespace QetZip
|
namespace QetZip
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -253,6 +253,58 @@ void projectDataBase::updateDB(const QDomDocument &document)
|
|||||||
updateDB();
|
updateDB();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief projectDataBase::documentRefusal
|
||||||
|
@return why populateFromDocument() will refuse @p document, for the
|
||||||
|
reasons it gives that the document alone can tell -- a folio, symbol
|
||||||
|
or wire with no saved uuid, two symbols sharing one, a folio number
|
||||||
|
using %autonum -- or an empty string. The other reasons need the
|
||||||
|
project's definitions or every folio's links; a document refused for
|
||||||
|
one of those is still filled from the folios, only later.
|
||||||
|
*/
|
||||||
|
QString projectDataBase::documentRefusal(const QDomDocument &document)
|
||||||
|
{
|
||||||
|
if (qEnvironmentVariableIntValue("QET_DATABASE_FROM_FOLIOS") == 1) {
|
||||||
|
return QStringLiteral("QET_DATABASE_FROM_FOLIOS is set");
|
||||||
|
}
|
||||||
|
QSet<QUuid> element_uuids;
|
||||||
|
const QDomNodeList diagram_nodes = document.elementsByTagName(QStringLiteral("diagram"));
|
||||||
|
for (int i = 0 ; i < diagram_nodes.size() ; ++i)
|
||||||
|
{
|
||||||
|
QDomElement diagram_xml = diagram_nodes.at(i).toElement();
|
||||||
|
if (QUuid(diagram_xml.attribute(QStringLiteral("uuid"))).isNull()) {
|
||||||
|
return QStringLiteral("a folio has no saved uuid");
|
||||||
|
}
|
||||||
|
if (diagram_xml.attribute(QStringLiteral("folio")).contains(QStringLiteral("%autonum"))) {
|
||||||
|
return QStringLiteral("a folio number uses %autonum");
|
||||||
|
}
|
||||||
|
for (QDomElement element_xml : QET::findInDomElement(
|
||||||
|
diagram_xml, QStringLiteral("elements"), QStringLiteral("element")))
|
||||||
|
{
|
||||||
|
//Skipped by Diagram::fromXml() as well
|
||||||
|
if (!Element::valideXml(element_xml)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QUuid uuid(element_xml.attribute(QStringLiteral("uuid")));
|
||||||
|
if (uuid.isNull() || element_uuids.contains(uuid)) {
|
||||||
|
return QStringLiteral("an element has no saved uuid, or shares one");
|
||||||
|
}
|
||||||
|
element_uuids.insert(uuid);
|
||||||
|
}
|
||||||
|
for (QDomElement conductor_xml : QET::findInDomElement(
|
||||||
|
diagram_xml, QStringLiteral("conductors"), QStringLiteral("conductor")))
|
||||||
|
{
|
||||||
|
if (Conductor::valideXml(conductor_xml)
|
||||||
|
&& (QUuid(conductor_xml.attribute(QStringLiteral("uuid"))).isNull()
|
||||||
|
|| !conductor_xml.hasAttribute(QStringLiteral("element1"))
|
||||||
|
|| !conductor_xml.hasAttribute(QStringLiteral("element2")))) {
|
||||||
|
return QStringLiteral("a conductor has no saved uuid, or names its ends the older way");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
struct DocumentTerminal
|
struct DocumentTerminal
|
||||||
{
|
{
|
||||||
@@ -2445,7 +2497,7 @@ void projectDataBase::populateElementTable()
|
|||||||
/**
|
/**
|
||||||
@brief projectDataBase::populateElementInfoTable
|
@brief projectDataBase::populateElementInfoTable
|
||||||
Populate the element info table from the symbol information store
|
Populate the element info table from the symbol information store
|
||||||
(DB-ACCESSORS-PLAN.md stage 4.3), in the order of the element table.
|
in the order of the element table.
|
||||||
The label column is the label a symbol shows, which its folio's
|
The label column is the label a symbol shows, which its folio's
|
||||||
numbering decides: the symbol holding the uuid gives it.
|
numbering decides: the symbol holding the uuid gives it.
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,14 @@ class projectDataBase : public QObject
|
|||||||
populateFromDocument()), from the built folios otherwise.
|
populateFromDocument()), from the built folios otherwise.
|
||||||
*/
|
*/
|
||||||
void updateDB(const QDomDocument &document);
|
void updateDB(const QDomDocument &document);
|
||||||
|
/**
|
||||||
|
Why updateDB(document) will fill the tables from the folios
|
||||||
|
rather than from @p document, for the reasons the document
|
||||||
|
alone can tell; empty when it knows of none. Read before the
|
||||||
|
folios are: a project whose tables will come from the folios
|
||||||
|
builds every folio as it opens, even with QET_LAZY_FOLIOS.
|
||||||
|
*/
|
||||||
|
static QString documentRefusal(const QDomDocument &document);
|
||||||
/**
|
/**
|
||||||
Suppress the full rebuild performed by updateDB().
|
Suppress the full rebuild performed by updateDB().
|
||||||
|
|
||||||
@@ -106,7 +114,7 @@ class projectDataBase : public QObject
|
|||||||
|
|
||||||
//The information of every symbol placed in the project, by
|
//The information of every symbol placed in the project, by
|
||||||
//symbol uuid: the store each symbol's information is written
|
//symbol uuid: the store each symbol's information is written
|
||||||
//through (DB-ACCESSORS-PLAN.md stage 4.1). A symbol not placed
|
//through. A symbol not placed
|
||||||
//(a preview, the symbol editor) keeps its own.
|
//(a preview, the symbol editor) keeps its own.
|
||||||
DiagramContext elementInformation(const QUuid &element) const;
|
DiagramContext elementInformation(const QUuid &element) const;
|
||||||
bool hasElementInformation(const QUuid &element) const;
|
bool hasElementInformation(const QUuid &element) const;
|
||||||
@@ -124,7 +132,7 @@ class projectDataBase : public QObject
|
|||||||
|
|
||||||
//The title block properties of every folio of the project, by
|
//The title block properties of every folio of the project, by
|
||||||
//folio uuid: the store each folio's title block is written
|
//folio uuid: the store each folio's title block is written
|
||||||
//through (DB-ACCESSORS-PLAN.md stage 4.4). A folio not in a
|
//through. A folio not in a
|
||||||
//project keeps its own.
|
//project keeps its own.
|
||||||
TitleBlockProperties folioTitleBlock(const QUuid &folio) const;
|
TitleBlockProperties folioTitleBlock(const QUuid &folio) const;
|
||||||
const TitleBlockProperties *storedFolioTitleBlock(const QUuid &folio) const;
|
const TitleBlockProperties *storedFolioTitleBlock(const QUuid &folio) const;
|
||||||
@@ -136,7 +144,7 @@ class projectDataBase : public QObject
|
|||||||
|
|
||||||
//The properties of every wire placed in the project, by wire
|
//The properties of every wire placed in the project, by wire
|
||||||
//uuid: the store each wire's properties are written through
|
//uuid: the store each wire's properties are written through
|
||||||
//(DB-ACCESSORS-PLAN.md stage 4.5). A wire not placed keeps its own.
|
//A wire not placed keeps its own.
|
||||||
ConductorProperties conductorProperties(const QUuid &conductor) const;
|
ConductorProperties conductorProperties(const QUuid &conductor) const;
|
||||||
bool hasConductorProperties(const QUuid &conductor) const;
|
bool hasConductorProperties(const QUuid &conductor) const;
|
||||||
int placedConductorCount(const QUuid &conductor) const;
|
int placedConductorCount(const QUuid &conductor) const;
|
||||||
@@ -145,7 +153,7 @@ class projectDataBase : public QObject
|
|||||||
QStringList conductorPropertiesMismatches() const;
|
QStringList conductorPropertiesMismatches() const;
|
||||||
|
|
||||||
//Fill the three stores from a project's document before its
|
//Fill the three stores from a project's document before its
|
||||||
//folios are built (DB-ACCESSORS-PLAN.md stage 4.6), then, once
|
//folios are built, then, once
|
||||||
//they are, say how many of those values building agreed with.
|
//they are, say how many of those values building agreed with.
|
||||||
void prefillFromDocument(const QDomDocument &document);
|
void prefillFromDocument(const QDomDocument &document);
|
||||||
void endPrefill(bool keep_unplaced = false);
|
void endPrefill(bool keep_unplaced = false);
|
||||||
|
|||||||
+32
-4
@@ -49,9 +49,12 @@
|
|||||||
#include "diagramsortkeys.h"
|
#include "diagramsortkeys.h"
|
||||||
#include "itemgroups.h"
|
#include "itemgroups.h"
|
||||||
#include "textgrid.h"
|
#include "textgrid.h"
|
||||||
|
#include "container/qetcontainer.h"
|
||||||
#include "foliogrid.h"
|
#include "foliogrid.h"
|
||||||
#include <QGraphicsView>
|
#include <QGraphicsView>
|
||||||
#include <QTextStream>
|
#include <QTextStream>
|
||||||
|
#include <QThreadPool>
|
||||||
|
#include <QtConcurrentMap>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <climits>
|
#include <climits>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
@@ -2005,12 +2008,37 @@ bool Diagram::fromXml(QDomElement &document,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load image
|
// Load image
|
||||||
|
// Decoding is most of the time of a folio with photos (a quarter of
|
||||||
|
// a second each), so the pictures are decoded first, in parallel,
|
||||||
|
// and the items built afterwards in this thread.
|
||||||
|
const QList<QDomElement> images_xml = QET::findInDomElement(
|
||||||
|
root, QStringLiteral("images"), QStringLiteral("image"));
|
||||||
|
QList<DiagramImageItem::Picture> pictures;
|
||||||
|
pictures.reserve(images_xml.size());
|
||||||
|
for (const QDomElement &image_xml : images_xml) {
|
||||||
|
const QString picture_file = QetContainer::pictureFile(image_xml);
|
||||||
|
pictures << DiagramImageItem::pictureOf(
|
||||||
|
image_xml,
|
||||||
|
!picture_file.isEmpty() && project()
|
||||||
|
? project() -> containerPicture(picture_file)
|
||||||
|
: QByteArray());
|
||||||
|
}
|
||||||
|
if (pictures.size() > 1) {
|
||||||
|
QThreadPool pool;
|
||||||
|
pool.setMaxThreadCount(std::min<int>(QThread::idealThreadCount(),
|
||||||
|
pictures.size()));
|
||||||
|
QtConcurrent::blockingMap(&pool, pictures,
|
||||||
|
&DiagramImageItem::decodePicture);
|
||||||
|
} else {
|
||||||
|
for (DiagramImageItem::Picture &picture : pictures)
|
||||||
|
DiagramImageItem::decodePicture(picture);
|
||||||
|
}
|
||||||
|
|
||||||
QList<DiagramImageItem *> added_images;
|
QList<DiagramImageItem *> added_images;
|
||||||
for (auto image_xml : QET::findInDomElement(root,
|
for (qsizetype i = 0 ; i < images_xml.size() ; ++i) {
|
||||||
QStringLiteral("images"),
|
const QDomElement &image_xml = images_xml.at(i);
|
||||||
QStringLiteral("image"))) {
|
|
||||||
DiagramImageItem *dii = new DiagramImageItem ();
|
DiagramImageItem *dii = new DiagramImageItem ();
|
||||||
dii -> fromXml(image_xml);
|
dii -> fromXml(image_xml, std::move(pictures[i]));
|
||||||
settle_uuid(dii, image_xml, QStringLiteral("image"), added_images.size());
|
settle_uuid(dii, image_xml, QStringLiteral("image"), added_images.size());
|
||||||
ItemGroups::setGroup(dii, ItemGroups::read(image_xml));
|
ItemGroups::setGroup(dii, ItemGroups::read(image_xml));
|
||||||
addItem(dii);
|
addItem(dii);
|
||||||
|
|||||||
+7
-1
@@ -133,6 +133,7 @@ class Diagram : public QGraphicsScene
|
|||||||
bool draw_terminals_;
|
bool draw_terminals_;
|
||||||
bool draw_terminal_names_;
|
bool draw_terminal_names_;
|
||||||
bool draw_colored_conductors_;
|
bool draw_colored_conductors_;
|
||||||
|
bool m_small_pictures = false;
|
||||||
|
|
||||||
QString m_conductors_autonum_name;
|
QString m_conductors_autonum_name;
|
||||||
DiagramEventInterface *m_event_interface;
|
DiagramEventInterface *m_event_interface;
|
||||||
@@ -211,7 +212,7 @@ class Diagram : public QGraphicsScene
|
|||||||
bool = true,
|
bool = true,
|
||||||
DiagramContent * = nullptr);
|
DiagramContent * = nullptr);
|
||||||
/// false while this folio's items wait in its XML to be
|
/// false while this folio's items wait in its XML to be
|
||||||
/// built (QET_LAZY_FOLIOS, LAZY-FOLIO-PLAN.md)
|
/// built (QET_LAZY_FOLIOS)
|
||||||
bool isLoaded() const {return m_deferred.isNull();}
|
bool isLoaded() const {return m_deferred.isNull();}
|
||||||
QDomElement deferredXml() const {return m_deferred;}
|
QDomElement deferredXml() const {return m_deferred;}
|
||||||
QString headerXml();
|
QString headerXml();
|
||||||
@@ -284,6 +285,11 @@ class Diagram : public QGraphicsScene
|
|||||||
void setDrawTerminalNames(bool);
|
void setDrawTerminalNames(bool);
|
||||||
bool drawColoredConductors() const;
|
bool drawColoredConductors() const;
|
||||||
void setDrawColoredConductors(bool);
|
void setDrawColoredConductors(bool);
|
||||||
|
/// While true, pictures draw a copy of a few times the size
|
||||||
|
/// they are drawn at, not their full resolution (SVG export,
|
||||||
|
/// ExportProperties::svg_small_pictures)
|
||||||
|
bool smallPictures() const {return m_small_pictures;}
|
||||||
|
void setSmallPictures(bool small) {m_small_pictures = small;}
|
||||||
|
|
||||||
QString title() const;
|
QString title() const;
|
||||||
bool toPaintDevice(QPaintDevice &, int = -1, int = -1,
|
bool toPaintDevice(QPaintDevice &, int = -1, int = -1,
|
||||||
|
|||||||
@@ -255,7 +255,7 @@
|
|||||||
<string>margin: 5px; font-weight: bold;</string>
|
<string>margin: 5px; font-weight: bold;</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Potential separation</string>
|
<string>Function separation</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|||||||
@@ -417,12 +417,14 @@ void ExportDialog::generateSvg(
|
|||||||
|
|
||||||
// genere une QPicture a partir du schema
|
// genere une QPicture a partir du schema
|
||||||
QPicture picture;
|
QPicture picture;
|
||||||
|
diagram -> setSmallPictures(epw -> exportProperties().svg_small_pictures);
|
||||||
diagram -> toPaintDevice(
|
diagram -> toPaintDevice(
|
||||||
picture,
|
picture,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
keep_aspect_ratio ? Qt::KeepAspectRatio : Qt::IgnoreAspectRatio
|
keep_aspect_ratio ? Qt::KeepAspectRatio : Qt::IgnoreAspectRatio
|
||||||
);
|
);
|
||||||
|
diagram -> setSmallPictures(false);
|
||||||
|
|
||||||
// "joue" la QPicture sur un QSvgGenerator
|
// "joue" la QPicture sur un QSvgGenerator
|
||||||
QSvgGenerator svg_engine;
|
QSvgGenerator svg_engine;
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ ExportProperties::ExportProperties() :
|
|||||||
draw_colored_conductors(true),
|
draw_colored_conductors(true),
|
||||||
dxf_blocks(false),
|
dxf_blocks(false),
|
||||||
dxf_attributes(false),
|
dxf_attributes(false),
|
||||||
|
svg_small_pictures(false),
|
||||||
exported_area(QET::BorderArea)
|
exported_area(QET::BorderArea)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -83,6 +84,8 @@ void ExportProperties::toSettings(QSettings &settings,
|
|||||||
dxf_blocks);
|
dxf_blocks);
|
||||||
settings.setValue(prefix % "dxfattributes",
|
settings.setValue(prefix % "dxfattributes",
|
||||||
dxf_attributes);
|
dxf_attributes);
|
||||||
|
settings.setValue(prefix % "svgsmallpictures",
|
||||||
|
svg_small_pictures);
|
||||||
settings.setValue(prefix % "area",
|
settings.setValue(prefix % "area",
|
||||||
QET::diagramAreaToString(exported_area));
|
QET::diagramAreaToString(exported_area));
|
||||||
}
|
}
|
||||||
@@ -125,6 +128,8 @@ void ExportProperties::fromSettings(QSettings &settings,
|
|||||||
false).toBool();
|
false).toBool();
|
||||||
dxf_attributes = settings.value(prefix % "dxfattributes",
|
dxf_attributes = settings.value(prefix % "dxfattributes",
|
||||||
false).toBool();
|
false).toBool();
|
||||||
|
svg_small_pictures = settings.value(prefix % "svgsmallpictures",
|
||||||
|
false).toBool();
|
||||||
|
|
||||||
exported_area = QET::diagramAreaFromString(
|
exported_area = QET::diagramAreaFromString(
|
||||||
settings.value(
|
settings.value(
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class ExportProperties {
|
|||||||
bool draw_colored_conductors; ///< Whether to render conductors colors
|
bool draw_colored_conductors; ///< Whether to render conductors colors
|
||||||
bool dxf_blocks; ///< DXF: each symbol as a block, placed with an INSERT
|
bool dxf_blocks; ///< DXF: each symbol as a block, placed with an INSERT
|
||||||
bool dxf_attributes; ///< DXF, with dxf_blocks: a symbol's texts as attributes of its INSERT
|
bool dxf_attributes; ///< DXF, with dxf_blocks: a symbol's texts as attributes of its INSERT
|
||||||
|
bool svg_small_pictures; ///< SVG: pictures at most 4x the size they are drawn, not full resolution
|
||||||
QET::DiagramArea exported_area; ///< Area of diagrams to be rendered
|
QET::DiagramArea exported_area; ///< Area of diagrams to be rendered
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ ExportProperties ExportPropertiesWidget::exportProperties() const
|
|||||||
export_properties.draw_colored_conductors = draw_colored_conductors -> isChecked();
|
export_properties.draw_colored_conductors = draw_colored_conductors -> isChecked();
|
||||||
export_properties.dxf_blocks = dxf_blocks -> isChecked();
|
export_properties.dxf_blocks = dxf_blocks -> isChecked();
|
||||||
export_properties.dxf_attributes = dxf_attributes -> isChecked();
|
export_properties.dxf_attributes = dxf_attributes -> isChecked();
|
||||||
|
export_properties.svg_small_pictures = svg_small_pictures -> isChecked();
|
||||||
export_properties.exported_area = export_border -> isChecked() ? QET::BorderArea : QET::ElementsArea;
|
export_properties.exported_area = export_border -> isChecked() ? QET::BorderArea : QET::ElementsArea;
|
||||||
|
|
||||||
return(export_properties);
|
return(export_properties);
|
||||||
@@ -94,6 +95,7 @@ void ExportPropertiesWidget::setExportProperties(const ExportProperties &export_
|
|||||||
dxf_blocks -> setChecked(export_properties.dxf_blocks);
|
dxf_blocks -> setChecked(export_properties.dxf_blocks);
|
||||||
dxf_attributes -> setChecked(export_properties.dxf_attributes);
|
dxf_attributes -> setChecked(export_properties.dxf_attributes);
|
||||||
dxf_attributes -> setEnabled(export_properties.dxf_blocks);
|
dxf_attributes -> setEnabled(export_properties.dxf_blocks);
|
||||||
|
svg_small_pictures -> setChecked(export_properties.svg_small_pictures);
|
||||||
|
|
||||||
if (export_properties.exported_area == QET::BorderArea) {
|
if (export_properties.exported_area == QET::BorderArea) {
|
||||||
export_border -> setChecked(true);
|
export_border -> setChecked(true);
|
||||||
@@ -125,6 +127,7 @@ void ExportPropertiesWidget::setPrintingMode(bool mode) {
|
|||||||
export_border -> setVisible(!mode);
|
export_border -> setVisible(!mode);
|
||||||
export_elements -> setVisible(!mode);
|
export_elements -> setVisible(!mode);
|
||||||
draw_bg_transparent -> setVisible(!mode);
|
draw_bg_transparent -> setVisible(!mode);
|
||||||
|
svg_small_pictures -> setVisible(!mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -235,6 +238,11 @@ void ExportPropertiesWidget::build()
|
|||||||
dxf_attributes -> setToolTip(tr("A symbol's texts (label, function...) become attributes of its block. LibreCAD does not show attributes."));
|
dxf_attributes -> setToolTip(tr("A symbol's texts (label, function...) become attributes of its block. LibreCAD does not show attributes."));
|
||||||
optionshlayout -> addWidget(dxf_attributes, 5, 1);
|
optionshlayout -> addWidget(dxf_attributes, 5, 1);
|
||||||
connect(dxf_blocks, &QCheckBox::toggled, dxf_attributes, &QCheckBox::setEnabled);
|
connect(dxf_blocks, &QCheckBox::toggled, dxf_attributes, &QCheckBox::setEnabled);
|
||||||
|
|
||||||
|
// pictures not at full resolution in an SVG: a much smaller file
|
||||||
|
svg_small_pictures = new QCheckBox(tr("SVG: smaller pictures"), groupbox_options);
|
||||||
|
svg_small_pictures -> setToolTip(tr("Pictures are stored at four times the size they have on the folio instead of their full resolution: a much smaller file, still sharp when printed"));
|
||||||
|
optionshlayout -> addWidget(svg_small_pictures, 5, 0);
|
||||||
|
|
||||||
vboxLayout -> addWidget(groupbox_options);
|
vboxLayout -> addWidget(groupbox_options);
|
||||||
|
|
||||||
@@ -252,6 +260,7 @@ void ExportPropertiesWidget::build()
|
|||||||
setTabOrder(draw_colored_conductors, draw_bg_transparent);
|
setTabOrder(draw_colored_conductors, draw_bg_transparent);
|
||||||
setTabOrder(draw_bg_transparent, dxf_blocks);
|
setTabOrder(draw_bg_transparent, dxf_blocks);
|
||||||
setTabOrder(dxf_blocks, dxf_attributes);
|
setTabOrder(dxf_blocks, dxf_attributes);
|
||||||
|
setTabOrder(draw_bg_transparent, svg_small_pictures);
|
||||||
|
|
||||||
// connexion du bouton permettant le choix du repertoire
|
// connexion du bouton permettant le choix du repertoire
|
||||||
connect(button_browse, &QPushButton::released, this, &ExportPropertiesWidget::slot_chooseADirectory);
|
connect(button_browse, &QPushButton::released, this, &ExportPropertiesWidget::slot_chooseADirectory);
|
||||||
@@ -269,6 +278,7 @@ void ExportPropertiesWidget::build()
|
|||||||
connect(draw_colored_conductors, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged);
|
connect(draw_colored_conductors, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged);
|
||||||
connect(dxf_blocks, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged);
|
connect(dxf_blocks, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged);
|
||||||
connect(dxf_attributes, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged);
|
connect(dxf_attributes, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged);
|
||||||
|
connect(svg_small_pictures, &QCheckBox::stateChanged, this, &ExportPropertiesWidget::optionChanged);
|
||||||
#else
|
#else
|
||||||
connect(draw_grid, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
|
connect(draw_grid, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
|
||||||
connect(draw_border, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
|
connect(draw_border, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
|
||||||
@@ -279,5 +289,6 @@ void ExportPropertiesWidget::build()
|
|||||||
connect(draw_colored_conductors, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
|
connect(draw_colored_conductors, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
|
||||||
connect(dxf_blocks, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
|
connect(dxf_blocks, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
|
||||||
connect(dxf_attributes, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
|
connect(dxf_attributes, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
|
||||||
|
connect(svg_small_pictures, &QCheckBox::checkStateChanged, this, &ExportPropertiesWidget::optionChanged);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ class ExportPropertiesWidget : public QWidget {
|
|||||||
QCheckBox *draw_colored_conductors;
|
QCheckBox *draw_colored_conductors;
|
||||||
QCheckBox *dxf_blocks;
|
QCheckBox *dxf_blocks;
|
||||||
QCheckBox *dxf_attributes;
|
QCheckBox *dxf_attributes;
|
||||||
|
QCheckBox *svg_small_pictures;
|
||||||
QRadioButton *export_border;
|
QRadioButton *export_border;
|
||||||
QRadioButton *export_elements;
|
QRadioButton *export_elements;
|
||||||
QButtonGroup *exported_content_choices;
|
QButtonGroup *exported_content_choices;
|
||||||
|
|||||||
@@ -107,10 +107,11 @@ class TerminalData : public PropertiesInterface
|
|||||||
@brief m_potential
|
@brief m_potential
|
||||||
An optional, symbol-author-chosen grouping identifier shared by
|
An optional, symbol-author-chosen grouping identifier shared by
|
||||||
terminals that belong to the same physical terminal within
|
terminals that belong to the same physical terminal within
|
||||||
a multi-terminal block. When potential_isolating is enabled
|
a multi-terminal block. When the function separation checkbox
|
||||||
|
(element information potential_isolating) is enabled
|
||||||
on the parent element, terminals sharing a non-empty,
|
on the parent element, terminals sharing a non-empty,
|
||||||
matching m_potential stay linked to each other; terminals
|
matching m_potential stay linked to each other; terminals
|
||||||
with different (or empty) values become isolated from each
|
with different (or empty) values become separated from each
|
||||||
other. Empty by default.
|
other. Empty by default.
|
||||||
*/
|
*/
|
||||||
QString m_potential;
|
QString m_potential;
|
||||||
|
|||||||
@@ -1969,6 +1969,16 @@ bool QETDiagramEditor::openAndAddProject(
|
|||||||
addProject(project);
|
addProject(project);
|
||||||
DialogWaiting::dropInstance();
|
DialogWaiting::dropInstance();
|
||||||
|
|
||||||
|
if (interactive && project->isFromLaterVersion()) {
|
||||||
|
QET::QetMessageBox::warning(
|
||||||
|
this,
|
||||||
|
tr("Opening the file read-only", "message box title"),
|
||||||
|
tr("This project was saved by a later version of QElectroTech. "
|
||||||
|
"It is opened read-only, so that saving it cannot lose what "
|
||||||
|
"this version does not know. Use Save As to keep a copy "
|
||||||
|
"in this version's format."));
|
||||||
|
}
|
||||||
|
|
||||||
//Report font descriptions which could not be read as-is (written by
|
//Report font descriptions which could not be read as-is (written by
|
||||||
//an incompatible Qt version or corrupted), so the user learns about
|
//an incompatible Qt version or corrupted), so the user learns about
|
||||||
//it from somewhere else than the console. See issue #553.
|
//it from somewhere else than the console. See issue #553.
|
||||||
|
|||||||
@@ -1857,7 +1857,7 @@ ConductorProperties Conductor::properties() const
|
|||||||
/**
|
/**
|
||||||
@brief Conductor::placedStore
|
@brief Conductor::placedStore
|
||||||
@return the project database holding this wire's properties
|
@return the project database holding this wire's properties
|
||||||
(DB-ACCESSORS-PLAN.md stage 4.5), or nullptr if it is not placed in a
|
or nullptr if it is not placed in a
|
||||||
project, or shares its uuid with another wire for the moment a copy
|
project, or shares its uuid with another wire for the moment a copy
|
||||||
takes to get its own
|
takes to get its own
|
||||||
*/
|
*/
|
||||||
@@ -2005,9 +2005,16 @@ void Conductor::displayedTextChanged()
|
|||||||
@param all_diagram : if true search in all diagram of the project,
|
@param all_diagram : if true search in all diagram of the project,
|
||||||
false search only in the parent diagram of this conductor
|
false search only in the parent diagram of this conductor
|
||||||
@param t_list : a list of terminal already found for this potential.
|
@param t_list : a list of terminal already found for this potential.
|
||||||
|
@param ignore_function_separation : if true, a terminal element whose
|
||||||
|
function separation is enabled does not stop the search, and the
|
||||||
|
conductors of both its sides are returned as one potential. Only
|
||||||
|
functionSeparatedConductors() passes true.
|
||||||
|
@sa functionSeparatedConductors
|
||||||
@return a QSet of conductor at the same potential.
|
@return a QSet of conductor at the same potential.
|
||||||
*/
|
*/
|
||||||
QSet<Conductor *> Conductor::relatedPotentialConductors(const bool all_diagram, QList <Terminal *> *t_list)
|
QSet<Conductor *> Conductor::relatedPotentialConductors(const bool all_diagram,
|
||||||
|
QList <Terminal *> *t_list,
|
||||||
|
const bool ignore_function_separation)
|
||||||
{
|
{
|
||||||
bool declar_t_list = false;
|
bool declar_t_list = false;
|
||||||
if (t_list == nullptr)
|
if (t_list == nullptr)
|
||||||
@@ -2030,7 +2037,8 @@ QSet<Conductor *> Conductor::relatedPotentialConductors(const bool all_diagram,
|
|||||||
|
|
||||||
//Get the other terminals of the parent element of @terminal, who share the same potential
|
//Get the other terminals of the parent element of @terminal, who share the same potential
|
||||||
//This is use for element type "folio report" and "terminal element"
|
//This is use for element type "folio report" and "terminal element"
|
||||||
for (Terminal *t : relatedPotentialTerminal(terminal, all_diagram))
|
for (Terminal *t : relatedPotentialTerminal(terminal, all_diagram,
|
||||||
|
ignore_function_separation))
|
||||||
{
|
{
|
||||||
if (!t_list->contains(t))
|
if (!t_list->contains(t))
|
||||||
{
|
{
|
||||||
@@ -2042,7 +2050,8 @@ QSet<Conductor *> Conductor::relatedPotentialConductors(const bool all_diagram,
|
|||||||
other_conductors_list_t.removeAll(this);
|
other_conductors_list_t.removeAll(this);
|
||||||
//Get the conductors at the same potential for each conductors of other_conductors_list_t
|
//Get the conductors at the same potential for each conductors of other_conductors_list_t
|
||||||
for (Conductor *c : other_conductors_list_t) {
|
for (Conductor *c : other_conductors_list_t) {
|
||||||
other_conductors += c->relatedPotentialConductors(all_diagram, t_list);
|
other_conductors += c->relatedPotentialConductors(all_diagram, t_list,
|
||||||
|
ignore_function_separation);
|
||||||
}
|
}
|
||||||
|
|
||||||
other_conductors += QSet<Conductor*>(other_conductors_list_t.begin(),other_conductors_list_t.end());
|
other_conductors += QSet<Conductor*>(other_conductors_list_t.begin(),other_conductors_list_t.end());
|
||||||
@@ -2055,6 +2064,36 @@ QSet<Conductor *> Conductor::relatedPotentialConductors(const bool all_diagram,
|
|||||||
return(other_conductors);
|
return(other_conductors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Conductor::functionSeparatedConductors
|
||||||
|
Return the conductors that belong to this conductor's potential only
|
||||||
|
by crossing a function separated terminal element -- a terminal
|
||||||
|
element whose function separation checkbox (element information
|
||||||
|
potential_isolating) is enabled, the checkbox the user ticks to stop
|
||||||
|
sharing a terminal's properties with the other side of the terminal.
|
||||||
|
|
||||||
|
Those conductors are deliberately not part of
|
||||||
|
relatedPotentialConductors(): no property may be copied over them
|
||||||
|
by default. One does cross that separation, though: the voltage /
|
||||||
|
protocol, which describes the electrical node itself. Callers that
|
||||||
|
push it over the whole potential use this method to reach the other
|
||||||
|
side (@see ConductorProperties::applyTensionProtocolChange).
|
||||||
|
|
||||||
|
It is simply the potential computed with the separations ignored,
|
||||||
|
minus the potential that respects them.
|
||||||
|
@param all_diagram : if true search in all diagram of the project,
|
||||||
|
false search only in the parent diagram of this conductor
|
||||||
|
@return a QSet of conductor, this conductor excluded
|
||||||
|
*/
|
||||||
|
QSet<Conductor *> Conductor::functionSeparatedConductors(const bool all_diagram)
|
||||||
|
{
|
||||||
|
QSet<Conductor *> separated = relatedPotentialConductors(all_diagram, nullptr, true);
|
||||||
|
separated.subtract(relatedPotentialConductors(all_diagram));
|
||||||
|
separated.remove(this);
|
||||||
|
|
||||||
|
return separated;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief Conductor::diagramEditor
|
@brief Conductor::diagramEditor
|
||||||
@return The parent diagram editor or nullptr;
|
@return The parent diagram editor or nullptr;
|
||||||
|
|||||||
@@ -151,7 +151,10 @@ class Conductor : public QGraphicsObject
|
|||||||
virtual void setHighlighted(Highlight);
|
virtual void setHighlighted(Highlight);
|
||||||
QSet<Conductor *> relatedPotentialConductors(
|
QSet<Conductor *> relatedPotentialConductors(
|
||||||
const bool all_diagram = true,
|
const bool all_diagram = true,
|
||||||
QList <Terminal *> *t_list=nullptr);
|
QList <Terminal *> *t_list=nullptr,
|
||||||
|
const bool ignore_function_separation = false);
|
||||||
|
QSet<Conductor *> functionSeparatedConductors(
|
||||||
|
const bool all_diagram = true);
|
||||||
QETDiagramEditor* diagramEditor() const;
|
QETDiagramEditor* diagramEditor() const;
|
||||||
void editProperty ();
|
void editProperty ();
|
||||||
|
|
||||||
|
|||||||
@@ -167,7 +167,28 @@ void DiagramImageItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DarkImageRendering::paintPixmap(painter, pixmap_, m_adapt_to_dark_theme);
|
// SVG export with smaller pictures: a copy four times the size the
|
||||||
|
// picture is drawn at, when that is smaller than the picture. An
|
||||||
|
// SVG stores what it is given, so a photo shown at a tenth of its
|
||||||
|
// size would otherwise be written whole.
|
||||||
|
const QTransform world = painter -> worldTransform();
|
||||||
|
const qreal drawn_scale = std::sqrt(std::abs(world.determinant()));
|
||||||
|
const QSize small_size = (QSizeF(pixmap_.size()) * drawn_scale * 4).toSize();
|
||||||
|
if (diagram() && diagram() -> smallPictures() && !pixmap_.isNull()
|
||||||
|
&& small_size.width() < pixmap_.width()
|
||||||
|
&& small_size.height() < pixmap_.height()
|
||||||
|
&& !small_size.isEmpty())
|
||||||
|
{
|
||||||
|
const QPixmap small = pixmap_.scaled(small_size, Qt::IgnoreAspectRatio,
|
||||||
|
Qt::SmoothTransformation);
|
||||||
|
painter -> save();
|
||||||
|
painter -> scale(qreal(pixmap_.width()) / small.width(),
|
||||||
|
qreal(pixmap_.height()) / small.height());
|
||||||
|
DarkImageRendering::paintPixmap(painter, small, m_adapt_to_dark_theme);
|
||||||
|
painter -> restore();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
DarkImageRendering::paintPixmap(painter, pixmap_, m_adapt_to_dark_theme);
|
||||||
|
|
||||||
Q_UNUSED(option); Q_UNUSED(widget);
|
Q_UNUSED(option); Q_UNUSED(widget);
|
||||||
|
|
||||||
@@ -1787,6 +1808,71 @@ const QByteArray &DiagramImageItem::encodedPng(const QPixmap &pixmap, QByteArray
|
|||||||
@return true if successfully loaded.
|
@return true if successfully loaded.
|
||||||
*/
|
*/
|
||||||
bool DiagramImageItem::fromXml(const QDomElement &e)
|
bool DiagramImageItem::fromXml(const QDomElement &e)
|
||||||
|
{
|
||||||
|
return fromXml(e, QByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief DiagramImageItem::fromXml
|
||||||
|
Load this image from xml element e, its picture being @p picture, not
|
||||||
|
the base64 text of @p e, unless @p picture is empty. A .qetz keeps its
|
||||||
|
pictures as files (QetContainer::join()).
|
||||||
|
*/
|
||||||
|
bool DiagramImageItem::fromXml(const QDomElement &e, const QByteArray &picture)
|
||||||
|
{
|
||||||
|
Picture p = pictureOf(e, picture);
|
||||||
|
decodePicture(p);
|
||||||
|
return fromXml(e, std::move(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief DiagramImageItem::pictureOf
|
||||||
|
@return the picture of @p e, @p picture unless it is empty, else the
|
||||||
|
base64 text of @p e, still encoded : decodePicture() does the slow part.
|
||||||
|
Reads the DOM, so GUI thread only.
|
||||||
|
*/
|
||||||
|
DiagramImageItem::Picture DiagramImageItem::pictureOf(const QDomElement &e,
|
||||||
|
const QByteArray &picture)
|
||||||
|
{
|
||||||
|
Picture p;
|
||||||
|
p.bytes = picture;
|
||||||
|
if (p.bytes.isEmpty())
|
||||||
|
{
|
||||||
|
// From the first text node only, not e.text(): that also collects
|
||||||
|
// the text of every child element, so on a cropped or colour-keyed
|
||||||
|
// picture it carries <image_base>'s bytes too.
|
||||||
|
const QDomNode image_node = e.firstChild();
|
||||||
|
if (image_node.isText())
|
||||||
|
p.base64 = image_node.toText().data();
|
||||||
|
else
|
||||||
|
p.found = false;
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief DiagramImageItem::decodePicture
|
||||||
|
Decodes the base64 text and the picture file of @p picture. Touches
|
||||||
|
nothing else, so it can run in any thread : a large photo takes about
|
||||||
|
a quarter of a second, and Diagram::fromXml() decodes a folio's
|
||||||
|
pictures in parallel.
|
||||||
|
*/
|
||||||
|
void DiagramImageItem::decodePicture(Picture &picture)
|
||||||
|
{
|
||||||
|
if (picture.bytes.isEmpty() && !picture.base64.isEmpty())
|
||||||
|
{
|
||||||
|
picture.bytes = QByteArray::fromBase64(picture.base64.toLatin1());
|
||||||
|
picture.base64.clear();
|
||||||
|
}
|
||||||
|
picture.image = QImage::fromData(picture.bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief DiagramImageItem::fromXml
|
||||||
|
Load this image from xml element e, its picture already decoded by
|
||||||
|
decodePicture().
|
||||||
|
*/
|
||||||
|
bool DiagramImageItem::fromXml(const QDomElement &e, Picture picture)
|
||||||
{
|
{
|
||||||
if (e.tagName() != "image") {
|
if (e.tagName() != "image") {
|
||||||
return (false);
|
return (false);
|
||||||
@@ -1796,28 +1882,17 @@ bool DiagramImageItem::fromXml(const QDomElement &e)
|
|||||||
const QUuid uuid(e.attribute(QStringLiteral("uuid")));
|
const QUuid uuid(e.attribute(QStringLiteral("uuid")));
|
||||||
if (!uuid.isNull() && uuid != m_uuid) setUuid(uuid);
|
if (!uuid.isNull() && uuid != m_uuid) setUuid(uuid);
|
||||||
|
|
||||||
QDomNode image_node = e.firstChild();
|
if (!picture.found) {
|
||||||
if (!image_node.isText()) {
|
|
||||||
return (false);
|
return (false);
|
||||||
}
|
}
|
||||||
|
const QByteArray array = picture.bytes;
|
||||||
|
|
||||||
//load xml image to QByteArray
|
//Set QPixmap from the decoded picture
|
||||||
QByteArray array;
|
QPixmap pixmap = QPixmap::fromImage(std::move(picture.image));
|
||||||
array = QByteArray::fromBase64(e.text().toLatin1());
|
|
||||||
|
|
||||||
//Set QPixmap from the array
|
|
||||||
QPixmap pixmap;
|
|
||||||
pixmap.loadFromData(array);
|
|
||||||
setPixmap(pixmap);
|
setPixmap(pixmap);
|
||||||
// From the first text node only, not e.text(): that also collects the
|
if (isPng(array) && !pixmap_.isNull())
|
||||||
// text of every child element, so on a cropped or colour-keyed picture
|
|
||||||
// it carries <image_base>'s bytes too. loadFromData() stops at the end
|
|
||||||
// of the first PNG and never notices; a cache written back verbatim
|
|
||||||
// would.
|
|
||||||
const QByteArray ownArray = QByteArray::fromBase64(image_node.toText().data().toLatin1());
|
|
||||||
if (isPng(ownArray) && !pixmap_.isNull())
|
|
||||||
{
|
{
|
||||||
m_png_cache = ownArray;
|
m_png_cache = array;
|
||||||
m_png_cache_key = pixmap_.cacheKey();
|
m_png_cache_key = pixmap_.cacheKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QFont>
|
#include <QFont>
|
||||||
|
#include <QImage>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
#include <QStyleOptionGraphicsItem>
|
#include <QStyleOptionGraphicsItem>
|
||||||
@@ -119,7 +120,20 @@ class DiagramImageItem : public QetGraphicsItem {
|
|||||||
*/
|
*/
|
||||||
int type() const override { return Type; }
|
int type() const override { return Type; }
|
||||||
|
|
||||||
|
/// A picture as read from its <image> element, before and after
|
||||||
|
/// decodePicture(), which may run outside the GUI thread.
|
||||||
|
struct Picture {
|
||||||
|
QString base64; ///< text of <image>, decoded when bytes is empty
|
||||||
|
QByteArray bytes; ///< the picture file (PNG, JPEG...)
|
||||||
|
QImage image; ///< bytes, decoded
|
||||||
|
bool found = true; ///< false : neither bytes nor text
|
||||||
|
};
|
||||||
|
static Picture pictureOf(const QDomElement &, const QByteArray &picture);
|
||||||
|
static void decodePicture(Picture &picture);
|
||||||
|
|
||||||
virtual bool fromXml(const QDomElement &);
|
virtual bool fromXml(const QDomElement &);
|
||||||
|
bool fromXml(const QDomElement &, const QByteArray &picture);
|
||||||
|
bool fromXml(const QDomElement &, Picture picture);
|
||||||
virtual QDomElement toXml(QDomDocument &) const;
|
virtual QDomElement toXml(QDomDocument &) const;
|
||||||
QUuid uuid() const {return m_uuid;}
|
QUuid uuid() const {return m_uuid;}
|
||||||
void setUuid(const QUuid &uuid) {m_uuid = uuid; emit uuidChanged();}
|
void setUuid(const QUuid &uuid) {m_uuid = uuid; emit uuidChanged();}
|
||||||
|
|||||||
@@ -1859,7 +1859,7 @@ void Element::setUuid(const QUuid &uuid)
|
|||||||
/**
|
/**
|
||||||
@brief Element::storeInformation
|
@brief Element::storeInformation
|
||||||
Write this symbol's information to its project's store, once it is
|
Write this symbol's information to its project's store, once it is
|
||||||
placed (DB-ACCESSORS-PLAN.md stage 4.1): every change of a placed
|
placed: every change of a placed
|
||||||
symbol's information comes through here.
|
symbol's information comes through here.
|
||||||
*/
|
*/
|
||||||
void Element::storeInformation()
|
void Element::storeInformation()
|
||||||
@@ -1925,7 +1925,7 @@ ElementData Element::elementData() const
|
|||||||
/**
|
/**
|
||||||
@brief Element::elementInformations
|
@brief Element::elementInformations
|
||||||
@return this symbol's information: for a placed symbol the project's
|
@return this symbol's information: for a placed symbol the project's
|
||||||
store holds it (DB-ACCESSORS-PLAN.md stage 4.2), for one not placed
|
store holds it, for one not placed
|
||||||
(a preview, the symbol editor) its own copy.
|
(a preview, the symbol editor) its own copy.
|
||||||
*/
|
*/
|
||||||
DiagramContext Element::elementInformations() const
|
DiagramContext Element::elementInformations() const
|
||||||
|
|||||||
@@ -1082,10 +1082,19 @@ void Terminal::setMasterLabelIndex(int index)
|
|||||||
@param terminal : to start search
|
@param terminal : to start search
|
||||||
@param all_diagram :if true return all related terminal,
|
@param all_diagram :if true return all related terminal,
|
||||||
false return only terminal in the same diagram of t
|
false return only terminal in the same diagram of t
|
||||||
|
@param ignore_function_separation : if true, a parent element whose
|
||||||
|
function separation (element information potential_isolating) is
|
||||||
|
enabled does not keep its terminals apart: they all count as one
|
||||||
|
potential, the way they do when the option is disabled. Only the
|
||||||
|
callers that need the conductors a separation hides -- so that the
|
||||||
|
one property still allowed to cross it, the voltage / protocol, can
|
||||||
|
be applied there (@see Conductor::functionSeparatedConductors) --
|
||||||
|
pass true; every other caller keeps the default.
|
||||||
@return the list of terminal at the same potential
|
@return the list of terminal at the same potential
|
||||||
*/
|
*/
|
||||||
QList<Terminal *> relatedPotentialTerminal (
|
QList<Terminal *> relatedPotentialTerminal (
|
||||||
const Terminal *terminal, const bool all_diagram)
|
const Terminal *terminal, const bool all_diagram,
|
||||||
|
const bool ignore_function_separation)
|
||||||
{
|
{
|
||||||
// If terminal parent element is a folio report.
|
// If terminal parent element is a folio report.
|
||||||
if (all_diagram && terminal -> parentElement() -> linkType() & Element::AllReport)
|
if (all_diagram && terminal -> parentElement() -> linkType() & Element::AllReport)
|
||||||
@@ -1110,14 +1119,18 @@ QList<Terminal *> relatedPotentialTerminal (
|
|||||||
// If terminal parent element is a Terminal element.
|
// If terminal parent element is a Terminal element.
|
||||||
else if (terminal -> parentElement() -> linkType() & Element::Terminale)
|
else if (terminal -> parentElement() -> linkType() & Element::Terminale)
|
||||||
{
|
{
|
||||||
// English: Check if the user activated the potential isolation checkbox for this terminal
|
// English: Check if the user activated the function separation
|
||||||
if (QET::infoFlagIsTrue(terminal->parentElement()->elementInformations().value(QStringLiteral("potential_isolating")).toString())) {
|
// checkbox for this terminal (the checkbox that used to be called
|
||||||
// English: Potential is isolated -- but terminals sharing this
|
// potential separation -- the stored flag is still
|
||||||
// terminal's own, non-empty potential-group identifier
|
// "potential_isolating" so existing elements keep their setting).
|
||||||
// (@see TerminalData::m_potential, set per-terminal in the
|
if (!ignore_function_separation
|
||||||
// element editor) still belong to the same physical terminal
|
&& QET::infoFlagIsTrue(terminal->parentElement()->elementInformations().value(QStringLiteral("potential_isolating")).toString())) {
|
||||||
|
// English: The terminals are function separated -- but terminals
|
||||||
|
// sharing this terminal's own, non-empty potential-group
|
||||||
|
// identifier (@see TerminalData::m_potential, set per-terminal in
|
||||||
|
// the element editor) still belong to the same physical terminal
|
||||||
// and stay linked to each other. An empty/unset potential on
|
// and stay linked to each other. An empty/unset potential on
|
||||||
// every terminal reproduces the previous, fully-isolated
|
// every terminal reproduces the previous, fully-separated
|
||||||
// behavior exactly, so existing elements are unaffected.
|
// behavior exactly, so existing elements are unaffected.
|
||||||
const QString this_potential = terminal->potential();
|
const QString this_potential = terminal->potential();
|
||||||
QList<Terminal *> same_potential;
|
QList<Terminal *> same_potential;
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ inline int Terminal::conductorsCount() const
|
|||||||
}
|
}
|
||||||
|
|
||||||
QList<Terminal *> relatedPotentialTerminal (const Terminal *terminal,
|
QList<Terminal *> relatedPotentialTerminal (const Terminal *terminal,
|
||||||
const bool all_diagram = true);
|
const bool all_diagram = true,
|
||||||
|
const bool ignore_function_separation = false);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+64
-14
@@ -46,6 +46,7 @@
|
|||||||
#include "qetversion.h"
|
#include "qetversion.h"
|
||||||
#include "undocommand/adddiagramcommand.h"
|
#include "undocommand/adddiagramcommand.h"
|
||||||
|
|
||||||
|
#include <QCryptographicHash>
|
||||||
#include <QElapsedTimer>
|
#include <QElapsedTimer>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
@@ -278,6 +279,20 @@ QUuid QETProject::uuid() const
|
|||||||
return m_uuid;
|
return m_uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief QETProject::containerPicture
|
||||||
|
The folio pictures of a .qetz are kept as bytes beside the document,
|
||||||
|
not as base64 text in it (QetContainer::join()), for as long as the
|
||||||
|
project is open: a folio built after opening still finds its pictures.
|
||||||
|
They cost almost no memory, sharing their bytes with the pictures
|
||||||
|
built from them.
|
||||||
|
@return the picture in @p file, or an empty array when there is none
|
||||||
|
*/
|
||||||
|
QByteArray QETProject::containerPicture(const QString &file) const
|
||||||
|
{
|
||||||
|
return m_container_pictures.value(file);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@brief QETProject::derivedItemUuid
|
@brief QETProject::derivedItemUuid
|
||||||
A uuid for an item of this project that was saved without one, the same
|
A uuid for an item of this project that was saved without one, the same
|
||||||
@@ -398,11 +413,25 @@ QUuid QETProject::derivedUuid(const QByteArray &content)
|
|||||||
static const QUuid project_namespace(
|
static const QUuid project_namespace(
|
||||||
QStringLiteral("{c8c75719-0fea-4b1c-9f4b-2dd179fb2f0c}"));
|
QStringLiteral("{c8c75719-0fea-4b1c-9f4b-2dd179fb2f0c}"));
|
||||||
|
|
||||||
QByteArray normalized(content);
|
//Same result as QUuid::createUuidV5(project_namespace,
|
||||||
normalized.replace('\r', QByteArray());
|
//"qet-project-legacy\n" + content without '\r'), hashed in place :
|
||||||
return QUuid::createUuidV5(project_namespace,
|
//copying the content twice took 2 s of opening a 263 MB project.
|
||||||
QByteArrayLiteral("qet-project-legacy\n")
|
QCryptographicHash hash(QCryptographicHash::Sha1);
|
||||||
+ normalized);
|
hash.addData(project_namespace.toRfc4122());
|
||||||
|
hash.addData(QByteArrayView("qet-project-legacy\n"));
|
||||||
|
const QByteArrayView view(content);
|
||||||
|
qsizetype start = 0;
|
||||||
|
while (start < view.size()) {
|
||||||
|
qsizetype cr = view.indexOf('\r', start);
|
||||||
|
if (cr < 0)
|
||||||
|
cr = view.size();
|
||||||
|
hash.addData(view.sliced(start, cr - start));
|
||||||
|
start = cr + 1;
|
||||||
|
}
|
||||||
|
QByteArray bytes = hash.result().left(16);
|
||||||
|
bytes[6] = char((bytes[6] & 0x0F) | 0x50); // version 5
|
||||||
|
bytes[8] = char((bytes[8] & 0x3F) | 0x80); // RFC 4122 variant
|
||||||
|
return QUuid::fromRfc4122(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -456,9 +485,12 @@ QETProject::ProjectState QETProject::openFile(QFile *file)
|
|||||||
// QetContainer::parse() does exactly that, for a .qet and for each part
|
// QetContainer::parse() does exactly that, for a .qet and for each part
|
||||||
// of a .qetz alike.
|
// of a .qetz alike.
|
||||||
QString read_error;
|
QString read_error;
|
||||||
if (!QetContainer::readDocument(content, &xml_project, &read_error))
|
int container_format = 0;
|
||||||
|
if (!QetContainer::readDocument(content, &xml_project, &read_error, &m_container_pictures,
|
||||||
|
&container_format))
|
||||||
{
|
{
|
||||||
qWarning().noquote() << "Unable to read" << fi.fileName() << ":" << read_error;
|
qWarning().noquote() << "Unable to read" << fi.fileName() << ":" << read_error;
|
||||||
|
m_container_pictures.clear();
|
||||||
if(opened_here) {
|
if(opened_here) {
|
||||||
file->close();
|
file->close();
|
||||||
}
|
}
|
||||||
@@ -489,9 +521,16 @@ QETProject::ProjectState QETProject::openFile(QFile *file)
|
|||||||
<< xml_parse_ms / 1000.0 << ", content "
|
<< xml_parse_ms / 1000.0 << ", content "
|
||||||
<< (total_ms - xml_parse_ms) / 1000.0 << ")";
|
<< (total_ms - xml_parse_ms) / 1000.0 << ")";
|
||||||
|
|
||||||
if (!fi.isWritable()) {
|
//Written by a later QElectroTech that lets this one read it: saving
|
||||||
|
//here would drop what this version does not know
|
||||||
|
m_from_later_version = container_format > QetContainer::Format;
|
||||||
|
if (!fi.isWritable() || m_from_later_version) {
|
||||||
setReadOnly(true);
|
setReadOnly(true);
|
||||||
}
|
}
|
||||||
|
if (m_from_later_version) {
|
||||||
|
qInfo().noquote() << QStringLiteral("Project %1 is of .qetz format %2 (this version writes %3): opened read-only")
|
||||||
|
.arg(fi.fileName()).arg(container_format).arg(QetContainer::Format);
|
||||||
|
}
|
||||||
if(opened_here) {
|
if(opened_here) {
|
||||||
file->close();
|
file->close();
|
||||||
}
|
}
|
||||||
@@ -544,7 +583,7 @@ QETProject::ProjectState QETProject::state() const
|
|||||||
QList<Diagram *> QETProject::diagrams() const
|
QList<Diagram *> QETProject::diagrams() const
|
||||||
{
|
{
|
||||||
//Every caller of diagrams() expects every folio built: those that
|
//Every caller of diagrams() expects every folio built: those that
|
||||||
//do not are moved to folios() one by one (LAZY-FOLIO-PLAN.md)
|
//do not are moved to folios() one by one
|
||||||
loadFolios();
|
loadFolios();
|
||||||
return(m_diagrams_list);
|
return(m_diagrams_list);
|
||||||
}
|
}
|
||||||
@@ -2291,8 +2330,7 @@ QDomDocument QETProject::toXml(QHash<int, QByteArray> *kept_folios)
|
|||||||
loadFolios();
|
loadFolios();
|
||||||
}
|
}
|
||||||
//QET_CHECK_ELEMENT_INFO_STORE=1: say whether the stores of symbol
|
//QET_CHECK_ELEMENT_INFO_STORE=1: say whether the stores of symbol
|
||||||
//information, folio title blocks and wire properties
|
//information, folio title blocks and wire properties agree with every placed symbol, folio and
|
||||||
//(DB-ACCESSORS-PLAN.md) agree with every placed symbol, folio and
|
|
||||||
//wire, at each save. A test and debugging aid; it changes nothing.
|
//wire, at each save. A test and debugging aid; it changes nothing.
|
||||||
if (qEnvironmentVariableIntValue("QET_CHECK_ELEMENT_INFO_STORE") == 1) {
|
if (qEnvironmentVariableIntValue("QET_CHECK_ELEMENT_INFO_STORE") == 1) {
|
||||||
const QStringList mismatches = m_data_base.elementInformationMismatches();
|
const QStringList mismatches = m_data_base.elementInformationMismatches();
|
||||||
@@ -3015,9 +3053,22 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
|
|||||||
|
|
||||||
//The stores of symbol information, folio title blocks and wire
|
//The stores of symbol information, folio title blocks and wire
|
||||||
//properties are filled from the document before the folios are
|
//properties are filled from the document before the folios are
|
||||||
//built (DB-ACCESSORS-PLAN.md stage 4.6)
|
//built
|
||||||
m_data_base.prefillFromDocument(xml_project);
|
m_data_base.prefillFromDocument(xml_project);
|
||||||
|
|
||||||
|
//QET_LAZY_FOLIOS leaves the folios to be built when needed only
|
||||||
|
//when the database can be filled from the document: otherwise
|
||||||
|
//filling it from the folios builds every one of them anyway, late
|
||||||
|
//and more slowly than reading them now
|
||||||
|
m_lazy_open = qEnvironmentVariableIntValue("QET_LAZY_FOLIOS") == 1;
|
||||||
|
if (m_lazy_open) {
|
||||||
|
const QString why = projectDataBase::documentRefusal(xml_project);
|
||||||
|
if (!why.isEmpty()) {
|
||||||
|
qInfo().noquote() << "Project opened with every folio built:" << why;
|
||||||
|
m_lazy_open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Load the diagrams
|
//Load the diagrams
|
||||||
readDiagramsXml(xml_project);
|
readDiagramsXml(xml_project);
|
||||||
const qint64 diagrams_ms = phase_timer.restart();
|
const qint64 diagrams_ms = phase_timer.restart();
|
||||||
@@ -3057,7 +3108,6 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
|
|||||||
//number can hold what a save rewrites (tst_freetextnonfinite,
|
//number can hold what a save rewrites (tst_freetextnonfinite,
|
||||||
//tst_foliofreezeflags): only with QET_LAZY_SAVE_KEPT=1, until a
|
//tst_foliofreezeflags): only with QET_LAZY_SAVE_KEPT=1, until a
|
||||||
//.qetz, whose manifest names its writer, does it
|
//.qetz, whose manifest names its writer, does it
|
||||||
//(QETZ-DB-TRUTH-PLAN.md D4)
|
|
||||||
m_kept_folios_trusted = qEnvironmentVariableIntValue("QET_LAZY_SAVE_KEPT") == 1
|
m_kept_folios_trusted = qEnvironmentVariableIntValue("QET_LAZY_SAVE_KEPT") == 1
|
||||||
&& unloadedFolioCount() > 0
|
&& unloadedFolioCount() > 0
|
||||||
&& m_project_qet_version == QetVersion::currentVersion()
|
&& m_project_qet_version == QetVersion::currentVersion()
|
||||||
@@ -3134,7 +3184,7 @@ void QETProject::readDiagramsXml(QDomDocument &xml_project)
|
|||||||
//Where each line of the file starts, for a folio's text: lines
|
//Where each line of the file starts, for a folio's text: lines
|
||||||
//[its start, the next element's start) (QETProject::toXml())
|
//[its start, the next element's start) (QETProject::toXml())
|
||||||
QList<qsizetype> line_starts;
|
QList<qsizetype> line_starts;
|
||||||
if (qEnvironmentVariableIntValue("QET_LAZY_FOLIOS") == 1 && !m_opened_content.isEmpty()) {
|
if (m_lazy_open && !m_opened_content.isEmpty()) {
|
||||||
line_starts << 0;
|
line_starts << 0;
|
||||||
for (qsizetype i = 0 ; i < m_opened_content.size() ; ++i)
|
for (qsizetype i = 0 ; i < m_opened_content.size() ; ++i)
|
||||||
if (m_opened_content.at(i) == '\n') line_starts << i + 1;
|
if (m_opened_content.at(i) == '\n') line_starts << i + 1;
|
||||||
@@ -3181,7 +3231,7 @@ void QETProject::readDiagramsXml(QDomDocument &xml_project)
|
|||||||
connect(diagram, &Diagram::usedTitleBlockTemplateChanged,
|
connect(diagram, &Diagram::usedTitleBlockTemplateChanged,
|
||||||
this, &QETProject::usedTitleBlockTemplateChanged);
|
this, &QETProject::usedTitleBlockTemplateChanged);
|
||||||
|
|
||||||
m_deferring_folios = qEnvironmentVariableIntValue("QET_LAZY_FOLIOS") == 1;
|
m_deferring_folios = m_lazy_open;
|
||||||
diagram->initFromXml(diagram_xml_element);
|
diagram->initFromXml(diagram_xml_element);
|
||||||
m_deferring_folios = false;
|
m_deferring_folios = false;
|
||||||
if (!diagram->isLoaded() && !line_starts.isEmpty())
|
if (!diagram->isLoaded() && !line_starts.isEmpty())
|
||||||
|
|||||||
+11
-1
@@ -119,10 +119,11 @@ class QETProject : public QObject
|
|||||||
projectDataBase *dataBase();
|
projectDataBase *dataBase();
|
||||||
QUuid uuid() const;
|
QUuid uuid() const;
|
||||||
QUuid derivedItemUuid(const QString &kind, const QString &key);
|
QUuid derivedItemUuid(const QString &kind, const QString &key);
|
||||||
|
QByteArray containerPicture(const QString &file) const;
|
||||||
ProjectState state() const;
|
ProjectState state() const;
|
||||||
QList<Diagram *> diagrams() const;
|
QList<Diagram *> diagrams() const;
|
||||||
//The folios whether their items are built or not
|
//The folios whether their items are built or not
|
||||||
//(QET_LAZY_FOLIOS, LAZY-FOLIO-PLAN.md); diagrams() builds them
|
//(QET_LAZY_FOLIOS); diagrams() builds them
|
||||||
QList<Diagram *> folios() const;
|
QList<Diagram *> folios() const;
|
||||||
bool deferringFolios() const {return m_deferring_folios;}
|
bool deferringFolios() const {return m_deferring_folios;}
|
||||||
int unloadedFolioCount() const;
|
int unloadedFolioCount() const;
|
||||||
@@ -318,6 +319,8 @@ class QETProject : public QObject
|
|||||||
bool close();
|
bool close();
|
||||||
QETResult write();
|
QETResult write();
|
||||||
bool isReadOnly() const;
|
bool isReadOnly() const;
|
||||||
|
/// Opened from a .qetz of a later format (opened read-only)
|
||||||
|
bool isFromLaterVersion() const {return m_from_later_version;}
|
||||||
void setReadOnly(bool);
|
void setReadOnly(bool);
|
||||||
bool isEmpty() const;
|
bool isEmpty() const;
|
||||||
ElementsLocation importElement(ElementsLocation &location);
|
ElementsLocation importElement(ElementsLocation &location);
|
||||||
@@ -448,6 +451,9 @@ class QETProject : public QObject
|
|||||||
static bool m_backup_enabled;
|
static bool m_backup_enabled;
|
||||||
/// Something changed since the last backup, see writeBackup()
|
/// Something changed since the last backup, see writeBackup()
|
||||||
bool m_backup_needed = true;
|
bool m_backup_needed = true;
|
||||||
|
/// The pictures of the .qetz this project was opened from, see
|
||||||
|
/// containerPicture()
|
||||||
|
QHash<QString, QByteArray> m_container_pictures;
|
||||||
/// File path this project is saved to
|
/// File path this project is saved to
|
||||||
QString m_file_path;
|
QString m_file_path;
|
||||||
/// Current state of the project
|
/// Current state of the project
|
||||||
@@ -462,6 +468,7 @@ class QETProject : public QObject
|
|||||||
bool m_modified = false;
|
bool m_modified = false;
|
||||||
/// Whether the project is read only
|
/// Whether the project is read only
|
||||||
bool m_read_only = false;
|
bool m_read_only = false;
|
||||||
|
bool m_from_later_version = false;
|
||||||
/// Filepath for which this project is considered read only
|
/// Filepath for which this project is considered read only
|
||||||
QString read_only_file_path_;
|
QString read_only_file_path_;
|
||||||
/// Default dimensions and properties for new diagrams created within the project
|
/// Default dimensions and properties for new diagrams created within the project
|
||||||
@@ -512,6 +519,9 @@ class QETProject : public QObject
|
|||||||
const DiagramContext &information,
|
const DiagramContext &information,
|
||||||
const QHash<QString, QStringList> &titles_by_formula) const;
|
const QHash<QString, QStringList> &titles_by_formula) const;
|
||||||
bool m_deferring_folios = false;
|
bool m_deferring_folios = false;
|
||||||
|
/// Opening with folios built only when needed: QET_LAZY_FOLIOS
|
||||||
|
/// is set and the database can be filled from the document
|
||||||
|
bool m_lazy_open = false;
|
||||||
/// Saving may write a folio not built as it was read
|
/// Saving may write a folio not built as it was read
|
||||||
/// (writesKeptFolios()): the file is this version's, its
|
/// (writesKeptFolios()): the file is this version's, its
|
||||||
/// database rows came from it, and nothing project-wide that
|
/// database rows came from it, and nothing project-wide that
|
||||||
|
|||||||
@@ -1189,6 +1189,10 @@ QString QetScriptApi::conductorProperty(int folioIndex, const QString &elementUu
|
|||||||
describes a potential and not one drawn segment. Setting it on one
|
describes a potential and not one drawn segment. Setting it on one
|
||||||
conductor and leaving the rest of the potential disagreeing would
|
conductor and leaving the rest of the potential disagreeing would
|
||||||
produce a file no GUI action could have produced.
|
produce a file no GUI action could have produced.
|
||||||
|
|
||||||
|
One thing stops at a terminal element with its function separation
|
||||||
|
enabled: every property but the voltage / protocol, which still
|
||||||
|
crosses it (@see Conductor::functionSeparatedConductors).
|
||||||
@return true if anything was changed, or if it already held that value
|
@return true if anything was changed, or if it already held that value
|
||||||
*/
|
*/
|
||||||
bool QetScriptApi::setConductorProperty(int folioIndex, const QString &elementUuid,
|
bool QetScriptApi::setConductorProperty(int folioIndex, const QString &elementUuid,
|
||||||
@@ -1209,13 +1213,17 @@ bool QetScriptApi::setConductorProperty(int folioIndex, const QString &elementUu
|
|||||||
Conductor *conductor = findConductor(folioIndex, elementUuid, terminalIndex, caller);
|
Conductor *conductor = findConductor(folioIndex, elementUuid, terminalIndex, caller);
|
||||||
if (!conductor) return false;
|
if (!conductor) return false;
|
||||||
|
|
||||||
ConductorProperties properties = conductor->properties();
|
//Captured before the pushes below: QPropertyUndoCommand applies on
|
||||||
|
//push, so reading the properties afterwards would already give back
|
||||||
|
//@a properties.
|
||||||
|
const ConductorProperties before = conductor->properties();
|
||||||
|
ConductorProperties properties = before;
|
||||||
if (!setConductorPropertyValue(properties, property, value)) {
|
if (!setConductorPropertyValue(properties, property, value)) {
|
||||||
log(QStringLiteral("qet.%1: '%2' is not a valid value for %3")
|
log(QStringLiteral("qet.%1: '%2' is not a valid value for %3")
|
||||||
.arg(caller, value, property));
|
.arg(caller, value, property));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (properties == conductor->properties()) return true; // already so
|
if (properties == before) return true; // already so
|
||||||
|
|
||||||
QSet<Conductor *> potential = conductor->relatedPotentialConductors(true);
|
QSet<Conductor *> potential = conductor->relatedPotentialConductors(true);
|
||||||
potential << conductor;
|
potential << conductor;
|
||||||
@@ -1227,6 +1235,25 @@ bool QetScriptApi::setConductorProperty(int folioIndex, const QString &elementUu
|
|||||||
old_value.setValue(c->properties());
|
old_value.setValue(c->properties());
|
||||||
new_value.setValue(properties);
|
new_value.setValue(properties);
|
||||||
m_project->undoStack()->push(new QPropertyUndoCommand(c, "properties", old_value, new_value));
|
m_project->undoStack()->push(new QPropertyUndoCommand(c, "properties", old_value, new_value));
|
||||||
|
}
|
||||||
|
//The other side of a function separated terminal element gets the
|
||||||
|
//voltage / protocol only: it is the one property that still
|
||||||
|
//describes the electrical node rather than one side of it. And it
|
||||||
|
//only arrives when this call really changed it -- before / after
|
||||||
|
//is the edit, not the far wire's own value, which differs from it
|
||||||
|
//just as often as not and would then be overwritten by every call,
|
||||||
|
//colour included.
|
||||||
|
for (Conductor *c : conductor->functionSeparatedConductors(true))
|
||||||
|
{
|
||||||
|
ConductorProperties separated_properties = c->properties();
|
||||||
|
separated_properties.applyTensionProtocolChange(before, properties);
|
||||||
|
if (separated_properties == c->properties())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
QVariant old_value, new_value;
|
||||||
|
old_value.setValue(c->properties());
|
||||||
|
new_value.setValue(separated_properties);
|
||||||
|
m_project->undoStack()->push(new QPropertyUndoCommand(c, "properties", old_value, new_value));
|
||||||
}
|
}
|
||||||
m_project->undoStack()->endMacro();
|
m_project->undoStack()->endMacro();
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -88,6 +88,37 @@ void ConductorPropertiesDialog::PropertiesDialog(Conductor *conductor,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//A function separated terminal element (element information
|
||||||
|
//potential_isolating) keeps the two sides of the terminal apart:
|
||||||
|
//every property but one stays local to the side it was edited on.
|
||||||
|
//The voltage / protocol still crosses, because it describes the
|
||||||
|
//electrical node and not one side of it.
|
||||||
|
if (cpd.applyAll())
|
||||||
|
{
|
||||||
|
//Only an edit that really changed the voltage / protocol passes
|
||||||
|
//it on, so the comparison is the edit itself -- what the dialog
|
||||||
|
//was filled with, against what the user left in it -- and never
|
||||||
|
//the far wire's own value, which differs from it just as often
|
||||||
|
//as not (a 24 V side against a 0 V one, say) and would then be
|
||||||
|
//overwritten by every edit, voltage included.
|
||||||
|
const ConductorProperties shown = conductor->properties();
|
||||||
|
foreach (Conductor *separated_conductor, conductor->functionSeparatedConductors())
|
||||||
|
{
|
||||||
|
ConductorProperties separated_properties = separated_conductor->properties();
|
||||||
|
separated_properties.applyTensionProtocolChange(
|
||||||
|
shown,
|
||||||
|
cpd.properties());
|
||||||
|
if (separated_properties == separated_conductor->properties())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
QVariant separated_old_value, separated_new_value;
|
||||||
|
separated_old_value.setValue(separated_conductor->properties());
|
||||||
|
separated_new_value.setValue(separated_properties);
|
||||||
|
new QPropertyUndoCommand (separated_conductor, "properties",
|
||||||
|
separated_old_value, separated_new_value, undo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
conductor->diagram()->undoStack().push(undo);
|
conductor->diagram()->undoStack().push(undo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -346,7 +346,10 @@ void ConductorPropertiesEditorWidget::updateUi()
|
|||||||
When "apply to all" is ticked, every conductor on the same potential as
|
When "apply to all" is ticked, every conductor on the same potential as
|
||||||
an edited one is updated too, in the same undo step (one undo reverts
|
an edited one is updated too, in the same undo step (one undo reverts
|
||||||
them all), as the modal dialog does
|
them all), as the modal dialog does
|
||||||
(ConductorPropertiesDialog::PropertiesDialog).
|
(ConductorPropertiesDialog::PropertiesDialog). A terminal element with
|
||||||
|
its function separation enabled is not crossed by any of that -- its
|
||||||
|
other side keeps its own text, function, color... -- but the voltage /
|
||||||
|
protocol still crosses (@see Conductor::functionSeparatedConductors).
|
||||||
*/
|
*/
|
||||||
QUndoCommand *ConductorPropertiesEditorWidget::associatedUndo() const
|
QUndoCommand *ConductorPropertiesEditorWidget::associatedUndo() const
|
||||||
{
|
{
|
||||||
@@ -360,10 +363,12 @@ QUndoCommand *ConductorPropertiesEditorWidget::associatedUndo() const
|
|||||||
if (c)
|
if (c)
|
||||||
selected << c.data();
|
selected << c.data();
|
||||||
|
|
||||||
return ConductorMultiEdit::undo(
|
QSet<Conductor *> function_separated;
|
||||||
ConductorMultiEdit::targets(
|
const QList<Conductor *> targets = ConductorMultiEdit::targets(
|
||||||
selected, m_apply_all_cb && m_apply_all_cb->isChecked()),
|
selected, m_apply_all_cb && m_apply_all_cb->isChecked(),
|
||||||
m_shown, edited);
|
&function_separated);
|
||||||
|
|
||||||
|
return ConductorMultiEdit::undo(targets, m_shown, edited, function_separated);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -313,8 +313,9 @@ void ElementInfoWidget::buildInterface()
|
|||||||
|
|
||||||
ui->scroll_vlayout->addStretch();
|
ui->scroll_vlayout->addStretch();
|
||||||
|
|
||||||
// Existing potential isolating checkbox
|
// Existing function separation checkbox (stored as the
|
||||||
m_potential_isolating_cb = new QCheckBox(tr("Potential separation"), this);
|
// "potential_isolating" element information)
|
||||||
|
m_potential_isolating_cb = new QCheckBox(tr("Function separation"), this);
|
||||||
m_potential_isolating_cb->setStyleSheet(QStringLiteral("margin: 5px; font-weight: bold;"));
|
m_potential_isolating_cb->setStyleSheet(QStringLiteral("margin: 5px; font-weight: bold;"));
|
||||||
|
|
||||||
// English: Initialize and style the BOM exclusion checkbox
|
// English: Initialize and style the BOM exclusion checkbox
|
||||||
|
|||||||
@@ -271,6 +271,19 @@ qet_add_app_test(tst_rotateselectionimage tst_rotateselectionimage.cpp)
|
|||||||
# it selected and records no undo step (#1414); the key goes to the view.
|
# it selected and records no undo step (#1414); the key goes to the view.
|
||||||
qet_add_app_test(tst_imagehandleescape tst_imagehandleescape.cpp)
|
qet_add_app_test(tst_imagehandleescape tst_imagehandleescape.cpp)
|
||||||
|
|
||||||
|
# A folio's pictures, decoded in parallel, each come back with its own
|
||||||
|
# pixels and position; an <image> with no picture does not stop the others.
|
||||||
|
qet_add_app_test(tst_folioimageload tst_folioimageload.cpp)
|
||||||
|
|
||||||
|
# SVG export with smaller pictures (--svg-small-pictures, the export dialog's
|
||||||
|
# option): a picture drawn at a tenth of its size is stored at four times
|
||||||
|
# that, a small one whole; without the option every picture is whole.
|
||||||
|
qet_add_app_test(tst_svgsmallpictures tst_svgsmallpictures.cpp)
|
||||||
|
add_dependencies(tst_svgsmallpictures qelectrotech)
|
||||||
|
target_link_libraries(tst_svgsmallpictures PRIVATE Qt::Svg)
|
||||||
|
target_compile_definitions(tst_svgsmallpictures PRIVATE
|
||||||
|
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||||
|
|
||||||
# TextResizeHandles: the corner handles that change the width of a text box,
|
# TextResizeHandles: the corner handles that change the width of a text box,
|
||||||
# on a plain QGraphicsTextItem with the "textWidth" property QET texts have.
|
# on a plain QGraphicsTextItem with the "textWidth" property QET texts have.
|
||||||
add_executable(
|
add_executable(
|
||||||
@@ -1253,4 +1266,6 @@ add_test(NAME tst_qetcontainer COMMAND tst_qetcontainer)
|
|||||||
target_include_directories(tst_qetcontainer PRIVATE ${QET_DIR}/sources)
|
target_include_directories(tst_qetcontainer PRIVATE ${QET_DIR}/sources)
|
||||||
target_link_libraries(tst_qetcontainer PRIVATE Qt::Test Qt::Xml SQLite3::SQLite3)
|
target_link_libraries(tst_qetcontainer PRIVATE Qt::Test Qt::Xml SQLite3::SQLite3)
|
||||||
target_compile_definitions(tst_qetcontainer PRIVATE
|
target_compile_definitions(tst_qetcontainer PRIVATE
|
||||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\""
|
||||||
|
"QET_FIXTURES_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/fixtures\""
|
||||||
|
"QET_FORMAT_SPEC=\"${QET_DIR}/sources/container/QETZ-FORMAT.md\"")
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,41 @@
|
|||||||
|
<project title="A large picture shown small" uuid="{3f6a2c91-8d47-4b5e-a0c3-91e2d7b4f600}" version="0.200.1">
|
||||||
|
<properties>
|
||||||
|
<property name="saveddate" show="1">10/2/26</property>
|
||||||
|
<property name="saveddate-eu" show="1">02-10-2026</property>
|
||||||
|
<property name="saveddate-us" show="1">2026-10-02</property>
|
||||||
|
<property name="savedfilename" show="1">svg_small_pictures</property>
|
||||||
|
<property name="savedtime" show="1">18:39</property>
|
||||||
|
</properties>
|
||||||
|
<usage enabled="true" time_spent="0"/>
|
||||||
|
<newdiagrams>
|
||||||
|
<border cols="17" colsize="60" displaycols="true" displayrows="true" rows="8" rowsize="80"/>
|
||||||
|
<inset author="" auto_page_num="" date="null" displayAt="bottom" filename="" folio="" indexrev="" locmach="" plant="" title="" version=""/>
|
||||||
|
<conductors bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||||
|
<report label="%f-%l%c"/>
|
||||||
|
<xrefs>
|
||||||
|
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="coil" xrefpos="AlignBottom"/>
|
||||||
|
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="commutator" xrefpos="AlignBottom"/>
|
||||||
|
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="plc" xrefpos="AlignBottom"/>
|
||||||
|
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="protection" xrefpos="AlignBottom"/>
|
||||||
|
</xrefs>
|
||||||
|
<conductors_autonums auto_break_conductors="false" current_autonum="" freeze_new_conductors="false"/>
|
||||||
|
<folio_autonums/>
|
||||||
|
<element_autonums current_autonum="" freeze_new_elements="false"/>
|
||||||
|
<guides/>
|
||||||
|
</newdiagrams>
|
||||||
|
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="660" indexrev="" locmach="" order="1" plant="" rows="8" rowsize="80" title="" uuid="{3f6a2c91-8d47-4b5e-a0c3-91e2d7b4f610}" version="0.200.1-dev">
|
||||||
|
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||||
|
<inputs/>
|
||||||
|
<images>
|
||||||
|
<image z="0" rotation="0" y="100" x="100" size="0.1" is_movable="1">iVBORw0KGgoAAAANSUhEUgAAAyAAAAJYCAIAAAAVFBUnAAAK8ElEQVR42u3ZMQ7CQBAEwTPi4TydiMCxOxihqogI6RyNeq9zPufnevbjP/7Bc3wQz/FBPMcH8Rwf5OE/vA4AACkDCwAg9r7lMAAAHlOwAABiChYAQEzBAgAwsAAAtjkRAgDEFCwAgJiCBQAQU7AAAGIKFgBATMECADCwAAC2ORECAMQULACAmIIFABBTsAAAYgoWAEBMwQIAMLAAALY5EQIAxBQsAICYggUAEFOwAABiChYAQEzBAgAwsAAAtjkRAgDEFCwAgJiCBQAQU7AAAGIKFgBATMECADCwAAC2ORECAMQULACAmIIFABBTsAAAYgoWAEBMwQIAMLAAALY5EQIAxBQsAICYggUAEFOwAAAMLACAbU6EAAAxBQsAIKZgAQDEFCwAgJiCBQAQU7AAAAwsAIBtToQAADEFCwAgpmABAMQULACAmIIFABBTsAAADCwAgG1OhAAAMQULACCmYAEAxBQsAICYggUAEFOwAAAMLACAbU6EAAAxBQsAIKZgAQDEFCwAgJiCBQAQU7AAAAwsAIBtToQAADEFCwAgpmABAMQULACAmIIFABBTsAAADCwAgG1OhAAAMQULACCmYAEAxBQsAAADCwBgmxMhAEBMwQIAiClYAAAxBQsAIKZgAQDEFCwAAAMLAGCbEyEAQEzBAgCIKVgAADEFCwAgpmABAMQULAAAAwsAYJsTIQBATMECAIgpWAAAMQULACCmYAEAxBQsAAADCwBgmxMhAEBMwQIAiClYAAAxBQsAIKZgAQDEFCwAAAMLAGCbEyEAQEzBAgCIKVgAADEFCwAgpmABAMQULAAAAwsAYJsTIQBATMECAIgpWAAAMQULAMDAAgDY5kQIABBTsAAAYgoWAEBMwQIAiClYAAAxBQsAwMACANjmRAgAEFOwAABiChYAQEzBAgCIKVgAADEFCwDAwAIA2OZECAAQU7AAAGIKFgBATMECAIgpWAAAMQULAMDAAgDY5kQIABBTsAAAYgoWAEBMwQIAiClYAAAxBQsAwMACANjmRAgAEFOwAABiChYAQEzBAgCIKVgAADEFCwDAwAIA2OZECAAQU7AAAGIKFgBATMECADCwAAC2ORECAMQULACAmIIFABBTsAAAYgoWAEBMwQIAMLAAALY5EQIAxBQsAICYggUAEFOwAABiChYAQEzBAgAwsAAAtjkRAgDEFCwAgJiCBQAQU7AAAGIKFgBATMECADCwAAC2ORECAMQULACAmIIFABBTsAAAYgoWAEBMwQIAMLAAALY5EQIAxBQsAICYggUAEFOwAABiChYAQEzBAgAwsAAAtjkRAgDEFCwAgJiCBQAQU7AAAAwsAIBtToQAADEFCwAgpmABAMQULACAmIIFABBTsAAADCwAgG1OhAAAMQULACCmYAEAxBQsAICYggUAEFOwAAAMLACAbU6EAAAxBQsAIKZgAQDEFCwAgJiCBQAQU7AAAAwsAIBtToQAADEFCwAgpmABAMQULACAmIIFABBTsAAADCwAgG1OhAAAMQULACCmYAEAxBQsAICYggUAEFOwAAAMLACAbU6EAAAxBQsAIKZgAQDEFCwAAAMLAGCbEyEAQEzBAgCIKVgAADEFCwAgpmABAMQULAAAAwsAYJsTIQBATMECAIgpWAAAMQULACCmYAEAxBQsAAADCwBgmxMhAEBMwQIAiClYAAAxBQsAIKZgAQDEFCwAAAMLAGCbEyEAQEzBAgCIKVgAADEFCwAgpmABAMQULAAAAwsAYJsTIQBATMECAIgpWAAAMQULACCmYAEAxBQsAAADCwBgmxMhAEBMwQIAiClYAAAxBQsAwMACANjmRAgAEFOwAABiChYAQEzBAgCIKVgAADEFCwDAwAIA2OZECAAQU7AAAGIKFgBATMECAIgpWAAAMQULAMDAAgDY5kQIABBTsAAAYgoWAEBMwQIAiClYAAAxBQsAwMACANjmRAgAEFOwAABiChYAQEzBAgCIKVgAADEFCwDAwAIA2OZECAAQU7AAAGIKFgBATMECAIgpWAAAMQULAMDAAgDY5kQIABBTsAAAYgoWAEBMwQIAMLAAALY5EQIAxBQsAICYggUAEFOwAABiChYAQEzBAgAwsAAAtjkRAgDEFCwAgJiCBQAQU7AAAGIKFgBATMECADCwAAC2ORECAMQULACAmIIFABBTsAAAYgoWAEBMwQIAMLAAALY5EQIAxBQsAICYggUAEFOwAABiChYAQEzBAgAwsAAAtjkRAgDEFCwAgJiCBQAQU7AAAGIKFgBATMECADCwAAC2ORECAMQULACAmIIFABBTsAAADCwAgG1OhAAAMQULACCmYAEAxBQsAICYggUAEFOwAAAMLACAbU6EAAAxBQsAIKZgAQDEFCwAgJiCBQAQU7AAAAwsAIBtToQAADEFCwAgpmABAMQULACAmIIFABBTsAAADCwAgG1OhAAAMQULACCmYAEAxBQsAICYggUAEFOwAAAMLACAbU6EAAAxBQsAIKZgAQDEFCwAgJiCBQAQU7AAAAwsAIBtToQAADEFCwAgpmABAMQULAAAAwsAYJsTIQBATMECAIgpWAAAMQULACCmYAEAxBQsAAADCwBgmxMhAEBMwQIAiClYAAAxBQsAIKZgAQDEFCwAAAMLAGCbEyEAQEzBAgCIKVgAADEFCwAgpmABAMQULAAAAwsAYJsTIQBATMECAIgpWAAAMQULACCmYAEAxBQsAAADCwBgmxMhAEBMwQIAiClYAAAxBQsAIKZgAQDEFCwAAAMLAGCbEyEAQEzBAgCIKVgAADEFCwDAwAIA2OZECAAQU7AAAGIKFgBATMECAIgpWAAAMQULAMDAAgDY5kQIABBTsAAAYgoWAEBMwQIAiClYAAAxBQsAwMACANjmRAgAEFOwAABiChYAQEzBAgCIKVgAADEFCwDAwAIA2OZECAAQU7AAAGIKFgBATMECAIgpWAAAMQULAMDAAgDY5kQIABBTsAAAYgoWAEBMwQIAiClYAAAxBQsAwMACANjmRAgAEFOwAABiChYAQEzBAgAwsAAAtjkRAgDEFCwAgJiCBQAQU7AAAGIKFgBATMECADCwAAC2ORECAMQULACAmIIFABBTsAAAYgoWAEBMwQIAMLAAALY5EQIAxBQsAICYggUAEFOwAABiChYAQEzBAgAwsAAAtjkRAgDEFCwAgJiCBQAQU7AAAGIKFgBATMECADCwAAC2ORECAMQULACAmIIFABBTsAAAYgoWAEBMwQIAMLAAALY5EQIAxBQsAICYggUAEFOwAAAMLACAbU6EAAAxBQsAIKZgAQDEFCwAgJiCBQAQU7AAAAwsAIBtToQAADEFCwAgpmABAMQULACAmIIFABBTsAAADCwAgG1OhAAAMQULACCmYAEAxBQsAICYggUAEFOwAAAMLACAbU6EAAAxBQsAIKZgAQDEFCwAgJiCBQAQU7AAAAwsAIBtToQAADEFCwAgpmABAMQULACAmIIFABBTsAAADCwAgG1OhAAAMQULACCmYAEAxBQsAAADCwBgmxMhAEBMwQIAiClYAAAxBQsAIKZgAQDEFCwAAAMLAGCbEyEAQEzBAgCIKVgAADEFCwAgpmABAMQULAAAAwsAYJsTIQBATMECAIgpWAAAMQULACCmYAEAxBQsAAADCwBgmxMhAEBMwQIAiClYAAAxBQsAIKZgAQDEFCwAAAMLAGCbEyEAQEzBAgCIKVgAADEFCwAg9gUwEwkox8wwtQAAAABJRU5ErkJggg==</image>
|
||||||
|
<image z="0" rotation="0" y="300" x="100" size="1" is_movable="1">iVBORw0KGgoAAAANSUhEUgAAACgAAAAeCAIAAADRv8uKAAAAP0lEQVR42u3PMQoAIAwEwRVC1F/7dFuxFoKwqS7XHNNgJSR0OMP1Pu+DQckFs2pYsWLFihUrVqxYsWLFir8Rb7mhAuQBqxaIAAAAAElFTkSuQmCC</image>
|
||||||
|
</images>
|
||||||
|
</diagram>
|
||||||
|
<collection>
|
||||||
|
<category name="import">
|
||||||
|
<names>
|
||||||
|
<name lang="en">Imported elements</name>
|
||||||
|
</names>
|
||||||
|
</category>
|
||||||
|
</collection>
|
||||||
|
</project>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
// Lazy folios (QET_LAZY_FOLIOS, LAZY-FOLIO-PLAN.md stages 5.2 and 5.3),
|
// Lazy folios (QET_LAZY_FOLIOS),
|
||||||
// linked against the application's objects: a folio shown in a project
|
// linked against the application's objects: a folio shown in a project
|
||||||
// opened without building its folios draws and links as in the project
|
// opened without building its folios draws and links as in the project
|
||||||
// opened as usual, building only it and the folios its links and tables
|
// opened as usual, building only it and the folios its links and tables
|
||||||
@@ -37,6 +37,8 @@
|
|||||||
#include "../../sources/undocommand/deleteqgraphicsitemcommand.h"
|
#include "../../sources/undocommand/deleteqgraphicsitemcommand.h"
|
||||||
#include "../../sources/factory/elementfactory.h"
|
#include "../../sources/factory/elementfactory.h"
|
||||||
#include "../../sources/undocommand/addgraphicsobjectcommand.h"
|
#include "../../sources/undocommand/addgraphicsobjectcommand.h"
|
||||||
|
#include "../../sources/dataBase/projectdatabase.h"
|
||||||
|
#include <QDomDocument>
|
||||||
|
|
||||||
static void check(bool value, const char *message) { if (!value) throw std::runtime_error(message); }
|
static void check(bool value, const char *message) { if (!value) throw std::runtime_error(message); }
|
||||||
|
|
||||||
@@ -567,6 +569,44 @@ int main(int argc, char **argv)
|
|||||||
check(potential(wire) == potential(usual_wire), "a potential followed as when built");
|
check(potential(wire) == potential(usual_wire), "a potential followed as when built");
|
||||||
std::printf("potential through reports: %d wires, %d folios not built\n",
|
std::printf("potential through reports: %d wires, %d folios not built\n",
|
||||||
int(potential(wire).size()), followed.unloadedFolioCount());
|
int(potential(wire).size()), followed.unloadedFolioCount());
|
||||||
|
}
|
||||||
|
//Two symbols sharing a uuid: the database is filled from the
|
||||||
|
//folios, which builds every one of them, so the project opens
|
||||||
|
//with every folio built rather than building them all later
|
||||||
|
{
|
||||||
|
QFile file(path);
|
||||||
|
check(file.open(QIODevice::ReadOnly), "fixture read");
|
||||||
|
QDomDocument document;
|
||||||
|
check(bool(document.setContent(&file)), "fixture parsed");
|
||||||
|
file.close();
|
||||||
|
check(projectDataBase::documentRefusal(document).isEmpty(), "the fixture fills the database itself");
|
||||||
|
QDomNodeList elements = document.elementsByTagName(QStringLiteral("element"));
|
||||||
|
QList<QDomElement> placed;
|
||||||
|
for (int i = 0; i < elements.size(); ++i) {
|
||||||
|
QDomElement e = elements.at(i).toElement();
|
||||||
|
if (Element::valideXml(e)) placed << e;
|
||||||
|
}
|
||||||
|
check(placed.size() > 1, "two placed symbols");
|
||||||
|
placed.last().setAttribute(QStringLiteral("uuid"), placed.first().attribute(QStringLiteral("uuid")));
|
||||||
|
check(!projectDataBase::documentRefusal(document).isEmpty(), "a shared uuid is seen before the folios are read");
|
||||||
|
const QString shared = output + "/lazy_folios_shared_uuid.qet";
|
||||||
|
QFile out(shared);
|
||||||
|
check(out.open(QIODevice::WriteOnly), "copy written");
|
||||||
|
out.write(document.toByteArray(4));
|
||||||
|
out.close();
|
||||||
|
qputenv("QET_LAZY_FOLIOS", "1");
|
||||||
|
qputenv("QET_LAZY_FOLIOS_TRACE", "1");
|
||||||
|
static int built_late; built_late = 0;
|
||||||
|
const QtMessageHandler previous = qInstallMessageHandler(
|
||||||
|
[](QtMsgType, const QMessageLogContext &, const QString &message) {
|
||||||
|
if (message.contains(QStringLiteral("built for a caller of diagrams()"))) ++built_late;
|
||||||
|
});
|
||||||
|
QETProject opened(shared);
|
||||||
|
qInstallMessageHandler(previous);
|
||||||
|
qunsetenv("QET_LAZY_FOLIOS_TRACE");
|
||||||
|
check(opened.state() == QETProject::Ok, "a project with a shared uuid opens");
|
||||||
|
check(opened.unloadedFolioCount() == 0, "a project with a shared uuid opens with every folio built");
|
||||||
|
check(built_late == 0, "a project with a shared uuid builds its folios as it reads them, not later");
|
||||||
}
|
}
|
||||||
std::printf("PASS: %d folios\n", int(folios.size()));
|
std::printf("PASS: %d folios\n", int(folios.size()));
|
||||||
} catch (const std::exception &error) {
|
} catch (const std::exception &error) {
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ class FakeWire : public QObject
|
|||||||
ConductorProperties properties() const { return m_properties; }
|
ConductorProperties properties() const { return m_properties; }
|
||||||
void setProperties(const ConductorProperties &p) { m_properties = p; }
|
void setProperties(const ConductorProperties &p) { m_properties = p; }
|
||||||
QSet<FakeWire *> relatedPotentialConductors() const { return m_potential; }
|
QSet<FakeWire *> relatedPotentialConductors() const { return m_potential; }
|
||||||
|
// The wires of the other side of a function separated terminal
|
||||||
|
// element: same electrical node, but only the voltage /
|
||||||
|
// protocol crosses.
|
||||||
|
QSet<FakeWire *> functionSeparatedConductors() const { return m_separated; }
|
||||||
QPointF pos() const { return m_pos; }
|
QPointF pos() const { return m_pos; }
|
||||||
|
|
||||||
// Joins wires into one potential; like the real one, a wire's
|
// Joins wires into one potential; like the real one, a wire's
|
||||||
@@ -41,10 +45,24 @@ class FakeWire : public QObject
|
|||||||
w->m_potential.insert(o);
|
w->m_potential.insert(o);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Marks two groups as separated by a function separated
|
||||||
|
// terminal element, without joining their potentials.
|
||||||
|
static void separate(const QList<FakeWire *> &one,
|
||||||
|
const QList<FakeWire *> &other)
|
||||||
|
{
|
||||||
|
for (FakeWire *w : one)
|
||||||
|
for (FakeWire *o : other)
|
||||||
|
{
|
||||||
|
w->m_separated.insert(o);
|
||||||
|
o->m_separated.insert(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ConductorProperties m_properties;
|
ConductorProperties m_properties;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QSet<FakeWire *> m_potential;
|
QSet<FakeWire *> m_potential;
|
||||||
|
QSet<FakeWire *> m_separated;
|
||||||
QPointF m_pos;
|
QPointF m_pos;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -102,6 +120,143 @@ class tst_conductormultiedit : public QObject
|
|||||||
(QSet<FakeWire *> {&a, &b, &c, &d}));
|
(QSet<FakeWire *> {&a, &b, &c, &d}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A function separated terminal element: the other side of the
|
||||||
|
// terminal is reached too, but only the voltage / protocol
|
||||||
|
// crosses it -- not the function, not the number.
|
||||||
|
void functionSeparatedGetsVoltageProtocolOnly()
|
||||||
|
{
|
||||||
|
FakeWire a("1", "fnA", "", {}), b("2", "fnB", "", {}),
|
||||||
|
c("3", "fnC", "", {});
|
||||||
|
FakeWire::join({&a, &b});
|
||||||
|
FakeWire::separate({&a}, {&c});
|
||||||
|
|
||||||
|
QSet<FakeWire *> separated;
|
||||||
|
const Wires t = ConductorMultiEdit::targets(Wires {&a}, true,
|
||||||
|
&separated);
|
||||||
|
QCOMPARE(QSet<FakeWire *>(t.begin(), t.end()),
|
||||||
|
(QSet<FakeWire *> {&a, &b, &c}));
|
||||||
|
QCOMPARE(separated, (QSet<FakeWire *> {&c}));
|
||||||
|
// A caller that does not know about the rule gets the
|
||||||
|
// separated side left out, as before
|
||||||
|
QCOMPARE(ConductorMultiEdit::targets(Wires {&a}, true),
|
||||||
|
(Wires {&a, &b}));
|
||||||
|
|
||||||
|
const ConductorProperties shown = a.properties();
|
||||||
|
ConductorProperties after = edited(shown,
|
||||||
|
&ConductorProperties::m_function, "PWR");
|
||||||
|
after.m_tension_protocol = "24V";
|
||||||
|
|
||||||
|
QUndoStack stack;
|
||||||
|
stack.push(ConductorMultiEdit::undo(t, shown, after, separated));
|
||||||
|
QCOMPARE(stack.count(), 1);
|
||||||
|
|
||||||
|
for (FakeWire *w : {&a, &b})
|
||||||
|
{
|
||||||
|
QCOMPARE(w->m_properties.m_function, QStringLiteral("PWR"));
|
||||||
|
QCOMPARE(w->m_properties.m_tension_protocol,
|
||||||
|
QStringLiteral("24V"));
|
||||||
|
}
|
||||||
|
QCOMPARE(c.m_properties.m_function, QStringLiteral("fnC"));
|
||||||
|
QCOMPARE(c.m_properties.text, QStringLiteral("3"));
|
||||||
|
QCOMPARE(c.m_properties.m_tension_protocol, QStringLiteral("24V"));
|
||||||
|
|
||||||
|
stack.undo();
|
||||||
|
QVERIFY(c.properties().m_tension_protocol.isEmpty());
|
||||||
|
QCOMPARE(c.m_properties.m_function, QStringLiteral("fnC"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The explicitly picked side of a separation: selecting one
|
||||||
|
// wire on each side and editing them edits them both, in
|
||||||
|
// full -- however the selection happens to be walked (#review)
|
||||||
|
void selectedWireIsNeverSeparated()
|
||||||
|
{
|
||||||
|
FakeWire a("1", "fnA", "", {}), c("3", "fnC", "", {});
|
||||||
|
FakeWire::separate({&a}, {&c});
|
||||||
|
|
||||||
|
const ConductorProperties original_a = a.properties();
|
||||||
|
const ConductorProperties original_c = c.properties();
|
||||||
|
const ConductorProperties after = edited(original_a,
|
||||||
|
&ConductorProperties::m_function, "PWR");
|
||||||
|
|
||||||
|
for (const Wires &selection : {Wires {&a, &c}, Wires {&c, &a}})
|
||||||
|
{
|
||||||
|
//Each order starts from the same, unedited state
|
||||||
|
a.setProperties(original_a);
|
||||||
|
c.setProperties(original_c);
|
||||||
|
|
||||||
|
QSet<FakeWire *> separated;
|
||||||
|
const Wires t = ConductorMultiEdit::targets(selection, true,
|
||||||
|
&separated);
|
||||||
|
//c is picked, so it is not "the far side" of anything
|
||||||
|
QCOMPARE(separated, (QSet<FakeWire *> {}));
|
||||||
|
|
||||||
|
QUndoStack stack;
|
||||||
|
QUndoCommand *undo = ConductorMultiEdit::undo(t, original_a,
|
||||||
|
after, separated);
|
||||||
|
QVERIFY(undo);
|
||||||
|
stack.push(undo);
|
||||||
|
QCOMPARE(a.m_properties.m_function, QStringLiteral("PWR"));
|
||||||
|
QCOMPARE(c.m_properties.m_function, QStringLiteral("PWR"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A wire that belongs to the potential as well -- on any path,
|
||||||
|
// however far that path runs -- takes the full edit, even when a
|
||||||
|
// separation also reaches it. The fake's potential is one hop
|
||||||
|
// deep, unlike the real one, so the shared potential is stated
|
||||||
|
// in full here.
|
||||||
|
void wireReachableBothWaysIsNotSeparated()
|
||||||
|
{
|
||||||
|
FakeWire a("1", "fnA", "", {}), b("2", "fnB", "", {}),
|
||||||
|
c("3", "fnC", "", {});
|
||||||
|
FakeWire::join({&a, &b, &c});
|
||||||
|
FakeWire::separate({&a}, {&c});
|
||||||
|
|
||||||
|
QSet<FakeWire *> separated;
|
||||||
|
const Wires t = ConductorMultiEdit::targets(Wires {&a}, true,
|
||||||
|
&separated);
|
||||||
|
QCOMPARE(QSet<FakeWire *>(t.begin(), t.end()),
|
||||||
|
(QSet<FakeWire *> {&a, &b, &c}));
|
||||||
|
QCOMPARE(separated, (QSet<FakeWire *> {}));
|
||||||
|
|
||||||
|
const ConductorProperties shown = a.properties();
|
||||||
|
QUndoStack stack;
|
||||||
|
stack.push(ConductorMultiEdit::undo(t, shown, edited(shown,
|
||||||
|
&ConductorProperties::m_function, "PWR"), separated));
|
||||||
|
QCOMPARE(c.m_properties.m_function, QStringLiteral("PWR"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The far side only takes the voltage when the edit changed it:
|
||||||
|
// editing the function of a 24 V wire must leave the 0 V one
|
||||||
|
// at 0 V, not drag it along (#review)
|
||||||
|
void separatedWireKeepsVoltageWhenNotEdited()
|
||||||
|
{
|
||||||
|
FakeWire a("1", "fnA", "", {}), c("3", "fnC", "", {});
|
||||||
|
FakeWire::separate({&a}, {&c});
|
||||||
|
a.m_properties.m_tension_protocol = "24V";
|
||||||
|
c.m_properties.m_tension_protocol = "0V";
|
||||||
|
|
||||||
|
QSet<FakeWire *> separated;
|
||||||
|
const Wires t = ConductorMultiEdit::targets(Wires {&a}, true,
|
||||||
|
&separated);
|
||||||
|
const ConductorProperties shown = a.properties();
|
||||||
|
QUndoStack stack;
|
||||||
|
|
||||||
|
//Only the function changes: the far side changes nothing
|
||||||
|
stack.push(ConductorMultiEdit::undo(t, shown, edited(shown,
|
||||||
|
&ConductorProperties::m_function, "PWR"), separated));
|
||||||
|
QCOMPARE(c.m_properties.m_function, QStringLiteral("fnC"));
|
||||||
|
QCOMPARE(c.m_properties.m_tension_protocol, QStringLiteral("0V"));
|
||||||
|
|
||||||
|
//Now the voltage changes too: it crosses, and only it
|
||||||
|
const ConductorProperties shown_now = a.properties();
|
||||||
|
stack.push(ConductorMultiEdit::undo(t, shown_now, edited(shown_now,
|
||||||
|
&ConductorProperties::m_tension_protocol, "30V"), separated));
|
||||||
|
QCOMPARE(c.m_properties.m_tension_protocol, QStringLiteral("30V"));
|
||||||
|
QCOMPARE(c.m_properties.m_function, QStringLiteral("fnC"));
|
||||||
|
QCOMPARE(a.m_properties.m_tension_protocol, QStringLiteral("30V"));
|
||||||
|
}
|
||||||
|
|
||||||
// One edit of three wires: each takes the function, keeps its
|
// One edit of three wires: each takes the function, keeps its
|
||||||
// own number and cable; one undo restores all three exactly
|
// own number and cable; one undo restores all three exactly
|
||||||
void undoRedoThreeWires()
|
void undoRedoThreeWires()
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
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/>.
|
||||||
|
*/
|
||||||
|
#include "qetapptest.h"
|
||||||
|
#include "diagramcontent.h"
|
||||||
|
#include "qetgraphicsitem/diagramimageitem.h"
|
||||||
|
|
||||||
|
#include <QDomDocument>
|
||||||
|
#include <QRandomGenerator>
|
||||||
|
|
||||||
|
/**
|
||||||
|
Diagram::fromXml() decodes a folio's pictures in parallel, then builds
|
||||||
|
the items in file order. Every picture must come back with its own
|
||||||
|
pixels and position, whatever thread decoded it.
|
||||||
|
*/
|
||||||
|
class tst_folioimageload : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
Diagram *m_diagram = nullptr;
|
||||||
|
|
||||||
|
/// A picture no other one equals: its own size and noise.
|
||||||
|
static QImage distinctImage(int n)
|
||||||
|
{
|
||||||
|
QImage image(40 + 7 * n, 30 + 3 * n, QImage::Format_ARGB32);
|
||||||
|
QRandomGenerator random(quint32(n + 1));
|
||||||
|
for (int y = 0 ; y < image.height() ; ++y)
|
||||||
|
for (int x = 0 ; x < image.width() ; ++x)
|
||||||
|
image.setPixel(x, y, random.generate() | 0xFF000000);
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QImage pixelsOf(const DiagramImageItem *item)
|
||||||
|
{
|
||||||
|
return item->pixmap().toImage()
|
||||||
|
.convertToFormat(QImage::Format_ARGB32);
|
||||||
|
}
|
||||||
|
|
||||||
|
void removePictures()
|
||||||
|
{
|
||||||
|
// A selected picture's handles are its children, in items()
|
||||||
|
// too: deleting the picture first would leave them dangling.
|
||||||
|
m_diagram->clearSelection();
|
||||||
|
for (QGraphicsItem *item : m_diagram->items())
|
||||||
|
if (item->type() == DiagramImageItem::Type)
|
||||||
|
delete item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void initTestCase()
|
||||||
|
{
|
||||||
|
QVERIFY(QetAppTest::editor());
|
||||||
|
m_diagram = QetAppTest::diagram();
|
||||||
|
QVERIFY(m_diagram);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cleanup()
|
||||||
|
{
|
||||||
|
m_diagram->clearSelection();
|
||||||
|
removePictures();
|
||||||
|
m_diagram->undoStack().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void everyPictureComesBackWithItsPixels()
|
||||||
|
{
|
||||||
|
QHash<int, QImage> expected; // by x position, each its own
|
||||||
|
for (int n = 0 ; n < 6 ; ++n) {
|
||||||
|
const QImage image = distinctImage(n);
|
||||||
|
auto *picture = new DiagramImageItem(QPixmap::fromImage(image));
|
||||||
|
m_diagram->addItem(picture);
|
||||||
|
picture->setPos(100 + 150 * n, 200);
|
||||||
|
picture->setSelected(true);
|
||||||
|
expected.insert(qRound(picture->pos().x()), image);
|
||||||
|
}
|
||||||
|
QDomDocument document = m_diagram->toXml(false);
|
||||||
|
removePictures();
|
||||||
|
|
||||||
|
DiagramContent content;
|
||||||
|
QVERIFY(m_diagram->fromXml(document, QPointF(), false, &content));
|
||||||
|
|
||||||
|
QCOMPARE(content.m_images.size(), expected.size());
|
||||||
|
for (const DiagramImageItem *picture : std::as_const(content.m_images)) {
|
||||||
|
const int x = qRound(picture->pos().x());
|
||||||
|
QVERIFY2(expected.contains(x), "picture moved");
|
||||||
|
QCOMPARE(picture->pos().y(), 200.0);
|
||||||
|
QCOMPARE(pixelsOf(picture), expected.value(x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void anImageWithoutPictureDoesNotStopTheOthers()
|
||||||
|
{
|
||||||
|
for (int n = 0 ; n < 3 ; ++n) {
|
||||||
|
auto *picture = new DiagramImageItem(
|
||||||
|
QPixmap::fromImage(distinctImage(n)));
|
||||||
|
m_diagram->addItem(picture);
|
||||||
|
picture->setSelected(true);
|
||||||
|
}
|
||||||
|
QDomDocument document = m_diagram->toXml(false);
|
||||||
|
removePictures();
|
||||||
|
QDomElement images = document.documentElement()
|
||||||
|
.firstChildElement(QStringLiteral("images"));
|
||||||
|
QVERIFY(!images.isNull());
|
||||||
|
QDomElement empty = document.createElement(QStringLiteral("image"));
|
||||||
|
images.insertBefore(empty, images.firstChild());
|
||||||
|
|
||||||
|
DiagramContent content;
|
||||||
|
QVERIFY(m_diagram->fromXml(document, QPointF(), false, &content));
|
||||||
|
|
||||||
|
int with_pixels = 0;
|
||||||
|
for (const DiagramImageItem *picture : std::as_const(content.m_images))
|
||||||
|
if (!picture->pixmap().isNull())
|
||||||
|
++with_pixels;
|
||||||
|
QCOMPARE(with_pixels, 3);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
QET_APP_TEST_MAIN(tst_folioimageload)
|
||||||
|
#include "tst_folioimageload.moc"
|
||||||
@@ -69,6 +69,57 @@ class tst_qetcontainer : public QObject
|
|||||||
return QetContainer::split(*original, QStringLiteral("tst_qetcontainer"), &error);
|
return QetContainer::split(*original, QStringLiteral("tst_qetcontainer"), &error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static QByteArray entry(const QList<QetZip::Entry> &entries, const QString &name)
|
||||||
|
{
|
||||||
|
for (const QetZip::Entry &e : entries)
|
||||||
|
if (e.name == name) return e.data;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static QByteArray folioFiles(const QList<QetZip::Entry> &entries)
|
||||||
|
{
|
||||||
|
QByteArray all;
|
||||||
|
for (const QetZip::Entry &e : entries)
|
||||||
|
if (e.name.startsWith(QStringLiteral("folios/"))) all += e.data;
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first column of the first row @p sql gives, from @p database
|
||||||
|
static QString query(const QByteArray &database, const char *sql)
|
||||||
|
{
|
||||||
|
sqlite3 *db = nullptr;
|
||||||
|
sqlite3_open(":memory:", &db);
|
||||||
|
unsigned char *copy = static_cast<unsigned char *>(sqlite3_malloc64(sqlite3_uint64(database.size())));
|
||||||
|
memcpy(copy, database.constData(), size_t(database.size()));
|
||||||
|
sqlite3_deserialize(db, "main", copy, database.size(), database.size(), SQLITE_DESERIALIZE_FREEONCLOSE);
|
||||||
|
sqlite3_stmt *st = nullptr;
|
||||||
|
QString out;
|
||||||
|
if (sqlite3_prepare_v2(db, sql, -1, &st, nullptr) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW)
|
||||||
|
out = QString::fromUtf8(reinterpret_cast<const char *>(sqlite3_column_text(st, 0)));
|
||||||
|
sqlite3_finalize(st);
|
||||||
|
sqlite3_close(db);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// grafcet.qet as QElectroTech saves it: every folio, symbol and wire
|
||||||
|
// with a uuid of its own, which the examples' wires and folios lack
|
||||||
|
static QDomDocument savedProject()
|
||||||
|
{
|
||||||
|
QList<QetZip::Entry> entries;
|
||||||
|
QDomDocument project;
|
||||||
|
QetZip::fromBytes(read(QStringLiteral(QET_FIXTURES_DIR "/qetz_format2.qetz")), &entries);
|
||||||
|
QetContainer::join(entries, &project);
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString backDifference(const QDomDocument &original, const QList<QetZip::Entry> &entries)
|
||||||
|
{
|
||||||
|
QDomDocument back;
|
||||||
|
QString error;
|
||||||
|
if (!QetContainer::join(entries, &back, &error)) return error;
|
||||||
|
return difference(original.documentElement(), back.documentElement(), QString());
|
||||||
|
}
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void sameDocumentBack_data()
|
void sameDocumentBack_data()
|
||||||
{
|
{
|
||||||
@@ -242,7 +293,145 @@ private slots:
|
|||||||
QVERIFY(pictures < 121); // the 121 placed pictures repeat a few files
|
QVERIFY(pictures < 121); // the 121 placed pictures repeat a few files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Given a map, join() hands the pictures over as bytes instead of
|
||||||
|
// putting them back in the document as base64
|
||||||
|
void picturesKeptAside()
|
||||||
|
{
|
||||||
|
QDomDocument original;
|
||||||
|
QList<QetZip::Entry> entries = splitFile(
|
||||||
|
QStringLiteral(QET_EXAMPLES_DIR "/weneedpolonez-Polonez_MR89_wiring_diagram.qet"), &original);
|
||||||
|
QVERIFY(!entries.isEmpty());
|
||||||
|
QHash<QString, QByteArray> files;
|
||||||
|
for (const QetZip::Entry &e : entries) files.insert(e.name, e.data);
|
||||||
|
|
||||||
|
QDomDocument back;
|
||||||
|
QetContainer::Pictures pictures;
|
||||||
|
QString error;
|
||||||
|
QVERIFY2(QetContainer::join(entries, &back, &error, &pictures), qPrintable(error));
|
||||||
|
const QDomNodeList images = back.elementsByTagName(QStringLiteral("image"));
|
||||||
|
const QDomNodeList original_images = original.elementsByTagName(QStringLiteral("image"));
|
||||||
|
QCOMPARE(images.size(), 121);
|
||||||
|
QCOMPARE(images.size(), original_images.size());
|
||||||
|
for (int i = 0 ; i < images.size() ; ++i) {
|
||||||
|
const QDomElement image = images.at(i).toElement();
|
||||||
|
const QString file = QetContainer::pictureFile(image);
|
||||||
|
QVERIFY(!file.isEmpty());
|
||||||
|
QVERIFY(!image.firstChild().isText());
|
||||||
|
QCOMPARE(pictures.value(file), files.value(file));
|
||||||
|
QCOMPARE(pictures.value(file),
|
||||||
|
QByteArray::fromBase64(original_images.at(i).firstChild().toText().data().toLatin1()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// still refused when a picture is missing
|
||||||
|
for (int i = 0 ; i < entries.size() ; ++i)
|
||||||
|
if (entries.at(i).name.startsWith(QStringLiteral("images/"))) { entries.removeAt(i); break; }
|
||||||
|
QDomDocument broken;
|
||||||
|
QVERIFY(!QetContainer::join(entries, &broken, &error, &pictures));
|
||||||
|
QVERIFY2(error.contains(QStringLiteral("missing")), qPrintable(error));
|
||||||
|
}
|
||||||
|
|
||||||
// A file whose manifest needs a later reader is refused, not half read
|
// A file whose manifest needs a later reader is refused, not half read
|
||||||
|
// A later format whose min-reader allows this one opens, and join() says
|
||||||
|
// it is later: QElectroTech opens it read-only
|
||||||
|
void laterFormatAllowedRead()
|
||||||
|
{
|
||||||
|
QDomDocument original;
|
||||||
|
QList<QetZip::Entry> entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"), &original);
|
||||||
|
QVERIFY(!entries.isEmpty());
|
||||||
|
const int later = QetContainer::Format + 1;
|
||||||
|
for (QetZip::Entry &e : entries)
|
||||||
|
if (e.name == QLatin1String("manifest.xml"))
|
||||||
|
e.data = QString::fromUtf8(e.data).replace(QRegularExpression(QStringLiteral(" format=\"\\d+\"")),
|
||||||
|
QStringLiteral(" format=\"%1\"").arg(later)).toUtf8();
|
||||||
|
alterDatabase(entries, QStringLiteral("UPDATE meta SET value = '%1' WHERE key = 'format'")
|
||||||
|
.arg(later).toUtf8().constData());
|
||||||
|
QDomDocument back;
|
||||||
|
QString error;
|
||||||
|
int format = 0;
|
||||||
|
QVERIFY2(QetContainer::join(entries, &back, &error, nullptr, &format), qPrintable(error));
|
||||||
|
QCOMPARE(format, later);
|
||||||
|
const QString d = difference(original.documentElement(), back.documentElement(), QString());
|
||||||
|
QVERIFY2(d.isEmpty(), qPrintable(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The database holds data, not where a block sat in the XML: the folio
|
||||||
|
// file's marker says where it goes back
|
||||||
|
void noPositionColumns()
|
||||||
|
{
|
||||||
|
QDomDocument original;
|
||||||
|
const QList<QetZip::Entry> entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"), &original);
|
||||||
|
const QByteArray database = entry(entries, QStringLiteral("project.sqlite"));
|
||||||
|
QCOMPARE(query(database, "SELECT group_concat(name) FROM pragma_table_info('element')"),
|
||||||
|
QStringLiteral("uuid,folio_uuid,prefix,freezeLabel,info_attrs,links_attrs"));
|
||||||
|
QCOMPARE(query(database, "SELECT count(*) FROM pragma_table_info('folio') WHERE name LIKE '%_at'"),
|
||||||
|
QStringLiteral("0"));
|
||||||
|
QCOMPARE(query(database, "SELECT count(*) FROM meta WHERE key = 'props_at'"), QStringLiteral("0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A project as QElectroTech saves it needs no JSON: every attribute it
|
||||||
|
// writes has a column, the show flag of a property too
|
||||||
|
void savedProjectNeedsNoJson()
|
||||||
|
{
|
||||||
|
QString error;
|
||||||
|
const QList<QetZip::Entry> entries = QetContainer::split(savedProject(), QString(), &error);
|
||||||
|
QVERIFY2(!entries.isEmpty(), qPrintable(error));
|
||||||
|
const QByteArray database = entry(entries, QStringLiteral("project.sqlite"));
|
||||||
|
QVERIFY(query(database, "SELECT count(*) FROM project_property").toInt() > 0);
|
||||||
|
QCOMPARE(query(database, "SELECT count(*) FROM project_property WHERE show IS NOT NULL"),
|
||||||
|
query(database, "SELECT count(*) FROM project_property"));
|
||||||
|
for (const char *sql : {"SELECT count(*) FROM project_property WHERE attrs IS NOT NULL",
|
||||||
|
"SELECT count(*) FROM folio_property WHERE attrs IS NOT NULL",
|
||||||
|
"SELECT count(*) FROM element_info WHERE attrs IS NOT NULL",
|
||||||
|
"SELECT count(*) FROM link WHERE attrs IS NOT NULL",
|
||||||
|
"SELECT count(*) FROM folio WHERE props_attrs IS NOT NULL",
|
||||||
|
"SELECT count(*) FROM element WHERE info_attrs IS NOT NULL OR links_attrs IS NOT NULL"})
|
||||||
|
QVERIFY2(query(database, sql) == QLatin1String("0"), sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The schema in QETZ-FORMAT.md is the one written: same tables, same
|
||||||
|
// columns in the same order, same types and keys
|
||||||
|
void specMatchesSchema()
|
||||||
|
{
|
||||||
|
const QString spec = QString::fromUtf8(read(QStringLiteral(QET_FORMAT_SPEC)));
|
||||||
|
const int from = spec.indexOf(QStringLiteral("```sql\n"));
|
||||||
|
const int to = spec.indexOf(QStringLiteral("```"), from + 7);
|
||||||
|
QVERIFY2(from >= 0 && to > from, "no schema in QETZ-FORMAT.md");
|
||||||
|
const QByteArray sql = spec.mid(from + 7, to - from - 7).toUtf8();
|
||||||
|
|
||||||
|
QDomDocument original;
|
||||||
|
const QList<QetZip::Entry> entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"), &original);
|
||||||
|
const QByteArray written = entry(entries, QStringLiteral("project.sqlite"));
|
||||||
|
|
||||||
|
auto describe = [](sqlite3 *db) {
|
||||||
|
QStringList out;
|
||||||
|
sqlite3_stmt *st = nullptr;
|
||||||
|
sqlite3_prepare_v2(db, "SELECT m.name, p.name, p.type, p.pk, p.\"notnull\" FROM sqlite_master m, "
|
||||||
|
"pragma_table_info(m.name) p WHERE m.type = 'table' ORDER BY m.name, p.cid",
|
||||||
|
-1, &st, nullptr);
|
||||||
|
while (st && sqlite3_step(st) == SQLITE_ROW) {
|
||||||
|
QStringList row;
|
||||||
|
for (int i = 0 ; i < 5 ; ++i)
|
||||||
|
row << QString::fromUtf8(reinterpret_cast<const char *>(sqlite3_column_text(st, i)));
|
||||||
|
out << row.join(QLatin1Char(' '));
|
||||||
|
}
|
||||||
|
sqlite3_finalize(st);
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
sqlite3 *from_spec = nullptr;
|
||||||
|
sqlite3_open(":memory:", &from_spec);
|
||||||
|
QCOMPARE(sqlite3_exec(from_spec, sql.constData(), nullptr, nullptr, nullptr), SQLITE_OK);
|
||||||
|
sqlite3 *from_file = nullptr;
|
||||||
|
sqlite3_open(":memory:", &from_file);
|
||||||
|
unsigned char *copy = static_cast<unsigned char *>(sqlite3_malloc64(sqlite3_uint64(written.size())));
|
||||||
|
memcpy(copy, written.constData(), size_t(written.size()));
|
||||||
|
sqlite3_deserialize(from_file, "main", copy, written.size(), written.size(), SQLITE_DESERIALIZE_FREEONCLOSE);
|
||||||
|
const QStringList spec_tables = describe(from_spec), file_tables = describe(from_file);
|
||||||
|
sqlite3_close(from_spec);
|
||||||
|
sqlite3_close(from_file);
|
||||||
|
QVERIFY(!spec_tables.isEmpty());
|
||||||
|
QCOMPARE(spec_tables, file_tables);
|
||||||
|
}
|
||||||
|
|
||||||
void laterFormatRefused()
|
void laterFormatRefused()
|
||||||
{
|
{
|
||||||
QDomDocument original;
|
QDomDocument original;
|
||||||
@@ -272,6 +461,86 @@ private slots:
|
|||||||
|
|
||||||
// A crafted project.sqlite whose "element" is a view, not a table, is
|
// A crafted project.sqlite whose "element" is a view, not a table, is
|
||||||
// refused: nothing in a file's schema is trusted.
|
// refused: nothing in a file's schema is trusted.
|
||||||
|
// project.sqlite changed by @p sql in @p entries
|
||||||
|
static void alterDatabase(QList<QetZip::Entry> &entries, const char *sql)
|
||||||
|
{
|
||||||
|
for (QetZip::Entry &e : entries) {
|
||||||
|
if (e.name != QLatin1String("project.sqlite")) continue;
|
||||||
|
sqlite3 *db = nullptr;
|
||||||
|
sqlite3_open(":memory:", &db);
|
||||||
|
unsigned char *copy = static_cast<unsigned char *>(sqlite3_malloc64(sqlite3_uint64(e.data.size())));
|
||||||
|
memcpy(copy, e.data.constData(), size_t(e.data.size()));
|
||||||
|
sqlite3_deserialize(db, "main", copy, e.data.size(), e.data.size(),
|
||||||
|
SQLITE_DESERIALIZE_FREEONCLOSE | SQLITE_DESERIALIZE_RESIZEABLE);
|
||||||
|
QCOMPARE(sqlite3_exec(db, sql, nullptr, nullptr, nullptr), SQLITE_OK);
|
||||||
|
sqlite3_int64 size = 0;
|
||||||
|
unsigned char *bytes = sqlite3_serialize(db, "main", &size, 0);
|
||||||
|
e.data = QByteArray(reinterpret_cast<const char *>(bytes), qsizetype(size));
|
||||||
|
sqlite3_free(bytes);
|
||||||
|
sqlite3_close(db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A database that does not match its folio files -- a row no item has,
|
||||||
|
// an item's row on another folio, entries of no symbol -- is refused
|
||||||
|
// with what is wrong, not read as far as it goes
|
||||||
|
void mismatchRefused_data()
|
||||||
|
{
|
||||||
|
QTest::addColumn<QByteArray>("sql");
|
||||||
|
QTest::addColumn<QString>("said");
|
||||||
|
QTest::newRow("symbol row of no symbol")
|
||||||
|
<< QByteArray("INSERT INTO element SELECT 'x', folio_uuid, prefix, freezeLabel, info_attrs,"
|
||||||
|
" links_attrs FROM element LIMIT 1")
|
||||||
|
<< QStringLiteral("symbol rows");
|
||||||
|
QTest::newRow("wire row of no wire")
|
||||||
|
<< QByteArray("INSERT INTO conductor SELECT 'x', folio_uuid, element1, terminal1, element2, terminal2,"
|
||||||
|
" num, formula, function, tension_protocol, conductor_color, conductor_section, cable,"
|
||||||
|
" bus, freezeLabel FROM conductor LIMIT 1")
|
||||||
|
<< QStringLiteral("wire rows");
|
||||||
|
QTest::newRow("symbol on another folio")
|
||||||
|
<< QByteArray("UPDATE element SET folio_uuid = (SELECT uuid FROM folio WHERE pos = 2)"
|
||||||
|
" WHERE folio_uuid = (SELECT uuid FROM folio WHERE pos = 1)")
|
||||||
|
<< QStringLiteral("the database says another");
|
||||||
|
QTest::newRow("information of no symbol")
|
||||||
|
<< QByteArray("UPDATE element_info SET element_uuid = 'x' WHERE rowid = 1")
|
||||||
|
<< QStringLiteral("information rows of no symbol");
|
||||||
|
}
|
||||||
|
void mismatchRefused()
|
||||||
|
{
|
||||||
|
QFETCH(QByteArray, sql);
|
||||||
|
QFETCH(QString, said);
|
||||||
|
QDomDocument original;
|
||||||
|
QList<QetZip::Entry> entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"), &original);
|
||||||
|
QVERIFY(!entries.isEmpty());
|
||||||
|
{
|
||||||
|
QDomDocument back;
|
||||||
|
QString error;
|
||||||
|
QVERIFY2(QetContainer::join(entries, &back, &error), qPrintable(error));
|
||||||
|
}
|
||||||
|
alterDatabase(entries, sql.constData());
|
||||||
|
QDomDocument back;
|
||||||
|
QString error;
|
||||||
|
QVERIFY(!QetContainer::join(entries, &back, &error));
|
||||||
|
QVERIFY2(error.contains(said), qPrintable(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A folio file says which symbol each item is (its type), read alone
|
||||||
|
void folioSaysWhichSymbol()
|
||||||
|
{
|
||||||
|
QDomDocument original;
|
||||||
|
const QList<QetZip::Entry> entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"), &original);
|
||||||
|
QVERIFY(!entries.isEmpty());
|
||||||
|
int typed = 0, symbols = 0;
|
||||||
|
for (const QetZip::Entry &e : entries) {
|
||||||
|
if (!e.name.startsWith(QStringLiteral("folios/"))) continue;
|
||||||
|
const QString text = QString::fromUtf8(e.data);
|
||||||
|
symbols += int(text.count(QStringLiteral("<element ")));
|
||||||
|
typed += int(text.count(QRegularExpression(QStringLiteral("<element [^>]*type=\"embed://"))));
|
||||||
|
}
|
||||||
|
QVERIFY(symbols > 0);
|
||||||
|
QCOMPARE(typed, symbols);
|
||||||
|
}
|
||||||
|
|
||||||
void viewInsteadOfTableRefused()
|
void viewInsteadOfTableRefused()
|
||||||
{
|
{
|
||||||
QDomDocument original;
|
QDomDocument original;
|
||||||
@@ -300,6 +569,104 @@ private slots:
|
|||||||
QVERIFY2(error.contains(QStringLiteral("element")), qPrintable(error));
|
QVERIFY2(error.contains(QStringLiteral("element")), qPrintable(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A row's key is its item's uuid, which the folio file keeps: no key
|
||||||
|
// made up from where an item is (format 2 had f1, f2...), no key
|
||||||
|
// attribute in the folio files of a project as QElectroTech saves it,
|
||||||
|
// true/false held as 1/0, and the format in the database's
|
||||||
|
// user_version.
|
||||||
|
void keysAreTheUuids()
|
||||||
|
{
|
||||||
|
const QDomDocument original = savedProject();
|
||||||
|
original.elementsByTagName(QStringLiteral("conductor")).at(0).toElement()
|
||||||
|
.setAttribute(QStringLiteral("freezeLabel"), QStringLiteral("true"));
|
||||||
|
QString error;
|
||||||
|
const QList<QetZip::Entry> entries = QetContainer::split(original, QString(), &error);
|
||||||
|
QVERIFY2(!entries.isEmpty(), qPrintable(error));
|
||||||
|
const QByteArray database = entry(entries, QStringLiteral("project.sqlite"));
|
||||||
|
const QByteArray folios = folioFiles(entries);
|
||||||
|
QVERIFY(!folios.contains("qetz-key"));
|
||||||
|
QVERIFY(!folios.contains("qetz-folio"));
|
||||||
|
QCOMPARE(query(database, "PRAGMA user_version"), QString::number(QetContainer::Format));
|
||||||
|
const QString first = original.elementsByTagName(QStringLiteral("diagram")).at(0).toElement()
|
||||||
|
.attribute(QStringLiteral("uuid"));
|
||||||
|
QCOMPARE(query(database, "SELECT uuid FROM folio WHERE pos = 1"), first);
|
||||||
|
QCOMPARE(query(database, "SELECT count(*) FROM folio WHERE uuid NOT LIKE '{%}'"), QStringLiteral("0"));
|
||||||
|
QCOMPARE(query(database, "SELECT count(*) FROM element WHERE uuid NOT LIKE '{%}'"), QStringLiteral("0"));
|
||||||
|
QCOMPARE(query(database, "SELECT count(*) FROM conductor WHERE uuid NOT LIKE '{%}'"), QStringLiteral("0"));
|
||||||
|
QCOMPARE(query(database, "SELECT count(*) FROM element WHERE typeof(freezeLabel) <> 'integer'"),
|
||||||
|
QStringLiteral("0"));
|
||||||
|
QCOMPARE(query(database, "SELECT count(*) FROM conductor WHERE typeof(freezeLabel) <> 'integer'"),
|
||||||
|
QStringLiteral("0"));
|
||||||
|
QCOMPARE(query(database, "SELECT sum(freezeLabel) FROM conductor"), QStringLiteral("1"));
|
||||||
|
const QString d = backDifference(original, entries);
|
||||||
|
QVERIFY2(d.isEmpty(), qPrintable(d));
|
||||||
|
QCOMPARE(query(database, "SELECT count(*) FROM element_info WHERE typeof(show) <> 'integer'"),
|
||||||
|
QStringLiteral("0"));
|
||||||
|
QCOMPARE(query(database, "SELECT count(*) FROM element_info WHERE attrs LIKE '%\"tag\"%'"),
|
||||||
|
QStringLiteral("0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two symbols sharing a uuid (F100) each get a key made from what they
|
||||||
|
// hold, the same on every save, and come back as they were.
|
||||||
|
void sharedUuidKeyed()
|
||||||
|
{
|
||||||
|
QDomDocument original = savedProject();
|
||||||
|
const QDomNodeList elements = original.elementsByTagName(QStringLiteral("element"));
|
||||||
|
QDomElement a, b;
|
||||||
|
for (int i = 0 ; i < elements.size() && b.isNull() ; ++i) {
|
||||||
|
const QDomElement e = elements.at(i).toElement();
|
||||||
|
if (e.parentNode().nodeName() != QLatin1String("elements")) continue;
|
||||||
|
if (a.isNull()) a = e; else b = e;
|
||||||
|
}
|
||||||
|
QVERIFY(!b.isNull());
|
||||||
|
b.setAttribute(QStringLiteral("uuid"), a.attribute(QStringLiteral("uuid")));
|
||||||
|
QString error;
|
||||||
|
const QList<QetZip::Entry> once = QetContainer::split(original, QString(), &error);
|
||||||
|
const QList<QetZip::Entry> twice = QetContainer::split(original, QString(), &error);
|
||||||
|
QVERIFY2(!once.isEmpty(), qPrintable(error));
|
||||||
|
QCOMPARE(folioFiles(once).count("qetz-key"), 2);
|
||||||
|
QCOMPARE(entry(once, QStringLiteral("project.sqlite")), entry(twice, QStringLiteral("project.sqlite")));
|
||||||
|
const QString d = backDifference(original, once);
|
||||||
|
QVERIFY2(d.isEmpty(), qPrintable(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A true/false attribute QElectroTech would not write stays in the
|
||||||
|
// folio file as it was.
|
||||||
|
void oddBooleanKept()
|
||||||
|
{
|
||||||
|
QDomDocument original;
|
||||||
|
QVERIFY(QetContainer::parse(read(QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet")), &original));
|
||||||
|
const QDomNodeList conductors = original.elementsByTagName(QStringLiteral("conductor"));
|
||||||
|
QVERIFY(conductors.size() > 0);
|
||||||
|
conductors.at(0).toElement().setAttribute(QStringLiteral("freezeLabel"), QStringLiteral("yes"));
|
||||||
|
QString error;
|
||||||
|
const QList<QetZip::Entry> entries = QetContainer::split(original, QString(), &error);
|
||||||
|
QVERIFY2(!entries.isEmpty(), qPrintable(error));
|
||||||
|
QVERIFY(folioFiles(entries).contains("freezeLabel=\"yes\""));
|
||||||
|
const QString d = backDifference(original, entries);
|
||||||
|
QVERIFY2(d.isEmpty(), qPrintable(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A format 2 file, as QElectroTech 0.200.1 development builds wrote it
|
||||||
|
// (grafcet.qet saved as .qetz), still opens, and saved again as the
|
||||||
|
// current format comes back the same.
|
||||||
|
void format2Read()
|
||||||
|
{
|
||||||
|
QList<QetZip::Entry> entries;
|
||||||
|
QString error;
|
||||||
|
QVERIFY2(QetZip::fromBytes(read(QStringLiteral(QET_FIXTURES_DIR "/qetz_format2.qetz")), &entries, &error),
|
||||||
|
qPrintable(error));
|
||||||
|
QVERIFY(entry(entries, QStringLiteral("manifest.xml")).contains("format=\"2\""));
|
||||||
|
QDomDocument project;
|
||||||
|
QVERIFY2(QetContainer::join(entries, &project, &error), qPrintable(error));
|
||||||
|
QVERIFY(project.toString().contains(QStringLiteral("<elementInformation ")));
|
||||||
|
QVERIFY(!project.toString().contains(QStringLiteral("qetz-")));
|
||||||
|
const QList<QetZip::Entry> resaved = QetContainer::split(project, QString(), &error);
|
||||||
|
QVERIFY2(!resaved.isEmpty(), qPrintable(error));
|
||||||
|
const QString d = backDifference(project, resaved);
|
||||||
|
QVERIFY2(d.isEmpty(), qPrintable(d));
|
||||||
|
}
|
||||||
|
|
||||||
void notAProjectRefused()
|
void notAProjectRefused()
|
||||||
{
|
{
|
||||||
QDomDocument back;
|
QDomDocument back;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include <QtTest>
|
#include <QtTest>
|
||||||
|
|
||||||
#include <QTemporaryDir>
|
#include <QTemporaryDir>
|
||||||
|
#include <QtEndian>
|
||||||
|
|
||||||
#include "container/qetzip.h"
|
#include "container/qetzip.h"
|
||||||
#include "container/miniz/miniz.h"
|
#include "container/miniz/miniz.h"
|
||||||
@@ -72,6 +73,33 @@ private slots:
|
|||||||
QCOMPARE(QetZip::toBytes(sample()), QetZip::toBytes(sample()));
|
QCOMPARE(QetZip::toBytes(sample()), QetZip::toBytes(sample()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// every entry carries a valid date in both of its headers, not the
|
||||||
|
// 0 miniz writes without a clock (shown as 1979 or 1901 by unzip tools)
|
||||||
|
void entriesHaveValidDate()
|
||||||
|
{
|
||||||
|
const QByteArray zip = QetZip::toBytes(sample());
|
||||||
|
QVERIFY(!zip.isEmpty());
|
||||||
|
mz_zip_archive archive;
|
||||||
|
mz_zip_zero_struct(&archive);
|
||||||
|
QVERIFY(mz_zip_reader_init_mem(&archive, zip.constData(), size_t(zip.size()), 0));
|
||||||
|
const mz_uint count = mz_zip_reader_get_num_files(&archive);
|
||||||
|
QCOMPARE(int(count), sample().size());
|
||||||
|
for (mz_uint i = 0 ; i < count ; ++i) {
|
||||||
|
mz_zip_archive_file_stat stat;
|
||||||
|
QVERIFY(mz_zip_reader_file_stat(&archive, i, &stat));
|
||||||
|
const char *local = zip.constData() + stat.m_local_header_ofs;
|
||||||
|
const char *central = zip.constData() + archive.m_central_directory_file_ofs + stat.m_central_dir_ofs;
|
||||||
|
QCOMPARE(qFromLittleEndian<quint16>(local), quint16(0x50 | 0x4b << 8)); // "PK"
|
||||||
|
QCOMPARE(qFromLittleEndian<quint16>(central), quint16(0x50 | 0x4b << 8));
|
||||||
|
// 1 Jan 1980: day 1, month 1, year 0 since 1980; time 00:00:00
|
||||||
|
QCOMPARE(qFromLittleEndian<quint16>(local + 12), quint16(0x21));
|
||||||
|
QCOMPARE(qFromLittleEndian<quint16>(central + 14), quint16(0x21));
|
||||||
|
QCOMPARE(qFromLittleEndian<quint16>(local + 10), quint16(0));
|
||||||
|
QCOMPARE(qFromLittleEndian<quint16>(central + 12), quint16(0));
|
||||||
|
}
|
||||||
|
mz_zip_reader_end(&archive);
|
||||||
|
}
|
||||||
|
|
||||||
void refusesUnsafeNamesOnWrite_data()
|
void refusesUnsafeNamesOnWrite_data()
|
||||||
{
|
{
|
||||||
QTest::addColumn<QString>("name");
|
QTest::addColumn<QString>("name");
|
||||||
|
|||||||
@@ -340,8 +340,8 @@ private slots:
|
|||||||
return QString::fromUtf8(proc.readAllStandardOutput() + proc.readAllStandardError());
|
return QString::fromUtf8(proc.readAllStandardOutput() + proc.readAllStandardError());
|
||||||
}
|
}
|
||||||
|
|
||||||
// The stores of symbol information and folio title blocks
|
// The stores of symbol information and folio title blocks agree with
|
||||||
// (DB-ACCESSORS-PLAN.md stages 4.1, 4.4) agree with every placed symbol
|
// every placed symbol
|
||||||
// and folio once a project is open: every example.
|
// and folio once a project is open: every example.
|
||||||
void elementInfoStoreAgrees_data()
|
void elementInfoStoreAgrees_data()
|
||||||
{
|
{
|
||||||
@@ -361,7 +361,7 @@ private slots:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The stores filled from the document before the folios are built
|
// The stores filled from the document before the folios are built
|
||||||
// (DB-ACCESSORS-PLAN.md stage 4.6) hold what building them gives, for a
|
// hold what building them gives, for a
|
||||||
// project saved by this version (every symbol, folio and wire carries
|
// project saved by this version (every symbol, folio and wire carries
|
||||||
// a uuid): nothing differs, nothing is left unplaced.
|
// a uuid): nothing differs, nothing is left unplaced.
|
||||||
void storesFilledBeforeBuilding()
|
void storesFilledBeforeBuilding()
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
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/>.
|
||||||
|
*/
|
||||||
|
#include "qetapptest.h"
|
||||||
|
#include "qetgraphicsitem/diagramimageitem.h"
|
||||||
|
|
||||||
|
#include <QBuffer>
|
||||||
|
#include <QDomDocument>
|
||||||
|
#include <QPicture>
|
||||||
|
#include <QProcess>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QSvgGenerator>
|
||||||
|
#include <QTemporaryDir>
|
||||||
|
|
||||||
|
/**
|
||||||
|
SVG export with "smaller pictures" (ExportProperties::svg_small_pictures,
|
||||||
|
--svg-small-pictures) stores each picture at four times the size it is
|
||||||
|
drawn at when that is smaller than the picture, and at full resolution
|
||||||
|
otherwise. fixtures/svg_small_pictures.qet has an 800x600 picture drawn
|
||||||
|
at a tenth of its size and a 40x30 one at full size.
|
||||||
|
*/
|
||||||
|
class tst_svgsmallpictures : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
QString m_fixture;
|
||||||
|
|
||||||
|
/// Pixel size of each picture embedded in @p svg, smallest first
|
||||||
|
static QList<QSize> embeddedSizes(const QByteArray &svg)
|
||||||
|
{
|
||||||
|
static const QRegularExpression data(
|
||||||
|
QStringLiteral("data:image/png;base64,([A-Za-z0-9+/=\\s]+)"));
|
||||||
|
QList<QSize> sizes;
|
||||||
|
auto it = data.globalMatch(QString::fromLatin1(svg));
|
||||||
|
while (it.hasNext())
|
||||||
|
sizes << QImage::fromData(QByteArray::fromBase64(
|
||||||
|
it.next().captured(1).toLatin1())).size();
|
||||||
|
std::sort(sizes.begin(), sizes.end(), [](const QSize &a, const QSize &b) {
|
||||||
|
return a.width() < b.width();
|
||||||
|
});
|
||||||
|
return sizes;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray exportWithCli(const QStringList &extra)
|
||||||
|
{
|
||||||
|
QTemporaryDir dir;
|
||||||
|
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||||
|
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||||
|
QProcess proc;
|
||||||
|
proc.setProcessEnvironment(env);
|
||||||
|
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
|
||||||
|
QStringList{QStringLiteral("--export-svg"), m_fixture, dir.path()} + extra);
|
||||||
|
if (!proc.waitForFinished(60000) || proc.exitCode() != 0)
|
||||||
|
return {};
|
||||||
|
const QStringList files = QDir(dir.path()).entryList({QStringLiteral("*.svg")});
|
||||||
|
QFile svg(QDir(dir.path()).filePath(files.value(0)));
|
||||||
|
return svg.open(QIODevice::ReadOnly) ? svg.readAll() : QByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// As ExportDialog::generateSvg(): the folio drawn into a QPicture,
|
||||||
|
/// played into the SVG generator
|
||||||
|
static QByteArray exportLikeTheDialog(Diagram *diagram, bool small)
|
||||||
|
{
|
||||||
|
const QSize size = diagram->imageSize();
|
||||||
|
QPicture picture;
|
||||||
|
diagram->setSmallPictures(small);
|
||||||
|
diagram->toPaintDevice(picture, size.width(), size.height());
|
||||||
|
diagram->setSmallPictures(false);
|
||||||
|
|
||||||
|
QByteArray svg;
|
||||||
|
QBuffer buffer(&svg);
|
||||||
|
buffer.open(QIODevice::WriteOnly);
|
||||||
|
QSvgGenerator generator;
|
||||||
|
generator.setSize(size);
|
||||||
|
generator.setOutputDevice(&buffer);
|
||||||
|
QPainter painter(&generator);
|
||||||
|
picture.play(&painter);
|
||||||
|
painter.end();
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void initTestCase()
|
||||||
|
{
|
||||||
|
m_fixture = QFINDTESTDATA("fixtures/svg_small_pictures.qet");
|
||||||
|
QVERIFY2(!m_fixture.isEmpty(), "fixture project not found");
|
||||||
|
QVERIFY(QetAppTest::editor());
|
||||||
|
}
|
||||||
|
|
||||||
|
void cliFullResolutionByDefault()
|
||||||
|
{
|
||||||
|
const QByteArray svg = exportWithCli({});
|
||||||
|
QVERIFY2(!svg.isEmpty(), "--export-svg failed");
|
||||||
|
QCOMPARE(embeddedSizes(svg), (QList<QSize>{QSize(40, 30), QSize(800, 600)}));
|
||||||
|
}
|
||||||
|
|
||||||
|
void cliSmallPictures()
|
||||||
|
{
|
||||||
|
const QByteArray svg = exportWithCli({QStringLiteral("--svg-small-pictures")});
|
||||||
|
QVERIFY2(!svg.isEmpty(), "--export-svg --svg-small-pictures failed");
|
||||||
|
// 800 x 600 drawn at 0.1, times 4; the 40 x 30 one is kept whole
|
||||||
|
QCOMPARE(embeddedSizes(svg), (QList<QSize>{QSize(40, 30), QSize(320, 240)}));
|
||||||
|
}
|
||||||
|
|
||||||
|
void dialogSmallPicturesThenFullAgain()
|
||||||
|
{
|
||||||
|
QFile file(m_fixture);
|
||||||
|
QVERIFY(file.open(QIODevice::ReadOnly));
|
||||||
|
QDomDocument document;
|
||||||
|
QVERIFY(document.setContent(&file));
|
||||||
|
const QDomElement image_xml = document.elementsByTagName(
|
||||||
|
QStringLiteral("image")).at(0).toElement();
|
||||||
|
Diagram *diagram = QetAppTest::diagram();
|
||||||
|
auto *picture = new DiagramImageItem();
|
||||||
|
QVERIFY(picture->fromXml(image_xml));
|
||||||
|
diagram->addItem(picture);
|
||||||
|
|
||||||
|
QCOMPARE(embeddedSizes(exportLikeTheDialog(diagram, true)),
|
||||||
|
QList<QSize>{QSize(320, 240)});
|
||||||
|
// The option lasts for one export only
|
||||||
|
QVERIFY(!diagram->smallPictures());
|
||||||
|
QCOMPARE(embeddedSizes(exportLikeTheDialog(diagram, false)),
|
||||||
|
QList<QSize>{QSize(800, 600)});
|
||||||
|
delete picture;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
QET_APP_TEST_MAIN(tst_svgsmallpictures)
|
||||||
|
#include "tst_svgsmallpictures.moc"
|
||||||
Reference in New Issue
Block a user