Compare commits

...

25 Commits

Author SHA1 Message Date
ispyisail 33a731b56f Merge pull request #1477 from ispyisail/perf/parallel-picture-decode
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m6s
Speed up opening folios with photos: decode pictures in parallel
2026-10-14 19:10:59 +13:00
ispyisail 373a705bbb Merge master into perf/parallel-picture-decode
tests/qttest/CMakeLists.txt: this branch's tst_folioimageload and
master's tst_svgsmallpictures (#1478) were added at the same place;
both are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:36:36 +13:00
ispyisail af142aba90 Merge pull request #1473 from ispyisail/fix/qetz-format-ids-types-version
Fix .qetz database: rows keyed by position, untyped, unversioned
2026-10-14 18:29:57 +13:00
ispyisail 8cda53f4ce Merge pull request #1456 from Kellermorph/terminal-potential
Terminal: rename potential separation to function separation
2026-10-14 18:26:00 +13:00
ispyisail 4e8af81fc6 Merge pull request #1478 from ispyisail/feature/svg-picture-limit
Add an SVG export option to store pictures smaller
2026-10-14 18:22:28 +13:00
ispyisail 9920c13bd9 Merge pull request #1476 from ispyisail/perf/derived-uuid-no-copy
Speed up opening older projects: work out the project uuid in place
2026-10-14 18:22:13 +13:00
ispyisail 1768518b44 Merge master into fix/qetz-format-ids-types-version
join() and readDocument() gained a parameter on both sides: master's
Pictures map (#1470) and this branch's format. Both are kept, pictures
first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:56:32 +13:00
ispyisail da6eb83035 Add an SVG export option to store pictures smaller
An SVG keeps every picture at full resolution, whatever size it is
drawn at: a folio with 22 phone photos shown at a tenth of their size
gave a 270 MB SVG and took 65 s to write, most of it compressing the
photos again.

The export dialog gets "SVG: smaller pictures" (off by default, kept
with the other export settings), and --export-svg gets
--svg-small-pictures. With it, a picture is stored at four times the
size it is drawn at when that is smaller than the picture; smaller
pictures stay whole. Four times is about 375 dpi on a printed folio.

The exporter sets Diagram::smallPictures() for its own drawing only,
so screen, print, PDF and PNG are unchanged. It is not part of
applyProperties(), which every format uses.

On that project: --export-svg 84.9 s -> 34.5 s (opening is 20 s of
both), 270 MB -> 57 MB, peak memory 2.45 -> 1.86 GB. Rendered at twice
the folio size, the two SVGs differ by more than 32/255 on 0.23 % of
pixels, along photo detail.

tst_svgsmallpictures: an 800x600 picture drawn at 0.1 is stored at
320x240 with the option, through --export-svg and through the
dialog's QPicture route, and at 800x600 without it; a 40x30 picture
stays whole. It fails when the factor is changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:12:38 +13:00
ispyisail 6751fc6f19 Speed up opening folios with photos: decode their pictures in parallel
Each picture of a folio was decoded in turn while its item was built,
a quarter of a second for a 12 megapixel photo. Diagram::fromXml() now
reads every picture of the folio first, decodes them all at once on a
pool of its own (one thread per core, at most one per picture), then
builds the items in file order in the GUI thread as before.

The decoding (base64 text, then the picture file) moves out of
DiagramImageItem::fromXml() into decodePicture(), which touches only
its argument. QImage::fromData() then QPixmap::fromImage() is what
QPixmap::loadFromData() does, so the pixels are the same.

On a 263 MB project with 22 photos on one folio: --info 21.3 s ->
12.5 s, the folio's content 10.6 s -> 3.2 s, opening its .qetz 15.6 s
-> 9.3 s. Peak memory +220 MB (1.81 -> 2.03 GB), the pictures' text
being converted by several threads at once. A resave is byte-identical
to master's, and so is a PNG export.

tst_folioimageload: six pictures come back with their own pixels and
positions, and an <image> without a picture does not stop the others.
It fails when the decoded pictures are handed to the wrong items.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:09:09 +13:00
ispyisail 2bf73585ba Open legacy projects faster: derive the project uuid without copying
QETProject::derivedUuid() copied the whole file content to drop '\r',
then copied it again to prepend the name prefix, before hashing.
On a 263 MB project with embedded photos that took 2 s of a 21 s open.
Hash the content in place, skipping '\r', and set the version 5 bits
as QUuid::createUuidV5() does.

The uuid is unchanged: same result as before on the 23 example
projects, their CRLF copies, edge cases (empty, only '\r') and the
263 MB project, and a resave of that project is byte-identical.
2026-10-14 11:25:29 +13:00
ispyisail f65b24250c Document the .qetz format
QETZ-FORMAT.md describes format 3: the archive and its files, the
manifest and the version rules, every table and column of project.sqlite
and what it comes from, how rows are keyed, the markers a moved block
leaves, and what reading checks. Asked for in #1440: the format had no
written specification.

tst_qetcontainer checks that the schema the document gives is the one
written: the same tables and columns, in the same order, with the same
types and keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:14:12 +13:00
ispyisail 590bd6e532 .qetz: a property's show flag gets a column
The JSON column of a property row held only its show flag, in every
project QElectroTech saves: the 25 examples and a 136-folio project used
it for nothing else. Folio and project properties now store it as a
number, as symbol information does, so a saved project's database holds
no JSON; JSON is left for attributes QElectroTech does not write (an
older or hand-edited file).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:36:19 +13:00
ispyisail 4c3d87e3f7 .qetz: no columns for where a block sat; open a later format read-only
The database held where each moved block sat among its parent's
children (props_at, info_at, links_at), though reading only asked
whether there was one: the marker the folio file keeps says that, and
where it goes back. The columns are gone; format 2 files leave the same
markers and still open.

A .qetz of a later format whose min-reader allows this version was
still refused by the database's own format check. It opens now,
read-only, with a message: saving it here could lose what this version
does not know. Save As writes a copy in this version's format.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:47:08 +13:00
ispyisail 5975b75766 .qetz: keep a symbol's type in its folio; refuse a database that does not match
A folio file in a .qetz did not say which symbol each item is: the type
(embed://...) was moved to project.sqlite, so a folio read alone could
not be drawn or diffed. It stays in the folio file now; format 2 files,
which moved it, are still read.

Nothing checked that project.sqlite and the folio files agree: a row no
item has, an item's row on another folio, or information rows of no
symbol were read as far as they went. Reading now refuses such a file and
says what does not match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:57:25 +13:00
Kellermorph 408ef5114d Terminal: pass the voltage across a separation only when it was edited
The three code paths that copied the voltage / protocol to the far
side of a function separated terminal -- the conductor properties
dialog, search/replace and the script API -- compared it against the
far wire's own properties instead of against the edit itself. The two
sides differ on that field just as often as not, so the comparison
held no matter what was edited and every change overwrote the far
side's voltage: setting a colour on a 24 V side dragged the 0 V one
along, where master left it alone. Compare the edit -- what the
dialog was filled with against what the user made of it -- so that a
change of the voltage / protocol is what crosses, which is what the
selection panel multi-edit already did. The values are read before
the undo commands are pushed, since QPropertyUndoCommand applies on
push.

An explicitly selected wire is never the far side of anything:
picking one wire on each side of a separation and editing them edits
them both, in full, however the selection happens to be walked. Nor
is a wire that belongs to a potential on any path, so reaching it
once normally and once across a separation gives it the full edit.
2026-10-13 20:13:18 +02:00
ispyisail 8d118faf36 .qetz: key database rows by uuid, type true/false, version the schema
Answers the points arummler raised in discussion #1440 about the
project.sqlite that .qetz files carry (format 2, PR #1462):

- Folio rows were keyed "f1", "f2"... by position, and every symbol and
  wire carried a separate key beside its uuid. A row's key is now the
  item's own uuid, which stays in the folio file; no key attribute is
  added to the folio files of a project as QElectroTech saves it. An item
  without a usable uuid (none, or one another item shares, F100) gets a
  uuid made from its own content, the same on every save, never from
  where it is.
- freezeLabel is stored as 0/1 (INTEGER) and an element information's
  "show" in its own column; a value QElectroTech would not write stays in
  the folio file verbatim. The tag is kept in the JSON only when it is
  not the usual one.
- The database's PRAGMA user_version holds the format.

Format 3. Format 2 files, written by development builds since #1462,
still open: tst_qetcontainer reads one (fixtures/qetz_format2.qetz,
grafcet.qet saved by master aa503aec8) and saves it again.

Checked: tst_qetcontainer 38/38, and 343/343 readable projects of the
legacy corpus round-trip exactly. Every example saved as .qetz then .qet
equals its plain resave, and every format 2 file read by this build
equals what master writes from it. Each new test goes red with the
behaviour it checks broken.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:34:35 +13:00
Laurent Trinques 8b680af938 Merge pull request #1471 from ispyisail/fix/lazy-refused-opens-eager
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m41s
Fix lazy folios opening older projects slower than without them
2026-10-13 12:47:40 +02:00
Laurent Trinques 7c2a09f2bc Merge pull request #1470 from ispyisail/perf/qetz-picture-load
Speed up opening .qetz projects that hold large pictures
2026-10-13 12:42:15 +02:00
Laurent Trinques 132eefb8b8 Merge pull request #1467 from ispyisail/fix/qetz-zip-entry-date
Fix the dates of the files inside a .qetz project
2026-10-13 12:41:42 +02:00
ispyisail 00e200b504 Remove references to notes that are not in this repository
Comments added with the lazy folios cited planning notes kept outside the
QElectroTech repository, which a reader of the source cannot open. The
sentences now stand on their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:14 +13:00
ispyisail 1c4618c8dd Open with every folio built when the database must come from them
With QET_LAZY_FOLIOS=1, a project whose database cannot be filled from
the file (folios or symbols with no saved uuid, two symbols sharing one,
a folio number using %autonum, wires naming their ends the older way)
had its folios left unbuilt, then built all at once while the database
was filled from them. That is slower than building them as they are
read: a 17 MB project reported on the pull request took 24.8 s to open
instead of 19.2 s, and most files saved by an older version have no
folio uuids.

The file is now checked for those reasons before the folios are read;
when one applies the project opens as without QET_LAZY_FOLIOS. The rarer
reasons, which need the definitions or every folio's links, still fall
back as before.

tst_lazyfolios opens a copy of industrial.qet with one symbol's uuid
given to another, and checks no folio is built after reading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:02:55 +13:00
ispyisail 1ad3c8e2b2 Speed up opening .qetz projects with large pictures
A .qetz keeps each folio picture as a PNG file. On opening, join() put
every picture back into the document as base64 text, and the folio then
decoded that text again, twice. On a project with 20 pictures of 7 MB
(140 MB of PNG, the shape of the project on PR #1462) that cost about
8 of the 12 seconds the open took:

  QDomDocument::createTextNode() on the base64   3.9 s
  DiagramImageItem::fromXml(), base64 decoded    3.9 s (2 x 1.9 s)
  the PNG decoding itself                        0.3 s

join() now takes an optional map. Given one, each picture stays a file:
the <image> keeps its qetz-file attribute and the bytes go in the map.
QETProject keeps the map for as long as the project is open, and
Diagram::fromXml() hands a picture's bytes to the new
DiagramImageItem::fromXml(element, picture). Kept, not cleared after
the load, so a folio built later finds its pictures too; the bytes are
shared with the PNG cache of the pictures built from them, so this costs
almost no memory. readDocument() without a map, as the automatic
numbering import uses it, still gives the whole document.

fromXml() of a .qet picture also decodes its base64 once instead of
twice: the first text node, which the PNG cache already used. e.text()
also collects <image_base>'s text, but loadFromData() stops at the end
of the first picture, so the picture shown is the same.

Measured, same file, 3 runs each, offscreen:
  .qetz  11.8 s -> 3.8 s, peak memory 1034 MB -> 655 MB
  .qet   15.7 s -> 13.6 s
Resaving either file with either binary gives the same bytes, a cropped
and colour-keyed picture included, and every folio exports to the same
PNG pixels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:02:47 +13:00
Laurent Trinques aa503aec81 Merge pull request #1468 from qelectrotech/resolve-1461
Speed up opening big projects with lazy folios
2026-10-13 11:10:10 +02:00
ispyisail c240e4e6f7 Fix the dates of the files inside a .qetz project
miniz is built with MINIZ_NO_TIME so that saving the same project twice
gives the same bytes. Without a clock it writes 0 as each entry's DOS
date: day 0 of month 0, 1980, which is not a date. Unzip tools show it
as 30 Nov 1979, others fail the conversion and show 13 Dec 1901
(reported on PR #1462).

Write 1 Jan 1980 00:00, the first valid DOS date, into the local and
central header of every entry once the archive is built. The bytes stay
reproducible, and miniz stays unmodified. Using the save time instead
was rejected: two saves would no longer give the same file, and miniz
converts through the local time zone, so even a fixed time_t would
differ between machines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:49:34 +13:00
Kellermorph 561ddc29bf Terminal: rename potential separation to function separation
The checkbox of a terminal element was an all-or-nothing switch: while
it was unticked every conductor property (function, wire colour,
section, number...) crossed to the other side of the terminal, and
once it was ticked nothing did at all.

Rename it to "Function separation" and let one property still cross:
the voltage / protocol, which describes the electrical node itself
rather than one side of it. Conductor::functionSeparatedConductors()
returns the conductors reachable only by crossing such a terminal --
the potential computed with the separations ignored, minus the one
that respects them -- and
ConductorProperties::applyTensionProtocolChange() gives them that one
property, in the conductor properties dialog, the selection panel
multi-edit, the script API and search/replace.

The stored element information stays "potential_isolating", so
existing symbols and projects keep their setting, and the existing
sub-case of terminals sharing a potential-group identifier is
untouched. The German translation of the new label is deliberately
left out of this commit and belongs with the other .ts updates.
2026-10-11 14:57:42 +02:00
50 changed files with 2088 additions and 234 deletions
@@ -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));
}
} }
} }
} }
+2 -2
View File
@@ -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
{ {
+2 -2
View File
@@ -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
+7 -2
View File
@@ -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
+4 -1
View File
@@ -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.
+61 -6
View File
@@ -62,26 +62,70 @@ 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;
//Already reached through an earlier conductor's potential or
//separation: its own potential adds nothing new to walk over
//-- 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); seen.insert(conductor);
list << conductor; list << conductor;
if (!apply_all) }
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))
{
seen.insert(other);
list << other;
}
}
if (function_separated)
{
const QSet<T *> separated = conductor->functionSeparatedConductors();
for (T *other : separated)
{
separated_reached.insert(other);
if (!seen.contains(other)) if (!seen.contains(other))
{ {
seen.insert(other); seen.insert(other);
@@ -89,6 +133,9 @@ namespace ConductorMultiEdit
} }
} }
} }
}
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,6 +166,9 @@ namespace ConductorMultiEdit
{ {
const ConductorProperties old_properties = conductor->properties(); const ConductorProperties old_properties = conductor->properties();
ConductorProperties properties = old_properties; ConductorProperties properties = old_properties;
if (function_separated.contains(conductor))
properties.applyTensionProtocolChange(shown, edited);
else
properties.applyChanges(shown, edited); properties.applyChanges(shown, edited);
if (properties == old_properties) if (properties == old_properties)
continue; continue;
+26
View File
@@ -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
+2
View 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();
+182
View File
@@ -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.
+25 -5
View File
@@ -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());
+24 -4
View File
@@ -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
+268 -127
View File
@@ -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.isNull()) {
if (links_at >= 0) {
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,17 +868,46 @@ 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; }
QVariantList v; if (s.value(conductor_data.size()).toString() != id) {
for (int i = 0 ; i < ConductorData.size() ; ++i) v << s.value(i); setError(error, QStringLiteral("wire %1 is on folio %2, the database says another").arg(key, id));
put(c, ConductorData, v, 0); return false;
} }
QVariantList v;
for (int i = 0 ; i < conductor_data.size() ; ++i) v << s.value(i);
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;
+4 -2
View File
@@ -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.
*/ */
+42 -1
View File
@@ -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;
} }
+2 -2
View File
@@ -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
{ {
+53 -1
View File
@@ -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.
+12 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+2
View File
@@ -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;
+5
View File
@@ -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(
+1
View File
@@ -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
+11
View File
@@ -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);
} }
/** /**
@@ -236,6 +239,11 @@ void ExportPropertiesWidget::build()
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);
setLayout(vboxLayout); setLayout(vboxLayout);
@@ -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
} }
+1
View File
@@ -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;
+3 -2
View File
@@ -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;
+10
View File
@@ -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.
+43 -4
View File
@@ -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;
+4 -1
View File
@@ -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 ();
+92 -17
View File
@@ -167,6 +167,27 @@ void DiagramImageItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *
return; return;
} }
// 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); 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();}
+2 -2
View File
@@ -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
+21 -8
View File
@@ -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;
+2 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+29 -2
View File
@@ -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;
+31
View File
@@ -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);
} }
+10 -5
View File
@@ -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);
} }
/** /**
+3 -2
View File
@@ -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
+16 -1
View File
@@ -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>
+41 -1
View File
@@ -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) {
+155
View File
@@ -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()
+132
View File
@@ -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"
+367
View File
@@ -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;
+28
View File
@@ -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");
+3 -3
View File
@@ -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()
+143
View File
@@ -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"