Compare commits

...

61 Commits

Author SHA1 Message Date
Laurent Trinques dbecfae35b Merge pull request #1481 from ispyisail/fix/pdf-orientation-per-folio
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m39s
Add per-folio page orientation to the PDF export (#1480)
2026-10-14 17:27:15 +02:00
Laurent Trinques 2ce508e373 Merge pull request #1472 from ispyisail/feat/qetz-db-truth-v2
Save as .qetz by default, with the project database as the source of truth
2026-10-14 17:22:44 +02:00
ispyisail f42958d2c2 PDF export: turn each page to its folio's orientation (#1480)
The print window exported every page in one orientation, landscape
unless changed, so a portrait folio was shrunk onto a landscape page.
With "Fit in page" on, a PDF export now turns each page to portrait or
landscape as its folio is drawn. The preview keeps one orientation
(QPrintPreviewWidget lays out every page at one paper size), and a
printer keeps it too (only Qt's PDF engine accepts a new page layout
once printing has started). The orientation chosen in the window is
put back afterwards, so it is still the one saved.

The cross-reference links took their target from painter->viewport(),
which QPainter sets once when it opens, so on a turned page they missed
their text. They now use the device's size, as render() does.

tst_printpdforientation exports through the window: an A3 landscape and
an A3 portrait folio get one page each way; tiled pages keep the chosen
orientation; and the links on examples/industrial.qet's turned pages
land where they do when the export starts portrait.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 23:38:00 +13:00
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 c0a5084bc4 Merge master into feat/qetz-db-truth-v2
The .qetz files take this branch's side: it already holds #1473, and
adds the folio parts to join().

qetscriptapi.cpp: #1456 gave setConductorProperty() the voltage that
crosses a function separated terminal element; here that function hands
its work to applyConductorProperties(), so the change goes there, with
the properties captured before the pushes. The database path for folios
not built already gives up at such a terminal, so it builds the folio and
takes the same route.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:57:29 +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 bf465f9245 Merge #1473's format document; describe format 4
QETZ-FORMAT.md now describes format 4: the computed text left out of the
folio files, QElectroTech's own database built on opening rather than
stored, the tables written without keys, and format 3 among those read.

Tables written without keys keep their declared column types (INTEGER
was written as INT); tst_qetcontainer checks the columns and types
against the document, and that the file has no keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:34:52 +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 a9ed99293e Merge #1473: a property's show flag gets a column
A folio is copied as it was only from a database whose folio_property and
element_info have the columns this writes too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:54:09 +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 286779a2e4 Merge #1473: no position columns, a later format opened read-only
A symbol changed on a folio kept as it was needs the place its
information goes back to: its part is checked for the marker
(QetContainer::split()), where the position column said it before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:17:21 +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 56c704be1f Merge #1473: a symbol's type stays in its folio, mismatched databases refused
The checks run once every folio is read. A .qetz written by this branch
has no keys in its tables, so a row twice is caught by the counts too.
A folio is copied as it was only from a database with the columns this
writes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:00:59 +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
ispyisail c259cae429 Keep each fact once in a .qetz: drop the stored project database
A .qetz held the engineering data twice: the document's tables, and
beside them QElectroTech's own project database (stage D2/D3), which
QElectroTech also builds from the document. Two copies of one fact can
disagree, and the copy cost a third of the file. Now project.sqlite holds
the document's tables only; QElectroTech builds its own project database
from them when the project opens, as it does for a .qet.

Measured (this build, lazy folios):
  industrial (50 folios): 620 -> 374 KB, open 0.37 -> 0.65 s
  f2099/nuri (136 folios): 3.27 -> 2.20 MB, open 2.2 -> 3.5 s
The same projects as .qet open in 3.3 s and 23 s.

Saving still copies the file of every folio not built, with its rows
(stage D4): the project keeps the part each folio was read from. nuri
opened and saved again: 6.3 s, no folio built, the same bytes.

A symbol's terminal list is left out of the folio file when every
folio, symbol and wire has a uuid of its own, now checked on the
document (QetContainer::ownUuids()) rather than by the stored database
being written.

Checked: 342 corpus projects open, .qetz -> .qet equals the plain
resave, a .qetz saved twice gives the same bytes; tst_lazyfolios,
tst_qetcontainer, tst_resaveunchanged (which now checks project.sqlite
holds only the document's tables).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:35:34 +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 39855e1bb5 Merge #1473 into feat/qetz-db-truth-v2: format 4
#1473 keys the .qetz database's rows by the items' uuids, stores
true/false as 1/0 and writes the format to PRAGMA user_version, as
format 3. This branch's own changes (computed text left out of the
folios, tables without keys, folios kept as they were) become format 4;
formats 2 and 3 are still read.

Folios kept as they were copy their rows by folio uuid, and the keys of
those rows are kept from the other items' keys. The document's tables
are named doc_<name> only beside the project database's own tables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:12:57 +13:00
ispyisail f48bd6717d Merge master into feat/qetz-db-truth-v2
#1470 keeps a .qetz's pictures out of the document; the index path still
reads them in, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:00:37 +13: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 fbda162e34 Find a .qetz's folios with cable wires in its database
Opened from its database, a .qetz's folios not built have no XML in
memory: reading each one's part only to look for wires referring to a
cable read every folio. The project database already knows which wires
carry a cable reference, and so which folios to build for the label pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:52 +13:00
ispyisail f7e0283e61 Include QDomDocument in wiringlistexport.h
The header declares projectDocument() returning a QDomDocument but did not
include it; it only built where the precompiled header pulled it in. CI
builds without one and failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:51 +13:00
ispyisail bfa9912c5f Make modal_quit_regression deterministic
The test interrupted QElectroTech at a random moment inside the dialog's
loop and called quitQET() there. Landing inside malloc(), the call waited
on the allocator's lock for ever: 1 run in 20 timed out at 120 s, on
master too. It now lets the program run on to the start of the main
thread's next QEventLoop::processEvents(), between two events, and calls
from there; the event loop watchdog wakes the loop every 50 ms.

A run could also stop before any dialog: SingleApplication keys a System V
shared memory block from a file in the temporary folder, 16 bits of the
key varying, and each run killed by gdb leaves its block; once a new
run's key fell on one of them (976 here), it refused to start. The run
now removes the blocks it leaves, attached to no process, and is tried
again (a new binary path, a new key, up to three times) when
SingleApplication stops it that way: the scenario never ran. The "dialog
entered" marker no longer prints when the program had exited.

60 runs under load, 976 old blocks present: 60 passed, one try repeated.
With refuseCloseWhileModal() disabled: 3 of 3 FAIL (signal 6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:51 +13:00
ispyisail 4104bb8d53 Write only the wire attributes reading needs in a .qetz (stage D3b)
A wire's properties are one row per attribute in the project database,
45 071 rows for f2099's 2 139 wires. The database a .qetz holds now keeps
only the attributes reading would not give anyway: each is left out in
turn when the properties read without it are the same (ConductorProperties
::fromXml(), its defaults); rows by wire and name, so the file does not
depend on memory order. Reading writes every attribute back into the
table, so queries and saves see what they saw before.

f2099: 9 387 rows, the file 3.60 MB -> 3.22 MB (5.14 MB before D3b);
industrial.qet 0.71 -> 0.61 MB. Gates with the master merge and the wire
edits by uuid: ctest 99/99 lazily, 98/99 with every folio built
(modal_quit_regression, the race fixed next); hostile .qetz all refused;
342 corpus projects save, export, round trip and re-save byte-stable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:51 +13:00
ispyisail 798010cc83 Change a wire's properties by uuid without building its folios (stage D5)
Scripts get conductorPropertyByUuid() and setConductorPropertyByUuid(),
the wire named by its uuid (conductorUuids()) rather than by a symbol's
terminal. A change goes to every wire at the wire's potential, as
setConductorProperty() does. When those wires are all on folios not
built, none is numbered by a formula, and what changes in each is what a
.qetz database holds in its own columns (number, function, voltage,
colour and section of the wire, cable, bus), it goes through the project
database: the store and the conductor rows, undoable by uuid. The
potential is followed in the database (projectDataBase::potentialWires():
through wire ends, folio reports and terminal blocks, as
Conductor::relatedPotentialConductors() follows it; a terminal block that
isolates its terminals answers nothing). Building a wire takes its change
(Conductor::fromXml()); a .qetz save writes it in the database, the folio
file copied; a folio written from its XML gets it there; a .qet builds.
Anything else builds the folio, as before.

tst_lazyfolios: every wire's potential from the database = as followed,
1 342 of 1 342 (industrial, eager and from a .qetz); a cable set by uuid
on a 14-wire potential builds no folio, reaches every wire, saves as when
built, undoes, shows once built (fails when building ignores the change,
and when the .qetz writer does). The test's run limit is raised (500 s ->
1 500 s, ctest 1 800 s): under load it ran past 500 s. ctest 96/96 both
ways.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:51 +13:00
ispyisail b8390c853c Read and write .qetz in the qet MCP server
Every tool now takes a .qetz as it takes a .qet. The tools that read a
project's XML read the .qet the configured QElectroTech saves from the
.qetz, made once per version of the file (path, size, modification
time); without a QElectroTech, they say to set QET_BINARY. The tools that
launch QElectroTech hand it the .qetz, and write one when the output path
ends in .qetz. qet_scan counts .qetz files too. The server's description
and README say so.

Reading through QElectroTech instead of the file's own layout keeps the
server right while the zipped format still changes; the first read of a
big .qetz takes as long as QElectroTech needs to save it.

test_qet_mcp.py: the read tools answer the same for a .qetz and its .qet,
qet_diff finds nothing between them, qet_scan counts both, a .qetz is
converted once, qet_query reads it, qet_edit writes one (four fail when
_root() parses the file itself); without QElectroTech the answer names
QET_BINARY. Whole suite: 430 tests pass with QET_ENABLE_SCRIPTING=1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:51 +13:00
ispyisail a6d81a21a8 Build the folios before reading the drawn items' view (stage D6)
A query of drawing_item_view, which is made of the shape, text and
picture tables, built no folio, and with folios built only when needed
(the default since D6) it found nothing: the drawn items are written to
the database by built folios. A query naming the view builds them, as
one naming the tables does.

Found by the qet MCP server's suite (four drawn-item tests failed lazily,
passed with every folio built). tst_lazyfolios: the view's rows from a
.qetz as when built (5 = 5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:51 +13:00
ispyisail 975ec57c74 Write a .qetz's database without keys or indexes (stage D3b)
The database a .qetz holds is read whole: its tables are now written
without their primary keys and indexes, in the order of their rows, and
the file compacted; the reader indexes what it looks folios up by once
it has read the file. Keys and indexes on text uuids were half of the
database once zipped. A file written before reads as before.

f2099 (136 folios) 5.14 MB -> 3.60 MB, industrial.qet 1.03 -> 0.71 MB;
opening and showing a folio take as long as before (2.5 s, 215 ms). Gates
on 342 corpus projects: saves, exports, round trip, re-save byte-stable,
all open; hostile .qetz all refused; ctest 96/96 both ways.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:51 +13:00
ispyisail 9d3b9ae789 Translate the .qetz strings into French
The strings the zipped project format added (Save a copy for an older
QElectroTech, the notice when a .qet is saved as .qetz beside it, the
.qetz file filters) and the undo text of a symbol's information changed
through the project database. Only French, by hand, in the wording the
file already uses ("Projets QElectroTech (*.qet)", "Modifier les
informations de l'élément", "Fichier"); the other languages are left for
their translators. lrelease: 0 unfinished; lupdate over sources keeps
every entry finished.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:51 +13:00
ispyisail 60489c9893 Fix two faults the end-state gates found (stage D7)
A .qetz re-saved after opening from its index saved wrong folio numbers
in its database: reading each folio's header wrote its row with a
number counted from the folios read so far ("1/1", "2/2" for "1/3",
"2/3"), and an index-opened project keeps the rows it was saved with,
so nothing wrote them again. The folio table's rows are no longer
written while the project is being read (projectDataBase::
diagramInfoChanged()). Folio list tables showed it.

A .qetz whose project.sqlite is not a database crashed QElectroTech: the
first statement could not be prepared and SQLite was handed a null
statement. A statement that cannot be prepared now binds nothing and
steps to no row, and the file is refused.

Gates: 342 corpus projects,
written by QElectroTech 0.3 to 0.200, all open; .qet -> .qetz -> .qet
equals the plain resave with every folio built; a .qetz re-saved twice
holds the same bytes entry by entry but the save's own name, path and
time (the first fault made the first re-save differ); hostile .qetz
(truncated, garbage database, view for a table, trigger, escaping and
repeated names, later format, missing folio, 2 GiB zip bomb) neither
crash nor hang and none opens; an unknown extra entry still opens.
tst_lazyfolios: folio numbers with totals from the index as when built
(fails without the fix); tst_qetcontainer: garbage database refused
(segfaults without it). ctest 96/96 both ways; save and export gates
same on 342.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:51 +13:00
ispyisail 5c4fa8fadf Build folios only when needed, by default (stage D6)
Every project now opens without building its folios; a folio is built
when it is shown or something needs its items (QETProject::lazyFolios()).
QET_LAZY_FOLIOS is gone; QET_EAGER_FOLIOS=1 builds every folio on
opening, as before, for comparisons and troubleshooting. Comments that
named the old variable now say "lazy folios"; the tests switch modes
with the new one.

The first save of a .qet as .qetz still builds every folio: a .qet does
not say which program wrote it, so its folios are written again rather
than copied. A .qetz opens and saves without building.

ctest 96/96 by default and with QET_EAGER_FOLIOS=1; 342 corpus projects
save the same in all four arms and export the same. In the GUI
(qet-gui-dialog): a 136-folio .qetz opens with no folio built, shows
folios 1 to 4, saves building none, the save = the original but the time
spent in it; a .qet opens lazily, saves as a .qetz beside it after the
notice, the .qet unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:51 +13:00
ispyisail b151f52428 Save a new title, author or revision without building the folio (stage D5)
A folio not built whose own data changed was built to save, with the
folios its links reach. When what changed is only title block fields
nothing on a folio is computed from -- its title, author, date,
revision index, version, file name, template; a formula reads the
folio's label, plant, location, extra fields and grid -- its items as
read are what building it would write: it is saved from them with its
own data as it is now (Diagram::deferredXmlWithHeader()), .qet and
.qetz alike, with the information of its symbols changed through the
project database (or built, when one of those needs it). Any other
change still builds it.

136-folio .qetz, revision index and author set on every folio by a
script: 0.8 s, saved in 6.8 s, no folio built; with every folio built,
the same file. tst_lazyfolios: a new revision on every folio of a .qet
and of a .qetz saved as when built, building only the folios holding a
table (fails, 47 built, when every change counts); a folio whose label
changed is built (fails when none counts). ctest 96/96 eager and lazy;
342 corpus projects save the same in all four arms and export the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:44 +13:00
ispyisail 244de05421 Change a symbol's information without building its folio (stage D5)
A script changing the information of a symbol on a folio not built, when
nothing else shows that information (no symbol built holds it, no
partner is linked to it, no label formula), changes it through the
project database: the store and the symbol's element info row, undoable
by uuid (ChangeStoredElementInformationCommand). Reading it back reads
the store. Building the symbol later takes the change
(projectDataBase::takeUnbuiltEdit(), in Element::fromXml()), the label
and texts following as for any change. Saving a .qetz writes the
changed information in the database in place of the rows the folio was
read with, its file copied, the folio still not built; a symbol whose
file holds no information block, or a .qet, builds the folio to save.

136-folio .qetz, "comment" set on the 3 513 symbols the database can
change: 0.9 s, no folio built, saved in 6.1 s, none built (27 s and 116
folios when saving built them). The same edits on the 4 039 symbols with
no partner and no formula, lazily and with every folio built: the two
saved projects are the same file. tst_lazyfolios: a script changes a
symbol not built, building none; saved as when built (fails when
building ignores the change, and when the .qetz writer does); undone;
the folio built afterwards shows it. ctest 96/96 eager and lazy; 342
corpus projects save the same in all four arms and export the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:44 +13:00
ispyisail 9078e4e321 Answer scripts from the folio asked for, without building every folio (stage D5)
The scripting API (scripts and the assistant's live mode) reached a folio
through diagrams(), which builds every folio, for every call. A folio's
own data -- count, title, uuid, title block, border, wire defaults --
now comes from folios(), built or not; a call reading or changing a
folio's items builds that folio, as showing it does (built()); selecting
a symbol by uuid builds the folio the project database places it on.
Search and replace and the continuity check still build every folio.

DiagramContent worked out one wire of each potential when made, which
follows potentials through folio reports and built the folios at the
other end, for every caller; it is now worked out when first asked
(Search and replace asks; a merge asks both sides, as before).

tst_lazyfolios: on a .qetz, a script reads every folio's title, uuid and
border building none (fails with diagrams()), and the symbols and wires
of the folio with the most links to other folios as when built, building
that folio only (27 before potentials were lazy). ctest 96/96 but
modal_quit_regression in both modes: a race in that test (gdb interrupts
the main thread inside malloc while the folio view repaints during the
dialog, master too), 2 of 23 runs here, 0 of 10 on master, passing alone.
342 corpus projects save the same in all four arms, and export the same
wiring list and wire numbers lazily.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:44 +13:00
ispyisail 65b10b01d9 Export the wiring list without building every folio (stage D5)
The wiring list export read the project as QETProject::toXml() writes
it, which builds every folio. It now reads a document made without
building: a built folio written as saving writes it
(QETProject::folioXml(), which keeps its symbols' links to partners not
built), a folio not built copied from its XML, and what QElectroTech
computes for a wire's ends -- its symbols' labels, their last partner's
label, the terminals' names -- filled in from the symbols built and from
the project database. A symbol still waiting for a partner not built
takes its partners from the database, saved order.

136-folio .qetz, wiring list from the command line: 30.1 s with every
folio built, 4.5 s lazily, same file. tst_lazyfolios: the wiring list of
a .qet and of a .qetz as when built, building no folio. New corpus gate
(it fails on
all 6 projects tried with terminal names broken): --export-cables and
--export-wires lazily from the .qet and the .qetz = eager on 342
projects; one (forum f3386) differed until waiting symbols took their
partners from the database. ctest 96/96 eager and lazy (one earlier
run had modal_quit_regression and tst_resaveunchanged fail once each,
both passing on rerun in both modes); 342 corpus projects save the same
in all four arms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:44 +13:00
ispyisail b972dbec4b Number terminals and export wire numbers without building every folio (stage D5)
Terminal numbering builds only the folios holding a terminal. The wire
numbers export reads the wires of the folios not built from the project
database, and walks a built folio's own wires instead of a
DiagramContent, which followed their potentials through folio reports
and built the folios at the other end. The wiring list export names a
folio from its title block, which a folio not built has; it still
builds every folio, writing the whole project out (toXml()).

tst_lazyfolios: wire numbers exported as when built, from a .qet and a
.qetz, building no folio (fails with DiagramContent: 27 built);
terminal numbering builds 7 folios, those with a terminal (fails with
diagrams(): 47); the wiring list as when built. ctest 96/96 eager and
lazy; 342 corpus projects save the same in all four arms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:43 +13:00
ispyisail e7fd820db6 Add, move or remove a folio without building the others (stage D5)
Adding, moving or removing a folio rewrote every folio's position in
the project database through diagrams(), which builds every folio: 3 s
on a 50-folio project opened lazily. A folio not built has its position
and title block already, so folios() is enough. The list of tables a
table can continue builds only the folios holding a table (from the
index of a .qetz), and the wiring list's count of wires left out looks
at built folios only: reading a wire drops it unless both its ends are
found.

tst_lazyfolios: a folio added, moved and removed builds no other, with
the folios' rows as when every folio is built (fails with diagrams():
47 built); the tables listed as when built, building the 12 folios that
hold one (fails when every folio is built). ctest 96/96 eager and lazy;
342 corpus projects save the same in all four arms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:43 +13:00
ispyisail 8ae937007c Answer numbering, searches and unused symbols from the database (stage D5)
The project database's element table now holds each symbol's definition
(the path its file names it by) and its sequential numbers, filled from
the document and from built folios alike. Numbering, the searches for
symbols of a kind (a link tab's candidates) and the list of unused
symbols read them for the folios not built, instead of reading those
folios' files. Saving looks up a folio's tables in the index it was
opened from.

A .qetz asked its numbering, searches and unused symbols reads only the
files of the folios it builds (tst_lazyfolios, which fails when the
unused symbols are read from the folio files: 50 read, 22 built).
ctest 96/96 eager and lazy; 342 corpus projects save the same in all
four arms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:43 +13:00
ispyisail b891b6354a Show a folio without building its partners' folios (stage D5b)
Showing a folio built it and every folio holding a partner of its
symbols. Now a partner is built alone, as a symbol, into its folio's
scene (Diagram::buildSymbols()), and reused when that folio is built.

- A linked folio report built alone had no wires when it was linked,
  so its texts showing a wire's properties stayed empty: after any
  build, linked reports on the built folios and their partners look at
  their wires again, as opening does. Unlinked reports keep their texts,
  as opening leaves them.
- A report whose partner was built alone builds the partner's folio
  when its potential is followed.
- tableChainFolios() parsed every unbuilt folio's file. The index now
  lists the drawn tables and the table each continues (folio_table),
  and a .qetz opened from it uses that list. An index without it falls
  back to the folio files.
- A .qetz's project.sqlite is opened once per project
  (QetContainerDb::FolioReader), not once per folio built.

nuri (136 folios), opened fresh, one folio shown: median 1104 -> 184 ms,
each show builds one folio; industrial 234 -> 87 ms.

tst_lazyfolios: a folio whose table continues another is shown first
from a .qetz (fails when the index's chains are ignored). ctest 96/96
eager and lazy; 342-project corpus: master, lazy, .qetz lazy and .qetz
kept all the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:43 +13:00
ispyisail 2f94724df3 Save a .qetz without building partners: symbols save the links they wait for (stage D5a)
A built symbol whose partner is on a folio not built yet (QET_LAZY_FOLIOS)
now saves that link too, in the order it was saved with
(Element::toXml(); QETProject::waitingLinkTargets() keeps it to partners
the project has, so a link to a symbol gone is dropped as it always was).
With that, and with no partner's text in a .qetz's folio files (D3c),
saving a .qetz no longer builds the folios of the partners built folios
wait for; table chains still are (a table saves the table before it), and
a .qet saved as read (QET_LAZY_SAVE_KEPT) still builds them, its folio
files holding partners' texts.

industrial.qetz: an untouched save builds no folio (47 of 47 stay not
built; 19 before), after everyday edits on its busiest folio 29 stay not
built (7 before).

tst_lazyfolios compares the links an edited .qetz saved without building
holds, symbol by symbol in their order, with those the same edits saved
with every folio built -- it fails when waiting links are not saved (the
drawing alone cannot tell: links are kept on both sides, and a symbol
reread gets its partner's back, in another order). Corpus, 342 projects:
a .qetz saved without building, read and saved as .qet = eager = master.
ctest 96/96 with and without QET_LAZY_FOLIOS=1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:43 +13:00
ispyisail 8ad33f6281 Save a .qetz without building the folios not touched (stage D4)
With QET_LAZY_FOLIOS=1, a project opened from a .qetz's index saves a
.qetz without building the folios it never built, when building them
would write the same: their parts are copied as they were and their rows
of the document's engineering data from the project.sqlite they were read
from (QetContainerDb::extract() with kept_from, QetContainer::split() with
the old entries, pictures included). Whether a folio may be kept follows
the rules of stage 5.5 (QETProject::toXml() with kept_parts): nothing
project-wide changed since opening; a folio whose own data changed is
built with its partners; after an edit, folios holding a table filled
from the database are built; the partners still waiting of every built
folio are built. Trusted only for a project opened from a format 3 index
written by this version whose folios are named by their uuid.

Format 3 names a folio by its uuid in the database (doc_meta folio_ids =
uuid) when every folio has one of its own, so that a kept folio keeps its
rows whatever its place; copies keep the rows' order (ORDER BY rowid).
A folio not built keeps the name of its part until it is built
(Diagram::deferredPart()).

Measured, the 136-folio f2099 opened from its .qetz and saved as a .qetz:
4.75 s with no folio built, 33 s with every folio built; its 136 folio
parts copied byte for byte; a kept save of a kept save is the same bytes.
industrial.qetz untouched: 28 of its 47 folios not built are built to save
(link chains, as in 5.5); after everyday edits on its busiest folio, 7
stay not built.

tst_lazyfolios: an untouched .qetz saved without building, read back,
holds the project; edits saved without building hold, read back, what the
same edits saved with every folio built hold (read back too: a wire
numbered from its potential is saved with no sequential numbers whichever
the way -- reading it back renumbers it, a separate issue); fails when a
kept folio's symbol information is not copied. Corpus, 342 projects: a
.qetz saved without building, read and saved as .qet = eager = master on
all 342. ctest
96/96 with and without QET_LAZY_FOLIOS=1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:43 +13:00
ispyisail 8ea051acfc A .qetz keeps nothing QElectroTech computes in its folio files (stage D3c)
Format 3 folio files lose what building a folio computes
(QetContainer::dropComputed()): a wire's description of its ends
(element1_label, element1_name, element1_linked, terminalname1 and the
same for end 2), the content of a symbol text showing the symbol's
information or a composition of it, and -- when the project database's
index is written, every symbol with a uuid of its own -- a symbol's list
of its terminals.

What may go was found by test, not by reading: on 342 projects saved by
this version, each kind taken out of the file, the project opened and
saved: the wire ends came back in all 244 that had them, the symbol texts
in all 201, the terminal lists in 305 of 306 -- not in the file whose
symbols share a uuid (#1408), where wires find their ends by them; such a
project gets no index, so it keeps them.

Stays, for now: labels made from a formula and wire numbers made from one,
in the database (opening from the index and numbering read them), and a
table's size (the user's as much as computed).

tst_qetcontainer compares a joined document with the original less what is
computed, and computedNotKept checks a file holding both kinds (one filled
in by the test) loses them -- it fails when nothing is dropped. Through
QElectroTech, 342 projects: a .qetz opened from its index and saved as .qet
= eager = master; every folio of industrial and f2099 drawn and linked as
eager (f2099 but its 3 render-noise folios). ctest 96/96 with and without
QET_LAZY_FOLIOS=1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:43 +13:00
ispyisail 2f98872d7a .qetz format 3: one project database (stage D3a)
project.sqlite now holds both what QElectroTech keeps in its project
database (the index D2 wrote as index.sqlite) and the document's
engineering data, the latter's tables named doc_<name> beside the
former's -- element, element_info, link and conductor exist in both
(QetContainerDb's SQL names them as before; Database::sql() puts the
prefix on). The manifest says format 3, min-reader 3: a format 2 reader
would find no tables of its names. Format 2 files, with or without an
index.sqlite entry, are read as before: the prefix is found from the
file (doc_meta).

A .qetz with an index opens from project.sqlite. Measured: the 136-folio
f2099 1.8 s, 5.2 MB (the two table sets still overlap: D3b).

Checked: tst_qetcontainer with the format 3 names, 34/34; a format 2 file
of the last commit opens from its index.sqlite and saves the same; corpus
342: 340 .qetz carry the index in project.sqlite and open from it, saved
back = eager = master on all 342. ctest 96/96 with and without
QET_LAZY_FOLIOS=1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:43 +13:00
ispyisail e89ccf2a40 Open a .qetz from its database, each folio read when built (stage D2)
A .qetz now carries index.sqlite: the project database as QElectroTech
keeps it -- every table but the drawing items, which exist for built
folios only -- with each folio's own data (Diagram::headerXml()) and a
stamp of the schema it was written with (projectDataBase::indexFile(),
QETProject::writeContainerFile(): Save, --resave, --set-titleblock, a
script's and live mode's save). Written when every folio, symbol and wire
has a uuid of its own; a reader that does not know the entry skips it.

With QET_LAZY_FOLIOS=1, a .qetz with an index opens from it: the project
document is joined without its folios (QetContainer::join() with
folio_files), the database and the stores of symbol information and wire
properties are filled from the index (fillFromIndex()), each folio is made
from its own data and keeps the name of its part, read when the folio is
built (Diagram::deferredXml(), QETProject::readFolioPart(),
QetContainer::readFolio() -- pictures and engineering data back from
project.sqlite, QetContainerDb::restoreFolio()). A terminal strip finds its
terminals' folios in the database. Any other .qetz, or one whose index
cannot be used, is read whole as before.

The index is untrusted: refused unless written with this database's
schema, every table read a plain table (not a view), nothing in its schema
run (trusted_schema off); refused, the project is read whole. Checked: an
index of garbage, a wrong stamp, a table made a view -- each refused and
the project saved as a plain save.

The 136-folio f2099 as .qetz opens in 1.8 s (3.65 s read whole, 29 s
building every folio); what is left is mostly its 7 MB of symbol
definitions (0.8 s). The file grows from 2.6 to 5.2 MB, the index
overlapping project.sqlite until D3.

tst_lazyfolios: a .qetz opened from its index builds only the terminal
strip's folios, numbering read as when built, every folio shown in turn
draws and links as when built, the save as when built; fails when a folio
part's engineering data is not put back. Corpus, 342 projects: 340 .qetz
carry an index and open from it (2 have symbols sharing a uuid); saved
back as .qet = eager = master on all 342. Probe on industrial and f2099
.qetz: every folio as eager but the 3 render-noise folios of f2099. In a
real window the .qetz opens and pages building only shown folios and
partners. ctest 96/96 with and without QET_LAZY_FOLIOS=1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:43 +13:00
ispyisail ecc7a39131 Save as .qetz by default; a .qet opened is saved beside it (stage D1)
The zipped project (.qetz) is the format QElectroTech saves.

- Save As and the first Save of a new project offer .qetz first
  ("untitled.qetz"); .qet can still be chosen.
- A project read from a .qet is saved as a .qetz beside it, the .qet left
  as it is (QETProject::write(), so Save, autosave, a script's save and the
  assistant's live save all do it). The project then works on the .qetz.
  A path chosen afterwards (Save As, a script naming one) is saved to as it
  is. The editor says so before the first such save, and asks before
  replacing a .qetz already there.
- File > Save a copy for an older QElectroTech writes a .qet copy, for a
  version that cannot open a .qetz; the project keeps its own file.
- The operating systems know the file: a freedesktop MIME type
  application/x-qelectrotech-project+zip (*.qetz, and by its mimetype
  entry when the name says nothing), the desktop file, macOS's document
  types and the Windows installer's association.

Crash backups stay XML, written beside the project as before.

tst_resaveunchanged::qetSavedAsQetzBeside: a .qet saved where it is leaves
the .qet byte for byte and writes a .qetz beside it holding what a plain
save of the .qet holds; it fails when the switch is off. In a real window:
Ctrl+S on perceuse.qet shows the notice, writes perceuse.qetz and leaves
perceuse.qet's md5 unchanged; the copy for an older version (reached by
command search) equals a plain save but for time_spent; a new project's
Save offers untitled.qetz. xdg-mime finds the type by name and by content.
ctest 96/96 with and without QET_LAZY_FOLIOS=1; srclint R-C clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:03:33 +13: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
83 changed files with 5716 additions and 681 deletions
+4
View File
@@ -338,6 +338,9 @@ Section ""
WriteRegStr HKEY_CLASSES_ROOT "qet_diagram_file\DefaultIcon" "" "$final_project_ico" WriteRegStr HKEY_CLASSES_ROOT "qet_diagram_file\DefaultIcon" "" "$final_project_ico"
WriteRegStr HKEY_CLASSES_ROOT "qet_diagram_file\shell\open\command" "" '"$final_qet_exe" "%1"' WriteRegStr HKEY_CLASSES_ROOT "qet_diagram_file\shell\open\command" "" '"$final_qet_exe" "%1"'
; File associations - .qetz, the zipped project: the same file type
WriteRegStr HKEY_CLASSES_ROOT ".qetz" "" "qet_diagram_file"
; File associations - .elmt ; File associations - .elmt
WriteRegStr HKEY_CLASSES_ROOT ".elmt" "" "qet_element_file" WriteRegStr HKEY_CLASSES_ROOT ".elmt" "" "qet_element_file"
WriteRegStr HKEY_CLASSES_ROOT "qet_element_file" "" "Element QET" WriteRegStr HKEY_CLASSES_ROOT "qet_element_file" "" "Element QET"
@@ -486,6 +489,7 @@ Section "Uninstall"
; Remove file associations registry keys ; Remove file associations registry keys
DeleteRegKey HKEY_CLASSES_ROOT "Applications\qelectrotech.exe" DeleteRegKey HKEY_CLASSES_ROOT "Applications\qelectrotech.exe"
DeleteRegKey HKEY_CLASSES_ROOT ".qet" DeleteRegKey HKEY_CLASSES_ROOT ".qet"
DeleteRegKey HKEY_CLASSES_ROOT ".qetz"
DeleteRegKey HKEY_CLASSES_ROOT "qet_diagram_file" DeleteRegKey HKEY_CLASSES_ROOT "qet_diagram_file"
DeleteRegKey HKEY_CLASSES_ROOT ".elmt" DeleteRegKey HKEY_CLASSES_ROOT ".elmt"
DeleteRegKey HKEY_CLASSES_ROOT "qet_element_file" DeleteRegKey HKEY_CLASSES_ROOT "qet_element_file"
+48
View File
@@ -10898,6 +10898,10 @@ Donnez-leur une autre numérotation d&apos;abord.</numerusform>
<source>Management</source> <source>Management</source>
<translation>Management</translation> <translation>Management</translation>
</message> </message>
<message>
<source>Project QElectroTech (*.qet *.qetz)</source>
<translation>Projets QElectroTech (*.qet *.qetz)</translation>
</message>
</context> </context>
<context> <context>
<name>ProjectDBModel</name> <name>ProjectDBModel</name>
@@ -11409,6 +11413,33 @@ Voulez-vous enregistrer les modifications ?</translation>
<comment>window title for a project-less ProjectView</comment> <comment>window title for a project-less ProjectView</comment>
<translation>Projet</translation> <translation>Projet</translation>
</message> </message>
<message>
<source>Zipped project QElectroTech (*.qetz)</source>
<comment>filetypes allowed when saving a project file</comment>
<translation>Projet QElectroTech compressé (*.qetz)</translation>
</message>
<message>
<source>Save a copy for an older QElectroTech</source>
<comment>dialog title</comment>
<translation>Enregistrer une copie pour une version antérieure de QElectroTech</translation>
</message>
<message>
<source>This project was opened from %1. It is saved as %2, beside it, in the zipped project format; %1 stays as it is.
To write a file for a version of QElectroTech that cannot open %2, use File &gt; Save a copy for an older QElectroTech.</source>
<translation>Ce projet a été ouvert depuis %1. Il est enregistré sous %2, à côté, au format de projet compressé ; %1 reste tel quel.
Pour écrire un fichier lisible par une version de QElectroTech qui ne peut pas ouvrir %2, utilisez Fichier &gt; Enregistrer une copie pour une version antérieure de QElectroTech.</translation>
</message>
<message>
<source>Save as a zipped project</source>
<comment>message box title</comment>
<translation>Enregistrement au format de projet compressé</translation>
</message>
<message>
<source>%1 already exists. Replace it?</source>
<translation>%1 existe déjà. Le remplacer ?</translation>
</message>
</context> </context>
<context> <context>
<name>PropertiesEditorDockWidget</name> <name>PropertiesEditorDockWidget</name>
@@ -13692,6 +13723,19 @@ Déponter et/ou supprimer les niveaux des bornes concerné afin de pouvoir les s
<comment>message box title</comment> <comment>message box title</comment>
<translation>Erreur</translation> <translation>Erreur</translation>
</message> </message>
<message>
<source>Save a copy for an older QElectroTech</source>
<translation>Enregistrer une copie pour une version antérieure de QElectroTech</translation>
</message>
<message>
<source>Writes a copy of the current project as a .qet file, which versions of QElectroTech that cannot open a .qetz open</source>
<comment>status bar tip</comment>
<translation>Enregistre une copie du projet courant au format .qet, que savent ouvrir les versions de QElectroTech qui ne peuvent pas ouvrir un .qetz</translation>
</message>
<message>
<source>QElectroTech project(*.qet *.qetz);;XML Files (*.xml);;All files (*)</source>
<translation>Projets QElectroTech (*.qet *.qetz);;Fichiers XML (*.xml);;Tous les fichiers (*)</translation>
</message>
</context> </context>
<context> <context>
<name>QETElementEditor</name> <name>QETElementEditor</name>
@@ -17242,6 +17286,10 @@ Pour l&apos;activer : Configurer QElectroTech &gt; Général &gt; Projets, ou d
<source>Ungroup a set of terminals</source> <source>Ungroup a set of terminals</source>
<translation>Dégrouper un ensemble de bornes</translation> <translation>Dégrouper un ensemble de bornes</translation>
</message> </message>
<message>
<source>Change the information of the element</source>
<translation>Modifier les informations de l&apos;élément</translation>
</message>
</context> </context>
<context> <context>
<name>QTextOrientationWidget</name> <name>QTextOrientationWidget</name>
+1
View File
@@ -32,6 +32,7 @@
<key>CFBundleTypeExtensions</key> <key>CFBundleTypeExtensions</key>
<array> <array>
<string>qet</string> <string>qet</string>
<string>qetz</string>
</array> </array>
<key>CFBundleTypeRole</key> <key>CFBundleTypeRole</key>
<string>Editor</string> <string>Editor</string>
+1 -1
View File
@@ -6,7 +6,7 @@ Exec=qelectrotech %F
Icon=qelectrotech Icon=qelectrotech
Terminal=false Terminal=false
Type=Application Type=Application
MimeType=application/x-qet-project;application/x-qet-element;application/x-qet-titleblock; MimeType=application/x-qet-project;application/x-qelectrotech-project+zip;application/x-qet-element;application/x-qet-titleblock;
Categories=Graphics; Categories=Graphics;
Keywords=Graphics;Science;Electricity;Engineering; Keywords=Graphics;Science;Electricity;Engineering;
Comment=Edit electrical diagrams. Comment=Edit electrical diagrams.
+8
View File
@@ -34,6 +34,14 @@
<comment xml:lang="zh">QElectroTech 项目文件</comment> <comment xml:lang="zh">QElectroTech 项目文件</comment>
<glob pattern="*.qet" /> <glob pattern="*.qet" />
</mime-type> </mime-type>
<mime-type type="application/x-qelectrotech-project+zip">
<sub-class-of type="application/zip"/>
<comment>QElectroTech zipped project file</comment>
<glob pattern="*.qetz" />
<magic priority="60">
<match type="string" offset="30" value="mimetypeapplication/x-qelectrotech-project+zip"/>
</magic>
</mime-type>
<mime-type type="application/x-qet-element"> <mime-type type="application/x-qet-element">
<sub-class-of type="text/xml"/> <sub-class-of type="text/xml"/>
<comment>QElectroTech element file</comment> <comment>QElectroTech element file</comment>
+7
View File
@@ -25,6 +25,13 @@ here read the model.
## Tools ## Tools
Every tool takes a `.qet` or a `.qetz` (the same project, zipped). The
tools that read a project's XML read the `.qet` QElectroTech saves from a
`.qetz`, made once per version of the file with the configured
QElectroTech (`QET_BINARY`); the first read of a big `.qetz` takes as long
as QElectroTech needs to save it. The tools that launch QElectroTech hand
it the `.qetz` itself, and write one when the output path ends in `.qetz`.
| Tool | What it answers | | Tool | What it answers |
|---|---| |---|---|
| `qet_project_info` | title, format version, folios with their uuids, element and conductor counts | | `qet_project_info` | title, format version, folios with their uuids, element and conductor counts |
+55 -7
View File
@@ -97,11 +97,56 @@ def _root(path: str) -> ET.Element:
if not p.is_file(): if not p.is_file():
raise ValueError(f"no such file: {p}") raise ValueError(f"no such file: {p}")
try: try:
return ET.parse(p).getroot() return ET.parse(_as_qet(p)).getroot()
except ET.ParseError as exc: except ET.ParseError as exc:
raise ValueError(f"{p.name} is not parseable XML: {exc}") from exc raise ValueError(f"{p.name} is not parseable XML: {exc}") from exc
# --------------------------------------------------------------------------
# zipped projects (.qetz)
# --------------------------------------------------------------------------
# A .qetz is QElectroTech's zipped project: a database and one file per
# folio. The tools that read a project's XML read the .qet QElectroTech
# saves from it, so they read what QElectroTech reads, whatever the zipped
# format holds; one copy per version of the file (path, size, time).
_QETZ_COPIES: dict = {}
def _is_qetz(path) -> bool:
return str(path).lower().endswith(".qetz")
def _as_qet(path) -> Path:
"""The file the XML tools read for `path`: itself, or for a .qetz the
.qet QElectroTech saves from it (made with the configured binary)."""
p = Path(path).expanduser()
if not _is_qetz(p):
return p
if not p.is_file():
raise ValueError(f"no such file: {p}")
st = p.stat()
key = (str(p.resolve()), st.st_size, st.st_mtime_ns)
cached = _QETZ_COPIES.get(key)
if cached is not None and cached.is_file():
return cached
binary = resolve_binary()
if not binary:
raise ValueError(f"{p.name} is a zipped project (.qetz): reading it needs "
"QElectroTech; set QET_BINARY")
folder = Path(tempfile.mkdtemp(prefix="qet-mcp-qetz-"))
out = folder / (p.stem + ".qet")
# Saving as .qet builds every folio: allow a big project its time
result = _run_qet(str(binary), ["--resave", str(p), str(out)], timeout=900)
if not out.is_file() or out.stat().st_size == 0:
shutil.rmtree(folder, ignore_errors=True)
detail = (result.get("stderr") or result.get("stdout") or "").strip()[-400:]
raise ValueError(f"QElectroTech could not read {p.name}"
+ (f": {detail}" if detail else ""))
_QETZ_COPIES[key] = out
return out
def _element_info(el: ET.Element) -> dict: def _element_info(el: ET.Element) -> dict:
"""The <elementInformations> bag, as a plain dict.""" """The <elementInformations> bag, as a plain dict."""
out = {} out = {}
@@ -796,7 +841,7 @@ def _num(v) -> float:
def tool_scan(directory: str, tag: str = "conductor", def tool_scan(directory: str, tag: str = "conductor",
attribute: str = "cable", recursive: bool = True) -> dict: attribute: str = "cable", recursive: bool = True) -> dict:
"""Sweep every .qet in a directory, counting how many <tag> carry a """Sweep every .qet and .qetz in a directory, counting how many <tag> carry a
non-empty `attribute`. non-empty `attribute`.
This is the corpus question: "3190 conductors, 0 cable values across This is the corpus question: "3190 conductors, 0 cable values across
@@ -805,13 +850,14 @@ def tool_scan(directory: str, tag: str = "conductor",
d = Path(directory).expanduser() d = Path(directory).expanduser()
if not d.is_dir(): if not d.is_dir():
raise ValueError(f"not a directory: {d}") raise ValueError(f"not a directory: {d}")
files = sorted(d.rglob("*.qet") if recursive else d.glob("*.qet")) files = sorted([f for pattern in ("*.qet", "*.qetz")
for f in (d.rglob(pattern) if recursive else d.glob(pattern))])
total = non_empty = 0 total = non_empty = 0
per_file, values, unreadable = [], {}, [] per_file, values, unreadable = [], {}, []
for f in files: for f in files:
try: try:
root = ET.parse(f).getroot() root = ET.parse(_as_qet(f)).getroot()
except ET.ParseError as exc: except (ET.ParseError, ValueError) as exc:
unreadable.append({"file": f.name, "error": str(exc)}) unreadable.append({"file": f.name, "error": str(exc)})
continue continue
n = ne = 0 n = ne = 0
@@ -3919,8 +3965,10 @@ def scripts_dir() -> Path:
SERVER_INSTRUCTIONS = ( SERVER_INSTRUCTIONS = (
"This server works with QElectroTech, a free editor for electrical " "This server works with QElectroTech, a free editor for electrical "
"diagrams. A project (.qet) holds folios (sheets) of symbols " "diagrams. A project (.qet, or .qetz: the same project zipped) holds "
"(elements) joined by wires (conductors). Folio indexes count from 0.\n" "folios (sheets) of symbols (elements) joined by wires (conductors). "
"Every tool takes either; a .qetz is read through QElectroTech, so the "
"first read of a big one takes a while. Folio indexes count from 0.\n"
"Start with qet_about: where QElectroTech keeps things, what is " "Start with qet_about: where QElectroTech keeps things, what is "
"switched on, the stored scripts, and house_style -- this " "switched on, the stored scripts, and house_style -- this "
"installation's own drawing conventions if the user set any. Follow " "installation's own drawing conventions if the user set any. Follow "
+89
View File
@@ -6476,6 +6476,95 @@ class CorpusIntegration(unittest.TestCase):
sb.close() sb.close()
class ZippedProjectWithoutBinary(unittest.TestCase):
def test_reading_a_qetz_without_qelectrotech_says_what_to_set(self):
"""A .qetz is read through QElectroTech (m._as_qet()): with none
configured, the answer names QET_BINARY rather than failing to
parse a zip as XML."""
with tempfile.TemporaryDirectory() as tmp:
z = Path(tmp) / "p.qetz"
z.write_bytes(b"PK\x03\x04 not read")
with mock.patch.object(m, "resolve_binary", return_value=None):
with self.assertRaises(ValueError) as caught:
m.tool_project_info(str(z))
self.assertIn("QET_BINARY", str(caught.exception))
@needs_examples
class ZippedProjects(unittest.TestCase):
"""Every tool reads and writes QElectroTech's zipped project (.qetz) as
it does the .qet: the XML tools read the .qet QElectroTech saves from
it, the tools that launch QElectroTech hand it the .qetz."""
@classmethod
def setUpClass(cls):
cls.tmp = tempfile.TemporaryDirectory()
root = Path(cls.tmp.name)
cls.env = mock.patch.dict(os.environ, {"QET_BINARY": BINARY, "QET_ENABLE_SCRIPTING": "1",
"QET_MCP_SCRIPTS_DIR": str(root / "scripts"),
"QET_MCP_INFO_FILE": str(root / "none.json")})
cls.env.start()
cls.qet, cls.qetz = root / "p.qet", root / "p.qetz"
source = str(Path(EXAMPLES) / "industrial.qet")
for out in (cls.qet, cls.qetz):
r = m._run_qet(BINARY, ["--resave", source, str(out)], timeout=600)
assert out.is_file(), r
assert cls.qetz.read_bytes()[:2] == b"PK"
@classmethod
def tearDownClass(cls):
cls.env.stop()
cls.tmp.cleanup()
def test_read_tools_answer_as_for_the_qet(self):
def strip(d):
return {k: v for k, v in d.items() if k not in ("path", "file")}
for tool in (m.tool_project_info, m.tool_elements, m.tool_conductors, m.tool_items):
self.assertEqual(strip(tool(str(self.qetz))), strip(tool(str(self.qet))), tool.__name__)
def test_diff_finds_nothing_between_the_qet_and_the_qetz(self):
d = m.tool_diff(str(self.qet), str(self.qetz))
self.assertTrue(d.get("identical") or not any(
v.get("added") or v.get("removed") or v.get("changed")
for v in d.values() if isinstance(v, dict)), d)
def test_scan_counts_a_qetz(self):
folder = Path(self.tmp.name) / "scan"
folder.mkdir(exist_ok=True)
for f in (self.qet, self.qetz):
shutil.copy2(f, folder / f.name)
scan = m.tool_scan(str(folder), "conductor", "cable", recursive=False)
self.assertEqual(scan["files"], 2)
self.assertEqual(scan["unreadable"], [])
self.assertEqual(scan["per_file"][0]["conductor"], scan["per_file"][1]["conductor"])
def test_a_qetz_is_converted_once_while_it_does_not_change(self):
m._QETZ_COPIES.clear()
real = m._run_qet
with mock.patch.object(m, "_run_qet", side_effect=real) as run:
m.tool_project_info(str(self.qetz))
m.tool_elements(str(self.qetz))
self.assertEqual(run.call_count, 1)
def test_query_reads_the_qetz_itself(self):
sql = "SELECT COUNT(*) AS n FROM element"
self.assertEqual(m.tool_query(BINARY, str(self.qetz), sql)["rows"],
m.tool_query(BINARY, str(self.qet), sql)["rows"])
def test_edit_writes_a_qetz(self):
first = m.tool_elements(str(self.qetz))["elements"][0]
out = Path(self.tmp.name) / "edited.qetz"
r = m.tool_edit(BINARY, str(self.qetz),
[{"op": "rotate_element", "folio": first["folio"] - 1,
"element": first["uuid"], "angle": 90}],
str(out), ELEMENTS or None)
self.assertTrue(r.get("ok"), r)
self.assertEqual(out.read_bytes()[:2], b"PK")
# The rotation is what the diff of the two .qetz finds
diff = json.dumps(m.tool_diff(str(self.qetz), str(out)))
self.assertIn(m._uuid_key(first["uuid"]) if hasattr(m, "_uuid_key") else first["uuid"], diff.lower())
if __name__ == "__main__": if __name__ == "__main__":
print(f"binary : {BINARY or '(none: integration tests skipped)'}") print(f"binary : {BINARY or '(none: integration tests skipped)'}")
print(f"elements : {ELEMENTS or '(none)'}") print(f"elements : {ELEMENTS or '(none)'}")
@@ -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));
}
} }
} }
} }
@@ -307,7 +307,7 @@ void SearchAndReplaceWidget::fillItemsList()
m_indi_text_qtwi->sortChildren(0, Qt::AscendingOrder); m_indi_text_qtwi->sortChildren(0, Qt::AscendingOrder);
for (Conductor *c : dc.m_potential_conductors) for (Conductor *c : dc.potentialConductors())
{ {
QTreeWidgetItem *qtwi = new QTreeWidgetItem(m_conductor_qtwi); QTreeWidgetItem *qtwi = new QTreeWidgetItem(m_conductor_qtwi);
qtwi->setText(0, c->properties().text); qtwi->setText(0, c->properties().text);
+4 -3
View File
@@ -1011,7 +1011,7 @@ bool TerminalStrip::fromXml(QDomElement &xml_element)
auto xml_layout = xml_element.firstChildElement(QStringLiteral("layout")); auto xml_layout = xml_element.firstChildElement(QStringLiteral("layout"));
if (!xml_layout.isNull()) if (!xml_layout.isNull())
{ {
//The folios holding its terminals are built (QET_LAZY_FOLIOS): //The folios holding its terminals are built (lazy folios):
//a strip is read with the project, before the database could //a strip is read with the project, before the database could
//tell where any symbol is //tell where any symbol is
QSet<QUuid> terminals; QSet<QUuid> terminals;
@@ -1020,8 +1020,9 @@ bool TerminalStrip::fromXml(QDomElement &xml_element)
terminals.insert(QUuid(xml_reals.at(i).toElement().attribute(QStringLiteral("element_uuid")))); terminals.insert(QUuid(xml_reals.at(i).toElement().attribute(QStringLiteral("element_uuid"))));
m_project->buildFoliosHoldingSymbols(terminals); m_project->buildFoliosHoldingSymbols(terminals);
//Get all free elements terminal of the project //Get all free elements terminal of the project: its own are on
const ElementProvider ep(m_project); //the folios just built
const ElementProvider ep(m_project->builtFolios());
auto free_terminals = ep.freeTerminal(); auto free_terminals = ep.freeTerminal();
//Read each physical terminal //Read each physical terminal
+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
+12 -9
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);
@@ -870,16 +873,15 @@ int exportLinks(QETProject &project, const QString &output)
int resaveProject(QETProject &project, const QString &output) int resaveProject(QETProject &project, const QString &output)
{ {
if (QETProject::isContainerPath(output)) { if (QETProject::isContainerPath(output)) {
const QDomDocument doc = project.toXml();
QString error; QString error;
if (!QETProject::writeContainer(doc, output, &error)) { if (!project.writeContainerFile(output, &error)) {
err << "Cannot write '" << output << "': " << error << "\n"; err << "Cannot write '" << output << "': " << error << "\n";
return 1; return 1;
} }
out << "Re-saved project -> " << output << "\n"; out << "Re-saved project -> " << output << "\n";
return 0; return 0;
} }
//Folios not built (QET_LAZY_FOLIOS) written as read when that is //Folios not built (lazy folios) written as read when that is
//what building them would write (QETProject::toXml()) //what building them would write (QETProject::toXml())
QHash<int, QByteArray> kept_folios; QHash<int, QByteArray> kept_folios;
const QDomDocument doc = project.toXml(&kept_folios); const QDomDocument doc = project.toXml(&kept_folios);
@@ -968,15 +970,14 @@ int setTitleBlock(QETProject &project, const QString &output,
} }
if (QETProject::isContainerPath(output)) { if (QETProject::isContainerPath(output)) {
const QDomDocument doc = project.toXml();
QString error; QString error;
if (!QETProject::writeContainer(doc, output, &error)) { if (!project.writeContainerFile(output, &error)) {
err << "Cannot write '" << output << "': " << error << "\n"; err << "Cannot write '" << output << "': " << error << "\n";
return 1; return 1;
} }
} else { } else {
//Folios not built (QET_LAZY_FOLIOS) written as read when that is //Folios not built (lazy folios) written as read when that
//what building them would write (QETProject::toXml()) //is what building them would write (QETProject::toXml())
QHash<int, QByteArray> kept_folios; QHash<int, QByteArray> kept_folios;
const QDomDocument doc = project.toXml(&kept_folios); const QDomDocument doc = project.toXml(&kept_folios);
QFile file(output); QFile file(output);
@@ -1014,6 +1015,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 +1087,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.
+64 -9
View File
@@ -62,33 +62,80 @@ namespace ConductorMultiEdit
@brief targets @brief targets
@param selected : the edited conductors @param selected : the edited conductors
@param apply_all : true to add every conductor of their potentials @param apply_all : true to add every conductor of their potentials
@param function_separated : set to the conductors that share the
potential of an edited one only by crossing a function separated
terminal element -- conductors undo() then gives the voltage /
protocol only, and no other property. nullptr, the default, leaves
those conductors out of the returned list entirely.
A conductor the user picked themselves is never part of it: the
selection is the edit reaching that conductor, so it takes the
full edit however remote it is from the other selected ones --
picking a wire on each side of a separation and editing them is
exactly that. Nor is a conductor that belongs to a potential, on
any path, however far that potential is reached.
@return each conductor an edit reaches, once @return each conductor an edit reaches, once
*/ */
template <typename T> template <typename T>
QList<T *> targets(const QList<T *> &selected, bool apply_all) QList<T *> targets(const QList<T *> &selected, bool apply_all,
QSet<T *> *function_separated = nullptr)
{ {
QList<T *> list; QList<T *> list;
QSet<T *> seen; QSet<T *> seen;
//The explicitly picked ones, so a separation met while walking
//the list cannot demote a wire the user asked to edit.
QSet<T *> chosen;
for (T *conductor : selected)
if (conductor)
chosen.insert(conductor);
//What the potentials reach, and what only a function separation
//reaches. A conductor can be in both -- wired normally on one
//path and across a separation on another -- and then belongs to
//the potential.
QSet<T *> potential_reached, separated_reached;
for (T *conductor : selected) for (T *conductor : selected)
{ {
//Already reached through an earlier conductor's potential: if (!conductor)
//its own potential is the same one.
if (!conductor || seen.contains(conductor))
continue; continue;
seen.insert(conductor); //Already reached through an earlier conductor's potential or
list << conductor; //separation: its own potential adds nothing new to walk over
if (!apply_all) //-- unless the user picked it, because a separation may have
//put it in the list before its potential was ever reached.
const bool fresh = !seen.contains(conductor);
if (fresh)
{
seen.insert(conductor);
list << conductor;
}
if (!apply_all || (!fresh && !chosen.contains(conductor)))
continue; continue;
const QSet<T *> potential = conductor->relatedPotentialConductors(); const QSet<T *> potential = conductor->relatedPotentialConductors();
for (T *other : potential) for (T *other : potential)
{ {
potential_reached.insert(other);
if (!seen.contains(other)) if (!seen.contains(other))
{ {
seen.insert(other); seen.insert(other);
list << other; list << other;
} }
} }
if (function_separated)
{
const QSet<T *> separated = conductor->functionSeparatedConductors();
for (T *other : separated)
{
separated_reached.insert(other);
if (!seen.contains(other))
{
seen.insert(other);
list << other;
}
}
}
} }
if (function_separated)
*function_separated = separated_reached - potential_reached - chosen;
return list; return list;
} }
@@ -97,13 +144,18 @@ namespace ConductorMultiEdit
@param targets : the conductors to change, see targets() @param targets : the conductors to change, see targets()
@param shown : the properties as the panel showed them @param shown : the properties as the panel showed them
@param edited : the properties after the user's edit @param edited : the properties after the user's edit
@param function_separated : the targets of targets() that only a
function separated terminal element links to the edited
conductors: they get the voltage / protocol and nothing else
(@see ConductorProperties::applyTensionProtocolChange)
@return one undo step giving each target the fields that differ @return one undo step giving each target the fields that differ
between shown and edited, or nullptr if no conductor changes between shown and edited, or nullptr if no conductor changes
*/ */
template <typename T> template <typename T>
QUndoCommand *undo(const QList<T *> &targets, QUndoCommand *undo(const QList<T *> &targets,
const ConductorProperties &shown, const ConductorProperties &shown,
const ConductorProperties &edited) const ConductorProperties &edited,
const QSet<T *> &function_separated = QSet<T *>())
{ {
if (edited == shown) if (edited == shown)
return nullptr; return nullptr;
@@ -114,7 +166,10 @@ namespace ConductorMultiEdit
{ {
const ConductorProperties old_properties = conductor->properties(); const ConductorProperties old_properties = conductor->properties();
ConductorProperties properties = old_properties; ConductorProperties properties = old_properties;
properties.applyChanges(shown, edited); if (function_separated.contains(conductor))
properties.applyTensionProtocolChange(shown, edited);
else
properties.applyChanges(shown, edited);
if (properties == old_properties) if (properties == old_properties)
continue; continue;
+25 -4
View File
@@ -19,11 +19,12 @@
#include "qetproject.h" #include "qetproject.h"
#include "qetapp.h" #include "qetapp.h"
#include "diagram.h" #include "diagram.h"
#include "diagramcontent.h"
#include "qetgraphicsitem/conductor.h" #include "qetgraphicsitem/conductor.h"
#include "qetgraphicsitem/conductortextitem.h" #include "qetgraphicsitem/conductortextitem.h"
#include "qetgraphicsitem/element.h" #include "qetgraphicsitem/element.h"
#include "qetgraphicsitem/terminal.h" #include "qetgraphicsitem/terminal.h"
#include "dataBase/projectdatabase.h"
#include "properties/elementdata.h"
#include <QFileDialog> #include <QFileDialog>
@@ -114,10 +115,30 @@ void ConductorNumExport::fillHash()
{ {
//We used this rx to avoid insert num composed only withe white space. //We used this rx to avoid insert num composed only withe white space.
QRegularExpression rx("^ *$"); QRegularExpression rx("^ *$");
for (Diagram *d : m_project->diagrams()) auto count = [this](const QString &num) {
m_hash.insert(num, m_hash.value(num, 0) + 1);
};
//The folios not built (lazy folios): their wires from the
//project database, by the same rules
QList<QUuid> unbuilt;
for (Diagram *d : m_project->folios())
if (!d->isLoaded()) unbuilt << d->uuid();
for (const auto &wire : m_project->dataBase()->wiresOnFolios(unbuilt))
{ {
DiagramContent dc(d, false); if (wire.text.isEmpty() || wire.text.contains(rx)) {
for (Conductor *c : dc.conductors()) continue;
}
const bool report1 = ElementData::typeFromString(wire.kind1) & ElementData::AllReport;
const bool report2 = ElementData::typeFromString(wire.kind2) & ElementData::AllReport;
if (!(report1 && report2)) {
count(wire.text);
}
}
for (Diagram *d : m_project->builtFolios())
{
//Its wires only: a DiagramContent would follow their
//potentials onto other folios, building those not built
for (Conductor *c : d->conductors())
{ {
QString num = c->textItem()->toPlainText(); QString num = c->textItem()->toPlainText();
if (num.isEmpty() || num.contains(rx)) { if (num.isEmpty() || num.contains(rx)) {
+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();
+199
View File
@@ -0,0 +1,199 @@
# The `.qetz` project file, format 4
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 4. `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 computed is kept.** What QElectroTech works out again when it
builds a folio is left out of the folio file: a wire's description of its
ends (`element1_label`, `element1_name`, `element1_linked`,
`terminalname1`, and the same for end 2), the text of a symbol's text
item when it shows the symbol's information or a composition of it, and,
when every folio, symbol and wire has a uuid of its own, a symbol's list
of its terminals.
- **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.
- **The database holds the project's data only.** QElectroTech's own
working database (search, numbering, cross-references) is built from it
when the project opens; it is not stored.
## 2. The archive
| Entry | Content |
|---|---|
| `mimetype` | `application/x-qelectrotech-project+zip`, the first entry, stored uncompressed |
| `manifest.xml` | `<qetz format="4" min-reader="4" 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 | #1473 | read: as format 4, but with keys and constraints on its tables, and the computed text left in the folio files |
| 4 | this document | read and written |
## 4. The database
### 4.1 Schema
The tables are written **without keys or constraints**: each has the
columns and types below, but no `PRIMARY KEY`, `NOT NULL`, `UNIQUE` or
`REFERENCES`. With text uuids, keys and their indexes were half the file. A
reader builds the indexes it needs once the file is read, and checks itself
what the keys would have (section 5).
<!-- 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 a row twice, 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.
+172 -7
View File
@@ -22,6 +22,8 @@
#include <QHash> #include <QHash>
#include <QMap> #include <QMap>
#include <QSet> #include <QSet>
#include <QRegularExpression>
#include <QUuid>
const char QetContainer::MimeType[] = "application/x-qelectrotech-project+zip"; const char QetContainer::MimeType[] = "application/x-qelectrotech-project+zip";
@@ -93,6 +95,20 @@ QList<QDomElement> childElements(const QDomElement &parent, const QString &tag)
return list; return list;
} }
//An element and the text nodes on either side of it out of its parent,
//one text node left where they were, so nothing is read twice
void removeElement(QDomElement e)
{
QDomNode parent = e.parentNode();
QDomNode before = e.previousSibling(), after = e.nextSibling();
parent.removeChild(e);
if (before.isText() && after.isText()) {
before.setNodeValue(before.nodeValue() + after.nodeValue());
parent.removeChild(after);
}
}
//The collection's symbol definitions, each to elements/<categories>/<name> //The collection's symbol definitions, each to elements/<categories>/<name>
void splitCollection(QDomDocument &document, const QDomElement &category, const QString &path, void splitCollection(QDomDocument &document, const QDomElement &category, const QString &path,
QList<QetZip::Entry> *parts, QSet<QString> *taken) QList<QetZip::Entry> *parts, QSet<QString> *taken)
@@ -153,15 +169,93 @@ 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, nullptr, pictures, format);
}
QString QetContainer::pictureFile(const QDomElement &image)
{
return image.attribute(ImageFileAttribute);
}
/**
@brief QetContainer::ownUuids
@return true if every folio, symbol and wire of @p project has a uuid
no other has: a wire then finds its ends without the symbols' terminal
lists, which split() leaves out
*/
bool QetContainer::ownUuids(const QDomElement &project)
{
QSet<QString> seen;
auto own = [&seen](const QDomElement &item) {
const QString uuid = item.attribute(QStringLiteral("uuid"));
if (QUuid(uuid).isNull() || seen.contains(uuid)) return false;
seen.insert(uuid);
return true;
};
for (const QDomElement &diagram : childElements(project, QStringLiteral("diagram"))) {
if (!own(diagram)) return false;
for (const QDomElement &list : childElements(diagram, QStringLiteral("elements")))
for (const QDomElement &element : childElements(list, QStringLiteral("element")))
if (!own(element)) return false;
for (const QDomElement &list : childElements(diagram, QStringLiteral("conductors")))
for (const QDomElement &wire : childElements(list, QStringLiteral("conductor")))
if (!own(wire)) return false;
}
return true;
}
/**
@brief QetContainer::dropComputed
Take out of the folio @p diagram what QElectroTech computes when it
builds the folio (stage D3c): a wire's description of
its ends (element1_label, element1_name, element1_linked, terminalname1,
and the same for end 2), a symbol text's content where it shows the
symbol's information or a composition of it, and -- with
@p terminal_lists, when every symbol has a uuid of its own -- a symbol's
list of its terminals. Checked on 342 projects: opened and saved, each
comes back as it was, but the terminal lists of a file whose symbols
share a uuid (F100), where wires find their ends by them.
*/
void QetContainer::dropComputed(QDomElement diagram, bool terminal_lists)
{
for (QDomElement list : childElements(diagram, QStringLiteral("conductors")))
for (QDomElement wire : childElements(list, QStringLiteral("conductor")))
for (const char *end : {"1", "2"})
for (const char *name : {"element%1_label", "element%1_name", "element%1_linked",
"terminalname%1"})
wire.removeAttribute(QString::fromLatin1(name).arg(QLatin1String(end)));
for (QDomElement list : childElements(diagram, QStringLiteral("elements")))
{
for (QDomElement element : childElements(list, QStringLiteral("element")))
{
if (terminal_lists)
for (QDomElement terminals : childElements(element, QStringLiteral("terminals")))
removeElement(terminals);
for (QDomElement texts : childElements(element, QStringLiteral("dynamic_texts")))
{
for (QDomElement text : childElements(texts, QStringLiteral("dynamic_elmt_text")))
{
const QString from = text.attribute(QStringLiteral("text_from"));
if (from != QLatin1String("ElementInfo") && from != QLatin1String("CompositeText"))
continue;
for (QDomElement content : childElements(text, QStringLiteral("text")))
while (content.hasChildNodes())
content.removeChild(content.firstChild());
}
}
}
}
} }
/** /**
@@ -172,7 +266,7 @@ bool QetContainer::readDocument(const QByteArray &content, QDomDocument *documen
set if @p project is not a project set if @p project is not a project
*/ */
QList<QetZip::Entry> QetContainer::split(const QDomDocument &project, const QString &writer, QList<QetZip::Entry> QetContainer::split(const QDomDocument &project, const QString &writer,
QString *error) QString *error, const QHash<QString, QByteArray> &kept_from)
{ {
QDomDocument document = project.cloneNode(true).toDocument(); QDomDocument document = project.cloneNode(true).toDocument();
QDomElement root = document.documentElement(); QDomElement root = document.documentElement();
@@ -182,7 +276,34 @@ QList<QetZip::Entry> QetContainer::split(const QDomDocument &project, const QStr
} }
//The engineering data out of the document, into project.sqlite //The engineering data out of the document, into project.sqlite
const QByteArray database = QetContainerDb::extract(root, error); //A folio written as it was read (<diagram qetz-kept-part="..."/>,
//stage D4) keeps its part and, through
//@p kept_from's project.sqlite, its rows
//A symbol changed on a folio kept as it was gets its information
//rows written anew: its part must have the place they go back to
for (const QDomElement &diagram : childElements(root, QStringLiteral("diagram")))
{
const QList<QDomElement> edited = childElements(diagram, QStringLiteral("qetz-edited-element"));
if (edited.isEmpty()) continue;
QDomDocument part;
if (!parse(kept_from.value(diagram.attribute(QStringLiteral("qetz-kept-part"))), &part, error))
return {};
QSet<QString> placed;
const QDomNodeList elements = part.elementsByTagName(QStringLiteral("element"));
for (int i = 0 ; i < elements.size() ; ++i) {
const QDomElement element = elements.at(i).toElement();
for (const QDomElement &marker : childElements(element, QStringLiteral("qetz-db")))
if (marker.attribute(QStringLiteral("block")) == QLatin1String("elementInformations"))
placed.insert(element.attribute(QStringLiteral("uuid")));
}
for (const QDomElement &symbol : edited)
if (!placed.contains(symbol.attribute(QStringLiteral("uuid")))) {
setError(error, QStringLiteral("a symbol changed on a folio kept as it was has no place for its information"));
return {};
}
}
const QByteArray database = QetContainerDb::extract(
root, error, kept_from.value(QStringLiteral("project.sqlite")));
if (database.isEmpty()) if (database.isEmpty())
return {}; return {};
@@ -191,7 +312,36 @@ QList<QetZip::Entry> QetContainer::split(const QDomDocument &project, const QStr
//Folios, each with its pictures taken out //Folios, each with its pictures taken out
int folio = 0; int folio = 0;
//Nothing QElectroTech computes is kept (format 4)
const bool own_uuids = ownUuids(root);
for (QDomElement diagram : childElements(root, QStringLiteral("diagram")))
dropComputed(diagram, own_uuids);
//The parts of the folios kept as they were, and their pictures
QMap<QString, QByteArray> pictures; //name -> png, once each QMap<QString, QByteArray> pictures; //name -> png, once each
for (QDomElement diagram : childElements(root, QStringLiteral("diagram")))
{
const QString kept = diagram.attribute(QStringLiteral("qetz-kept-part"));
if (kept.isEmpty()) continue;
if (!kept_from.contains(kept)) {
setError(error, QStringLiteral("the part \"%1\" of a folio kept as it was is missing").arg(kept));
return {};
}
taken.insert(kept);
const QByteArray part = kept_from.value(kept);
static const QRegularExpression image_file(QStringLiteral("%1=\"(images/[^\"]+)\"").arg(ImageFileAttribute));
for (const auto &match : image_file.globalMatch(QString::fromUtf8(part))) {
const QString name = match.captured(1);
if (!kept_from.contains(name)) {
setError(error, QStringLiteral("the picture \"%1\" of a folio kept as it was is missing").arg(name));
return {};
}
pictures.insert(name, kept_from.value(name));
}
parts.append(QetZip::Entry{kept, part, true});
root.replaceChild(placeholder(document, kept), diagram);
}
for (QDomElement diagram : childElements(root, QStringLiteral("diagram"))) for (QDomElement diagram : childElements(root, QStringLiteral("diagram")))
{ {
++folio; ++folio;
@@ -264,8 +414,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. With @p folio_files, the part each folio was read from, in
order. @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,
QStringList *folio_files, 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 +441,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;
@@ -297,6 +454,9 @@ bool QetContainer::join(const QList<QetZip::Entry> &entries, QDomDocument *proje
for (QDomElement part : placeholders) for (QDomElement part : placeholders)
{ {
const QString file = part.attribute(FileAttribute); const QString file = part.attribute(FileAttribute);
if (folio_files && part.parentNode() == project->documentElement()
&& file.startsWith(QLatin1String("folios/")))
folio_files->append(file);
if (!files.contains(file)) { if (!files.contains(file)) {
setError(error, QStringLiteral("part \"%1\" is missing").arg(file)); setError(error, QStringLiteral("part \"%1\" is missing").arg(file));
return false; return false;
@@ -310,7 +470,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 +482,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());
+34 -5
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,54 @@
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 = 4;
/// 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.
/// 4: what QElectroTech computes left out of the folios
/// (dropComputed()), the tables written without keys or indexes.
/// project.sqlite holds the engineering data once: QElectroTech's
/// own project database is built from it when the project opens.
/// Formats 2 and 3 are 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,
const QHash<QString, QByteArray> &kept_from = QHash<QString, QByteArray>());
bool join(const QList<QetZip::Entry> &entries, QDomDocument *project, bool join(const QList<QetZip::Entry> &entries, QDomDocument *project,
QString *error = nullptr); QString *error = nullptr, QStringList *folio_files = 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);
/// Take what QElectroTech computes out of a folio (format 4)
void dropComputed(QDomElement diagram, bool terminal_lists);
/// true if every folio, symbol and wire has a uuid of its own
bool ownUuids(const QDomElement &project);
/// 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
File diff suppressed because it is too large Load Diff
+8 -3
View File
@@ -30,15 +30,20 @@
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.
*/ */
namespace QetContainerDb namespace QetContainerDb
{ {
QByteArray extract(QDomElement project, QString *error = nullptr); QByteArray extract(QDomElement project, QString *error = nullptr,
const QByteArray &kept_from = QByteArray());
bool restore(QDomElement project, const QByteArray &database, QString *error = nullptr); bool restore(QDomElement project, const QByteArray &database, QString *error = nullptr);
/// true if @p database is of the format this writes
bool currentFormat(const QByteArray &database);
} }
#endif // QETCONTAINERDB_H #endif // QETCONTAINERDB_H
+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
{ {
+567 -15
View File
@@ -49,6 +49,8 @@
#include <QMetaEnum> #include <QMetaEnum>
#include <QTextDocument> #include <QTextDocument>
#include <QFile> #include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QRegularExpression> #include <QRegularExpression>
#include <QSqlDriver> #include <QSqlDriver>
#include <QSqlError> #include <QSqlError>
@@ -137,7 +139,7 @@ void projectDataBase::updateDB()
@brief projectDataBase::keepUnbuiltFolioRows @brief projectDataBase::keepUnbuiltFolioRows
Before the tables are rebuilt from the folios: keep the element, Before the tables are rebuilt from the folios: keep the element,
element_info, terminal, conductor and link rows of the folios not built element_info, terminal, conductor and link rows of the folios not built
yet (QET_LAZY_FOLIOS). They came from the document, nothing of those yet (lazy folios). They came from the document, nothing of those
folios can have changed since, and the rebuild puts them back where folios can have changed since, and the rebuild puts them back where
each folio comes (restoreKeptRows()). Nothing to do when every folio each folio comes (restoreKeptRows()). Nothing to do when every folio
is built. is built.
@@ -253,6 +255,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
{ {
@@ -305,6 +359,8 @@ struct DocumentElement
QString type; QString type;
QString sub_type; QString sub_type;
QVariant group; QVariant group;
QString location;
autonum::sequentialNumbers sequence;
DiagramContext informations; DiagramContext informations;
QString label; QString label;
QHash<QUuid, DocumentTerminal> terminals; QHash<QUuid, DocumentTerminal> terminals;
@@ -325,6 +381,27 @@ bool readSequence(const QDomElement &item, autonum::sequentialNumbers *sequence)
return true; return true;
} }
//The sequential values an element was saved with, as Element::fromXml()
//reads them, the older attributes included
autonum::sequentialNumbers elementSequence(const QDomElement &item)
{
autonum::sequentialNumbers sequence;
if (readSequence(item, &sequence)) {
return sequence;
}
auto load = [&item](const QString &prefix, QStringList *list) {
for (int i = 1 ; !item.attribute(prefix + QString::number(i)).isEmpty() ; ++i)
list->append(item.attribute(prefix + QString::number(i)));
};
load(QStringLiteral("sequ_"), &sequence.unit);
load(QStringLiteral("sequf_"), &sequence.unit_folio);
load(QStringLiteral("seqt_"), &sequence.ten);
load(QStringLiteral("seqtf_"), &sequence.ten_folio);
load(QStringLiteral("seqh_"), &sequence.hundred);
load(QStringLiteral("seqhf_"), &sequence.hundred_folio);
return sequence;
}
struct DocumentConductor struct DocumentConductor
{ {
QString uuid; QString uuid;
@@ -376,7 +453,7 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString
//The folios, in the order the project read them (readDiagramsXml()), //The folios, in the order the project read them (readDiagramsXml()),
//each with its saved uuid -- the one the built folio has. //each with its saved uuid -- the one the built folio has.
const QDomNodeList diagram_nodes = document.elementsByTagName(QStringLiteral("diagram")); const QDomNodeList diagram_nodes = document.elementsByTagName(QStringLiteral("diagram"));
//Built or not (QET_LAZY_FOLIOS): only their uuids are read, and the //Built or not (lazy folios): only their uuids are read, and the
//conductors of those built //conductors of those built
const QList<Diagram *> diagrams = m_project->folios(); const QList<Diagram *> diagrams = m_project->folios();
if (diagram_nodes.size() != diagrams.size()) { if (diagram_nodes.size() != diagrams.size()) {
@@ -555,6 +632,8 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString
element.type = known->type; element.type = known->type;
element.sub_type = known->sub_type; element.sub_type = known->sub_type;
element.terminals = known->terminals; element.terminals = known->terminals;
element.location = element_xml.attribute(QStringLiteral("type"));
element.sequence = elementSequence(element_xml);
const QUuid group = ItemGroups::read(element_xml); const QUuid group = ItemGroups::read(element_xml);
element.group = group.isNull() ? QVariant() : QVariant(group.toString()); element.group = group.isNull() ? QVariant() : QVariant(group.toString());
element.informations.fromXml( element.informations.fromXml(
@@ -756,6 +835,8 @@ bool projectDataBase::populateFromDocument(const QDomDocument &document, QString
m_insert_elements_query.bindValue(QStringLiteral(":type"), element.type); m_insert_elements_query.bindValue(QStringLiteral(":type"), element.type);
m_insert_elements_query.bindValue(QStringLiteral(":sub_type"), element.sub_type); m_insert_elements_query.bindValue(QStringLiteral(":sub_type"), element.sub_type);
m_insert_elements_query.bindValue(QStringLiteral(":group_uuid"), element.group); m_insert_elements_query.bindValue(QStringLiteral(":group_uuid"), element.group);
m_insert_elements_query.bindValue(QStringLiteral(":definition"), element.location);
m_insert_elements_query.bindValue(QStringLiteral(":sequence"), sequenceValue(element.sequence));
if (!m_insert_elements_query.exec()) { if (!m_insert_elements_query.exec()) {
qDebug() << "projectDataBase::populateFromDocument element insert error : " << m_insert_elements_query.lastError(); qDebug() << "projectDataBase::populateFromDocument element insert error : " << m_insert_elements_query.lastError();
} }
@@ -829,7 +910,7 @@ QSet<QUuid> projectDataBase::foliosWithUnjoinedWires() const
@brief projectDataBase::elementTypesOnFolios @brief projectDataBase::elementTypesOnFolios
@return the kind (ElementData::typeToString()) of every symbol on the @return the kind (ElementData::typeToString()) of every symbol on the
folios @p folios, by uuid, read from the element table as it is: for folios @p folios, by uuid, read from the element table as it is: for
folios not built yet (QET_LAZY_FOLIOS) its rows came from the document folios not built yet (lazy folios) its rows came from the document
and nothing can have changed them. A symbol whose definition was not and nothing can have changed them. A symbol whose definition was not
found has no row. found has no row.
*/ */
@@ -850,11 +931,466 @@ QHash<QUuid, QString> projectDataBase::elementTypesOnFolios(const QSet<QUuid> &f
return types; return types;
} }
/**
@brief projectDataBase::symbolsOnFolios
@return the symbols on the folios @p folios, folio by folio in that
order and on each in the order of its rows -- the order the folio
places them -- read from the element table as it is (see
elementTypesOnFolios()). A symbol whose definition was not found has
no row.
*/
QList<projectDataBase::FolioSymbol> projectDataBase::symbolsOnFolios(const QList<QUuid> &folios) const
{
QList<FolioSymbol> symbols;
QSqlQuery query(m_data_base);
query.prepare(QStringLiteral("SELECT uuid, type, definition, sequence FROM element "
"WHERE diagram_uuid = :folio ORDER BY rowid"));
for (const QUuid &folio : folios) {
query.bindValue(QStringLiteral(":folio"), folio.toString());
if (!query.exec()) {
continue;
}
while (query.next()) {
symbols.append({QUuid(query.value(0).toString()), folio, query.value(1).toString(),
query.value(2).toString(), sequenceFromValue(query.value(3))});
}
}
return symbols;
}
/**
@brief projectDataBase::wiresOnFolios
@return the wires on the folios @p folios, folio by folio in that order,
read from the conductor table as it is (see elementTypesOnFolios())
*/
QList<projectDataBase::FolioWire> projectDataBase::wiresOnFolios(const QList<QUuid> &folios) const
{
QList<FolioWire> wires;
QSqlQuery query(m_data_base);
query.prepare(QStringLiteral("SELECT c.text, e1.type, e2.type FROM conductor c "
"LEFT JOIN element e1 ON e1.uuid = c.terminal1_element_uuid "
"LEFT JOIN element e2 ON e2.uuid = c.terminal2_element_uuid "
"WHERE c.diagram_uuid = :folio ORDER BY c.rowid"));
for (const QUuid &folio : folios) {
query.bindValue(QStringLiteral(":folio"), folio.toString());
if (!query.exec()) {
continue;
}
while (query.next()) {
wires.append({query.value(0).toString(), query.value(1).toString(),
query.value(2).toString()});
}
}
return wires;
}
/**
@brief projectDataBase::elementLabel
@return the label the symbol @p element shows, read from the element
info table as it is (see elementTypesOnFolios())
*/
QString projectDataBase::elementLabel(const QUuid &element) const
{
QSqlQuery query(m_data_base);
query.prepare(QStringLiteral("SELECT label FROM element_info WHERE element_uuid = :uuid"));
query.bindValue(QStringLiteral(":uuid"), element.toString());
return query.exec() && query.next() ? query.value(0).toString() : QString();
}
/**
@brief projectDataBase::terminalName
@return the name of the terminal @p terminal of the symbol @p element,
read from the terminal table as it is (see elementTypesOnFolios()): a
terminal a wire ends on has a row
*/
QString projectDataBase::terminalName(const QUuid &terminal, const QUuid &element) const
{
QSqlQuery query(m_data_base);
query.prepare(QStringLiteral("SELECT name FROM terminal WHERE uuid = :uuid AND element_uuid = :element"));
query.bindValue(QStringLiteral(":uuid"), terminal.toString());
query.bindValue(QStringLiteral(":element"), element.toString());
return query.exec() && query.next() ? query.value(0).toString() : QString();
}
/**
@brief projectDataBase::potentialWires
@return the wires at the potential of @p wire, @p wire included, read
from the conductor, element and link tables as they are (see
elementTypesOnFolios()), followed as Conductor::relatedPotentialConductors()
follows them over every folio: through a wire's ends, from a folio
report to the terminals of the first symbol it is linked to, and from a
terminal block's terminal to its others. Nothing when a terminal block
on the way isolates its terminals (their groups are in its definition,
not in the database) or @p wire has no row.
*/
std::optional<QSet<QUuid>> projectDataBase::potentialWires(const QUuid &wire) const
{
using Node = std::pair<QString, QString>; //element uuid, terminal uuid
QHash<QString, std::pair<Node, Node>> ends;
QHash<Node, QStringList> wires_at;
QHash<QString, QList<Node>> nodes_of;
{
QSqlQuery query(m_data_base);
query.exec(QStringLiteral("SELECT uuid, terminal1_element_uuid, terminal1_uuid, "
"terminal2_element_uuid, terminal2_uuid FROM conductor"));
while (query.next()) {
const Node a{query.value(1).toString(), query.value(2).toString()};
const Node b{query.value(3).toString(), query.value(4).toString()};
const QString uuid = query.value(0).toString();
ends.insert(uuid, {a, b});
for (const Node &n : {a, b}) {
wires_at[n] << uuid;
if (!nodes_of.value(n.first).contains(n)) nodes_of[n.first] << n;
}
}
}
const auto start = ends.constFind(wire.toString());
if (start == ends.constEnd()) {
return std::nullopt;
}
QHash<QString, ElementData::Type> kinds;
{
QSqlQuery query(m_data_base);
query.exec(QStringLiteral("SELECT uuid, type FROM element"));
while (query.next())
kinds.insert(query.value(0).toString(), ElementData::typeFromString(query.value(1).toString()));
}
QSet<QUuid> wires;
QSet<Node> seen;
QList<Node> todo{start->first, start->second};
while (!todo.isEmpty()) {
const Node node = todo.takeLast();
if (seen.contains(node)) continue;
seen.insert(node);
for (const QString &w : wires_at.value(node)) {
if (wires.contains(QUuid(w))) continue;
wires.insert(QUuid(w));
const auto e = ends.value(w);
todo << e.first << e.second;
}
const ElementData::Type kind = kinds.value(node.first, ElementData::Simple);
if (kind & ElementData::AllReport) {
const QList<QUuid> partners = linkedSymbols(QUuid(node.first));
if (!partners.isEmpty())
todo << nodes_of.value(partners.first().toString());
} else if (kind & ElementData::Terminal) {
if (QET::infoFlagIsTrue(m_element_information.value(QUuid(node.first))
.value(QStringLiteral("potential_isolating")).toString()))
return std::nullopt;
todo << nodes_of.value(node.first);
}
}
return wires;
}
/**
@brief projectDataBase::linkedSymbols
@return the symbols @p element is linked to, read from the link table
as it is (see elementTypesOnFolios())
*/
QList<QUuid> projectDataBase::linkedSymbols(const QUuid &element) const
{
QList<QUuid> linked;
QSqlQuery query(m_data_base);
query.prepare(QStringLiteral("SELECT linked_uuid FROM link WHERE element_uuid = :uuid ORDER BY rowid"));
query.bindValue(QStringLiteral(":uuid"), element.toString());
if (query.exec()) {
while (query.next())
linked.append(QUuid(query.value(0).toString()));
}
return linked;
}
/**
@brief projectDataBase::sequenceValue
@return @p sequence as the element table's sequence column holds it:
each part the file would save (sequentialNumbers::toXml()), by its tag,
as JSON; null when the file would save none. sequenceFromValue() reads
it back as reading the file would (sequentialNumbers::fromXml()).
*/
QVariant projectDataBase::sequenceValue(const autonum::sequentialNumbers &sequence)
{
QDomDocument document;
const QDomElement xml = sequence.toXml(document);
if (!xml.hasChildNodes()) {
return QVariant();
}
QJsonObject parts;
for (QDomElement part = xml.firstChildElement() ; !part.isNull() ;
part = part.nextSiblingElement())
parts.insert(part.tagName(), part.text());
return QString::fromUtf8(QJsonDocument(parts).toJson(QJsonDocument::Compact));
}
/**
@brief projectDataBase::sequenceFromValue
@return the sequential numbers held by @p value (sequenceValue())
*/
autonum::sequentialNumbers projectDataBase::sequenceFromValue(const QVariant &value)
{
autonum::sequentialNumbers sequence;
if (value.isNull()) {
return sequence;
}
const QJsonObject parts = QJsonDocument::fromJson(value.toString().toUtf8()).object();
QDomDocument document;
QDomElement xml = document.createElement(QStringLiteral("sequentialNumbers"));
for (auto it = parts.constBegin() ; it != parts.constEnd() ; ++it) {
QDomElement part = document.createElement(it.key());
part.appendChild(document.createTextNode(it.value().toString()));
xml.appendChild(part);
}
sequence.fromXml(xml);
return sequence;
}
/**
@brief projectDataBase::canEditUnbuilt
@return true if the information of the symbol @p element can be changed
through the store alone, its folio not built (lazy folios): it is
placed once, on a folio not built, and nothing else shows its
information -- no symbol built (alone or with its folio) holds it, no
partner is linked to it, and it has no label formula, which could read
what changes
*/
bool projectDataBase::canEditUnbuilt(const QUuid &element) const
{
if (!hasElementInformation(element) || placedElementCount(element) != 0
|| !linkedSymbols(element).isEmpty()
|| !m_element_information.value(element).value(QStringLiteral("formula")).toString().isEmpty()) {
return false;
}
QSqlQuery query(m_data_base);
query.prepare(QStringLiteral("SELECT COUNT(*) FROM element WHERE uuid = :uuid"));
query.bindValue(QStringLiteral(":uuid"), element.toString());
return query.exec() && query.next() && query.value(0).toInt() == 1;
}
/**
@brief projectDataBase::editUnbuiltElementInformation
Change the information of the symbol @p element, on a folio not built
(canEditUnbuilt()), to @p information, as Element::setElementInformations()
would: the store and the symbol's element info row. Building the symbol
takes it (takeUnbuiltEdit()); saving builds its folio first
(foliosWithUnbuiltEdits()).
*/
void projectDataBase::editUnbuiltElementInformation(const QUuid &element, DiagramContext information)
{
if (information.contains(QStringLiteral("formula_id"))
&& information.value(QStringLiteral("formula")).toString().isEmpty()) {
information.remove(QStringLiteral("formula_id"));
}
QUuid folio;
{
QSqlQuery query(m_data_base);
query.prepare(QStringLiteral("SELECT diagram_uuid FROM element WHERE uuid = :uuid"));
query.bindValue(QStringLiteral(":uuid"), element.toString());
if (query.exec() && query.next()) folio = QUuid(query.value(0).toString());
}
//Whether the file has a block of information for it: a symbol
//saves one when it has any (Element::toXml())
const bool had_information = m_unbuilt_edits.contains(element)
? m_unbuilt_edits.value(element).had_information
: !m_element_information.value(element).keys().isEmpty();
m_unbuilt_edits.insert(element, {information, folio, had_information});
if (!storeElementInformation(element, information)) {
return;
}
m_content_changed = true;
if (!m_update_blocked) {
bindElementInfoValues(m_update_element_query, element.toString(), information,
information.value(QStringLiteral("label")).toString());
if (!m_update_element_query.exec()) {
qDebug() << "projectDataBase::editUnbuiltElementInformation update error : " << m_update_element_query.lastError();
}
}
emit dataBaseUpdated();
}
/**
@brief projectDataBase::takeUnbuiltEdit
@return the information @p element was given while its folio was not
built (editUnbuiltElementInformation()), forgotten here: the symbol
being built takes it. Nothing if it was given none.
*/
std::optional<DiagramContext> projectDataBase::takeUnbuiltEdit(const QUuid &element)
{
const auto edit = m_unbuilt_edits.constFind(element);
if (edit == m_unbuilt_edits.constEnd()) {
return std::nullopt;
}
const DiagramContext information = edit->information;
m_unbuilt_edits.erase(edit);
return information;
}
/**
@brief projectDataBase::foliosWithUnbuiltEdits
@return the folios holding a symbol whose information was changed while
they were not built (editUnbuiltElementInformation())
*/
QSet<QUuid> projectDataBase::foliosWithUnbuiltEdits() const
{
QSet<QUuid> folios;
for (const auto &edit : m_unbuilt_edits) folios.insert(edit.folio);
for (const auto &edit : m_unbuilt_wire_edits) folios.insert(edit.folio);
return folios;
}
/**
@brief projectDataBase::unbuiltWire
@return true if the wire @p wire is on a folio not built (lazy folios):
it has a row and its properties are stored, and no wire built holds its
uuid. @p folio is set to its folio.
*/
bool projectDataBase::unbuiltWire(const QUuid &wire, QUuid *folio) const
{
if (placedConductorCount(wire) != 0 || !hasConductorProperties(wire)) {
return false;
}
QSqlQuery query(m_data_base);
query.prepare(QStringLiteral("SELECT diagram_uuid FROM conductor WHERE uuid = :uuid"));
query.bindValue(QStringLiteral(":uuid"), wire.toString());
if (!query.exec() || !query.next()) {
return false;
}
if (folio) *folio = QUuid(query.value(0).toString());
return !query.next();
}
/**
@brief projectDataBase::editUnbuiltConductorProperties
Give the wire @p wire, on a folio not built (unbuiltWire()), the
properties @p properties as Conductor::setProperties() would: the store,
its rows and its conductor row's text. Building it takes them
(takeUnbuiltWireEdit()); a .qetz saves them in its database (QETProject::
toXml()), anything else builds its folio to save.
*/
void projectDataBase::editUnbuiltConductorProperties(const QUuid &wire, const ConductorProperties &properties)
{
QUuid folio;
if (!unbuiltWire(wire, &folio)) {
return;
}
m_unbuilt_wire_edits.insert(wire, {properties, folio});
if (!storeConductorProperties(wire, properties)) {
return;
}
m_content_changed = true;
if (!m_update_blocked) {
m_update_conductor_query.bindValue(QStringLiteral(":uuid"), wire.toString());
m_update_conductor_query.bindValue(QStringLiteral(":text"), properties.text);
if (!m_update_conductor_query.exec()) {
qDebug() << "projectDataBase::editUnbuiltConductorProperties update error : " << m_update_conductor_query.lastError();
}
}
emit dataBaseUpdated();
}
/**
@brief projectDataBase::takeUnbuiltWireEdit
@return the properties @p wire was given while its folio was not built,
forgotten here: the wire being built takes them
*/
std::optional<ConductorProperties> projectDataBase::takeUnbuiltWireEdit(const QUuid &wire)
{
const auto edit = m_unbuilt_wire_edits.constFind(wire);
if (edit == m_unbuilt_wire_edits.constEnd()) {
return std::nullopt;
}
const ConductorProperties properties = edit->properties;
m_unbuilt_wire_edits.erase(edit);
return properties;
}
/**
@brief projectDataBase::unbuiltWireEditsOnFolio
@return the properties given to the wires of the folio @p folio while it
was not built, by wire
*/
QHash<QUuid, ConductorProperties> projectDataBase::unbuiltWireEditsOnFolio(const QUuid &folio) const
{
QHash<QUuid, ConductorProperties> edits;
for (auto it = m_unbuilt_wire_edits.cbegin() ; it != m_unbuilt_wire_edits.cend() ; ++it)
if (it->folio == folio) edits.insert(it.key(), it->properties);
return edits;
}
/**
@brief projectDataBase::setPlacedConductorProperties
Give the wire @p wire its properties @p properties: through the wire when
it is built, through the store when it is not (unbuiltWire()). What
undoing and redoing a change of wires not built does, the wires having
perhaps been built since.
@return false if neither can
*/
bool projectDataBase::setPlacedConductorProperties(const QUuid &wire, const ConductorProperties &properties)
{
const QList<QPointer<Conductor>> placed = m_placed_conductors.values(wire);
if (placed.size() == 1 && placed.first()) {
placed.first()->setProperties(properties);
return true;
}
if (placed.isEmpty() && unbuiltWire(wire)) {
editUnbuiltConductorProperties(wire, properties);
return true;
}
return false;
}
/**
@brief projectDataBase::unbuiltEditsOnFolio
@return the information changed through the store of the symbols on the
folio @p folio, not built (editUnbuiltElementInformation()), by symbol.
@p need_build is set when a .qetz cannot save one in place of the rows it
was read with: the file has no block of information for the symbol, or
the change empties it (a symbol with none saves no block)
*/
QHash<QUuid, DiagramContext> projectDataBase::unbuiltEditsOnFolio(const QUuid &folio, bool *need_build) const
{
QHash<QUuid, DiagramContext> edits;
if (need_build) *need_build = false;
for (auto it = m_unbuilt_edits.cbegin() ; it != m_unbuilt_edits.cend() ; ++it) {
if (it->folio != folio) continue;
edits.insert(it.key(), it->information);
if (need_build && (!it->had_information || it->information.keys().isEmpty()))
*need_build = true;
}
return edits;
}
/**
@brief projectDataBase::setPlacedElementInformation
Give the symbol @p element its information @p information: through the
symbol when it is built, through the store when it is not
(canEditUnbuilt()). What undoing and redoing a change of a symbol not
built does, the symbol having perhaps been built since.
@return false if neither can
*/
bool projectDataBase::setPlacedElementInformation(const QUuid &element, const DiagramContext &information)
{
const QList<QPointer<Element>> placed = m_placed_elements.values(element);
if (placed.size() == 1 && placed.first()) {
placed.first()->setElementInformations(information);
elementInfoChanged(placed.first().data());
return true;
}
if (placed.isEmpty() && m_unbuilt_edits.contains(element)) {
editUnbuiltElementInformation(element, information);
return true;
}
if (placed.isEmpty() && canEditUnbuilt(element)) {
editUnbuiltElementInformation(element, information);
return true;
}
return false;
}
/** /**
@brief projectDataBase::folioUuidsOfElements @brief projectDataBase::folioUuidsOfElements
@return the uuids of the folios the symbols @p elements are on, read @return the uuids of the folios the symbols @p elements are on, read
from the element table as it is, without bringing it up to date: for a from the element table as it is, without bringing it up to date: for a
symbol on a folio not built yet (QET_LAZY_FOLIOS) its row came from the symbol on a folio not built yet (lazy folios) its row came from the
document and nothing can have changed it. document and nothing can have changed it.
*/ */
QSet<QUuid> projectDataBase::folioUuidsOfElements(const QSet<QUuid> &elements) const QSet<QUuid> projectDataBase::folioUuidsOfElements(const QSet<QUuid> &elements) const
@@ -992,10 +1528,11 @@ QSqlQuery projectDataBase::newQuery(const QString &query, QString *error) {
//storeConductorProperties(). //storeConductorProperties().
//The drawing tables hold what only built items know (a text's //The drawing tables hold what only built items know (a text's
//laid-out size, a picture's pixels): with folios not built yet //laid-out size, a picture's pixels): with folios not built yet
//(QET_LAZY_FOLIOS) a read of them builds those first. Every other //(lazy folios) a read of them builds those first, and so does a
//table was filled from the document. //read of the view made of them. Every other table was filled from
//the document.
static const QRegularExpression drawing_tables( static const QRegularExpression drawing_tables(
QStringLiteral("\\b(shape|independent_text|image)\\b"), QStringLiteral("\\b(shape|independent_text|image|drawing_item_view)\\b"),
QRegularExpression::CaseInsensitiveOption); QRegularExpression::CaseInsensitiveOption);
if (m_project && m_project->unloadedFolioCount() && drawing_tables.match(query).hasMatch()) { if (m_project && m_project->unloadedFolioCount() && drawing_tables.match(query).hasMatch()) {
m_project->loadFolios(); m_project->loadFolios();
@@ -1062,8 +1599,10 @@ int projectDataBase::excludedConductorCount() const
return 0; return 0;
} }
//A folio not built (lazy folios) has none: reading a wire
//drops it unless both its ends are found (Conductor::fromXml())
int count = 0; int count = 0;
for (auto *diagram : m_project->diagrams()) for (auto *diagram : m_project->builtFolios())
{ {
const auto conductor_list = diagram->conductors(); const auto conductor_list = diagram->conductors();
for (auto *conductor : conductor_list) for (auto *conductor : conductor_list)
@@ -1248,11 +1787,12 @@ void projectDataBase::flushElementPositions()
/** /**
@brief projectDataBase::updateFolioPositions @brief projectDataBase::updateFolioPositions
Write every folio's position and folio number again, after a folio was Write every folio's position and folio number again, after a folio was
added, removed or moved. added, removed or moved. A folio not built yet (lazy folios) has
its position and title block already: it is not built for this.
*/ */
void projectDataBase::updateFolioPositions() void projectDataBase::updateFolioPositions()
{ {
for (auto diagram : m_project->diagrams()) for (auto diagram : m_project->folios())
{ {
m_diagram_order_changed.bindValue(":pos", m_project->folioIndex(diagram)+1); m_diagram_order_changed.bindValue(":pos", m_project->folioIndex(diagram)+1);
m_diagram_order_changed.bindValue(":uuid", diagram->uuid().toString()); m_diagram_order_changed.bindValue(":uuid", diagram->uuid().toString());
@@ -1381,6 +1921,13 @@ void projectDataBase::removeDiagram(Diagram *diagram)
void projectDataBase::diagramInfoChanged(Diagram *diagram) void projectDataBase::diagramInfoChanged(Diagram *diagram)
{ {
m_content_changed = true; m_content_changed = true;
//Not while the project is being read: a folio's header read gives
//it a folio number counted from the folios read so far ("1/1" for
//the first of three); the rows are filled once every folio is
//read, from the document or from the index a .qetz was saved with
if (m_update_blocked) {
return;
}
bindDiagramInfoValues(m_update_diagram_info_query, diagram); bindDiagramInfoValues(m_update_diagram_info_query, diagram);
if (!m_update_diagram_info_query.exec()) { if (!m_update_diagram_info_query.exec()) {
@@ -1912,7 +2459,7 @@ void projectDataBase::populateDrawingItemTables()
//Queued directly, not through addDrawingItem(): every one of them //Queued directly, not through addDrawingItem(): every one of them
//came onto its folio through Diagram::addItem(), which connected it //came onto its folio through Diagram::addItem(), which connected it
//already, and a full rebuild runs on every load. A folio not built //already, and a full rebuild runs on every load. A folio not built
//yet (QET_LAZY_FOLIOS) has none: its items write their rows as it //yet (lazy folios) has none: its items write their rows as it
//is built. //is built.
for (auto diagram : m_project->folios()) for (auto diagram : m_project->folios())
{ {
@@ -1965,6 +2512,8 @@ bool projectDataBase::createDataBase()
"type VARCHAR(50)," "type VARCHAR(50),"
"sub_type VARCHAR(50)," "sub_type VARCHAR(50),"
"group_uuid VARCHAR(50)," "group_uuid VARCHAR(50),"
"definition TEXT,"
"sequence TEXT,"
"FOREIGN KEY (diagram_uuid) REFERENCES diagram (uuid)" "FOREIGN KEY (diagram_uuid) REFERENCES diagram (uuid)"
")"); ")");
if (!query_.exec(element_table)) { if (!query_.exec(element_table)) {
@@ -2445,7 +2994,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.
@@ -3214,7 +3763,7 @@ void projectDataBase::prefillFromDocument(const QDomDocument &document)
@brief projectDataBase::endPrefill @brief projectDataBase::endPrefill
The folios are built: compare what prefillFromDocument() stored with The folios are built: compare what prefillFromDocument() stored with
what building stored, and forget what nothing was placed for -- unless what building stored, and forget what nothing was placed for -- unless
@p keep_unplaced, when some folios are not built yet (QET_LAZY_FOLIOS): @p keep_unplaced, when some folios are not built yet (lazy folios):
the store then answers for their items until they are. the store then answers for their items until they are.
*/ */
void projectDataBase::endPrefill(bool keep_unplaced) void projectDataBase::endPrefill(bool keep_unplaced)
@@ -3426,7 +3975,7 @@ void projectDataBase::flushLinks()
qDebug() << "projectDataBase::flushLinks insert error : " << insert.lastError(); qDebug() << "projectDataBase::flushLinks insert error : " << insert.lastError();
} }
} }
//Partners on a folio not built (QET_LAZY_FOLIOS), which it will //Partners on a folio not built (lazy folios), which it will
//be linked to once that folio is //be linked to once that folio is
for (const auto &waiting : element->waitingLinks()) for (const auto &waiting : element->waitingLinks())
{ {
@@ -3628,7 +4177,8 @@ void projectDataBase::prepareQuery()
m_diagram_info_order_changed.prepare("UPDATE diagram_info SET folio = :folio WHERE diagram_uuid = :uuid"); m_diagram_info_order_changed.prepare("UPDATE diagram_info SET folio = :folio WHERE diagram_uuid = :uuid");
//INSERT ELEMENT //INSERT ELEMENT
QString insert_element_query("INSERT INTO element (uuid, diagram_uuid, pos, type, sub_type, group_uuid) VALUES (:uuid, :diagram_uuid, :pos, :type, :sub_type, :group_uuid)"); QString insert_element_query("INSERT INTO element (uuid, diagram_uuid, pos, type, sub_type, group_uuid, definition, sequence) "
"VALUES (:uuid, :diagram_uuid, :pos, :type, :sub_type, :group_uuid, :definition, :sequence)");
m_insert_elements_query = QSqlQuery(m_data_base); m_insert_elements_query = QSqlQuery(m_data_base);
m_insert_elements_query.prepare(insert_element_query); m_insert_elements_query.prepare(insert_element_query);
@@ -3743,6 +4293,8 @@ void projectDataBase::bindElementValues(QSqlQuery &query, Element *element, Diag
query.bindValue(QStringLiteral(":type"), element_data.typeToString()); query.bindValue(QStringLiteral(":type"), element_data.typeToString());
query.bindValue(QStringLiteral(":sub_type"), element_data.masterTypeToString()); query.bindValue(QStringLiteral(":sub_type"), element_data.masterTypeToString());
query.bindValue(QStringLiteral(":group_uuid"), groupValue(element)); query.bindValue(QStringLiteral(":group_uuid"), groupValue(element));
query.bindValue(QStringLiteral(":definition"), element->location().path());
query.bindValue(QStringLiteral(":sequence"), sequenceValue(element->sequenceStruct()));
} }
/** /**
+55 -6
View File
@@ -29,9 +29,12 @@
#include <QSet> #include <QSet>
#include <QUuid> #include <QUuid>
#include <optional>
#include "../diagramcontext.h" #include "../diagramcontext.h"
#include "../titleblockproperties.h" #include "../titleblockproperties.h"
#include "../conductorproperties.h" #include "../conductorproperties.h"
#include "../autoNum/assignvariables.h"
class Element; class Element;
class QETProject; class QETProject;
@@ -67,6 +70,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 +117,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 +135,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 +147,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,11 +156,11 @@ 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);
//A folio of a project opened with QET_LAZY_FOLIOS is being built: //A folio of a project opened with lazy folios is being built:
//its rows came from the document already, so none are written //its rows came from the document already, so none are written
//and the database is not marked changed //and the database is not marked changed
void setBuildingFolio(bool building); void setBuildingFolio(bool building);
@@ -158,7 +169,37 @@ class projectDataBase : public QObject
QString prefillReport() const; QString prefillReport() const;
QSet<QUuid> foliosWithUnjoinedWires() const; QSet<QUuid> foliosWithUnjoinedWires() const;
QSet<QUuid> folioUuidsOfElements(const QSet<QUuid> &elements) const; QSet<QUuid> folioUuidsOfElements(const QSet<QUuid> &elements) const;
bool canEditUnbuilt(const QUuid &element) const;
void editUnbuiltElementInformation(const QUuid &element, DiagramContext information);
std::optional<DiagramContext> takeUnbuiltEdit(const QUuid &element);
QSet<QUuid> foliosWithUnbuiltEdits() const;
QHash<QUuid, DiagramContext> unbuiltEditsOnFolio(const QUuid &folio, bool *need_build = nullptr) const;
bool setPlacedElementInformation(const QUuid &element, const DiagramContext &information);
bool unbuiltWire(const QUuid &wire, QUuid *folio = nullptr) const;
void editUnbuiltConductorProperties(const QUuid &wire, const ConductorProperties &properties);
std::optional<ConductorProperties> takeUnbuiltWireEdit(const QUuid &wire);
QHash<QUuid, ConductorProperties> unbuiltWireEditsOnFolio(const QUuid &folio) const;
bool setPlacedConductorProperties(const QUuid &wire, const ConductorProperties &properties);
QHash<QUuid, QString> elementTypesOnFolios(const QSet<QUuid> &folios) const; QHash<QUuid, QString> elementTypesOnFolios(const QSet<QUuid> &folios) const;
/// A symbol placed on a folio, as the element table has it
struct FolioSymbol {
QUuid uuid;
QUuid folio;
QString kind; ///< ElementData::typeToString()
QString location; ///< its definition, ElementsLocation::path()
autonum::sequentialNumbers sequence;
};
QList<FolioSymbol> symbolsOnFolios(const QList<QUuid> &folios) const;
QList<QUuid> linkedSymbols(const QUuid &element) const;
/// A wire placed on a folio: its text and the kinds of the
/// symbols at its ends (ElementData::typeToString())
struct FolioWire { QString text; QString kind1; QString kind2; };
QList<FolioWire> wiresOnFolios(const QList<QUuid> &folios) const;
QString elementLabel(const QUuid &element) const;
QString terminalName(const QUuid &terminal, const QUuid &element) const;
std::optional<QSet<QUuid>> potentialWires(const QUuid &wire) const;
static QVariant sequenceValue(const autonum::sequentialNumbers &sequence);
static autonum::sequentialNumbers sequenceFromValue(const QVariant &value);
void addElement (Element *element); void addElement (Element *element);
void removeElement (Element *element); void removeElement (Element *element);
@@ -295,6 +336,14 @@ class projectDataBase : public QObject
//briefly gives a copy its original's uuid, so one uuid can have //briefly gives a copy its original's uuid, so one uuid can have
//two symbols until the copy's is renewed. //two symbols until the copy's is renewed.
QMultiHash<QUuid, QPointer<Element>> m_placed_elements; QMultiHash<QUuid, QPointer<Element>> m_placed_elements;
/// Information changed through the store for symbols on folios
/// not built (editUnbuiltElementInformation()), with the folio
/// each is on: building the symbol takes it (takeUnbuiltEdit())
struct UnbuiltEdit { DiagramContext information; QUuid folio; bool had_information; };
QHash<QUuid, UnbuiltEdit> m_unbuilt_edits;
/// The same for wires (editUnbuiltConductorProperties())
struct UnbuiltWireEdit { ConductorProperties properties; QUuid folio; };
QHash<QUuid, UnbuiltWireEdit> m_unbuilt_wire_edits;
QHash<QUuid, TitleBlockProperties> m_folio_titleblocks; QHash<QUuid, TitleBlockProperties> m_folio_titleblocks;
QHash<QUuid, QPointer<Diagram>> m_placed_folios; QHash<QUuid, QPointer<Diagram>> m_placed_folios;
QHash<QUuid, ConductorProperties> m_conductor_properties; QHash<QUuid, ConductorProperties> m_conductor_properties;
@@ -306,7 +355,7 @@ class projectDataBase : public QObject
//The folios whose rows a rebuild keeps (keepUnbuiltFolioRows()) //The folios whose rows a rebuild keeps (keepUnbuiltFolioRows())
QSet<Diagram *> m_kept_folios; QSet<Diagram *> m_kept_folios;
//The tables' rows were filled from the document: those of a //The tables' rows were filled from the document: those of a
//folio not built yet are right (QET_LAZY_FOLIOS) //folio not built yet are right (lazy folios)
bool m_rows_from_document = false; bool m_rows_from_document = false;
void keepUnbuiltFolioRows(); void keepUnbuiltFolioRows();
void restoreKeptRows(const QString &table, Diagram *folio, const QString &where); void restoreKeptRows(const QString &table, Diagram *folio, const QString &where);
+213 -63
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>
@@ -772,13 +775,13 @@ void Diagram::keyReleaseEvent(QKeyEvent *e)
/** /**
@brief Diagram::ensureLoaded @brief Diagram::ensureLoaded
Build this folio's items from the XML kept when the project was Build this folio's items from the XML kept when the project was
opened with QET_LAZY_FOLIOS, and do for them what opening does for opened with lazy folios, and do for them what opening does for
every folio's (QETProject::folioLoaded()). Nothing for a folio built every folio's (QETProject::folioLoaded()). Nothing for a folio built
already. already.
*/ */
void Diagram::ensureLoaded() void Diagram::ensureLoaded()
{ {
if (m_deferred.isNull()) { if (isLoaded()) {
return; return;
} }
if (m_project) { if (m_project) {
@@ -788,6 +791,99 @@ void Diagram::ensureLoaded()
} }
} }
/**
@brief Diagram::buildSymbols
Build, of this folio not built yet (lazy folios), the symbols
@p uuids only -- partners a shown folio draws from -- and keep them for
when the folio is built (stage D5b). They belong to the
folio's scene; the folio stays not built.
@return the symbols built now
*/
QList<Element *> Diagram::buildSymbols(const QSet<QUuid> &uuids)
{
QList<Element *> built;
if (isLoaded() || uuids.isEmpty()) {
return built;
}
const QDomElement xml = deferredXml();
if (xml.isNull()) {
return built;
}
projectDataBase *db = m_project ? m_project->dataBase() : nullptr;
if (db) db->setBuildingFolio(true);
for (QDomElement element_xml :
QET::findInDomElement(xml, QStringLiteral("elements"), QStringLiteral("element")))
{
const QUuid uuid(element_xml.attribute(QStringLiteral("uuid")));
if (uuid.isNull() || !uuids.contains(uuid) || m_partial_elements.contains(uuid)
|| !Element::valideXml(element_xml)) {
continue;
}
if (Element *element = buildElement(element_xml, m_partial_table, true)) {
m_partial_elements.insert(uuid, element);
//A partner unlinked or linked again: the folio is not
//saved as it was read any more
connect(element, &Element::linkedElementChanged, this, [this]() {
if (!isLoaded()) m_partial_changed = true;
});
built << element;
}
}
if (db) db->setBuildingFolio(false);
return built;
}
namespace {
//The title block fields nothing on a folio is computed from: a formula
//reads the folio's label, plant, location, extra fields and grid
//(autonum::FormulaContext), none of these
const QStringList ShownOnlyFields = {
QStringLiteral("title"), QStringLiteral("author"), QStringLiteral("date"),
QStringLiteral("indexrev"), QStringLiteral("version"), QStringLiteral("filename"),
QStringLiteral("titleblocktemplate"), QStringLiteral("titleblocktemplateCollection")};
QString headerWithoutShownOnly(const QString &header)
{
QDomDocument document;
document.setContent(header);
QDomElement root = document.documentElement();
for (const QString &field : ShownOnlyFields) root.removeAttribute(field);
return document.toString();
}
}
/**
@brief Diagram::headerChangeShowsOnly
@return true if what changed in this folio's own data since it was read
(headerChanged()) is only title block fields nothing on the folio is
computed from (its title, author, date, revision, file name, template):
its items, as read, are what building it would write
*/
bool Diagram::headerChangeShowsOnly()
{
return headerWithoutShownOnly(headerXml()) == headerWithoutShownOnly(m_opened_header);
}
/**
@brief Diagram::deferredXmlWithHeader
@return this folio, not built, as it saves: its own data as it is now
(writeHeader()) and its items as read (deferredXml()), in @p document --
when its items are what building it would write (headerChangeShowsOnly())
*/
QDomElement Diagram::deferredXmlWithHeader(QDomDocument &document)
{
QDomElement folio = document.createElement(QStringLiteral("diagram"));
writeHeader(document, folio);
QSet<QString> header_children;
for (QDomElement child = folio.firstChildElement() ; !child.isNull() ; child = child.nextSiblingElement())
header_children.insert(child.tagName());
const QDomElement items = deferredXml();
for (QDomElement child = items.firstChildElement() ; !child.isNull() ; child = child.nextSiblingElement())
if (!header_children.contains(child.tagName()))
folio.appendChild(document.importNode(child, true));
return folio;
}
/** /**
@brief Diagram::buildDeferred @brief Diagram::buildDeferred
Build this folio's items from the XML kept for later, and nothing else. Build this folio's items from the XML kept for later, and nothing else.
@@ -797,17 +893,21 @@ void Diagram::ensureLoaded()
*/ */
bool Diagram::buildDeferred() bool Diagram::buildDeferred()
{ {
if (m_deferred.isNull()) { if (isLoaded()) {
return false; return false;
} }
QDomElement xml = m_deferred; QDomElement xml = m_deferred;
m_deferred = QDomElement(); m_deferred = QDomElement();
m_kept_part.clear();
m_kept_text.clear(); m_kept_text.clear();
projectDataBase *db = m_project ? m_project->dataBase() : nullptr; projectDataBase *db = m_project ? m_project->dataBase() : nullptr;
if (db) db->setBuildingFolio(true); if (db) db->setBuildingFolio(true);
m_loading_deferred = true; m_loading_deferred = true;
fromXml(xml, QPointF(), true, nullptr); fromXml(xml, QPointF(), true, nullptr);
m_loading_deferred = false; m_loading_deferred = false;
m_partial_elements.clear();
m_partial_table.clear();
m_partial_changed = false;
if (db) db->setBuildingFolio(false); if (db) db->setBuildingFolio(false);
return true; return true;
} }
@@ -1133,7 +1233,7 @@ QList < QSet <Conductor *> > Diagram::potentials()
\~French Un Document XML (QDomDocument) \~French Un Document XML (QDomDocument)
*/ */
QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) { QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) {
//A folio of a project opened with QET_LAZY_FOLIOS is written from //A folio of a project opened with lazy folios is written from
//its built items (QETProject::toXml() may write one not built as //its built items (QETProject::toXml() may write one not built as
//it was read instead) //it was read instead)
ensureLoaded(); ensureLoaded();
@@ -1246,7 +1346,7 @@ QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) {
//Wires that tie on their ends (common: several from one terminal) //Wires that tie on their ends (common: several from one terminal)
//keep the order they were made in, which for a project's is its //keep the order they were made in, which for a project's is its
//file's -- the order the scene gave them so far, except for a folio //file's -- the order the scene gave them so far, except for a folio
//built late (QET_LAZY_FOLIOS), whose scene order differed //built late (lazy folios), whose scene order differed
std::stable_sort(list_conductors.begin(), list_conductors.end(), std::stable_sort(list_conductors.begin(), list_conductors.end(),
[](Conductor *a, Conductor *b) { [](Conductor *a, Conductor *b) {
const QString ka = conductorSortKey(a), kb = conductorSortKey(b); const QString ka = conductorSortKey(a), kb = conductorSortKey(b);
@@ -1486,6 +1586,73 @@ void Diagram::folioSequentialsToXml(QHash<QString,
} }
} }
/**
@brief Diagram::buildElement
Make the symbol @p element_xml describes and add it to this folio, as
fromXml() does for each symbol it reads.
@return the symbol, or nullptr if it could not be made (said in the log)
*/
Element *Diagram::buildElement(QDomElement element_xml, QHash<int, Terminal *> &table_adr_id,
bool consider_informations)
{
// cree un element dont le type correspond a l'id type
QString type_id = element_xml.attribute(QStringLiteral("type"));
ElementsLocation element_location;
if (type_id.startsWith(QStringLiteral("embed://"))) {
element_location = ElementsLocation(type_id, m_project);
}
else {
element_location = ElementsLocation(type_id);
}
int state = 0;
Element *nvel_elmt =
ElementFactory::Instance() -> createElement(
element_location, nullptr, &state);
if (state)
{
QString debug_message =
QString("Diagram::fromXml() : Le chargement de "
"la description de l'element %1 a "
"echoue avec le code d'erreur %2").arg(
element_location.path()).arg(state);
qDebug() << qPrintable(debug_message);
delete nvel_elmt;
return nullptr;
}
addItem(nvel_elmt);
//Loading fail, remove item from the diagram
if (!nvel_elmt->fromXml(element_xml, table_adr_id))
{
removeItem(nvel_elmt);
delete nvel_elmt;
qDebug() << QStringLiteral("Diagram::fromXml() : Le chargement des parametres d'un element a echoue");
return nullptr;
} else {
//A symbol saved without a uuid got a random one from
//Element::fromXml(): a different identity on every load,
//written out on the next save. Derive it instead from what
//the symbol is and where it sits on its folio -- never from
//the folio's index, so inserting or moving a folio does not
//change it. Only for a folio being loaded: a paste renews
//uuids anyway.
if (consider_informations && m_project
&& QUuid(element_xml.attribute(QStringLiteral("uuid"))).isNull()) {
nvel_elmt->setUuid(m_project->derivedItemUuid(
QStringLiteral("element"),
QStringList{type_id,
element_xml.attribute(QStringLiteral("x")),
element_xml.attribute(QStringLiteral("y")),
element_xml.attribute(QStringLiteral("orientation"))}
.join(QLatin1Char('\n'))));
}
ItemGroups::setGroup(nvel_elmt, ItemGroups::read(element_xml));
return nvel_elmt;
}
return nullptr;
}
/** /**
@brief Diagram::fromXml @brief Diagram::fromXml
Imports the described schema into an XML document. Imports the described schema into an XML document.
@@ -1861,7 +2028,7 @@ bool Diagram::fromXml(QDomElement &document,
return(true); return(true);
} }
//A project opened with QET_LAZY_FOLIOS builds a folio's items when //A project opened with lazy folios builds a folio's items when
//they are first needed: until then the folio has what it says //they are first needed: until then the folio has what it says
//about itself (uuid, title block, border, numbering) and keeps its //about itself (uuid, title block, border, numbering) and keeps its
//XML, which belongs to the project's document. //XML, which belongs to the project's document.
@@ -1907,65 +2074,23 @@ bool Diagram::fromXml(QDomElement &document,
//Load all elements from the XML //Load all elements from the XML
QList<Element *> added_elements; QList<Element *> added_elements;
QHash<int, Terminal *> table_adr_id; QHash<int, Terminal *> table_adr_id;
if (m_loading_deferred) {
table_adr_id = m_partial_table; //the terminals of symbols built already
}
for (auto element_xml : for (auto element_xml :
QET::findInDomElement(root, QStringLiteral("elements"), QStringLiteral("element"))) QET::findInDomElement(root, QStringLiteral("elements"), QStringLiteral("element")))
{ {
if (!Element::valideXml(element_xml)) continue; if (!Element::valideXml(element_xml)) continue;
//Built already as another folio's partner (buildSymbols())
// cree un element dont le type correspond a l'id type if (m_loading_deferred) {
QString type_id = element_xml.attribute(QStringLiteral("type")); Element *built = m_partial_elements.value(QUuid(element_xml.attribute(QStringLiteral("uuid"))));
ElementsLocation element_location; if (built) {
if (type_id.startsWith(QStringLiteral("embed://"))) { added_elements << built;
element_location = ElementsLocation(type_id, m_project); continue;
}
else {
element_location = ElementsLocation(type_id);
}
int state = 0;
Element *nvel_elmt =
ElementFactory::Instance() -> createElement(
element_location, nullptr, &state);
if (state)
{
QString debug_message =
QString("Diagram::fromXml() : Le chargement de "
"la description de l'element %1 a "
"echoue avec le code d'erreur %2").arg(
element_location.path()).arg(state);
qDebug() << qPrintable(debug_message);
delete nvel_elmt;
continue;
}
addItem(nvel_elmt);
//Loading fail, remove item from the diagram
if (!nvel_elmt->fromXml(element_xml, table_adr_id))
{
removeItem(nvel_elmt);
delete nvel_elmt;
qDebug() << QStringLiteral("Diagram::fromXml() : Le chargement des parametres d'un element a echoue");
} else {
//A symbol saved without a uuid got a random one from
//Element::fromXml(): a different identity on every load,
//written out on the next save. Derive it instead from what
//the symbol is and where it sits on its folio -- never from
//the folio's index, so inserting or moving a folio does not
//change it. Only for a folio being loaded: a paste renews
//uuids anyway.
if (consider_informations && m_project
&& QUuid(element_xml.attribute(QStringLiteral("uuid"))).isNull()) {
nvel_elmt->setUuid(m_project->derivedItemUuid(
QStringLiteral("element"),
QStringList{type_id,
element_xml.attribute(QStringLiteral("x")),
element_xml.attribute(QStringLiteral("y")),
element_xml.attribute(QStringLiteral("orientation"))}
.join(QLatin1Char('\n'))));
} }
ItemGroups::setGroup(nvel_elmt, ItemGroups::read(element_xml));
added_elements << nvel_elmt;
} }
if (Element *nvel_elmt = buildElement(element_xml, table_adr_id, consider_informations))
added_elements << nvel_elmt;
} }
//Texts, images and shapes written before they carried a uuid (or //Texts, images and shapes written before they carried a uuid (or
@@ -2005,12 +2130,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);
+26 -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;
@@ -142,6 +143,12 @@ class Diagram : public QGraphicsScene
QUuid m_uuid = QUuid::createUuid(); QUuid m_uuid = QUuid::createUuid();
/// The XML of this folio's items, while they are not built yet /// The XML of this folio's items, while they are not built yet
QDomElement m_deferred; QDomElement m_deferred;
QString m_kept_part;
QHash<QUuid, Element *> m_partial_elements;
QHash<int, Terminal *> m_partial_table;
bool m_partial_changed = false;
Element *buildElement(QDomElement element_xml, QHash<int, Terminal *> &table_adr_id,
bool consider_informations);
QString m_opened_header; QString m_opened_header;
QByteArray m_kept_text; QByteArray m_kept_text;
bool m_loading_deferred = false; bool m_loading_deferred = false;
@@ -211,9 +218,20 @@ 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 (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;}
/// The part @p file of the .qetz the project was opened from
/// holds this folio as it is while not built: saving may copy it
/// (QETProject::toXml(), stage D4)
void setKeptPart(const QString &file) {m_kept_part = file;}
QString keptPart() const {return m_kept_part;}
QList<Element *> buildSymbols(const QSet<QUuid> &uuids);
/// Symbols built as partners (buildSymbols()) changed since:
/// the folio must be built to be saved
bool partialChanged() const {return m_partial_changed;}
/// The symbols built as partners (buildSymbols()), by uuid
QHash<QUuid, Element *> partialElements() const {return m_partial_elements;}
QString headerXml(); QString headerXml();
/// The folio's text in the file it was read from, while it is /// The folio's text in the file it was read from, while it is
/// not built (QETProject::toXml()) /// not built (QETProject::toXml())
@@ -223,6 +241,8 @@ class Diagram : public QGraphicsScene
/// headerChanged() (QETProject::toXml()) /// headerChanged() (QETProject::toXml())
void rememberHeader() {m_opened_header = headerXml();} void rememberHeader() {m_opened_header = headerXml();}
bool headerChanged() {return headerXml() != m_opened_header;} bool headerChanged() {return headerXml() != m_opened_header;}
bool headerChangeShowsOnly();
QDomElement deferredXmlWithHeader(QDomDocument &document);
void ensureLoaded(); void ensureLoaded();
bool buildDeferred(); bool buildDeferred();
//The last argument is only for a copy fragment: when it is //The last argument is only for a copy fragment: when it is
@@ -284,6 +304,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,
+29 -10
View File
@@ -79,13 +79,7 @@ DiagramContent::DiagramContent(Diagram *diagram, bool selected) :
m_other_conductors << c; m_other_conductors << c;
} }
if (m_potential_conductors.isEmpty()) { m_potential_candidates << c;
m_potential_conductors << c;
} else {
if (!potentialIsManaged(c->relatedPotentialConductors(true).values())) {
m_potential_conductors << c;
}
}
break; break;
} }
case DiagramImageItem::Type: { m_images << qgraphicsitem_cast<DiagramImageItem *>(item); break;} case DiagramImageItem::Type: { m_images << qgraphicsitem_cast<DiagramImageItem *>(item); break;}
@@ -379,12 +373,14 @@ DiagramContent &DiagramContent::operator+=(const DiagramContent &other)
if(!m_selected_items.contains(qgi)) if(!m_selected_items.contains(qgi))
m_selected_items << qgi; m_selected_items << qgi;
for (Conductor *c : other.m_potential_conductors) QList<Conductor *> potentials = potentialConductors();
for (Conductor *c : other.potentialConductors())
{ {
QList<Conductor *> c_list = c->relatedPotentialConductors(true).values(); QList<Conductor *> c_list = c->relatedPotentialConductors(true).values();
c_list << c; c_list << c;
if (!potentialIsManaged(c_list)) { if (!potentialIsManaged(c_list)) {
m_potential_conductors << c; potentials << c;
m_potential_conductors = potentials;
} }
} }
@@ -402,17 +398,40 @@ DiagramContent &DiagramContent::operator+=(const DiagramContent &other)
*/ */
bool DiagramContent::potentialIsManaged(QList<Conductor *> conductors) bool DiagramContent::potentialIsManaged(QList<Conductor *> conductors)
{ {
const QList<Conductor *> potentials = potentialConductors();
bool b = false; bool b = false;
for (Conductor *c : conductors) for (Conductor *c : conductors)
{ {
if (m_potential_conductors.contains(c)) if (potentials.contains(c))
b = true; b = true;
} }
return b; return b;
} }
/**
@brief DiagramContent::potentialConductors
@return one wire of each potential of the wires of this content, worked
out the first time it is asked (see m_potential_conductors)
*/
QList<Conductor *> DiagramContent::potentialConductors() const
{
if (!m_potential_conductors) {
QList<Conductor *> potentials;
for (Conductor *c : m_potential_candidates) {
bool managed = false;
if (!potentials.isEmpty())
for (Conductor *related : c->relatedPotentialConductors(true))
if (potentials.contains(related)) { managed = true; break; }
if (!managed)
potentials << c;
}
m_potential_conductors = potentials;
}
return *m_potential_conductors;
}
/** /**
@brief DiagramContent::hasTextEditing @brief DiagramContent::hasTextEditing
@return true if handle a text currently in editing interaction @return true if handle a text currently in editing interaction
+8 -1
View File
@@ -19,6 +19,7 @@
#define DIAGRAM_CONTENT_H #define DIAGRAM_CONTENT_H
#include <QSet> #include <QSet>
#include <optional>
#include <QVector> #include <QVector>
#include "../qetgraphicsitem/terminalelement.h" #include "../qetgraphicsitem/terminalelement.h"
@@ -77,7 +78,6 @@ class DiagramContent
QList<Conductor *> m_conductors_to_update; QList<Conductor *> m_conductors_to_update;
QList<Conductor *> m_conductors_to_move; QList<Conductor *> m_conductors_to_move;
QList<Conductor *> m_other_conductors; QList<Conductor *> m_other_conductors;
QList<Conductor *> m_potential_conductors;
QSet<DynamicElementTextItem *> m_element_texts; QSet<DynamicElementTextItem *> m_element_texts;
QSet<ElementTextItemGroup *> m_texts_groups; QSet<ElementTextItemGroup *> m_texts_groups;
QList<QGraphicsItem *> m_selected_items; QList<QGraphicsItem *> m_selected_items;
@@ -89,6 +89,7 @@ class DiagramContent
QList<DiagramTextItem *> selectedTexts() const; QList<DiagramTextItem *> selectedTexts() const;
QList<ElementTextItemGroup *> selectedTextsGroup() const; QList<ElementTextItemGroup *> selectedTextsGroup() const;
QList<Conductor *> conductors(int = AnyConductor) const; QList<Conductor *> conductors(int = AnyConductor) const;
QList<Conductor *> potentialConductors() const;
bool hasDeletableItems() const; bool hasDeletableItems() const;
bool hasCopiableItems() const; bool hasCopiableItems() const;
QList<QGraphicsItem *> items(int = All) const; QList<QGraphicsItem *> items(int = All) const;
@@ -103,6 +104,12 @@ class DiagramContent
private: private:
int removePinnedGroups(); int removePinnedGroups();
/// The wires potentialConductors() is made from, in order
QList<Conductor *> m_potential_candidates;
/// One wire of each potential, worked out when first asked:
/// following a potential goes through folio reports to other
/// folios, building those not built yet (lazy folios)
mutable std::optional<QList<Conductor *>> m_potential_conductors;
}; };
QDebug &operator<<(QDebug, DiagramContent &); QDebug &operator<<(QDebug, DiagramContent &);
#endif #endif
+1 -1
View File
@@ -1649,7 +1649,7 @@ void DiagramView::editDiagramProperties()
@brief DiagramView::buildFolio @brief DiagramView::buildFolio
Build the items of the folio this view shows, and those of the folios Build the items of the folio this view shows, and those of the folios
its cross-references need, if the project was opened without them its cross-references need, if the project was opened without them
(QET_LAZY_FOLIOS); then fit the view to them. (lazy folios); then fit the view to them.
*/ */
void DiagramView::buildFolio() void DiagramView::buildFolio()
{ {
@@ -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>
+10 -3
View File
@@ -55,7 +55,7 @@ ElementProvider::ElementProvider(const QList<Diagram *> &diagrams) :
@brief ElementProvider::folios @brief ElementProvider::folios
@return the folios to search for symbols of the kinds @p kinds (free @return the folios to search for symbols of the kinds @p kinds (free
ones only if @p free_only). For a project, the folios not built yet ones only if @p free_only). For a project, the folios not built yet
(QET_LAZY_FOLIOS) holding such a symbol are built first, and only them (lazy folios) holding such a symbol are built first, and only them
(QETProject::buildFoliosHolding()). (QETProject::buildFoliosHolding()).
*/ */
QList<Diagram *> ElementProvider::folios(ElementData::Types kinds, bool free_only) const QList<Diagram *> ElementProvider::folios(ElementData::Types kinds, bool free_only) const
@@ -98,7 +98,7 @@ QVector <QPointer<Element>> ElementProvider::freeElement(ElementData::Types filt
QList<Element *> elmt_list; QList<Element *> elmt_list;
const QList<Diagram *> searched = folios(filter, true); const QList<Diagram *> searched = folios(filter, true);
//A symbol whose saved partner is on a folio not built yet //A symbol whose saved partner is on a folio not built yet
//(QET_LAZY_FOLIOS) is not linked yet, but is not free: it will be //(lazy folios) is not linked yet, but is not free: it will be
const QSet<QUuid> waiting = m_project ? m_project->symbolsOnUnbuiltFolios() const QSet<QUuid> waiting = m_project ? m_project->symbolsOnUnbuiltFolios()
: QSet<QUuid>(); : QSet<QUuid>();
@@ -199,7 +199,14 @@ QVector<QetGraphicsTableItem *> ElementProvider::table(
} }
} }
for (auto d : allFolios()) { //Only the folios holding a table are built for it
QList<Diagram *> folios = m_diagram_list;
if (m_project) {
m_project->buildFoliosHoldingTables();
folios = m_project->builtFolios();
folios.removeOne(m_excluded);
}
for (auto d : std::as_const(folios)) {
for (auto item_ : d->items()) for (auto item_ : d->items())
{ {
if(item_->type() == QetGraphicsTableItem::Type) if(item_->type() == QetGraphicsTableItem::Type)
+1 -1
View File
@@ -55,7 +55,7 @@ class ElementProvider
QList <Diagram *> m_diagram_list; QList <Diagram *> m_diagram_list;
//Searching a project: its folios are asked for when searched, //Searching a project: its folios are asked for when searched,
//so that a project opened with QET_LAZY_FOLIOS builds only //so that a project opened with lazy folios builds only
//those a search can find something on //those a search can find something on
QETProject *m_project = nullptr; QETProject *m_project = nullptr;
Diagram *m_excluded = nullptr; Diagram *m_excluded = nullptr;
+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;
+48 -4
View File
@@ -232,6 +232,20 @@ void ProjectPrintWindow::requestPaint()
} }
} }
// A PDF export turns each page to its folio's orientation (#1480): a
// portrait folio gets a portrait page instead of being shrunk onto a
// landscape one. Only the real export, with one page per folio: the
// preview draws every page at one paper size, and only Qt's PDF engine
// accepts a new page layout once printing has started.
const bool per_folio_orientation = m_printing
&& m_printer->outputFormat() == QPrinter::PdfFormat
&& ui->m_fit_in_page_cb->isChecked();
const auto chosen_orientation = m_printer->pageLayout().orientation();
const auto diagrams = selectedDiagram();
if (per_folio_orientation && !diagrams.isEmpty()) {
setOrientationForDiagram(diagrams.first());
}
bool first = true; bool first = true;
QPainter painter(m_printer); QPainter painter(m_printer);
int annotIndex = 0; int annotIndex = 0;
@@ -245,12 +259,23 @@ void ProjectPrintWindow::requestPaint()
// should be fixed by original author by evaluating function-result! // should be fixed by original author by evaluating function-result!
(void)pdfExport; (void)pdfExport;
for (auto diagram : selectedDiagram()) for (auto diagram : diagrams)
{ {
first ? first = false : m_printer->newPage(); if (first) {
first = false;
} else {
if (per_folio_orientation) {
setOrientationForDiagram(diagram);
}
m_printer->newPage();
}
printDiagram(diagram, ui->m_fit_in_page_cb->isChecked(), &painter, m_printer, diagramPageMap, annotIndex); printDiagram(diagram, ui->m_fit_in_page_cb->isChecked(), &painter, m_printer, diagramPageMap, annotIndex);
} }
if (per_folio_orientation) {
m_printer->setPageOrientation(chosen_orientation);
}
// Note: do NOT call painter.end() or pdfConvertUriToGoTo() here. // Note: do NOT call painter.end() or pdfConvertUriToGoTo() here.
// We are inside the paintRequested slot: the QPrintPreviewWidget still // We are inside the paintRequested slot: the QPrintPreviewWidget still
// owns the paint cycle. On macOS arm64 (Metal/CALayer compositor), // owns the paint cycle. On macOS arm64 (Metal/CALayer compositor),
@@ -259,6 +284,19 @@ void ProjectPrintWindow::requestPaint()
// pdfConvertUriToGoTo() is deferred to print() via QTimer::singleShot(0). // pdfConvertUriToGoTo() is deferred to print() via QTimer::singleShot(0).
} }
/**
* @brief ProjectPrintWindow::setOrientationForDiagram
* Turn the next page to portrait or landscape, as @diagram is drawn.
* @param diagram
*/
void ProjectPrintWindow::setOrientationForDiagram(Diagram *diagram)
{
const QRect rect = diagramRect(diagram, exportProperties());
m_printer->setPageOrientation(rect.height() > rect.width()
? QPageLayout::Portrait
: QPageLayout::Landscape);
}
/** /**
* @brief ProjectPrintWindow::printDiagram * @brief ProjectPrintWindow::printDiagram
* Print @diagram on the @printer * Print @diagram on the @printer
@@ -356,8 +394,12 @@ void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter
// est revenu a l'identite ici. On reconstruit DONC explicitement la // est revenu a l'identite ici. On reconstruit DONC explicitement la
// transform appliquee par : // transform appliquee par :
// diagram->render(painter, QRectF(), diagram_rect, KeepAspectRatio) // diagram->render(painter, QRectF(), diagram_rect, KeepAspectRatio)
// cible vide => painter->viewport() ; source = diagram_rect ; centre. // cible vide => taille du device ; source = diagram_rect ; centre.
const QRectF target = QRectF(painter->viewport()); // The target render() used for an empty one: the device's
// size. Not painter->viewport(), which is set once when the
// painter opens and misses a page turned since (#1480).
const QRectF target(0, 0, painter->device()->width(),
painter->device()->height());
const QRectF source = QRectF(diagram_rect); // meme source que render() const QRectF source = QRectF(diagram_rect); // meme source que render()
// render() ANCRE en haut-gauche (pas de centrage) : // render() ANCRE en haut-gauche (pas de centrage) :
@@ -852,8 +894,10 @@ void ProjectPrintWindow::print()
const bool isPdf = (m_printer->outputFormat() == QPrinter::PdfFormat); const bool isPdf = (m_printer->outputFormat() == QPrinter::PdfFormat);
const QString pdfFile = isPdf ? m_printer->outputFileName() : QString(); const QString pdfFile = isPdf ? m_printer->outputFileName() : QString();
m_printing = true;
m_preview->print(); // triggers requestPaint() synchronously; painter m_preview->print(); // triggers requestPaint() synchronously; painter
// is created/destroyed inside that call // is created/destroyed inside that call
m_printing = false;
savePageSetupForCurrentPrinter(); savePageSetupForCurrentPrinter();
savePrintProperties(); savePrintProperties();
+2
View File
@@ -96,6 +96,7 @@ class ProjectPrintWindow : public QMainWindow
void saveReloadDiagramParameters(Diagram *diagram, const ExportProperties &options, bool save); void saveReloadDiagramParameters(Diagram *diagram, const ExportProperties &options, bool save);
QList<Diagram *> selectedDiagram() const; QList<Diagram *> selectedDiagram() const;
void exportToPDF(); void exportToPDF();
void setOrientationForDiagram(Diagram *diagram);
private: private:
@@ -106,6 +107,7 @@ class ProjectPrintWindow : public QMainWindow
QColor m_backup_diagram_background_color; QColor m_backup_diagram_background_color;
QHash<Diagram *, QCheckBox *> m_diagram_list_hash; QHash<Diagram *, QCheckBox *> m_diagram_list_hash;
QList<PdfLinks::ComponentInfo> m_componentInfoList; QList<PdfLinks::ComponentInfo> m_componentInfoList;
bool m_printing = false;
}; };
#endif // PROJECTPRINTWINDOW_H #endif // PROJECTPRINTWINDOW_H
+56 -3
View File
@@ -341,13 +341,15 @@ QString ProjectView::askUserForFilePath(bool assign) {
// ask the user for a filepath in order to save the project // ask the user for a filepath in order to save the project
const QString qet_filter = tr("Project QElectroTech (*.qet)", "filetypes allowed when saving a project file"); const QString qet_filter = tr("Project QElectroTech (*.qet)", "filetypes allowed when saving a project file");
const QString qetz_filter = tr("Zipped project QElectroTech (*.qetz)", "filetypes allowed when saving a project file"); const QString qetz_filter = tr("Zipped project QElectroTech (*.qetz)", "filetypes allowed when saving a project file");
QString selected_filter = QETProject::isContainerPath(m_project->filePath()) ? qetz_filter : qet_filter; //A project is saved as a .qetz unless a .qet is chosen
//(stage D1)
QString selected_filter = qetz_filter;
QString filepath = QFileDialog::getSaveFileName( QString filepath = QFileDialog::getSaveFileName(
this, this,
tr("Save as", "dialog title"), tr("Save as", "dialog title"),
m_project -> currentDir() + "/" + tr("untitled") m_project -> currentDir() + "/" + tr("untitled")
+ (selected_filter == qetz_filter ? ".qetz" : ".qet"), + (selected_filter == qetz_filter ? ".qetz" : ".qet"),
qet_filter + ";;" + qetz_filter, qetz_filter + ";;" + qet_filter,
&selected_filter &selected_filter
); );
@@ -725,6 +727,32 @@ QETResult ProjectView::saveAs()
return(doSave()); return(doSave());
} }
/**
@brief ProjectView::saveCopyForOlderVersions
Ask for a .qet path and write the project there, for a version of
QElectroTech that cannot open a .qetz. The project keeps its file.
@return a QETResult object reflecting the situation; a valid one if
the operation was cancelled
*/
QETResult ProjectView::saveCopyForOlderVersions()
{
if (!m_project) return(noProjectResult());
QString base = m_project->filePath().isEmpty()
? m_project->currentDir() + "/" + tr("untitled")
: m_project->filePath();
for (const QString &suffix : {QStringLiteral(".qetz"), QStringLiteral(".qet")})
if (base.endsWith(suffix, Qt::CaseInsensitive)) base.chop(suffix.size());
QString path = QFileDialog::getSaveFileName(
this,
tr("Save a copy for an older QElectroTech", "dialog title"),
base + ".qet",
tr("Project QElectroTech (*.qet)", "filetypes allowed when saving a project file"));
if (path.isEmpty()) return(QETResult());
if (!path.endsWith(".qet", Qt::CaseInsensitive)) path += ".qet";
return(m_project->writeCopyForOlderVersions(path));
}
/** /**
Save project content, then write the project file. May Save project content, then write the project file. May
call saveAs if no filepath was provided before. call saveAs if no filepath was provided before.
@@ -741,6 +769,31 @@ QETResult ProjectView::doSave()
return(saveAs()); return(saveAs());
} }
//A project read from a .qet is saved as a .qetz beside it: say so,
//and ask before replacing a .qetz already there
if (m_project->savesBesideOpenedFile()) {
const QString beside = m_project->besideContainerPath();
const QString message = tr("This project was opened from %1. It is saved as %2, "
"beside it, in the zipped project format; %1 stays as "
"it is.\n\nTo write a file for a version of "
"QElectroTech that cannot open %2, use File > Save a "
"copy for an older QElectroTech.")
.arg(QFileInfo(m_project->filePath()).fileName(), QFileInfo(beside).fileName());
if (QFileInfo::exists(beside)) {
if (QET::QetMessageBox::question(
this, tr("Save as a zipped project", "message box title"),
message + QStringLiteral("\n\n")
+ tr("%1 already exists. Replace it?").arg(QFileInfo(beside).fileName()),
QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel)
!= QMessageBox::Yes) {
return(QETResult());
}
} else {
QET::QetMessageBox::information(
this, tr("Save as a zipped project", "message box title"), message);
}
}
// write to file // write to file
QETResult result = m_project -> write(); QETResult result = m_project -> write();
updateWindowTitle(); updateWindowTitle();
@@ -1204,7 +1257,7 @@ void ProjectView::rebuildDiagramsMap()
*/ */
void ProjectView::tabChanged(int tab_id) void ProjectView::tabChanged(int tab_id)
{ {
//A folio opened without its items (QET_LAZY_FOLIOS) is built when //A folio opened without its items (lazy folios) is built when
//its tab is first shown. Read the tab itself: the first tab is //its tab is first shown. Read the tab itself: the first tab is
//shown while loadDiagrams() is still filling m_diagram_ids //shown while loadDiagrams() is still filling m_diagram_ids
if (auto dv = qobject_cast<DiagramView *>(m_tab->widget(tab_id))) if (auto dv = qobject_cast<DiagramView *>(m_tab->widget(tab_id)))
+1
View File
@@ -131,6 +131,7 @@ class ProjectView : public QWidget
QETResult save(); QETResult save();
QETResult saveAs(); QETResult saveAs();
QETResult doSave(); QETResult doSave();
QETResult saveCopyForOlderVersions();
int cleanProject(); int cleanProject();
void updateWindowTitle(); void updateWindowTitle();
void updateTabTitle(DiagramView *); void updateTabTitle(DiagramView *);
+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;
+26
View File
@@ -978,6 +978,8 @@ void QETDiagramEditor::setUpActions()
QAction *open_file = m_file_actions_group.addAction(QET::Icons::DocumentOpen, tr("&Open")); QAction *open_file = m_file_actions_group.addAction(QET::Icons::DocumentOpen, tr("&Open"));
m_save_file = m_file_actions_group.addAction(QET::Icons::DocumentSave, tr("&Save")); m_save_file = m_file_actions_group.addAction(QET::Icons::DocumentSave, tr("&Save"));
m_save_file_as = m_file_actions_group.addAction(QET::Icons::DocumentSaveAs, tr("Save as")); m_save_file_as = m_file_actions_group.addAction(QET::Icons::DocumentSaveAs, tr("Save as"));
m_save_copy_for_older = m_file_actions_group.addAction(QET::Icons::DocumentSaveAs,
tr("Save a copy for an older QElectroTech"));
m_close_file = m_file_actions_group.addAction(QET::Icons::ProjectClose, tr("&Close")); m_close_file = m_file_actions_group.addAction(QET::Icons::ProjectClose, tr("&Close"));
ShortcutManager::instance().registerAction(new_file, "diagrameditor.new_file", tr("Diagram editors"), QKeySequence::New); ShortcutManager::instance().registerAction(new_file, "diagrameditor.new_file", tr("Diagram editors"), QKeySequence::New);
@@ -991,6 +993,19 @@ void QETDiagramEditor::setUpActions()
m_close_file ->setStatusTip( tr("Closes the current project", "status bar tip") ); m_close_file ->setStatusTip( tr("Closes the current project", "status bar tip") );
m_save_file ->setStatusTip( tr("Saves the current project and all its sheets", "status bar tip") ); m_save_file ->setStatusTip( tr("Saves the current project and all its sheets", "status bar tip") );
m_save_file_as ->setStatusTip( tr("Saves the current project with a different file name", "status bar tip") ); m_save_file_as ->setStatusTip( tr("Saves the current project with a different file name", "status bar tip") );
m_save_copy_for_older->setStatusTip(tr("Writes a copy of the current project as a .qet file, which "
"versions of QElectroTech that cannot open a .qetz open",
"status bar tip"));
ShortcutManager::instance().registerAction(m_save_copy_for_older, "diagrameditor.save_copy_for_older",
tr("Diagram editors"), QKeySequence());
connect(m_save_copy_for_older, &QAction::triggered, this, [this]() {
if (ProjectView *project_view = currentProjectView()) {
const QETResult result = project_view->saveCopyForOlderVersions();
if (!result.isOk()) {
showError(result);
}
}
});
connect(m_save_file_as, &QAction::triggered, this, &QETDiagramEditor::saveAs); connect(m_save_file_as, &QAction::triggered, this, &QETDiagramEditor::saveAs);
connect(m_save_file, &QAction::triggered, this, &QETDiagramEditor::save); connect(m_save_file, &QAction::triggered, this, &QETDiagramEditor::save);
@@ -1969,6 +1984,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.
@@ -2750,6 +2775,7 @@ void QETDiagramEditor::slot_updateActions()
m_close_file-> setEnabled(opened_project); m_close_file-> setEnabled(opened_project);
m_save_file-> setEnabled(opened_project); m_save_file-> setEnabled(opened_project);
m_save_file_as-> setEnabled(opened_project); m_save_file_as-> setEnabled(opened_project);
m_save_copy_for_older-> setEnabled(opened_project);
m_rotate_texts-> setEnabled(editable_project); m_rotate_texts-> setEnabled(editable_project);
m_export_to_images-> setEnabled(opened_diagram); m_export_to_images-> setEnabled(opened_diagram);
m_print-> setEnabled(opened_diagram); m_print-> setEnabled(opened_diagram);
+1
View File
@@ -301,6 +301,7 @@ class QETDiagramEditor : public QETMainWindow
*m_close_file, ///< Close current project file *m_close_file, ///< Close current project file
*m_save_file, ///< Save current project *m_save_file, ///< Save current project
*m_save_file_as, ///< Save current project as a specific file *m_save_file_as, ///< Save current project as a specific file
*m_save_copy_for_older, ///< Write a .qet copy for an older QElectroTech
*m_find = nullptr, *m_find = nullptr,
*m_jump_to_element = nullptr; ///< Open the "jump to element" quick-open popup *m_jump_to_element = nullptr; ///< Open the "jump to element" quick-open popup
@@ -323,7 +323,7 @@ void ProjectDBModel::setIdentifier(const QString &identifier) {
/** /**
@brief ProjectDBModel::folioBuilt @brief ProjectDBModel::folioBuilt
The folio of a table made while it was being built (QET_LAZY_FOLIOS) The folio of a table made while it was being built (lazy folios)
is built: read the database, as opening does once it has filled it. is built: read the database, as opening does once it has filled it.
Nothing for any other model. Nothing for any other model.
*/ */
@@ -391,7 +391,7 @@ void ProjectDBModel::fillValue()
{ {
m_record.clear(); m_record.clear();
//Opening builds a table before the database is filled, and fills //Opening builds a table before the database is filled, and fills
//it after: a table on a folio built late (QET_LAZY_FOLIOS) does //it after: a table on a folio built late (lazy folios) does
//the same, so that it is laid out as it would have been on //the same, so that it is laid out as it would have been on
//opening -- empty until its folio is built (folioBuilt()) //opening -- empty until its folio is built (folioBuilt())
if (m_fill_when_built) { if (m_fill_when_built) {
@@ -566,7 +566,7 @@ void QetGraphicsTableItem::initLink()
if (!m_pending_previous_table_uuid.isNull()) if (!m_pending_previous_table_uuid.isNull())
{ {
//The built folios only: a previous table on a folio not built //The built folios only: a previous table on a folio not built
//yet (QET_LAZY_FOLIOS) is linked when that folio is //yet (lazy folios) is linked when that folio is
QETProject *project = this->diagram()->project(); QETProject *project = this->diagram()->project();
ElementProvider provider_(project->builtFolios()); ElementProvider provider_(project->builtFolios());
auto previous_table = provider_.tableFromUuid(m_pending_previous_table_uuid); auto previous_table = provider_.tableFromUuid(m_pending_previous_table_uuid);
@@ -583,7 +583,7 @@ void QetGraphicsTableItem::initLink()
/** /**
@brief QetGraphicsTableItem::previousTableUuid @brief QetGraphicsTableItem::previousTableUuid
@return the uuid of the table before this one in its chain, linked or @return the uuid of the table before this one in its chain, linked or
still waiting for its folio to be built (QET_LAZY_FOLIOS); null if none still waiting for its folio to be built (lazy folios); null if none
*/ */
QUuid QetGraphicsTableItem::previousTableUuid() const QUuid QetGraphicsTableItem::previousTableUuid() const
{ {
+48 -4
View File
@@ -1154,6 +1154,11 @@ bool Conductor::fromXml(QDomElement &dom_element)
m_freeze_label = dom_element.attribute("freezeLabel") == "true" ? true : false; m_freeze_label = dom_element.attribute("freezeLabel") == "true" ? true : false;
//Changed through the project database while its folio was not
//built (lazy folios): the change, not what the file says
if (diagram() && diagram()->project())
if (const auto edited = diagram()->project()->dataBase()->takeUnbuiltWireEdit(m_uuid))
pr = *edited;
setProperties(pr); setProperties(pr);
return retval; return retval;
@@ -1857,7 +1862,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 +2010,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 +2042,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 +2055,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 +2069,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 ();
+93 -18
View File
@@ -167,7 +167,28 @@ void DiagramImageItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *
return; return;
} }
DarkImageRendering::paintPixmap(painter, pixmap_, m_adapt_to_dark_theme); // SVG export with smaller pictures: a copy four times the size the
// picture is drawn at, when that is smaller than the picture. An
// SVG stores what it is given, so a photo shown at a tenth of its
// size would otherwise be written whole.
const QTransform world = painter -> worldTransform();
const qreal drawn_scale = std::sqrt(std::abs(world.determinant()));
const QSize small_size = (QSizeF(pixmap_.size()) * drawn_scale * 4).toSize();
if (diagram() && diagram() -> smallPictures() && !pixmap_.isNull()
&& small_size.width() < pixmap_.width()
&& small_size.height() < pixmap_.height()
&& !small_size.isEmpty())
{
const QPixmap small = pixmap_.scaled(small_size, Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
painter -> save();
painter -> scale(qreal(pixmap_.width()) / small.width(),
qreal(pixmap_.height()) / small.height());
DarkImageRendering::paintPixmap(painter, small, m_adapt_to_dark_theme);
painter -> restore();
}
else
DarkImageRendering::paintPixmap(painter, pixmap_, m_adapt_to_dark_theme);
Q_UNUSED(option); Q_UNUSED(widget); Q_UNUSED(option); Q_UNUSED(widget);
@@ -1787,6 +1808,71 @@ const QByteArray &DiagramImageItem::encodedPng(const QPixmap &pixmap, QByteArray
@return true if successfully loaded. @return true if successfully loaded.
*/ */
bool DiagramImageItem::fromXml(const QDomElement &e) bool DiagramImageItem::fromXml(const QDomElement &e)
{
return fromXml(e, QByteArray());
}
/**
@brief DiagramImageItem::fromXml
Load this image from xml element e, its picture being @p picture, not
the base64 text of @p e, unless @p picture is empty. A .qetz keeps its
pictures as files (QetContainer::join()).
*/
bool DiagramImageItem::fromXml(const QDomElement &e, const QByteArray &picture)
{
Picture p = pictureOf(e, picture);
decodePicture(p);
return fromXml(e, std::move(p));
}
/**
@brief DiagramImageItem::pictureOf
@return the picture of @p e, @p picture unless it is empty, else the
base64 text of @p e, still encoded : decodePicture() does the slow part.
Reads the DOM, so GUI thread only.
*/
DiagramImageItem::Picture DiagramImageItem::pictureOf(const QDomElement &e,
const QByteArray &picture)
{
Picture p;
p.bytes = picture;
if (p.bytes.isEmpty())
{
// From the first text node only, not e.text(): that also collects
// the text of every child element, so on a cropped or colour-keyed
// picture it carries <image_base>'s bytes too.
const QDomNode image_node = e.firstChild();
if (image_node.isText())
p.base64 = image_node.toText().data();
else
p.found = false;
}
return p;
}
/**
@brief DiagramImageItem::decodePicture
Decodes the base64 text and the picture file of @p picture. Touches
nothing else, so it can run in any thread : a large photo takes about
a quarter of a second, and Diagram::fromXml() decodes a folio's
pictures in parallel.
*/
void DiagramImageItem::decodePicture(Picture &picture)
{
if (picture.bytes.isEmpty() && !picture.base64.isEmpty())
{
picture.bytes = QByteArray::fromBase64(picture.base64.toLatin1());
picture.base64.clear();
}
picture.image = QImage::fromData(picture.bytes);
}
/**
@brief DiagramImageItem::fromXml
Load this image from xml element e, its picture already decoded by
decodePicture().
*/
bool DiagramImageItem::fromXml(const QDomElement &e, Picture picture)
{ {
if (e.tagName() != "image") { if (e.tagName() != "image") {
return (false); return (false);
@@ -1796,28 +1882,17 @@ bool DiagramImageItem::fromXml(const QDomElement &e)
const QUuid uuid(e.attribute(QStringLiteral("uuid"))); const QUuid uuid(e.attribute(QStringLiteral("uuid")));
if (!uuid.isNull() && uuid != m_uuid) setUuid(uuid); if (!uuid.isNull() && uuid != m_uuid) setUuid(uuid);
QDomNode image_node = e.firstChild(); if (!picture.found) {
if (!image_node.isText()) {
return (false); return (false);
} }
const QByteArray array = picture.bytes;
//load xml image to QByteArray //Set QPixmap from the decoded picture
QByteArray array; QPixmap pixmap = QPixmap::fromImage(std::move(picture.image));
array = QByteArray::fromBase64(e.text().toLatin1());
//Set QPixmap from the array
QPixmap pixmap;
pixmap.loadFromData(array);
setPixmap(pixmap); setPixmap(pixmap);
// From the first text node only, not e.text(): that also collects the if (isPng(array) && !pixmap_.isNull())
// text of every child element, so on a cropped or colour-keyed picture
// it carries <image_base>'s bytes too. loadFromData() stops at the end
// of the first PNG and never notices; a cache written back verbatim
// would.
const QByteArray ownArray = QByteArray::fromBase64(image_node.toText().data().toLatin1());
if (isPng(ownArray) && !pixmap_.isNull())
{ {
m_png_cache = ownArray; m_png_cache = array;
m_png_cache_key = pixmap_.cacheKey(); m_png_cache_key = pixmap_.cacheKey();
} }
@@ -24,6 +24,7 @@
#include <QColor> #include <QColor>
#include <QFont> #include <QFont>
#include <QImage>
#include <QList> #include <QList>
#include <QUuid> #include <QUuid>
#include <QStyleOptionGraphicsItem> #include <QStyleOptionGraphicsItem>
@@ -119,7 +120,20 @@ class DiagramImageItem : public QetGraphicsItem {
*/ */
int type() const override { return Type; } int type() const override { return Type; }
/// A picture as read from its <image> element, before and after
/// decodePicture(), which may run outside the GUI thread.
struct Picture {
QString base64; ///< text of <image>, decoded when bytes is empty
QByteArray bytes; ///< the picture file (PNG, JPEG...)
QImage image; ///< bytes, decoded
bool found = true; ///< false : neither bytes nor text
};
static Picture pictureOf(const QDomElement &, const QByteArray &picture);
static void decodePicture(Picture &picture);
virtual bool fromXml(const QDomElement &); virtual bool fromXml(const QDomElement &);
bool fromXml(const QDomElement &, const QByteArray &picture);
bool fromXml(const QDomElement &, Picture picture);
virtual QDomElement toXml(QDomDocument &) const; virtual QDomElement toXml(QDomDocument &) const;
QUuid uuid() const {return m_uuid;} QUuid uuid() const {return m_uuid;}
void setUuid(const QUuid &uuid) {m_uuid = uuid; emit uuidChanged();} void setUuid(const QUuid &uuid) {m_uuid = uuid; emit uuidChanged();}
+43 -13
View File
@@ -1115,6 +1115,11 @@ bool Element::fromXml(QDomElement &e,
DiagramContext dc; DiagramContext dc;
dc.fromXml(e.firstChildElement(QStringLiteral("elementInformations")), dc.fromXml(e.firstChildElement(QStringLiteral("elementInformations")),
QStringLiteral("elementInformation")); QStringLiteral("elementInformation"));
//Changed through the project database while its folio was not
//built (lazy folios): the change, not what the file says
if (diagram() && diagram()->project())
if (const auto edited = diagram()->project()->dataBase()->takeUnbuiltEdit(m_uuid))
dc = *edited;
//Load override properties (For now, only used when the element is a terminal) //Load override properties (For now, only used when the element is a terminal)
if (m_data.m_type == ElementData::Terminal) if (m_data.m_type == ElementData::Terminal)
@@ -1250,17 +1255,41 @@ QDomElement Element::toXml(
//if this element is linked to other elements, //if this element is linked to other elements,
//save the uuid of each other elements //save the uuid of each other elements
if (! isFree()) { //A partner on a folio not built yet (lazy folios), which this
//symbol will be linked to, is saved too, in the order the symbol was
//saved with: saving needs no partner built (stage D5a)
QList<std::pair<QUuid, int>> links;
if (keepsSavedLinks() && diagram() && diagram()->project()) {
const QSet<QUuid> waiting = diagram()->project()->waitingLinkTargets();
QSet<Element *> written;
for (const auto &link_info : std::as_const(tmp_uuids_link)) {
Element *linked = nullptr;
for (Element *elmt : std::as_const(connected_elements))
if (elmt->uuid() == link_info.uuid && !written.contains(elmt)) { linked = elmt; break; }
if (linked) {
written.insert(linked);
links.append({linked->uuid(), m_group_index_map.value(linked, -1)});
} else if (waiting.contains(link_info.uuid)) {
links.append({link_info.uuid, link_info.group_index});
}
}
for (Element *elmt : std::as_const(connected_elements))
if (!written.contains(elmt))
links.append({elmt->uuid(), m_group_index_map.value(elmt, -1)});
} else {
for (Element *elmt : std::as_const(connected_elements))
links.append({elmt->uuid(), m_group_index_map.value(elmt, -1)});
}
if (!links.isEmpty()) {
QDomElement links_uuids = document.createElement(QStringLiteral("links_uuids")); QDomElement links_uuids = document.createElement(QStringLiteral("links_uuids"));
foreach (Element *elmt, connected_elements) { for (const auto &link : std::as_const(links)) {
QDomElement link_uuid = QDomElement link_uuid =
document.createElement(QStringLiteral("link_uuid")); document.createElement(QStringLiteral("link_uuid"));
link_uuid.setAttribute(QStringLiteral("uuid"), elmt->uuid().toString()); link_uuid.setAttribute(QStringLiteral("uuid"), link.first.toString());
// Save group index if assigned (for slave->master links) // Save group index if assigned (for slave->master links)
int gi = m_group_index_map.value(elmt, -1); if (link.second >= 0) {
if (gi >= 0) { link_uuid.setAttribute(QStringLiteral("group_index"), link.second);
link_uuid.setAttribute(QStringLiteral("group_index"), gi);
} }
links_uuids.appendChild(link_uuid); links_uuids.appendChild(link_uuid);
@@ -1630,10 +1659,11 @@ void Element::initLink(QETProject *prj)
// if nothing to link return now // if nothing to link return now
if (tmp_uuids_link.isEmpty()) return; if (tmp_uuids_link.isEmpty()) return;
//The built folios only: a partner on a folio not built yet //The symbols built: on the folios built, and those built alone as
//(QET_LAZY_FOLIOS) is linked when its folio is, and the saved list //partners on folios not built (Diagram::buildSymbols()). A partner
//is kept until then, for the order it gives //not built yet (lazy folios) is linked when it is, and the
ElementProvider ep(prj->builtFolios()); //saved list is kept until then, for the order it gives
ElementProvider ep(prj->folios());
QList<QUuid> uuids; QList<QUuid> uuids;
for (const auto &linkInfo : tmp_uuids_link) { for (const auto &linkInfo : tmp_uuids_link) {
uuids.append(linkInfo.uuid); uuids.append(linkInfo.uuid);
@@ -1659,7 +1689,7 @@ void Element::initLink(QETProject *prj)
/** /**
@brief Element::pendingLinks @brief Element::pendingLinks
@return the uuids of the partners this symbol was saved linked to and @return the uuids of the partners this symbol was saved linked to and
is not linked to yet: those on a folio not built yet (QET_LAZY_FOLIOS), is not linked to yet: those on a folio not built yet (lazy folios),
until initLink() runs again once it is until initLink() runs again once it is
*/ */
QSet<QUuid> Element::pendingLinks() const QSet<QUuid> Element::pendingLinks() const
@@ -1859,7 +1889,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 +1955,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
+30 -9
View File
@@ -1082,15 +1082,24 @@ 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)
{ {
//The report's partner on a folio not built yet (QET_LAZY_FOLIOS) //The report's partner on a folio not built yet (lazy folios)
//is built, so that a potential is followed as far as when every //is built, so that a potential is followed as far as when every
//folio is; not while folios are being read or built, which //folio is; not while folios are being read or built, which
//would build whole report chains //would build whole report chains
@@ -1102,6 +1111,14 @@ QList<Terminal *> relatedPotentialTerminal (
project->buildFoliosHoldingSymbols(waiting); project->buildFoliosHoldingSymbols(waiting);
} }
QList <Element *> elmt_list = terminal -> parentElement() -> linkedElements(); QList <Element *> elmt_list = terminal -> parentElement() -> linkedElements();
//A partner built alone (stage D5b) has no wires
//yet: its folio is built
if (!elmt_list.isEmpty() && project && elmt_list.first()->diagram()
&& !elmt_list.first()->diagram()->isLoaded()
&& project->state() != QETProject::ProjectParsingRunning) {
project->buildFolios({elmt_list.first()->diagram()});
elmt_list = terminal->parentElement()->linkedElements();
}
if (!elmt_list.isEmpty()) if (!elmt_list.isEmpty())
{ {
return (elmt_list.first()->terminals()); return (elmt_list.first()->terminals());
@@ -1110,14 +1127,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
+437 -105
View File
@@ -18,6 +18,7 @@
#include "qetproject.h" #include "qetproject.h"
#include "container/qetcontainer.h" #include "container/qetcontainer.h"
#include "container/qetcontainerdb.h"
#include "ElementsCollection/xmlelementcollection.h" #include "ElementsCollection/xmlelementcollection.h"
#include "autoNum/assignvariables.h" #include "autoNum/assignvariables.h"
@@ -30,6 +31,8 @@
#include "autoNum/elementautonumschemecommand.h" #include "autoNum/elementautonumschemecommand.h"
#include "diagram.h" #include "diagram.h"
#include "qetgraphicsitem/element.h" #include "qetgraphicsitem/element.h"
#include "qetgraphicsitem/conductor.h"
#include "qetgraphicsitem/dynamicelementtextitem.h"
#include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h" #include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h"
#include "qetgraphicsitem/ViewItem/projectdbmodel.h" #include "qetgraphicsitem/ViewItem/projectdbmodel.h"
#include "qetapp.h" #include "qetapp.h"
@@ -46,6 +49,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>
@@ -192,6 +196,8 @@ QETProject::QETProject(KAutoSaveFile *backup, QObject *parent) :
} }
//Set the real path, instead of the path of the backup. //Set the real path, instead of the path of the backup.
setFilePath(backup->managedFile().path()); setFilePath(backup->managedFile().path());
//A project read from a .qet is saved as a .qetz beside it
m_opened_from_qet = !isContainerPath(m_file_path);
delete backup; delete backup;
//Set the project to read only mode if the file it. //Set the project to read only mode if the file it.
@@ -278,6 +284,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
@@ -322,7 +342,7 @@ void QETProject::init()
connect(&m_titleblocks_collection, &TitleBlockTemplatesCollection::changed, this, &QETProject::updateDiagramsTitleBlockTemplate); connect(&m_titleblocks_collection, &TitleBlockTemplatesCollection::changed, this, &QETProject::updateDiagramsTitleBlockTemplate);
connect(&m_titleblocks_collection, &TitleBlockTemplatesCollection::aboutToRemove, this, &QETProject::removeDiagramsTitleBlockTemplate); connect(&m_titleblocks_collection, &TitleBlockTemplatesCollection::aboutToRemove, this, &QETProject::removeDiagramsTitleBlockTemplate);
//What changes the saved items of folios not built (QET_LAZY_FOLIOS): //What changes the saved items of folios not built (lazy folios):
//a folio's position, number or the folio total in their texts, //a folio's position, number or the folio total in their texts,
//the report and cross-reference settings, a symbol's definition. //the report and cross-reference settings, a symbol's definition.
//A folio not built is then built to be saved (writesKeptFolios()) //A folio not built is then built to be saved (writesKeptFolios())
@@ -398,11 +418,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);
} }
/** /**
@@ -437,8 +471,12 @@ QETProject::ProjectState QETProject::openFile(QFile *file)
//Extract the content of the xml. The raw bytes are kept : a project //Extract the content of the xml. The raw bytes are kept : a project
//file without a persisted uuid derives its uuid from them. //file without a persisted uuid derives its uuid from them.
const QByteArray content = file->readAll(); const QByteArray content = file->readAll();
//Each folio's text is kept while it is not built (readDiagramsXml()) //Each folio's text is kept while it is not built (readDiagramsXml()):
m_opened_content = content; //from a .qet only, a .qetz is a zip of parts
if (!content.startsWith("PK\x03\x04"))
m_opened_content = content;
//A project read from a .qet is saved as a .qetz beside it
m_opened_from_qet = !content.startsWith("PK\x03\x04");
QDomDocument xml_project; QDomDocument xml_project;
// PreserveSpacingOnlyNodes: without it, a text node that is entirely // PreserveSpacingOnlyNodes: without it, a text node that is entirely
// whitespace -- e.g. a title-block custom variable deliberately set to // whitespace -- e.g. a title-block custom variable deliberately set to
@@ -456,9 +494,28 @@ 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)) //A .qetz: its entries are kept, for a save to copy the parts of the
//folios not built (stage D4)
m_container_files.clear();
m_folio_parts.clear();
bool read = false;
int container_format = 0;
if (QetContainer::isZip(content)) {
QList<QetZip::Entry> entries;
read = QetZip::fromBytes(content, &entries, &read_error)
&& QetContainer::join(entries, &xml_project, &read_error, &m_folio_parts,
&m_container_pictures, &container_format);
if (read)
for (const QetZip::Entry &entry : std::as_const(entries))
m_container_files.insert(entry.name, entry.data);
} else {
read = QetContainer::readDocument(content, &xml_project, &read_error);
}
if (!read)
{ {
qWarning().noquote() << "Unable to read" << fi.fileName() << ":" << read_error; qWarning().noquote() << "Unable to read" << fi.fileName() << ":" << read_error;
m_container_pictures.clear();
m_folio_parts.clear();
if(opened_here) { if(opened_here) {
file->close(); file->close();
} }
@@ -489,9 +546,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 +608,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);
} }
@@ -552,7 +616,7 @@ QList<Diagram *> QETProject::diagrams() const
/** /**
@brief QETProject::folios @brief QETProject::folios
@return the folios of this project, whether their items are built yet @return the folios of this project, whether their items are built yet
or not (a project opened with QET_LAZY_FOLIOS); see Diagram::isLoaded() or not (a project opened with lazy folios); see Diagram::isLoaded()
*/ */
QList<Diagram *> QETProject::folios() const QList<Diagram *> QETProject::folios() const
{ {
@@ -564,9 +628,9 @@ QList<Diagram *> QETProject::folios() const
@return the folios CableManager::refreshLabels() works on, built: every @return the folios CableManager::refreshLabels() works on, built: every
folio when the project has cables (a core is followed through folio folio when the project has cables (a core is followed through folio
reports to any folio); without cables, the folios built and those not reports to any folio); without cables, the folios built and those not
built yet (QET_LAZY_FOLIOS) holding a wire that still refers to a built yet holding a wire that still refers to a cable -- the only
cable -- the only wires the pass changes then, taking the reference wires the pass changes then, taking the reference away, read from the
away -- found in the XML kept for them folios' XML.
*/ */
QList<Diagram *> QETProject::cableLabelFolios() QList<Diagram *> QETProject::cableLabelFolios()
{ {
@@ -590,7 +654,7 @@ QList<Diagram *> QETProject::cableLabelFolios()
/** /**
@brief QETProject::builtFolios @brief QETProject::builtFolios
@return the folios of this project whose items are built: all of them, @return the folios of this project whose items are built: all of them,
but for a project opened with QET_LAZY_FOLIOS but for a project opened with lazy folios
*/ */
QList<Diagram *> QETProject::builtFolios() const QList<Diagram *> QETProject::builtFolios() const
{ {
@@ -683,6 +747,21 @@ void QETProject::buildFolios(const QList<Diagram *> &folios) const
for (Element *elmt : d->elements()) for (Element *elmt : d->elements())
if (elmt->keepsSavedLinks()) if (elmt->keepsSavedLinks())
elmt->initLink(self); elmt->initLink(self);
//A linked folio report's texts show a wire's properties: the
//report's own wire, else its partner's. A report built alone
//first, as a partner (buildPartnerSymbols()), linked before it had
//wires, and a report elsewhere may watch that partner: both look
//again now, as opening has them do once every wire exists (an
//unlinked report keeps its texts, as opening leaves them)
for (Diagram *d : std::as_const(built))
for (Element *elmt : d->elements())
if ((elmt->linkType() & Element::AllReport) && !elmt->linkedElements().isEmpty())
{
emit elmt->linkedElementChanged();
for (Element *partner : elmt->linkedElements())
if (!built.contains(partner->diagram()))
emit partner->linkedElementChanged();
}
//Likewise a table whose previous table was on a folio not built //Likewise a table whose previous table was on a folio not built
for (Diagram *d : builtFolios()) for (Diagram *d : builtFolios())
for (QGraphicsItem *item : d->items()) for (QGraphicsItem *item : d->items())
@@ -707,7 +786,7 @@ void QETProject::buildFolios(const QList<Diagram *> &folios) const
/** /**
@brief QETProject::buildFoliosHolding @brief QETProject::buildFoliosHolding
Build the folios not built yet (QET_LAZY_FOLIOS) holding a symbol of Build the folios not built yet (lazy folios) holding a symbol of
one of the kinds @p kinds -- if @p free_only, one which may be linked one of the kinds @p kinds -- if @p free_only, one which may be linked
to nothing once built: none of the partners it was saved linked to is to nothing once built: none of the partners it was saved linked to is
in the project. What a search for such symbols must build to find in the project. What a search for such symbols must build to find
@@ -734,29 +813,22 @@ void QETProject::buildFoliosHolding(ElementData::Types kinds, bool free_only)
for (auto it = types.cbegin() ; it != types.cend() ; ++it) for (auto it = types.cbegin() ; it != types.cend() ; ++it)
placed.insert(it.key()); placed.insert(it.key());
} }
//The symbols and their links as the database has them
QList<Diagram *> to_build; QList<Diagram *> to_build;
for (Diagram *d : std::as_const(m_diagrams_list)) for (Diagram *d : std::as_const(m_diagrams_list))
{ {
if (d->isLoaded()) { if (d->isLoaded()) {
continue; continue;
} }
for (const QDomElement &element_xml : for (const projectDataBase::FolioSymbol &symbol : m_data_base.symbolsOnFolios({d->uuid()}))
QET::findInDomElement(d->deferredXml(),
QStringLiteral("elements"),
QStringLiteral("element")))
{ {
const auto type = types.constFind(QUuid(element_xml.attribute(QStringLiteral("uuid")))); if (!(kinds & ElementData::typeFromString(symbol.kind))) {
if (type == types.constEnd()
|| !(kinds & ElementData::typeFromString(*type))) {
continue; continue;
} }
bool linked = false; bool linked = false;
if (free_only) { if (free_only) {
for (const QDomElement &link : for (const QUuid &link : m_data_base.linkedSymbols(symbol.uuid)) {
QET::findInDomElement(element_xml, if (placed.contains(link)) {
QStringLiteral("links_uuids"),
QStringLiteral("link_uuid"))) {
if (placed.contains(QUuid(link.attribute(QStringLiteral("uuid"))))) {
linked = true; linked = true;
break; break;
} }
@@ -781,8 +853,9 @@ void QETProject::buildFoliosHolding(ElementData::Types kinds, bool free_only)
/** /**
@brief QETProject::buildFoliosHoldingSymbols @brief QETProject::buildFoliosHoldingSymbols
Build the folios not built yet (QET_LAZY_FOLIOS) holding one of the Build the folios not built yet (lazy folios) holding one of the
symbols @p symbols, found in the XML kept for them: what needs those symbols @p symbols, found in the project database once it holds the
folios not built, else in the XML kept for them: what needs those
symbols while the project is being read, before the database knows symbols while the project is being read, before the database knows
where they are (a terminal strip's terminals) where they are (a terminal strip's terminals)
*/ */
@@ -792,6 +865,15 @@ void QETProject::buildFoliosHoldingSymbols(const QSet<QUuid> &symbols)
return; return;
} }
QList<Diagram *> to_build; QList<Diagram *> to_build;
//Filled from the document, the database knows where every symbol is
if (m_data_base.rowsFromDocument()) {
const QSet<QUuid> folios = m_data_base.folioUuidsOfElements(symbols);
for (Diagram *d : std::as_const(m_diagrams_list))
if (!d->isLoaded() && folios.contains(d->uuid()))
to_build << d;
buildFolios(to_build);
return;
}
for (Diagram *d : std::as_const(m_diagrams_list)) for (Diagram *d : std::as_const(m_diagrams_list))
{ {
if (d->isLoaded()) { if (d->isLoaded()) {
@@ -820,7 +902,7 @@ void QETProject::buildFoliosHoldingSymbols(const QSet<QUuid> &symbols)
/** /**
@brief QETProject::symbolsOnUnbuiltFolios @brief QETProject::symbolsOnUnbuiltFolios
@return the uuids of the symbols on the folios not built yet @return the uuids of the symbols on the folios not built yet
(QET_LAZY_FOLIOS) that building them places (lazy folios) that building them places
*/ */
QSet<QUuid> QETProject::symbolsOnUnbuiltFolios() const QSet<QUuid> QETProject::symbolsOnUnbuiltFolios() const
{ {
@@ -928,11 +1010,43 @@ void QETProject::buildFolioToShow(Diagram *folio)
return; return;
} }
if (qEnvironmentVariableIntValue("QET_LAZY_FOLIOS_TRACE") == 1) { if (qEnvironmentVariableIntValue("QET_LAZY_FOLIOS_TRACE") == 1) {
qWarning().noquote() << "lazy folios:" << to_build.size() qWarning().noquote() << "lazy folios: the partners of folio" << folio->folioIndex() + 1
<< "folios built for the partners of folio" << "built alone, on" << to_build.size() << "folios";
<< folio->folioIndex() + 1;
} }
buildFolios(to_build); //The partners alone, not their folios (stage D5b)
buildPartnerSymbols(to_build, partners);
}
/**
@brief QETProject::buildPartnerSymbols
Build on the folios @p folios, not built, the symbols @p partners alone
(Diagram::buildSymbols()), as opening builds them: linked to the symbols
built already, their texts refreshed, in the state opening has.
*/
void QETProject::buildPartnerSymbols(const QList<Diagram *> &folios, const QSet<QUuid> &partners)
{
const ProjectState state = m_state;
m_state = ProjectParsingRunning;
QList<Element *> built;
for (Diagram *d : folios) {
const QList<Element *> symbols = d->buildSymbols(partners);
if (!symbols.isEmpty()) {
built += symbols;
linkElementsToElementAutoNums(d);
}
}
for (Element *element : std::as_const(built)) {
element->initLink(this);
for (auto &text : element->dynamicTextItems())
text->refreshLabelConnection();
}
//A built symbol linked to one of them puts its partners back in
//their saved order
for (Diagram *d : builtFolios())
for (Element *element : d->elements())
if (element->keepsSavedLinks())
element->initLink(this);
m_state = state;
} }
/** /**
@@ -1007,6 +1121,8 @@ void QETProject::setFilePath(const QString &filepath)
} }
m_next_backup_slot = 0; m_next_backup_slot = 0;
m_file_path = filepath; m_file_path = filepath;
//A path chosen is saved to as it is
m_opened_from_qet = false;
QFileInfo fi(m_file_path); QFileInfo fi(m_file_path);
if (fi.isWritable()) { if (fi.isWritable()) {
@@ -1774,9 +1890,9 @@ std::optional<QUuid> QETProject::elementAutoNumIdToLink(
@brief QETProject::symbolNumbering @brief QETProject::symbolNumbering
@return the numbering of every symbol placed on the folios, read from @return the numbering of every symbol placed on the folios, read from
the symbol on a folio built and, on a folio not built yet the symbol on a folio built and, on a folio not built yet
(QET_LAZY_FOLIOS), from what building it would give: its information (lazy folios), from what building it would give: its information
in the project database's store, the scheme it would be linked to, in the project database's store, the scheme it would be linked to,
its sequential numbers and its kind as the file has them. its sequential numbers and its kind as the database has them.
*/ */
QList<QETProject::SymbolNumbering> QETProject::symbolNumbering() const QList<QETProject::SymbolNumbering> QETProject::symbolNumbering() const
{ {
@@ -1807,38 +1923,27 @@ QList<QETProject::SymbolNumbering> QETProject::symbolNumbering() const
return list; return list;
} }
//A symbol the database has no row for is one building leaves out //The symbols of the folios not built, from the database. A symbol
//(its definition is missing) //it has no row for is one building leaves out (its definition is
const QHash<QUuid, QString> types = m_data_base.elementTypesOnFolios(unbuilt); //missing)
const QHash<QString, QStringList> titles_by_formula = elementAutoNumTitlesByFormula(); QList<QUuid> unbuilt_in_order;
for (Diagram *d : m_diagrams_list) for (Diagram *d : m_diagrams_list)
if (!d->isLoaded()) unbuilt_in_order << d->uuid();
const QHash<QString, QStringList> titles_by_formula = elementAutoNumTitlesByFormula();
for (const projectDataBase::FolioSymbol &row : m_data_base.symbolsOnFolios(unbuilt_in_order))
{ {
if (d->isLoaded()) { const DiagramContext info = m_data_base.elementInformation(row.uuid);
continue; SymbolNumbering symbol;
} symbol.label = info.value(QETInformation::ELMT_LABEL).toString();
for (const QDomElement &element_xml : symbol.formula = info.value(QETInformation::ELMT_FORMULA).toString();
QET::findInDomElement(d->deferredXml(), const auto linked = elementAutoNumIdToLink(info, titles_by_formula);
QStringLiteral("elements"), symbol.scheme = linked ? *linked
QStringLiteral("element"))) : QUuid(info.value(QETInformation::ELMT_FORMULA_ID).toString());
{ symbol.sequence = row.sequence;
const QUuid uuid(element_xml.attribute(QStringLiteral("uuid"))); const ElementData::Type kind = ElementData::typeFromString(row.kind);
const auto type = types.constFind(uuid); symbol.numbered = kind != ElementData::Slave
if (type == types.constEnd()) { && !(kind & ElementData::AllReport);
continue; list << symbol;
}
const DiagramContext info = m_data_base.elementInformation(uuid);
SymbolNumbering symbol;
symbol.label = info.value(QETInformation::ELMT_LABEL).toString();
symbol.formula = info.value(QETInformation::ELMT_FORMULA).toString();
const auto linked = elementAutoNumIdToLink(info, titles_by_formula);
symbol.scheme = linked ? *linked
: QUuid(info.value(QETInformation::ELMT_FORMULA_ID).toString());
symbol.sequence.fromXml(element_xml.firstChildElement(QStringLiteral("sequentialNumbers")));
const ElementData::Type kind = ElementData::typeFromString(*type);
symbol.numbered = kind != ElementData::Slave
&& !(kind & ElementData::AllReport);
list << symbol;
}
} }
return list; return list;
} }
@@ -2237,9 +2342,13 @@ void QETProject::autoFolioNumberingSelectedFolios(int from,
@brief QETProject::toXml @brief QETProject::toXml
@return un document XML representant le projet @return un document XML representant le projet
*/ */
QDomDocument QETProject::toXml(QHash<int, QByteArray> *kept_folios) QDomDocument QETProject::toXml(QHash<int, QByteArray> *kept_folios, QStringList *kept_parts)
{ {
//With @p kept_folios, a folio not built (QET_LAZY_FOLIOS) is //With @p kept_parts, for a .qetz: a
//folio not built is written as <diagram qetz-kept-part="..."/>, its
//part and its rows copied by QetContainer::split()
//(stage D4)
//With @p kept_folios, a folio not built (lazy folios) is
//written as a placeholder <diagram qet_kept_folio="n"/> and its text //written as a placeholder <diagram qet_kept_folio="n"/> and its text
//as read from the file goes in kept_folios, for //as read from the file goes in kept_folios, for
//spliceKeptFolios() -- when building it would write that same //spliceKeptFolios() -- when building it would write that same
@@ -2252,31 +2361,58 @@ QDomDocument QETProject::toXml(QHash<int, QByteArray> *kept_folios)
//is built with the folios its links reach, as when it is shown: //is built with the folios its links reach, as when it is shown:
//their saved texts can show its data (a report, its partner's //their saved texts can show its data (a report, its partner's
//folio label). //folio label).
const bool keep = kept_folios && unloadedFolioCount() const bool keep = unloadedFolioCount() && !m_kept_folios_stale
&& m_kept_folios_trusted && !m_kept_folios_stale; && ((kept_folios && m_kept_folios_trusted) || (kept_parts && m_kept_parts_trusted));
if (keep) { if (keep) {
for (Diagram *diagram : std::as_const(m_diagrams_list)) //Partners built alone and linked or unlinked since: their folio
if (diagram->headerChanged()) //is not what was read
//and symbols whose information was changed through the project
//database while their folio was not built: built together
//-- a .qetz writes those in its database instead, when it can
const QSet<QUuid> edited = m_data_base.foliosWithUnbuiltEdits();
QList<Diagram *> changed;
for (Diagram *diagram : std::as_const(m_diagrams_list)) {
if (diagram->isLoaded()) continue;
bool need_build = !kept_parts;
if (edited.contains(diagram->uuid()) && kept_parts)
m_data_base.unbuiltEditsOnFolio(diagram->uuid(), &need_build);
if (diagram->partialChanged() || (edited.contains(diagram->uuid()) && need_build))
changed << diagram;
}
buildFolios(changed);
//-- unless it changed only in what nothing is computed from (its
//title, date, revision...): written from its items as read
//(Diagram::deferredXmlWithHeader())
for (Diagram *diagram : std::as_const(m_diagrams_list)) {
if (!diagram->headerChanged()) continue;
bool edits_need_build = false;
if (!diagram->isLoaded())
m_data_base.unbuiltEditsOnFolio(diagram->uuid(), &edits_need_build);
if (diagram->isLoaded() || !diagram->headerChangeShowsOnly() || edits_need_build)
buildFolioToShow(diagram); buildFolioToShow(diagram);
}
//A table filled from the database saves the size its rows //A table filled from the database saves the size its rows
//give it, which any edit can change //give it, which any edit can change
if (m_edited_since_open) if (m_edited_since_open)
for (Diagram *diagram : std::as_const(m_diagrams_list)) for (Diagram *diagram : std::as_const(m_diagrams_list))
if (!diagram->isLoaded() && !diagram->deferredXml().elementsByTagName( if (!diagram->isLoaded() && unbuiltFolioHasTables(diagram))
QetGraphicsTableItem::xmlTagName()).isEmpty())
buildFolioToShow(diagram); buildFolioToShow(diagram);
//A built folio is written from its items, which need their //A built folio is written from its items, which need their
//partners: a symbol saves its links, a report the position of //partners: a table saves the table before it, and in a .qet a
//its partner. Build the folios of every partner still waiting, //report the position of its partner. Build the folios of every
//and of every table chain, until none is //table chain, and for a .qet of every partner still waiting,
//until none is. A symbol saves the partners it waits for itself
//(Element::toXml()), and a .qetz keeps no partner's text (D3c):
//saving a .qetz builds no partner (stage D5a)
for (int built = -1 ; built != unloadedFolioCount() ; ) for (int built = -1 ; built != unloadedFolioCount() ; )
{ {
built = unloadedFolioCount(); built = unloadedFolioCount();
QSet<QUuid> waiting; QSet<QUuid> waiting;
QSet<Diagram *> chained; QSet<Diagram *> chained;
for (Diagram *diagram : builtFolios()) { for (Diagram *diagram : builtFolios()) {
for (Element *element : diagram->elements()) if (kept_folios)
waiting.unite(element->pendingLinks()); for (Element *element : diagram->elements())
waiting.unite(element->pendingLinks());
for (Diagram *other : tableChainFolios(diagram)) for (Diagram *other : tableChainFolios(diagram))
if (!other->isLoaded()) chained.insert(other); if (!other->isLoaded()) chained.insert(other);
} }
@@ -2290,9 +2426,9 @@ QDomDocument QETProject::toXml(QHash<int, QByteArray> *kept_folios)
} else { } else {
loadFolios(); loadFolios();
} }
m_waiting_link_targets = unloadedFolioCount() ? symbolsOnUnbuiltFolios() : QSet<QUuid>();
//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();
@@ -2368,6 +2504,65 @@ QDomDocument QETProject::toXml(QHash<int, QByteArray> *kept_folios)
<< "[" << "["
<< diagram << diagram
<< "]"; << "]";
if (keep && !diagram->isLoaded() && diagram->headerChanged()) {
QDomElement xml_diagram = diagram->deferredXmlWithHeader(xml_doc);
//Its symbols whose information changed while it was not built
const QHash<QUuid, DiagramContext> edits = m_data_base.unbuiltEditsOnFolio(diagram->uuid());
for (const QDomElement &list : QET::findInDomElement(xml_diagram, QStringLiteral("elements")))
for (QDomElement element = list.firstChildElement(QStringLiteral("element")) ; !element.isNull() ;
element = element.nextSiblingElement(QStringLiteral("element"))) {
const auto edit = edits.constFind(QUuid(element.attribute(QStringLiteral("uuid"))));
if (edit == edits.constEnd()) continue;
QDomElement information = xml_doc.createElement(QStringLiteral("elementInformations"));
edit->toXml(information, QStringLiteral("elementInformation"));
const QDomElement old_information = element.firstChildElement(QStringLiteral("elementInformations"));
element.replaceChild(information, old_information); //it has one (unbuiltEditsOnFolio())
}
//Its wires whose properties changed
const QHash<QUuid, ConductorProperties> wire_edits = m_data_base.unbuiltWireEditsOnFolio(diagram->uuid());
if (!wire_edits.isEmpty())
for (const QDomElement &list : QET::findInDomElement(xml_diagram, QStringLiteral("conductors")))
for (QDomElement wire = list.firstChildElement(QStringLiteral("conductor")) ; !wire.isNull() ;
wire = wire.nextSiblingElement(QStringLiteral("conductor"))) {
const auto edit = wire_edits.constFind(QUuid(wire.attribute(QStringLiteral("uuid"))));
if (edit == wire_edits.constEnd()) continue;
for (const auto &attribute : edit->attributes())
wire.setAttribute(attribute.first, attribute.second);
}
xml_diagram.setAttribute("order", order_num ++);
project_root.appendChild(xml_diagram);
continue;
}
if (keep && !diagram->isLoaded() && kept_parts) {
auto placeholder = xml_doc.createElement(QStringLiteral("diagram"));
placeholder.setAttribute(QStringLiteral("qetz-kept-part"), diagram->keptPart());
placeholder.setAttribute(QStringLiteral("uuid"), diagram->uuid().toString());
//Symbols whose information changed while the folio was
//not built: the rows QetContainer::split() writes for them
const QHash<QUuid, DiagramContext> edits = m_data_base.unbuiltEditsOnFolio(diagram->uuid());
for (auto it = edits.cbegin() ; it != edits.cend() ; ++it) {
auto symbol = xml_doc.createElement(QStringLiteral("qetz-edited-element"));
symbol.setAttribute(QStringLiteral("uuid"), it.key().toString());
auto information = xml_doc.createElement(QStringLiteral("elementInformations"));
it.value().toXml(information, QStringLiteral("elementInformation"));
symbol.appendChild(information);
placeholder.appendChild(symbol);
}
//Its wires whose properties changed: what the .qetz database
//holds of them (only that can change, QetScriptApi)
const QHash<QUuid, ConductorProperties> wire_edits = m_data_base.unbuiltWireEditsOnFolio(diagram->uuid());
for (auto it = wire_edits.cbegin() ; it != wire_edits.cend() ; ++it) {
auto wire = xml_doc.createElement(QStringLiteral("qetz-edited-conductor"));
wire.setAttribute(QStringLiteral("uuid"), it.key().toString());
for (const auto &attribute : it.value().attributes())
wire.setAttribute(attribute.first, attribute.second);
placeholder.appendChild(wire);
}
kept_parts->append(diagram->keptPart());
project_root.appendChild(placeholder);
++order_num;
continue;
}
if (keep && !diagram->isLoaded()) { if (keep && !diagram->isLoaded()) {
auto placeholder = xml_doc.createElement(QStringLiteral("diagram")); auto placeholder = xml_doc.createElement(QStringLiteral("diagram"));
placeholder.setAttribute(QStringLiteral("qet_kept_folio"), order_num); placeholder.setAttribute(QStringLiteral("qet_kept_folio"), order_num);
@@ -2409,6 +2604,7 @@ QDomDocument QETProject::toXml(QHash<int, QByteArray> *kept_folios)
// Write the elements collection. // Write the elements collection.
project_root.appendChild(m_elements_collection->root().cloneNode(true)); project_root.appendChild(m_elements_collection->root().cloneNode(true));
m_waiting_link_targets.clear();
return(xml_doc); return(xml_doc);
} }
@@ -2464,21 +2660,72 @@ bool QETProject::isContainerPath(const QString &path)
document, in parts, zipped (#1440). The file is replaced only once document, in parts, zipped (#1440). The file is replaced only once
the whole archive is written. the whole archive is written.
*/ */
bool QETProject::writeContainer(const QDomDocument &project, const QString &path, QString *error) bool QETProject::writeContainer(const QDomDocument &project, const QString &path, QString *error,
const QHash<QString, QByteArray> &kept_from)
{ {
const QList<QetZip::Entry> entries = QetContainer::split( QList<QetZip::Entry> entries = QetContainer::split(
project, project,
QStringLiteral("QElectroTech %1").arg(QetVersion::currentVersion().toString()), QStringLiteral("QElectroTech %1").arg(QetVersion::currentVersion().toString()),
error); error, kept_from);
return !entries.isEmpty() && QetZip::write(path, entries, error); return !entries.isEmpty() && QetZip::write(path, entries, error);
} }
/**
@brief QETProject::writeContainerFile
Write this project to @p path as a .qetz
*/
bool QETProject::writeContainerFile(const QString &path, QString *error)
{
//The folios not built, written as they were read, when that is
//what building them would write (toXml()); every other folio built
QStringList kept;
const QDomDocument document = toXml(nullptr, &kept);
return writeContainer(document, path, error,
kept.isEmpty() ? QHash<QString, QByteArray>() : m_container_files);
}
/**
@brief QETProject::besideContainerPath
@return the .qetz a project read from a .qet is saved to: the same
name, beside it
*/
QString QETProject::besideContainerPath() const
{
QString path = m_file_path;
if (path.endsWith(QStringLiteral(".qet"), Qt::CaseInsensitive)) {
path.chop(4);
}
return path + QStringLiteral(".qetz");
}
/**
@brief QETProject::writeCopyForOlderVersions
Write this project as a .qet at @p path, which a QElectroTech that
reads no .qetz opens. The project keeps its own file and state.
*/
QETResult QETProject::writeCopyForOlderVersions(const QString &path)
{
QHash<int, QByteArray> kept_folios;
QDomDocument xml_project(toXml(&kept_folios));
QString error_message;
if (!QET::writeXmlText(spliceKeptFolios(xml_project.toString(4), kept_folios),
path, &error_message))
return(error_message);
return(QETResult());
}
QETResult QETProject::write() QETResult QETProject::write()
{ {
// this operation requires a filepath // this operation requires a filepath
if (m_file_path.isEmpty()) if (m_file_path.isEmpty())
return(QString("unable to save project to file: no filepath was specified")); return(QString("unable to save project to file: no filepath was specified"));
//A project read from a .qet is saved as a .qetz beside it; the
//.qet stays as it was (stage D1)
if (m_opened_from_qet) {
setFilePath(besideContainerPath());
}
// If the project was opened read-only, only refuse when the target // If the project was opened read-only, only refuse when the target
// really can't be written: an existing file that is not writable, or a // really can't be written: an existing file that is not writable, or a
// new file (e.g. "Save As" to another location) whose directory is not // new file (e.g. "Save As" to another location) whose directory is not
@@ -2496,13 +2743,15 @@ QETResult QETProject::write()
QString error_message; QString error_message;
if (isContainerPath(m_file_path)) if (isContainerPath(m_file_path))
{ {
// A container must contain complete folios, not lazy placeholders. //A container is written from the whole document: every folio
QDomDocument xml_project(toXml()); //built (stage D4 writes it from the database)
if (!writeContainer(xml_project, m_file_path, &error_message)) if (!writeContainerFile(m_file_path, &error_message))
return(error_message); return(error_message);
} }
else else
{ {
//Folios not built (lazy folios) written as read when that
//is what building them would write (toXml())
QHash<int, QByteArray> kept_folios; QHash<int, QByteArray> kept_folios;
QDomDocument xml_project(toXml(&kept_folios)); QDomDocument xml_project(toXml(&kept_folios));
if (!QET::writeXmlText(spliceKeptFolios(xml_project.toString(4), kept_folios), if (!QET::writeXmlText(spliceKeptFolios(xml_project.toString(4), kept_folios),
@@ -2743,24 +2992,77 @@ bool QETProject::usesElement(const ElementsLocation &location) const
return(unbuiltFolioElements().contains(location)); return(unbuiltFolioElements().contains(location));
} }
/**
@brief QETProject::folioXml
@return the built folio @p folio written as saving writes it: its
symbols keep the links to partners on folios not built yet
(lazy folios), which Diagram::toXml() alone leaves out
*/
QDomDocument QETProject::folioXml(Diagram *folio)
{
m_waiting_link_targets = unloadedFolioCount() ? symbolsOnUnbuiltFolios() : QSet<QUuid>();
const QDomDocument xml = folio->toXml();
m_waiting_link_targets.clear();
return xml;
}
/**
@brief QETProject::buildFoliosHoldingTables
Build the folios not built yet (lazy folios) that hold a drawn
table: what a list of the project's tables needs, and no more
*/
void QETProject::buildFoliosHoldingTables()
{
QList<Diagram *> to_build;
for (Diagram *d : std::as_const(m_diagrams_list))
if (!d->isLoaded() && unbuiltFolioHasTables(d))
to_build << d;
if (to_build.isEmpty()) {
return;
}
if (qEnvironmentVariableIntValue("QET_LAZY_FOLIOS_TRACE") == 1) {
qWarning().noquote() << "lazy folios:" << to_build.size() << "folios built for their tables";
}
buildFolios(to_build);
}
/**
@brief QETProject::unbuiltFolioHasTables
@return true if @p folio, not built yet (lazy folios), has a drawn
table, read from its XML
*/
bool QETProject::unbuiltFolioHasTables(Diagram *folio) const
{
return !folio->deferredXml().elementsByTagName(QetGraphicsTableItem::xmlTagName()).isEmpty();
}
/** /**
@brief QETProject::unbuiltFolioElements @brief QETProject::unbuiltFolioElements
@return the locations of the symbols placed on the folios not built yet @return the locations of the symbols placed on the folios not built yet
(QET_LAZY_FOLIOS), each once, read from the XML kept for them as (lazy folios), each once, read from the project database once it
building them reads it (Diagram::fromXml()) holds those folios, else from the XML kept for them as building them
reads it (Diagram::fromXml())
*/ */
QList<ElementsLocation> QETProject::unbuiltFolioElements() const QList<ElementsLocation> QETProject::unbuiltFolioElements() const
{ {
QSet<QString> types; QSet<QString> types;
for (Diagram *diagram : m_diagrams_list) { if (m_data_base.rowsFromDocument()) {
if (diagram->isLoaded()) { QList<QUuid> unbuilt;
continue; for (Diagram *diagram : m_diagrams_list)
} if (!diagram->isLoaded()) unbuilt << diagram->uuid();
for (const QDomElement &element_xml : for (const projectDataBase::FolioSymbol &symbol : m_data_base.symbolsOnFolios(unbuilt))
QET::findInDomElement(diagram->deferredXml(), types.insert(symbol.location);
QStringLiteral("elements"), } else {
QStringLiteral("element"))) { for (Diagram *diagram : m_diagrams_list) {
types.insert(element_xml.attribute(QStringLiteral("type"))); if (diagram->isLoaded()) {
continue;
}
for (const QDomElement &element_xml :
QET::findInDomElement(diagram->deferredXml(),
QStringLiteral("elements"),
QStringLiteral("element"))) {
types.insert(element_xml.attribute(QStringLiteral("type")));
}
} }
} }
QList<ElementsLocation> locations; QList<ElementsLocation> locations;
@@ -3015,9 +3317,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);
//Folios are left to be built when needed (lazyFolios()) 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 = lazyFolios();
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 +3372,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()
@@ -3065,9 +3379,16 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
for (Diagram *diagram : std::as_const(m_diagrams_list)) for (Diagram *diagram : std::as_const(m_diagrams_list))
if (!diagram->isLoaded() && diagram->keptText().isEmpty()) if (!diagram->isLoaded() && diagram->keptText().isEmpty())
m_kept_folios_trusted = false; m_kept_folios_trusted = false;
m_kept_parts_trusted = !m_folio_parts.isEmpty() && unloadedFolioCount() > 0
&& m_project_qet_version == QetVersion::currentVersion()
&& m_data_base.rowsFromDocument()
&& QetContainerDb::currentFormat(m_container_files.value(QStringLiteral("project.sqlite")));
for (Diagram *diagram : std::as_const(m_diagrams_list))
if (!diagram->isLoaded() && diagram->keptPart().isEmpty())
m_kept_parts_trusted = false;
m_kept_folios_stale = false; m_kept_folios_stale = false;
m_edited_since_open = false; m_edited_since_open = false;
if (m_kept_folios_trusted) if (m_kept_folios_trusted || m_kept_parts_trusted)
for (Diagram *diagram : std::as_const(m_diagrams_list)) for (Diagram *diagram : std::as_const(m_diagrams_list))
diagram->rememberHeader(); diagram->rememberHeader();
if (qEnvironmentVariableIntValue("QET_LAZY_FOLIOS_TRACE") == 1) if (qEnvironmentVariableIntValue("QET_LAZY_FOLIOS_TRACE") == 1)
@@ -3090,6 +3411,15 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
m_state = Ok; m_state = Ok;
} }
/**
@brief QETProject::lazyFolios
@see the declaration
*/
bool QETProject::lazyFolios()
{
return qEnvironmentVariableIntValue("QET_EAGER_FOLIOS") != 1;
}
/** /**
@brief QETProject::readDiagramsXml @brief QETProject::readDiagramsXml
Load the diagrams from the xml description of the project. Load the diagrams from the xml description of the project.
@@ -3134,7 +3464,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,11 +3511,13 @@ 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())
diagram->setKeptText(folio_text(diagram_xml_element)); diagram->setKeptText(folio_text(diagram_xml_element));
if (!diagram->isLoaded() && m_folio_parts.size() == diagram_nodes.length())
diagram->setKeptPart(m_folio_parts.at(i));
m_data_base.placeFolio(diagram); m_data_base.placeFolio(diagram);
if(dlgWaiting) if(dlgWaiting)
dlgWaiting->setDetail(diagram->title()); dlgWaiting->setDetail(diagram->title());
+48 -4
View File
@@ -32,6 +32,7 @@
#include "wiringrules.h" #include "wiringrules.h"
#include "diagram.h" #include "diagram.h"
#include "autoNum/assignvariables.h" #include "autoNum/assignvariables.h"
#include "container/qetcontainerdb.h"
#ifdef BUILD_WITHOUT_KF #ifdef BUILD_WITHOUT_KF
# include "ui/nokde/kautosavefile.h" # include "ui/nokde/kautosavefile.h"
#else #else
@@ -45,6 +46,7 @@
#include <QFuture> #include <QFuture>
#include <array> #include <array>
#include <memory>
#include <optional> #include <optional>
class Diagram; class Diagram;
@@ -119,10 +121,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 //(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;
@@ -130,9 +133,15 @@ class QETProject : public QObject
QList<Diagram *> cableLabelFolios(); QList<Diagram *> cableLabelFolios();
QList<Diagram *> foliosWithWiresNotReconnected(); QList<Diagram *> foliosWithWiresNotReconnected();
void buildFolioToShow(Diagram *folio); void buildFolioToShow(Diagram *folio);
void buildPartnerSymbols(const QList<Diagram *> &folios, const QSet<QUuid> &partners);
void buildFoliosHolding(ElementData::Types kinds, bool free_only); void buildFoliosHolding(ElementData::Types kinds, bool free_only);
void buildFoliosHoldingSymbols(const QSet<QUuid> &symbols); void buildFoliosHoldingSymbols(const QSet<QUuid> &symbols);
void buildFoliosHoldingTables();
QSet<QUuid> symbolsOnUnbuiltFolios() const; QSet<QUuid> symbolsOnUnbuiltFolios() const;
/// While saving: the symbols on folios not built a built symbol
/// may be saved linked to (Element::toXml())
QSet<QUuid> waitingLinkTargets() const {return m_waiting_link_targets;}
QDomDocument folioXml(Diagram *folio);
QSet<Diagram *> tableChainFolios(Diagram *folio) const; QSet<Diagram *> tableChainFolios(Diagram *folio) const;
void loadFolios() const; void loadFolios() const;
void buildFolios(const QList<Diagram *> &folios) const; void buildFolios(const QList<Diagram *> &folios) const;
@@ -154,6 +163,12 @@ class QETProject : public QObject
/// process can destroy the project before the write finishes (crash). /// process can destroy the project before the write finishes (crash).
static void setBackupEnabled(bool enabled); static void setBackupEnabled(bool enabled);
/// Whether a folio is built only when it is needed, not all on
/// opening (stage D6): yes, unless the
/// environment variable QET_EAGER_FOLIOS=1 asks for every folio
/// built on opening, as QElectroTech did before
static bool lazyFolios();
/// Number of crash-recovery snapshots kept per project, written in /// Number of crash-recovery snapshots kept per project, written in
/// turn by writeBackup(), so one bad write cannot replace the only copy /// turn by writeBackup(), so one bad write cannot replace the only copy
static constexpr int BackupGenerations = 3; static constexpr int BackupGenerations = 3;
@@ -259,7 +274,7 @@ class QETProject : public QObject
static QString normalizedAutoNumName(const QString &name); static QString normalizedAutoNumName(const QString &name);
QVector<Element *> elementsUsingElementAutoNum(const QString &title) const; QVector<Element *> elementsUsingElementAutoNum(const QString &title) const;
/// A placed symbol's numbering, whether its folio is built or /// A placed symbol's numbering, whether its folio is built or
/// not (QET_LAZY_FOLIOS): what is asked of every symbol of the /// not (lazy folios): what is asked of every symbol of the
/// project when one is numbered /// project when one is numbered
struct SymbolNumbering { struct SymbolNumbering {
const Element *element = nullptr; ///< null on a folio not built const Element *element = nullptr; ///< null on a folio not built
@@ -313,11 +328,14 @@ class QETProject : public QObject
void autoFolioNumberingNewFolios (); void autoFolioNumberingNewFolios ();
void autoFolioNumberingSelectedFolios(int, int, const QString&); void autoFolioNumberingSelectedFolios(int, int, const QString&);
QDomDocument toXml(QHash<int, QByteArray> *kept_folios = nullptr); QDomDocument toXml(QHash<int, QByteArray> *kept_folios = nullptr,
QStringList *kept_parts = nullptr);
static QString spliceKeptFolios(QString text, const QHash<int, QByteArray> &kept_folios); static QString spliceKeptFolios(QString text, const QHash<int, QByteArray> &kept_folios);
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);
@@ -325,6 +343,7 @@ class QETProject : public QObject
bool usesElement(const ElementsLocation &) const; bool usesElement(const ElementsLocation &) const;
QList <ElementsLocation> unusedElements() const; QList <ElementsLocation> unusedElements() const;
QList<ElementsLocation> unbuiltFolioElements() const; QList<ElementsLocation> unbuiltFolioElements() const;
bool unbuiltFolioHasTables(Diagram *folio) const;
bool usesTitleBlockTemplate(const TitleBlockTemplateLocation &); bool usesTitleBlockTemplate(const TitleBlockTemplateLocation &);
bool projectWasModified(); bool projectWasModified();
bool projectOptionsWereModified(); bool projectOptionsWereModified();
@@ -436,8 +455,16 @@ class QETProject : public QObject
ProjectState openFile(QFile *file); ProjectState openFile(QFile *file);
public: public:
static bool isContainerPath(const QString &path); static bool isContainerPath(const QString &path);
/// Saving writes a .qetz: one read from a .qet is saved as
/// a .qetz beside it, the .qet left as it is
/// (stage D1)
bool savesBesideOpenedFile() const {return m_opened_from_qet;}
QString besideContainerPath() const;
QETResult writeCopyForOlderVersions(const QString &path);
static bool writeContainer(const QDomDocument &project, const QString &path, static bool writeContainer(const QDomDocument &project, const QString &path,
QString *error = nullptr); QString *error = nullptr,
const QHash<QString, QByteArray> &kept_from = QHash<QString, QByteArray>());
bool writeContainerFile(const QString &path, QString *error = nullptr);
private: private:
static QUuid derivedUuid(const QByteArray &content); static QUuid derivedUuid(const QByteArray &content);
void refresh(); void refresh();
@@ -448,6 +475,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 +492,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,11 +543,24 @@ 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
/// a folio's saved items depend on has changed since /// a folio's saved items depend on has changed since
bool m_kept_folios_trusted = false; bool m_kept_folios_trusted = false;
/// Read from a .qet whose path nobody chose since: saved beside it
bool m_opened_from_qet = false;
/// Saving a .qetz may copy the part of a folio not built
/// (toXml(), stage D4)
bool m_kept_parts_trusted = false;
QSet<QUuid> m_waiting_link_targets;
/// The entries of the .qetz opened, and the part each folio
/// was read from, in order
QHash<QString, QByteArray> m_container_files;
QStringList m_folio_parts;
bool m_kept_folios_stale = false; bool m_kept_folios_stale = false;
/// Something was edited since the project was opened: a table /// Something was edited since the project was opened: a table
/// filled from the database may have changed /// filled from the database may have changed
+3 -3
View File
@@ -919,11 +919,11 @@ QJsonObject LiveServer::snapshot(const QString &raw_path)
if (raw_path.trimmed().isEmpty() || info.isRelative()) if (raw_path.trimmed().isEmpty() || info.isRelative())
return failure(QStringLiteral("path must be absolute")); return failure(QStringLiteral("path must be absolute"));
if (info.exists()) return failure(QStringLiteral("%1 already exists").arg(raw_path)); if (info.exists()) return failure(QStringLiteral("%1 already exists").arg(raw_path));
QDomDocument xml(project->toXml());
QString error; QString error;
if (QETProject::isContainerPath(info.absoluteFilePath()) if (QETProject::isContainerPath(info.absoluteFilePath())
? !QETProject::writeContainer(xml, info.absoluteFilePath(), &error) ? !project->writeContainerFile(info.absoluteFilePath(), &error)
: !QET::writeXmlFile(xml, info.absoluteFilePath(), &error)) : ![&]() { QDomDocument xml(project->toXml());
return QET::writeXmlFile(xml, info.absoluteFilePath(), &error); }())
return failure(QStringLiteral("snapshot not written: %1").arg(error)); return failure(QStringLiteral("snapshot not written: %1").arg(error));
return {{QStringLiteral("ok"), true}, {QStringLiteral("path"), info.absoluteFilePath()}, return {{QStringLiteral("ok"), true}, {QStringLiteral("path"), info.absoluteFilePath()},
{QStringLiteral("folio"), e->currentDiagramView() {QStringLiteral("folio"), e->currentDiagramView()
+341 -78
View File
@@ -126,7 +126,7 @@ QString QetScriptApi::filePath() const
int QetScriptApi::folioCount() const int QetScriptApi::folioCount() const
{ {
return m_project ? m_project->diagrams().count() : 0; return m_project ? m_project->folios().count() : 0;
} }
/** /**
@@ -183,7 +183,7 @@ QStringList QetScriptApi::signatures()
int QetScriptApi::currentFolio() const int QetScriptApi::currentFolio() const
{ {
if (!m_project) return -1; if (!m_project) return -1;
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (m_view && m_view->diagram()) { if (m_view && m_view->diagram()) {
return diagrams.indexOf(m_view->diagram()); return diagrams.indexOf(m_view->diagram());
} }
@@ -193,7 +193,7 @@ int QetScriptApi::currentFolio() const
QString QetScriptApi::folioTitle(int index) const QString QetScriptApi::folioTitle(int index) const
{ {
if (!m_project) return QString(); if (!m_project) return QString();
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (index < 0 || index >= diagrams.count()) return QString(); if (index < 0 || index >= diagrams.count()) return QString();
return diagrams.at(index)->title(); return diagrams.at(index)->title();
} }
@@ -207,7 +207,7 @@ QString QetScriptApi::folioTitle(int index) const
QString QetScriptApi::folioUuid(int index) const QString QetScriptApi::folioUuid(int index) const
{ {
if (!m_project) return QString(); if (!m_project) return QString();
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (index < 0 || index >= diagrams.count()) return QString(); if (index < 0 || index >= diagrams.count()) return QString();
return diagrams.at(index)->uuid().toString(); return diagrams.at(index)->uuid().toString();
} }
@@ -222,7 +222,7 @@ int QetScriptApi::folioIndex(const QString &uuid) const
if (!m_project) return -1; if (!m_project) return -1;
const QUuid wanted(uuid); const QUuid wanted(uuid);
if (wanted.isNull()) return -1; if (wanted.isNull()) return -1;
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
for (int i = 0 ; i < diagrams.count() ; ++i) { for (int i = 0 ; i < diagrams.count() ; ++i) {
if (diagrams.at(i)->uuid() == wanted) return i; if (diagrams.at(i)->uuid() == wanted) return i;
} }
@@ -232,18 +232,18 @@ int QetScriptApi::folioIndex(const QString &uuid) const
int QetScriptApi::elementCount(int folioIndex) const int QetScriptApi::elementCount(int folioIndex) const
{ {
if (!m_project) return 0; if (!m_project) return 0;
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return 0; if (folioIndex < 0 || folioIndex >= diagrams.count()) return 0;
DiagramContent content(diagrams.at(folioIndex), false); DiagramContent content(built(diagrams.at(folioIndex)), false);
return content.m_elements.count(); return content.m_elements.count();
} }
int QetScriptApi::conductorCount(int folioIndex) const int QetScriptApi::conductorCount(int folioIndex) const
{ {
if (!m_project) return 0; if (!m_project) return 0;
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return 0; if (folioIndex < 0 || folioIndex >= diagrams.count()) return 0;
DiagramContent content(diagrams.at(folioIndex), false); DiagramContent content(built(diagrams.at(folioIndex)), false);
return content.conductors(DiagramContent::AnyConductor).count(); return content.conductors(DiagramContent::AnyConductor).count();
} }
@@ -434,11 +434,11 @@ bool QetScriptApi::save(const QString &output)
return true; return true;
} }
QDomDocument xml_doc(m_project->toXml());
QString error_message; QString error_message;
if (QETProject::isContainerPath(output) if (QETProject::isContainerPath(output)
? !QETProject::writeContainer(xml_doc, output, &error_message) ? !m_project->writeContainerFile(output, &error_message)
: !QET::writeXmlFile(xml_doc, output, &error_message)) { : ![&]() { QDomDocument xml_doc(m_project->toXml());
return QET::writeXmlFile(xml_doc, output, &error_message); }()) {
log(QStringLiteral("qet.save: %1").arg(error_message)); log(QStringLiteral("qet.save: %1").arg(error_message));
return false; return false;
} }
@@ -462,6 +462,41 @@ void QetScriptApi::setLive(QStringList *log)
m_live_log = log; m_live_log = log;
} }
/**
@brief QetScriptApi::unbuiltInformation
@return the information of the symbol @p elementUuid on the folio
@p folioIndex when that folio is not built (lazy folios) and the
symbol's information can be read and changed through the project
database alone (projectDataBase::canEditUnbuilt()); nothing otherwise,
the folio to be built
*/
std::optional<DiagramContext> QetScriptApi::unbuiltInformation(int folioIndex, const QString &elementUuid) const
{
if (!m_project || folioIndex < 0 || folioIndex >= m_project->folios().count()) return std::nullopt;
Diagram *folio = m_project->folios().at(folioIndex);
const QUuid uuid(elementUuid);
projectDataBase *database = m_project->dataBase();
if (folio->isLoaded() || !database->rowsFromDocument()
|| !database->folioUuidsOfElements({uuid}).contains(folio->uuid())
|| !database->canEditUnbuilt(uuid)) {
return std::nullopt;
}
return database->elementInformation(uuid);
}
/**
@brief QetScriptApi::built
@return @p folio, built if it was not yet (lazy folios), as showing
it builds it: what a call reading or changing its items needs. A
folio's own data (title block, border, wire defaults) needs no build.
*/
Diagram *QetScriptApi::built(Diagram *folio) const
{
if (folio && m_project)
m_project->buildFolioToShow(folio);
return folio;
}
/** /**
@brief QetScriptApi::findElement @brief QetScriptApi::findElement
@param folioIndex @param folioIndex
@@ -472,9 +507,9 @@ void QetScriptApi::setLive(QStringList *log)
Element *QetScriptApi::findElement(int folioIndex, const QString &elementUuid) const Element *QetScriptApi::findElement(int folioIndex, const QString &elementUuid) const
{ {
if (!m_project) return nullptr; if (!m_project) return nullptr;
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return nullptr; if (folioIndex < 0 || folioIndex >= diagrams.count()) return nullptr;
DiagramContent content(diagrams.at(folioIndex), false); DiagramContent content(built(diagrams.at(folioIndex)), false);
for (Element *elmt : std::as_const(content.m_elements)) { for (Element *elmt : std::as_const(content.m_elements)) {
if (elmt->uuid().toString() == elementUuid) if (elmt->uuid().toString() == elementUuid)
return elmt; return elmt;
@@ -653,6 +688,19 @@ bool QetScriptApi::setInfoKey(int folioIndex, const QString &elementUuid,
log(QStringLiteral("qet.%1: \"%2\" is internal, follow a numbering with numberElement()") log(QStringLiteral("qet.%1: \"%2\" is internal, follow a numbering with numberElement()")
.arg(caller, key)); .arg(caller, key));
return false; return false;
}
//A symbol on a folio not built whose information nothing else
//shows is changed through the project database, its folio left
//unbuilt (stage D5)
if (key != QETInformation::ELMT_FORMULA) {
if (const auto stored = unbuiltInformation(folioIndex, elementUuid)) {
if (stored->value(key).toString() == value) return true; // nothing to push
DiagramContext new_info = *stored;
new_info.addValue(key, value);
m_project->undoStack()->push(new ChangeStoredElementInformationCommand(
m_project, QUuid(elementUuid), *stored, new_info));
return true;
}
} }
Element *element = findElement(folioIndex, elementUuid); Element *element = findElement(folioIndex, elementUuid);
if (!element) return false; if (!element) return false;
@@ -705,9 +753,9 @@ QString QetScriptApi::addElement(int folioIndex, const QString &locationPath, do
log(QStringLiteral("qet.addElement: project is read-only")); log(QStringLiteral("qet.addElement: project is read-only"));
return QString(); return QString();
} }
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return QString(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return QString();
Diagram *diagram = diagrams.at(folioIndex); Diagram *diagram = built(diagrams.at(folioIndex));
// ElementsLocation::setPath() forces ANY path to embed:// as soon as a // ElementsLocation::setPath() forces ANY path to embed:// as soon as a
// non-null project is given, common://custom:// included -- passing // non-null project is given, common://custom:// included -- passing
@@ -817,7 +865,7 @@ bool QetScriptApi::deleteElement(int folioIndex, const QString &elementUuid)
} }
Element *element = findElement(folioIndex, elementUuid); Element *element = findElement(folioIndex, elementUuid);
if (!element) return false; if (!element) return false;
Diagram *diagram = m_project->diagrams().at(folioIndex); Diagram *diagram = built(m_project->folios().at(folioIndex));
DiagramContent content; DiagramContent content;
content.m_elements << element; content.m_elements << element;
@@ -906,9 +954,9 @@ QStringList QetScriptApi::elementUuids(int folioIndex) const
{ {
QStringList uuids; QStringList uuids;
if (!m_project) return uuids; if (!m_project) return uuids;
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return uuids; if (folioIndex < 0 || folioIndex >= diagrams.count()) return uuids;
DiagramContent content(diagrams.at(folioIndex), false); DiagramContent content(built(diagrams.at(folioIndex)), false);
for (Element *elmt : std::as_const(content.m_elements)) { for (Element *elmt : std::as_const(content.m_elements)) {
uuids << elmt->uuid().toString(); uuids << elmt->uuid().toString();
} }
@@ -996,6 +1044,8 @@ int QetScriptApi::terminalIndex(int folioIndex, const QString &elementUuid,
QString QetScriptApi::elementInfo(int folioIndex, const QString &elementUuid, const QString &key) const QString QetScriptApi::elementInfo(int folioIndex, const QString &elementUuid, const QString &key) const
{ {
if (const auto stored = unbuiltInformation(folioIndex, elementUuid))
return stored->value(key).toString();
Element *element = findElement(folioIndex, elementUuid); Element *element = findElement(folioIndex, elementUuid);
if (!element) return QString(); if (!element) return QString();
return element->elementInformations().value(key).toString(); return element->elementInformations().value(key).toString();
@@ -1077,7 +1127,7 @@ bool QetScriptApi::addConductor(int folioIndex,
return false; return false;
} }
Diagram *diagram = m_project->diagrams().at(folioIndex); Diagram *diagram = built(m_project->folios().at(folioIndex));
ConductorCreator creator(diagram, terminals); ConductorCreator creator(diagram, terminals);
Q_UNUSED(creator) Q_UNUSED(creator)
@@ -1110,10 +1160,10 @@ QStringList QetScriptApi::conductors(int folioIndex) const
{ {
QStringList list; QStringList list;
if (!m_project) return list; if (!m_project) return list;
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return list; if (folioIndex < 0 || folioIndex >= diagrams.count()) return list;
DiagramContent content(diagrams.at(folioIndex), false); DiagramContent content(built(diagrams.at(folioIndex)), false);
const QList<Conductor *> all = content.conductors(DiagramContent::AnyConductor); const QList<Conductor *> all = content.conductors(DiagramContent::AnyConductor);
for (Conductor *c : all) for (Conductor *c : all)
{ {
@@ -1132,10 +1182,10 @@ QStringList QetScriptApi::conductorUuids(int folioIndex) const
{ {
QStringList list; QStringList list;
if (!m_project) return list; if (!m_project) return list;
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return list; if (folioIndex < 0 || folioIndex >= diagrams.count()) return list;
DiagramContent content(diagrams.at(folioIndex), false); DiagramContent content(built(diagrams.at(folioIndex)), false);
for (Conductor *c : content.conductors(DiagramContent::AnyConductor)) for (Conductor *c : content.conductors(DiagramContent::AnyConductor))
list << c->uuid().toString(); list << c->uuid().toString();
return list; return list;
@@ -1152,12 +1202,12 @@ QStringList QetScriptApi::conductorUuids(int folioIndex) const
QStringList QetScriptApi::conductorEnds(int folioIndex, const QString &uuid) const QStringList QetScriptApi::conductorEnds(int folioIndex, const QString &uuid) const
{ {
if (!m_project) return {}; if (!m_project) return {};
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return {}; if (folioIndex < 0 || folioIndex >= diagrams.count()) return {};
const QUuid wanted(uuid); const QUuid wanted(uuid);
if (wanted.isNull()) return {}; if (wanted.isNull()) return {};
DiagramContent content(diagrams.at(folioIndex), false); DiagramContent content(built(diagrams.at(folioIndex)), false);
for (Conductor *c : content.conductors(DiagramContent::AnyConductor)) for (Conductor *c : content.conductors(DiagramContent::AnyConductor))
if (c->uuid() == wanted) if (c->uuid() == wanted)
return {describeEnd(c->terminal1), describeEnd(c->terminal2)}; return {describeEnd(c->terminal1), describeEnd(c->terminal2)};
@@ -1189,6 +1239,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,
@@ -1215,7 +1269,21 @@ bool QetScriptApi::setConductorProperty(int folioIndex, const QString &elementUu
.arg(caller, value, property)); .arg(caller, value, property));
return false; return false;
} }
if (properties == conductor->properties()) return true; // already so return applyConductorProperties(conductor, properties);
}
/**
@brief QetScriptApi::applyConductorProperties
Give @p conductor and every wire at its potential the properties
@p properties, one undo step
*/
bool QetScriptApi::applyConductorProperties(Conductor *conductor, const ConductorProperties &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();
if (properties == before) return true; // already so
QSet<Conductor *> potential = conductor->relatedPotentialConductors(true); QSet<Conductor *> potential = conductor->relatedPotentialConductors(true);
potential << conductor; potential << conductor;
@@ -1227,11 +1295,198 @@ 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;
} }
namespace {
//The wire properties a .qetz's database holds in its own columns, which
//nothing on a folio computes from but a wire's number formula
const QStringList &storedConductorPropertyNames()
{
static const QStringList names {
QStringLiteral("num"), QStringLiteral("function"), QStringLiteral("bus"),
QStringLiteral("cable"), QStringLiteral("tension_protocol"),
QStringLiteral("conductor_color"), QStringLiteral("conductor_section")};
return names;
}
//true if @p a and @p b differ only in storedConductorPropertyNames()
bool differOnlyInStored(const ConductorProperties &a, const ConductorProperties &b)
{
QHash<QString, QString> left, right;
for (const auto &attribute : a.attributes()) left.insert(attribute.first, attribute.second);
for (const auto &attribute : b.attributes()) right.insert(attribute.first, attribute.second);
QSet<QString> names(left.keyBegin(), left.keyEnd());
for (const QString &name : right.keys()) names.insert(name);
for (const QString &name : std::as_const(names))
if (left.value(name) != right.value(name) && !storedConductorPropertyNames().contains(name))
return false;
return true;
}
//Wires given their properties by uuid, built or not (lazy folios):
//projectDataBase::setPlacedConductorProperties()
class ChangeStoredConductorPropertiesCommand : public QUndoCommand
{
public:
ChangeStoredConductorPropertiesCommand(QETProject *project,
const QHash<QUuid, std::pair<ConductorProperties, ConductorProperties>> &changes) :
m_project(project), m_changes(changes)
{ setText(QObject::tr("Edit the conductor properties")); }
void undo() override { apply(false); }
void redo() override { apply(true); }
private:
void apply(bool forward) {
if (!m_project) return;
for (auto it = m_changes.cbegin() ; it != m_changes.cend() ; ++it)
m_project->dataBase()->setPlacedConductorProperties(
it.key(), forward ? it->second : it->first);
}
QPointer<QETProject> m_project;
QHash<QUuid, std::pair<ConductorProperties, ConductorProperties>> m_changes;
};
}
/**
@brief QetScriptApi::findConductorByUuid
@return the wire @p conductorUuid of the folio @p folioIndex, the folio
built, or nullptr (said in the log)
*/
Conductor *QetScriptApi::findConductorByUuid(int folioIndex, const QString &conductorUuid,
const QString &caller)
{
if (!m_project || folioIndex < 0 || folioIndex >= m_project->folios().count()) {
log(QStringLiteral("qet.%1: no folio at index %2").arg(caller).arg(folioIndex));
return nullptr;
}
const QUuid wanted(conductorUuid);
for (Conductor *c : built(m_project->folios().at(folioIndex))->conductors())
if (c->uuid() == wanted) return c;
log(QStringLiteral("qet.%1: no conductor %2 on folio %3").arg(caller, conductorUuid).arg(folioIndex));
return nullptr;
}
/**
@brief QetScriptApi::unbuiltConductorChange
Change the property @p property of the wire @p wire, on the folio
@p folioIndex not built (lazy folios), and of every wire at its potential,
through the project database, as applyConductorProperties() would: when
they are all on folios not built, none is numbered by a formula, and what
changes in each is only what the .qetz database holds
(storedConductorPropertyNames()) -- nothing then is computed from it.
@return true if done; false for the folio to be built, @p refused set when
the value itself is not one
*/
bool QetScriptApi::unbuiltConductorChange(int folioIndex, const QUuid &wire, const QString &property,
const QString &value, bool *refused)
{
*refused = false;
projectDataBase *database = m_project->dataBase();
Diagram *folio = m_project->folios().at(folioIndex);
QUuid on;
if (folio->isLoaded() || !database->rowsFromDocument() || !storedConductorPropertyNames().contains(property)
|| !database->unbuiltWire(wire, &on) || on != folio->uuid()) {
return false;
}
ConductorProperties properties = database->conductorProperties(wire);
if (!setConductorPropertyValue(properties, property, value)) {
*refused = true;
return false;
}
if (!properties.m_formula.isEmpty()) {
return false;
}
const auto potential = database->potentialWires(wire);
if (!potential) {
return false;
}
QHash<QUuid, std::pair<ConductorProperties, ConductorProperties>> changes;
for (const QUuid &other : *potential) {
if (!database->unbuiltWire(other)) return false;
const ConductorProperties old = database->conductorProperties(other);
if (!differOnlyInStored(old, properties)) return false;
if (!(old == properties)) changes.insert(other, {old, properties});
}
if (!changes.isEmpty())
m_project->undoStack()->push(new ChangeStoredConductorPropertiesCommand(m_project, changes));
return true;
}
QString QetScriptApi::conductorPropertyByUuid(int folioIndex, const QString &conductorUuid,
const QString &property) const
{
if (!m_project || folioIndex < 0 || folioIndex >= m_project->folios().count()) return QString();
Diagram *folio = m_project->folios().at(folioIndex);
projectDataBase *database = m_project->dataBase();
QUuid on;
if (!folio->isLoaded() && database->rowsFromDocument()
&& database->unbuiltWire(QUuid(conductorUuid), &on) && on == folio->uuid())
return conductorPropertyValue(database->conductorProperties(QUuid(conductorUuid)), property);
auto *self = const_cast<QetScriptApi *>(this);
Conductor *conductor = self->findConductorByUuid(folioIndex, conductorUuid,
QStringLiteral("conductorPropertyByUuid"));
return conductor ? conductorPropertyValue(conductor->properties(), property) : QString();
}
bool QetScriptApi::setConductorPropertyByUuid(int folioIndex, const QString &conductorUuid,
const QString &property, const QString &value)
{
if (!m_project) return false;
const QString caller = QStringLiteral("setConductorPropertyByUuid");
if (m_project->isReadOnly()) {
log(QStringLiteral("qet.%1: project is read-only").arg(caller));
return false;
}
if (!conductorPropertyNames().contains(property)) {
log(QStringLiteral("qet.%1: unknown property '%2'; expected one of %3")
.arg(caller, property, conductorPropertyNames().join(QStringLiteral(", "))));
return false;
}
if (folioIndex < 0 || folioIndex >= m_project->folios().count()) {
log(QStringLiteral("qet.%1: no folio at index %2").arg(caller).arg(folioIndex));
return false;
}
//The folio left unbuilt when the change can go through the project
//database (stage D5)
bool refused = false;
if (unbuiltConductorChange(folioIndex, QUuid(conductorUuid), property, value, &refused))
return true;
if (refused) {
log(QStringLiteral("qet.%1: '%2' is not a valid value for %3").arg(caller, value, property));
return false;
}
Conductor *conductor = findConductorByUuid(folioIndex, conductorUuid, caller);
if (!conductor) return false;
ConductorProperties properties = conductor->properties();
if (!setConductorPropertyValue(properties, property, value)) {
log(QStringLiteral("qet.%1: '%2' is not a valid value for %3")
.arg(caller, value, property));
return false;
}
return applyConductorProperties(conductor, properties);
}
/** /**
@brief QetScriptApi::conductorSegments @brief QetScriptApi::conductorSegments
List the drawn path of the conductor on a terminal, one line per List the drawn path of the conductor on a terminal, one line per
@@ -1787,18 +2042,18 @@ QList<T *> sortedByPosition(const QSet<T *> &items)
QList<IndependentTextItem *> QetScriptApi::sortedTexts(int folioIndex) const QList<IndependentTextItem *> QetScriptApi::sortedTexts(int folioIndex) const
{ {
if (!m_project) return {}; if (!m_project) return {};
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return {}; if (folioIndex < 0 || folioIndex >= diagrams.count()) return {};
DiagramContent content(diagrams.at(folioIndex), false); DiagramContent content(built(diagrams.at(folioIndex)), false);
return sortedByPosition(content.m_text_fields); return sortedByPosition(content.m_text_fields);
} }
QList<QetShapeItem *> QetScriptApi::sortedShapes(int folioIndex) const QList<QetShapeItem *> QetScriptApi::sortedShapes(int folioIndex) const
{ {
if (!m_project) return {}; if (!m_project) return {};
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return {}; if (folioIndex < 0 || folioIndex >= diagrams.count()) return {};
DiagramContent content(diagrams.at(folioIndex), false); DiagramContent content(built(diagrams.at(folioIndex)), false);
return sortedByPosition(content.m_shapes); return sortedByPosition(content.m_shapes);
} }
@@ -1843,9 +2098,9 @@ int QetScriptApi::addText(int folioIndex, const QString &text, double x, double
log(QStringLiteral("qet.addText: project is read-only")); log(QStringLiteral("qet.addText: project is read-only"));
return -1; return -1;
} }
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1; if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1;
Diagram *diagram = diagrams.at(folioIndex); Diagram *diagram = built(diagrams.at(folioIndex));
auto *item = new IndependentTextItem(); auto *item = new IndependentTextItem();
item->setPlainText(text); item->setPlainText(text);
@@ -1929,7 +2184,7 @@ bool QetScriptApi::deleteText(int folioIndex, int textIndex)
} }
IndependentTextItem *item = findText(folioIndex, textIndex, QStringLiteral("deleteText")); IndependentTextItem *item = findText(folioIndex, textIndex, QStringLiteral("deleteText"));
if (!item) return false; if (!item) return false;
Diagram *diagram = m_project->diagrams().at(folioIndex); Diagram *diagram = built(m_project->folios().at(folioIndex));
DiagramContent content; DiagramContent content;
content.m_text_fields << item; content.m_text_fields << item;
@@ -1969,7 +2224,7 @@ int QetScriptApi::addShape(int folioIndex, const QString &type,
log(QStringLiteral("qet.addShape: project is read-only")); log(QStringLiteral("qet.addShape: project is read-only"));
return -1; return -1;
} }
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1; if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1;
QetShapeItem::ShapeType shape_type; QetShapeItem::ShapeType shape_type;
@@ -1984,7 +2239,7 @@ int QetScriptApi::addShape(int folioIndex, const QString &type,
return -1; return -1;
} }
Diagram *diagram = diagrams.at(folioIndex); Diagram *diagram = built(diagrams.at(folioIndex));
auto *shape = new QetShapeItem(QPointF(x1, y1), QPointF(x2, y2), shape_type); auto *shape = new QetShapeItem(QPointF(x1, y1), QPointF(x2, y2), shape_type);
diagram->undoStack().push(new AddGraphicsObjectCommand(shape, diagram, QPointF(0, 0))); diagram->undoStack().push(new AddGraphicsObjectCommand(shape, diagram, QPointF(0, 0)));
return sortedShapes(folioIndex).indexOf(shape); return sortedShapes(folioIndex).indexOf(shape);
@@ -2116,7 +2371,7 @@ bool QetScriptApi::deleteShape(int folioIndex, int shapeIndex)
.arg(folioIndex).arg(list.count()).arg(shapeIndex)); .arg(folioIndex).arg(list.count()).arg(shapeIndex));
return false; return false;
} }
Diagram *diagram = m_project->diagrams().at(folioIndex); Diagram *diagram = built(m_project->folios().at(folioIndex));
DiagramContent content; DiagramContent content;
content.m_shapes << list.at(shapeIndex); content.m_shapes << list.at(shapeIndex);
@@ -2223,7 +2478,7 @@ int QetScriptApi::addPolygon(int folioIndex, const QVariantList &points, bool cl
log(QStringLiteral("qet.addPolygon: project is read-only")); log(QStringLiteral("qet.addPolygon: project is read-only"));
return -1; return -1;
} }
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1; if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1;
if (points.count() < 2) { if (points.count() < 2) {
log(QStringLiteral("qet.addPolygon: at least 2 points are required, got %1") log(QStringLiteral("qet.addPolygon: at least 2 points are required, got %1")
@@ -2237,7 +2492,7 @@ int QetScriptApi::addPolygon(int folioIndex, const QVariantList &points, bool cl
return -1; return -1;
} }
Diagram *diagram = diagrams.at(folioIndex); Diagram *diagram = built(diagrams.at(folioIndex));
auto *shape = new QetShapeItem(poly.first(), poly.last(), QetShapeItem::Polygon); auto *shape = new QetShapeItem(poly.first(), poly.last(), QetShapeItem::Polygon);
shape->setPolygon(poly); shape->setPolygon(poly);
shape->setClosed(closed); shape->setClosed(closed);
@@ -2321,7 +2576,7 @@ int QetScriptApi::addPath(int folioIndex, const QVariantList &nodes, bool closed
log(QStringLiteral("qet.addPath: project is read-only")); log(QStringLiteral("qet.addPath: project is read-only"));
return -1; return -1;
} }
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1; if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1;
if (nodes.count() < 2) { if (nodes.count() < 2) {
log(QStringLiteral("qet.addPath: at least 2 nodes are required, got %1").arg(nodes.count())); log(QStringLiteral("qet.addPath: at least 2 nodes are required, got %1").arg(nodes.count()));
@@ -2338,7 +2593,7 @@ int QetScriptApi::addPath(int folioIndex, const QVariantList &nodes, bool closed
path_nodes << node; path_nodes << node;
} }
Diagram *diagram = diagrams.at(folioIndex); Diagram *diagram = built(diagrams.at(folioIndex));
auto *shape = new QetShapeItem(path_nodes.first().anchor, path_nodes.last().anchor, auto *shape = new QetShapeItem(path_nodes.first().anchor, path_nodes.last().anchor,
QetShapeItem::Path); QetShapeItem::Path);
shape->setPathNodes(path_nodes); shape->setPathNodes(path_nodes);
@@ -2582,7 +2837,7 @@ bool QetScriptApi::deleteConductor(int folioIndex, const QString &elementUuid, i
} }
Conductor *conductor = findConductor(folioIndex, elementUuid, terminalIndex, caller); Conductor *conductor = findConductor(folioIndex, elementUuid, terminalIndex, caller);
if (!conductor) return false; if (!conductor) return false;
Diagram *diagram = m_project->diagrams().at(folioIndex); Diagram *diagram = built(m_project->folios().at(folioIndex));
DiagramContent content; DiagramContent content;
content.m_conductors_to_move << conductor; content.m_conductors_to_move << conductor;
@@ -2603,10 +2858,10 @@ bool QetScriptApi::removeFolio(int folioIndex)
log(QStringLiteral("qet.removeFolio: project is read-only")); log(QStringLiteral("qet.removeFolio: project is read-only"));
return false; return false;
} }
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return false; if (folioIndex < 0 || folioIndex >= diagrams.count()) return false;
m_project->undoStack()->push(new RemoveDiagramCommand(m_project, diagrams.at(folioIndex))); m_project->undoStack()->push(new RemoveDiagramCommand(m_project, diagrams.at(folioIndex)));
return m_project->diagrams().count() == diagrams.count() - 1; return m_project->folios().count() == diagrams.count() - 1;
} }
namespace { namespace {
@@ -2640,7 +2895,7 @@ const QStringList &titleBlockFieldNames()
QString QetScriptApi::folioProperty(int folioIndex, const QString &property) const QString QetScriptApi::folioProperty(int folioIndex, const QString &property) const
{ {
if (!m_project) return QString(); if (!m_project) return QString();
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return QString(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return QString();
Diagram *diagram = diagrams.at(folioIndex); Diagram *diagram = diagrams.at(folioIndex);
if (property == QLatin1String("template")) if (property == QLatin1String("template"))
@@ -2665,7 +2920,7 @@ bool QetScriptApi::setFolioProperty(int folioIndex, const QString &property, con
log(QStringLiteral("qet.setFolioProperty: project is read-only")); log(QStringLiteral("qet.setFolioProperty: project is read-only"));
return false; return false;
} }
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return false; if (folioIndex < 0 || folioIndex >= diagrams.count()) return false;
Diagram *diagram = diagrams.at(folioIndex); Diagram *diagram = diagrams.at(folioIndex);
@@ -2958,9 +3213,9 @@ bool QetScriptApi::sortTerminalStrip(int stripIndex)
QList<QetGraphicsTableItem *> QetScriptApi::sortedTables(int folioIndex) const QList<QetGraphicsTableItem *> QetScriptApi::sortedTables(int folioIndex) const
{ {
if (!m_project) return {}; if (!m_project) return {};
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return {}; if (folioIndex < 0 || folioIndex >= diagrams.count()) return {};
DiagramContent content(diagrams.at(folioIndex), false); DiagramContent content(built(diagrams.at(folioIndex)), false);
return sortedByPosition(content.m_tables); return sortedByPosition(content.m_tables);
} }
@@ -3020,7 +3275,7 @@ int QetScriptApi::addTable(int folioIndex, const QString &kind, const QString &n
log(QStringLiteral("qet.%1: project is read-only").arg(caller)); log(QStringLiteral("qet.%1: project is read-only").arg(caller));
return -1; return -1;
} }
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) { if (folioIndex < 0 || folioIndex >= diagrams.count()) {
log(QStringLiteral("qet.%1: no folio at index %2").arg(caller).arg(folioIndex)); log(QStringLiteral("qet.%1: no folio at index %2").arg(caller).arg(folioIndex));
return -1; return -1;
@@ -3089,7 +3344,7 @@ int QetScriptApi::addTable(int folioIndex, const QString &kind, const QString &n
dialog.setAddNewTableToNewDiagram(false); dialog.setAddNewTableToNewDiagram(false);
const QList<QetGraphicsTableItem *> before = sortedTables(folioIndex); const QList<QetGraphicsTableItem *> before = sortedTables(folioIndex);
QetGraphicsTableFactory::create(diagrams.at(folioIndex), &dialog); QetGraphicsTableFactory::create(built(diagrams.at(folioIndex)), &dialog);
const QList<QetGraphicsTableItem *> after = sortedTables(folioIndex); const QList<QetGraphicsTableItem *> after = sortedTables(folioIndex);
for (QetGraphicsTableItem *t : after) { for (QetGraphicsTableItem *t : after) {
if (!before.contains(t)) return after.indexOf(t); if (!before.contains(t)) return after.indexOf(t);
@@ -3105,9 +3360,9 @@ bool QetScriptApi::deleteTable(int folioIndex, int tableIndex)
log(QStringLiteral("qet.deleteTable: project is read-only")); log(QStringLiteral("qet.deleteTable: project is read-only"));
return false; return false;
} }
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return false; if (folioIndex < 0 || folioIndex >= diagrams.count()) return false;
Diagram *diagram = diagrams.at(folioIndex); Diagram *diagram = built(diagrams.at(folioIndex));
const QList<QetGraphicsTableItem *> all = sortedTables(folioIndex); const QList<QetGraphicsTableItem *> all = sortedTables(folioIndex);
if (tableIndex < 0 || tableIndex >= all.count()) { if (tableIndex < 0 || tableIndex >= all.count()) {
@@ -3381,7 +3636,7 @@ bool QetScriptApi::useConductorAutoNum(int folioIndex, const QString &name)
log(QStringLiteral("qet.useConductorAutoNum: project is read-only")); log(QStringLiteral("qet.useConductorAutoNum: project is read-only"));
return false; return false;
} }
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return false; if (folioIndex < 0 || folioIndex >= diagrams.count()) return false;
if (!name.isEmpty() && !m_project->conductorAutoNum().contains(name)) { if (!name.isEmpty() && !m_project->conductorAutoNum().contains(name)) {
log(QStringLiteral("qet.useConductorAutoNum: no conductor auto-numbering named '%1'").arg(name)); log(QStringLiteral("qet.useConductorAutoNum: no conductor auto-numbering named '%1'").arg(name));
@@ -3395,9 +3650,9 @@ bool QetScriptApi::useConductorAutoNum(int folioIndex, const QString &name)
QList<DiagramImageItem *> QetScriptApi::sortedImages(int folioIndex) const QList<DiagramImageItem *> QetScriptApi::sortedImages(int folioIndex) const
{ {
if (!m_project) return {}; if (!m_project) return {};
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return {}; if (folioIndex < 0 || folioIndex >= diagrams.count()) return {};
DiagramContent content(diagrams.at(folioIndex), false); DiagramContent content(built(diagrams.at(folioIndex)), false);
return sortedByPosition(content.m_images); return sortedByPosition(content.m_images);
} }
@@ -3491,7 +3746,7 @@ int QetScriptApi::addImage(int folioIndex, const QString &filePath, double x, do
log(QStringLiteral("qet.addImage: project is read-only")); log(QStringLiteral("qet.addImage: project is read-only"));
return -1; return -1;
} }
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1; if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1;
// Same checks as a picture dropped on a folio: a regular file of at // Same checks as a picture dropped on a folio: a regular file of at
@@ -3503,7 +3758,7 @@ int QetScriptApi::addImage(int folioIndex, const QString &filePath, double x, do
return -1; return -1;
} }
Diagram *diagram = diagrams.at(folioIndex); Diagram *diagram = built(diagrams.at(folioIndex));
auto *item = new DiagramImageItem(QPixmap::fromImage(image)); auto *item = new DiagramImageItem(QPixmap::fromImage(image));
diagram->undoStack().push(new AddGraphicsObjectCommand(item, diagram, QPointF(x, y))); diagram->undoStack().push(new AddGraphicsObjectCommand(item, diagram, QPointF(x, y)));
return sortedImages(folioIndex).indexOf(item); return sortedImages(folioIndex).indexOf(item);
@@ -3619,7 +3874,7 @@ bool QetScriptApi::deleteImage(int folioIndex, int imageIndex)
.arg(folioIndex).arg(list.count()).arg(imageIndex)); .arg(folioIndex).arg(list.count()).arg(imageIndex));
return false; return false;
} }
Diagram *diagram = m_project->diagrams().at(folioIndex); Diagram *diagram = built(m_project->folios().at(folioIndex));
DiagramContent content; DiagramContent content;
content.m_images << list.at(imageIndex); content.m_images << list.at(imageIndex);
diagram->undoStack().push(new DeleteQGraphicsItemCommand(diagram, content)); diagram->undoStack().push(new DeleteQGraphicsItemCommand(diagram, content));
@@ -3675,7 +3930,7 @@ int QetScriptApi::addPdfPage(int folioIndex, const QString &pdfPath, int pageNum
log(QStringLiteral("qet.addPdfPage: project is read-only")); log(QStringLiteral("qet.addPdfPage: project is read-only"));
return -1; return -1;
} }
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1; if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1;
if (dpi <= 0) { if (dpi <= 0) {
log(QStringLiteral("qet.addPdfPage: dpi must be positive, got %1").arg(dpi)); log(QStringLiteral("qet.addPdfPage: dpi must be positive, got %1").arg(dpi));
@@ -3724,7 +3979,7 @@ int QetScriptApi::addPdfPage(int folioIndex, const QString &pdfPath, int pageNum
painter.drawImage(0, 0, rendered); painter.drawImage(0, 0, rendered);
painter.end(); painter.end();
Diagram *diagram = diagrams.at(folioIndex); Diagram *diagram = built(diagrams.at(folioIndex));
auto *item = new DiagramImageItem(QPixmap::fromImage(background)); auto *item = new DiagramImageItem(QPixmap::fromImage(background));
diagram->undoStack().push(new AddGraphicsObjectCommand(item, diagram, QPointF(x, y))); diagram->undoStack().push(new AddGraphicsObjectCommand(item, diagram, QPointF(x, y)));
return sortedImages(folioIndex).indexOf(item); return sortedImages(folioIndex).indexOf(item);
@@ -3948,7 +4203,7 @@ bool QetScriptApi::deleteElementText(int folioIndex, const QString &elementUuid,
DynamicElementTextItem *t = findElementText(folioIndex, elementUuid, textIndex, DynamicElementTextItem *t = findElementText(folioIndex, elementUuid, textIndex,
QStringLiteral("deleteElementText")); QStringLiteral("deleteElementText"));
if (!t) return false; if (!t) return false;
Diagram *diagram = m_project->diagrams().at(folioIndex); Diagram *diagram = built(m_project->folios().at(folioIndex));
DiagramContent content; DiagramContent content;
content.m_element_texts << t; content.m_element_texts << t;
diagram->undoStack().push(new DeleteQGraphicsItemCommand(diagram, content)); diagram->undoStack().push(new DeleteQGraphicsItemCommand(diagram, content));
@@ -4178,7 +4433,7 @@ QStringList QetScriptApi::duplicateElements(int fromFolioIndex, const QStringLis
log(QStringLiteral("qet.%1: project is read-only").arg(caller)); log(QStringLiteral("qet.%1: project is read-only").arg(caller));
return created; return created;
} }
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (fromFolioIndex < 0 || fromFolioIndex >= diagrams.count() if (fromFolioIndex < 0 || fromFolioIndex >= diagrams.count()
|| toFolioIndex < 0 || toFolioIndex >= diagrams.count()) { || toFolioIndex < 0 || toFolioIndex >= diagrams.count()) {
log(QStringLiteral("qet.%1: folio index out of range").arg(caller)); log(QStringLiteral("qet.%1: folio index out of range").arg(caller));
@@ -4188,8 +4443,8 @@ QStringList QetScriptApi::duplicateElements(int fromFolioIndex, const QStringLis
log(QStringLiteral("qet.%1: no elements named").arg(caller)); log(QStringLiteral("qet.%1: no elements named").arg(caller));
return created; return created;
} }
Diagram *source = diagrams.at(fromFolioIndex); Diagram *source = built(diagrams.at(fromFolioIndex));
Diagram *target = diagrams.at(toFolioIndex); Diagram *target = built(diagrams.at(toFolioIndex));
QList<Element *> chosen; QList<Element *> chosen;
for (const QString &uuid : elementUuids) { for (const QString &uuid : elementUuids) {
@@ -4273,7 +4528,7 @@ const QStringList &folioBorderNames()
QString QetScriptApi::folioBorder(int folioIndex, const QString &property) const QString QetScriptApi::folioBorder(int folioIndex, const QString &property) const
{ {
if (!m_project) return QString(); if (!m_project) return QString();
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return QString(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return QString();
const BorderProperties b = diagrams.at(folioIndex)->border_and_titleblock.exportBorder(); const BorderProperties b = diagrams.at(folioIndex)->border_and_titleblock.exportBorder();
if (property == QLatin1String("columns")) return QString::number(b.columns_count); if (property == QLatin1String("columns")) return QString::number(b.columns_count);
@@ -4412,7 +4667,7 @@ bool QetScriptApi::setFolioBorder(int folioIndex, const QString &property, const
.arg(caller, property, folioBorderNames().join(QStringLiteral(", ")))); .arg(caller, property, folioBorderNames().join(QStringLiteral(", "))));
return false; return false;
} }
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return false; if (folioIndex < 0 || folioIndex >= diagrams.count()) return false;
Diagram *diagram = diagrams.at(folioIndex); Diagram *diagram = diagrams.at(folioIndex);
@@ -4498,7 +4753,7 @@ QString QetScriptApi::conductorDefault(int folioIndex, const QString &property)
if (folioIndex == -1) { if (folioIndex == -1) {
p = m_project->defaultConductorProperties(); p = m_project->defaultConductorProperties();
} else { } else {
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return QString(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return QString();
p = diagrams.at(folioIndex)->defaultConductorProperties; p = diagrams.at(folioIndex)->defaultConductorProperties;
} }
@@ -4524,7 +4779,7 @@ bool QetScriptApi::setConductorDefault(int folioIndex, const QString &property,
} }
Diagram *diagram = nullptr; Diagram *diagram = nullptr;
if (folioIndex != -1) { if (folioIndex != -1) {
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return false; if (folioIndex < 0 || folioIndex >= diagrams.count()) return false;
diagram = diagrams.at(folioIndex); diagram = diagrams.at(folioIndex);
} }
@@ -4633,12 +4888,12 @@ QVariantMap QetScriptApi::terminalPosition(int folioIndex, const QString &elemen
QVariantList QetScriptApi::conductorPath(int folioIndex, const QString &conductorUuid) const QVariantList QetScriptApi::conductorPath(int folioIndex, const QString &conductorUuid) const
{ {
if (!m_project) return {}; if (!m_project) return {};
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return {}; if (folioIndex < 0 || folioIndex >= diagrams.count()) return {};
const QUuid wanted(conductorUuid); const QUuid wanted(conductorUuid);
if (wanted.isNull()) return {}; if (wanted.isNull()) return {};
DiagramContent content(diagrams.at(folioIndex), false); DiagramContent content(built(diagrams.at(folioIndex)), false);
for (Conductor *c : content.conductors(DiagramContent::AnyConductor)) { for (Conductor *c : content.conductors(DiagramContent::AnyConductor)) {
if (c->uuid() != wanted) continue; if (c->uuid() != wanted) continue;
QVariantList points; QVariantList points;
@@ -4673,14 +4928,14 @@ int QetScriptApi::insertFolio(int position)
log(QStringLiteral("qet.insertFolio: project is read-only")); log(QStringLiteral("qet.insertFolio: project is read-only"));
return -1; return -1;
} }
if (position < 0 || position > m_project->diagrams().count()) { if (position < 0 || position > m_project->folios().count()) {
log(QStringLiteral("qet.insertFolio: position %1 is outside 0..%2") log(QStringLiteral("qet.insertFolio: position %1 is outside 0..%2")
.arg(position).arg(m_project->diagrams().count())); .arg(position).arg(m_project->folios().count()));
return -1; return -1;
} }
Diagram *diagram = m_project->addNewDiagram(position); Diagram *diagram = m_project->addNewDiagram(position);
if (!diagram) return -1; if (!diagram) return -1;
return m_project->diagrams().indexOf(diagram); return m_project->folios().indexOf(diagram);
} }
/** /**
@@ -4760,7 +5015,7 @@ int QetScriptApi::addFolio()
} }
Diagram *diagram = m_project->addNewDiagram(); Diagram *diagram = m_project->addNewDiagram();
if (!diagram) return -1; if (!diagram) return -1;
return m_project->diagrams().indexOf(diagram); return m_project->folios().indexOf(diagram);
} }
bool QetScriptApi::setFolioTitle(int folioIndex, const QString &title) bool QetScriptApi::setFolioTitle(int folioIndex, const QString &title)
@@ -4770,7 +5025,7 @@ bool QetScriptApi::setFolioTitle(int folioIndex, const QString &title)
log(QStringLiteral("qet.setFolioTitle: project is read-only")); log(QStringLiteral("qet.setFolioTitle: project is read-only"));
return false; return false;
} }
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return false; if (folioIndex < 0 || folioIndex >= diagrams.count()) return false;
Diagram *diagram = diagrams.at(folioIndex); Diagram *diagram = diagrams.at(folioIndex);
@@ -5202,7 +5457,15 @@ QVariantList QetScriptApi::checkContinuity(int folioIndex)
bool QetScriptApi::selectElement(const QString &elementUuid) bool QetScriptApi::selectElement(const QString &elementUuid)
{ {
if (!m_project) return false; if (!m_project) return false;
for (Diagram *diagram : m_project->diagrams()) { //The folio the project database places it on, built; every folio
//when it places it on none
QList<Diagram *> folios;
const QSet<QUuid> on = m_project->dataBase()->folioUuidsOfElements({QUuid(elementUuid)});
for (Diagram *diagram : m_project->folios())
if (on.contains(diagram->uuid())) folios << built(diagram);
if (folios.isEmpty())
folios = m_project->diagrams();
for (Diagram *diagram : std::as_const(folios)) {
DiagramContent content(diagram, false); DiagramContent content(diagram, false);
for (Element *elmt : std::as_const(content.m_elements)) { for (Element *elmt : std::as_const(content.m_elements)) {
if (elmt->uuid().toString() == elementUuid) { if (elmt->uuid().toString() == elementUuid) {
@@ -5218,7 +5481,7 @@ QStringList QetScriptApi::selectedElements(int folioIndex) const
{ {
QStringList list; QStringList list;
if (!m_project) return list; if (!m_project) return list;
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return list; if (folioIndex < 0 || folioIndex >= diagrams.count()) return list;
for (QGraphicsItem *item : diagrams.at(folioIndex)->selectedItems()) { for (QGraphicsItem *item : diagrams.at(folioIndex)->selectedItems()) {
if (item->type() == Element::Type) { if (item->type() == Element::Type) {
@@ -5232,7 +5495,7 @@ QStringList QetScriptApi::selectedElements(int folioIndex) const
void QetScriptApi::deselectAll(int folioIndex) void QetScriptApi::deselectAll(int folioIndex)
{ {
if (!m_project) return; if (!m_project) return;
const QList<Diagram *> diagrams = m_project->diagrams(); const QList<Diagram *> diagrams = m_project->folios();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return; if (folioIndex < 0 || folioIndex >= diagrams.count()) return;
diagrams.at(folioIndex)->clearSelection(); diagrams.at(folioIndex)->clearSelection();
} }
+17
View File
@@ -24,11 +24,17 @@
#include <QVariantList> #include <QVariantList>
#include <QVariantMap> #include <QVariantMap>
#include <optional>
#include "../diagramcontext.h"
class QETProject; class QETProject;
class Diagram;
class DiagramView; class DiagramView;
class Element; class Element;
class Terminal; class Terminal;
class Conductor; class Conductor;
class ConductorProperties;
class IndependentTextItem; class IndependentTextItem;
class QetShapeItem; class QetShapeItem;
class DiagramImageItem; class DiagramImageItem;
@@ -435,6 +441,11 @@ class QetScriptApi : public QObject
Q_INVOKABLE bool setConductorProperty(int folioIndex, const QString &elementUuid, Q_INVOKABLE bool setConductorProperty(int folioIndex, const QString &elementUuid,
int terminalIndex, const QString &property, int terminalIndex, const QString &property,
const QString &value); const QString &value);
// The same, the wire named by its uuid (conductorUuids())
Q_INVOKABLE QString conductorPropertyByUuid(int folioIndex, const QString &conductorUuid,
const QString &property) const;
Q_INVOKABLE bool setConductorPropertyByUuid(int folioIndex, const QString &conductorUuid,
const QString &property, const QString &value);
// -- a conductor's own drawn path, not the whole potential's // -- a conductor's own drawn path, not the whole potential's
// properties above -- one conductor only, addressed the same way -- // properties above -- one conductor only, addressed the same way --
@@ -642,6 +653,12 @@ class QetScriptApi : public QObject
private: private:
bool runFlag(const QString &flag, const QStringList &args); bool runFlag(const QString &flag, const QStringList &args);
Diagram *built(Diagram *folio) const;
Conductor *findConductorByUuid(int folioIndex, const QString &conductorUuid, const QString &caller);
bool applyConductorProperties(Conductor *conductor, const ConductorProperties &properties);
bool unbuiltConductorChange(int folioIndex, const QUuid &wire, const QString &property,
const QString &value, bool *refused);
std::optional<DiagramContext> unbuiltInformation(int folioIndex, const QString &elementUuid) const;
Element *findElement(int folioIndex, const QString &elementUuid) const; Element *findElement(int folioIndex, const QString &elementUuid) const;
Terminal *findTerminal(int folioIndex, const QString &elementUuid, int terminalIndex, Terminal *findTerminal(int folioIndex, const QString &elementUuid, int terminalIndex,
const QString &caller); const QString &caller);
+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
+12 -2
View File
@@ -13,6 +13,16 @@
#include <QSet> #include <QSet>
#include <algorithm> #include <algorithm>
namespace {
//The folios holding a terminal, built: a folio not built yet
//(lazy folios) holding none is not built for this
QList<Diagram *> terminalFolios(QETProject *project)
{
project->buildFoliosHolding(ElementData::Terminal, false);
return project->builtFolios();
}
}
/** /**
* @brief TerminalNumberingDialog::TerminalNumberingDialog * @brief TerminalNumberingDialog::TerminalNumberingDialog
* Constructor * Constructor
@@ -31,7 +41,7 @@ TerminalNumberingDialog::TerminalNumberingDialog(QWidget *parent, QETProject *pr
// Collect all unique terminal strip prefixes from the project // Collect all unique terminal strip prefixes from the project
if (project) { if (project) {
QSet<QString> prefixes; QSet<QString> prefixes;
foreach (Diagram *diagram, project->diagrams()) { foreach (Diagram *diagram, terminalFolios(project)) {
foreach (QGraphicsItem *qgi, diagram->items()) { foreach (QGraphicsItem *qgi, diagram->items()) {
if (Element *elmt = qgraphicsitem_cast<Element *>(qgi)) { if (Element *elmt = qgraphicsitem_cast<Element *>(qgi)) {
if (elmt->elementData().m_type == ElementData::Terminal) { if (elmt->elementData().m_type == ElementData::Terminal) {
@@ -153,7 +163,7 @@ QUndoCommand* TerminalNumberingDialog::getUndoCommand(QETProject *project) const
QList<TermInfo> termList; QList<TermInfo> termList;
// 2. Collect all terminals from all folios in the project // 2. Collect all terminals from all folios in the project
foreach (Diagram *diagram, project->diagrams()) { foreach (Diagram *diagram, terminalFolios(project)) {
int fIndex = diagram->folioIndex(); int fIndex = diagram->folioIndex();
foreach (QGraphicsItem *qgi, diagram->items()) { foreach (QGraphicsItem *qgi, diagram->items()) {
if (Element *elmt = qgraphicsitem_cast<Element *>(qgi)) { if (Element *elmt = qgraphicsitem_cast<Element *>(qgi)) {
@@ -119,3 +119,27 @@ void ChangeElementInformationCommand::updateProjectDB()
elmt->diagram()->project()->dataBase()->elementInfoChanged(list_); elmt->diagram()->project()->dataBase()->elementInfoChanged(list_);
} }
} }
ChangeStoredElementInformationCommand::ChangeStoredElementInformationCommand(
QETProject *project, const QUuid &element, const DiagramContext &old_info,
const DiagramContext &new_info, QUndoCommand *parent) :
QUndoCommand(parent),
m_project(project),
m_element(element),
m_old_info(old_info),
m_new_info(new_info)
{
setText(QObject::tr("Change the information of the element"));
}
void ChangeStoredElementInformationCommand::undo()
{
if (m_project)
m_project->dataBase()->setPlacedElementInformation(m_element, m_old_info);
}
void ChangeStoredElementInformationCommand::redo()
{
if (m_project)
m_project->dataBase()->setPlacedElementInformation(m_element, m_new_info);
}
@@ -21,8 +21,11 @@
#include "../diagramcontext.h" #include "../diagramcontext.h"
#include <QUndoCommand> #include <QUndoCommand>
#include <QUuid>
#include <QPointer>
class Element; class Element;
class QETProject;
/** /**
@brief The ChangeElementInformationCommand class @brief The ChangeElementInformationCommand class
@@ -52,4 +55,26 @@ class ChangeElementInformationCommand : public QUndoCommand
QMap<QPointer<Element>, QPair<DiagramContext, DiagramContext>> m_map; QMap<QPointer<Element>, QPair<DiagramContext, DiagramContext>> m_map;
}; };
/**
@brief The ChangeStoredElementInformationCommand class
Change the information of a symbol given by its uuid, built or not
(lazy folios): through the symbol when it is built, through the
project database when it is not (projectDataBase::canEditUnbuilt()).
*/
class ChangeStoredElementInformationCommand : public QUndoCommand
{
public:
ChangeStoredElementInformationCommand(QETProject *project, const QUuid &element,
const DiagramContext &old_info,
const DiagramContext &new_info,
QUndoCommand *parent = nullptr);
void undo() override;
void redo() override;
private:
QPointer<QETProject> m_project;
QUuid m_element;
DiagramContext m_old_info, m_new_info;
};
#endif // CHANGEELEMENTINFORMATIONCOMMAND_H #endif // CHANGEELEMENTINFORMATIONCOMMAND_H
+89 -3
View File
@@ -1,6 +1,9 @@
#include "wiringlistexport.h" #include "wiringlistexport.h"
#include "qetproject.h" #include "qetproject.h"
#include "diagram.h" #include "diagram.h"
#include "dataBase/projectdatabase.h"
#include "ElementsCollection/xmlelementcollection.h"
#include "qetgraphicsitem/element.h"
#include <QFileDialog> #include <QFileDialog>
#include <QMessageBox> #include <QMessageBox>
#include <QTextStream> #include <QTextStream>
@@ -28,8 +31,8 @@ QString WiringListExport::findDiagramFolio(const QDomElement &diagramElem) const
// The folio number as the folio shows it ("3/12"). The saved "folio" // The folio number as the folio shows it ("3/12"). The saved "folio"
// attribute is its template ("%id/%total"). // attribute is its template ("%id/%total").
const int index = diagramElem.attribute("order").toInt(); const int index = diagramElem.attribute("order").toInt();
if (m_project && index >= 1 && index <= m_project->diagrams().size()) { if (m_project && index >= 1 && index <= m_project->folios().size()) {
return m_project->diagrams().at(index - 1)->border_and_titleblock return m_project->folios().at(index - 1)->border_and_titleblock
.titleblockInformation().value("folio").toString(); .titleblockInformation().value("folio").toString();
} }
if (diagramElem.hasAttribute("folio")) return diagramElem.attribute("folio"); if (diagramElem.hasAttribute("folio")) return diagramElem.attribute("folio");
@@ -48,6 +51,89 @@ QDomElement WiringListExport::climbToDiagram(QDomNode node) const
return QDomElement(); return QDomElement();
} }
/**
The project as the export reads it: its symbol collection and its folios
in order, each with its "order". A folio not built (lazy folios) is
not built for it: its XML is copied, and what QElectroTech computes for
its wires' ends -- the labels of the symbols they end on and of their
partners, the terminals' names -- is filled in from the symbols built
(on built folios or alone, as partners) and from the project database.
*/
QDomDocument WiringListExport::projectDocument() const
{
QDomDocument document;
QDomElement root = document.createElement(QStringLiteral("project"));
document.appendChild(root);
if (XmlElementCollection *collection = m_project->embeddedElementCollection())
root.appendChild(document.importNode(collection->root(), true));
//A folio whose partners built alone were linked or unlinked since
//is not what its XML says: built, as saving builds it
for (Diagram *diagram : m_project->folios())
if (!diagram->isLoaded() && diagram->partialChanged())
m_project->buildFolios({diagram});
QHash<QUuid, Element *> built;
for (Diagram *diagram : m_project->folios()) {
if (diagram->isLoaded()) {
for (Element *element : diagram->elements()) built.insert(element->uuid(), element);
} else {
const QHash<QUuid, Element *> partial = diagram->partialElements();
for (auto it = partial.cbegin() ; it != partial.cend() ; ++it) built.insert(it.key(), it.value());
}
}
projectDataBase *database = m_project->dataBase();
auto label = [&built, database](const QUuid &uuid) {
Element *element = built.value(uuid);
return element ? element->actualLabel() : database->elementLabel(uuid);
};
//Conductor::toXml(): the label of the last of the symbol's partners.
//A symbol still waiting for a partner not built has its links as
//saved, in the database
auto linked = [&built, database, &label](const QUuid &uuid) -> std::optional<QString> {
Element *element = built.value(uuid);
if (element && element->pendingLinks().isEmpty()) {
if (element->linkedElements().isEmpty()) return std::nullopt;
return element->linkedElements().last()->actualLabel();
}
const QList<QUuid> links = database->linkedSymbols(uuid);
if (links.isEmpty()) return std::nullopt;
return label(links.last());
};
const QList<Diagram *> folios = m_project->folios();
for (int i = 0 ; i < folios.size() ; ++i) {
Diagram *diagram = folios.at(i);
QDomElement folio;
const bool is_built = diagram->isLoaded();
folio = document.importNode(is_built ? m_project->folioXml(diagram).documentElement()
: diagram->deferredXml(), true).toElement();
//A built folio's wires describe their ends but a partner not
//built yet; a folio not built has none of it in its part (.qetz)
//or as read, before any change (.qet)
QDomNodeList wires = folio.elementsByTagName(QStringLiteral("conductor"));
for (int w = 0 ; w < wires.size() ; ++w) {
QDomElement wire = wires.at(w).toElement();
for (const QString end : {QStringLiteral("1"), QStringLiteral("2")}) {
const QUuid element(wire.attribute(QStringLiteral("element") + end));
if (element.isNull()) continue; //an end saved the older way
if (const auto partner = linked(element))
wire.setAttribute(QStringLiteral("element") + end + QStringLiteral("_linked"), *partner);
else
wire.removeAttribute(QStringLiteral("element") + end + QStringLiteral("_linked"));
if (is_built) continue;
wire.setAttribute(QStringLiteral("element") + end + QStringLiteral("_label"), label(element));
wire.setAttribute(QStringLiteral("terminalname") + end,
database->terminalName(QUuid(wire.attribute(QStringLiteral("terminal") + end)),
element));
}
}
folio.setAttribute(QStringLiteral("order"), i + 1);
root.appendChild(folio);
}
return document;
}
QMap<QString, ElementInfo> WiringListExport::collectElementsInfo(const QDomElement &root) const QMap<QString, ElementInfo> WiringListExport::collectElementsInfo(const QDomElement &root) const
{ {
QMap<QString, ElementInfo> infoMap; QMap<QString, ElementInfo> infoMap;
@@ -191,7 +277,7 @@ QString WiringListExport::toCsvString() const
{ {
if (!m_project) return QString(); if (!m_project) return QString();
QDomDocument doc = m_project->toXml(); QDomDocument doc = projectDocument();
if (doc.isNull()) return QString(); if (doc.isNull()) return QString();
QSet<QString> conductorDefinitionTypes; QSet<QString> conductorDefinitionTypes;
+2
View File
@@ -6,6 +6,7 @@
#include <QMap> #include <QMap>
#include <QList> #include <QList>
#include <QStringList> #include <QStringList>
#include <QDomDocument>
class QETProject; class QETProject;
class QWidget; class QWidget;
@@ -60,6 +61,7 @@ private:
QString normalizeUuid(const QString &u) const; QString normalizeUuid(const QString &u) const;
QString findDiagramFolio(const QDomElement &diagramElem) const; QString findDiagramFolio(const QDomElement &diagramElem) const;
QDomElement climbToDiagram(QDomNode node) const; QDomElement climbToDiagram(QDomNode node) const;
QDomDocument projectDocument() const;
QMap<QString, ElementInfo> collectElementsInfo(const QDomElement &root) const; QMap<QString, ElementInfo> collectElementsInfo(const QDomElement &root) const;
QList<ConductorData> collectConductors(const QDomElement &root) const; QList<ConductorData> collectConductors(const QDomElement &root) const;
+57 -11
View File
@@ -33,7 +33,13 @@
# 2. let it run for a moment, then interrupt it -- the main thread is now # 2. let it run for a moment, then interrupt it -- the main thread is now
# inside the dialog's nested loop, which is the only place the bug lives # inside the dialog's nested loop, which is the only place the bug lives
# (a deleteLater() posted before exec() started is not processed by it); # (a deleteLater() posted before exec() started is not processed by it);
# 3. call QETApp::quitQET(), which closes every editor; # 3. let it run on to the start of the loop's next QEventLoop::processEvents()
# on the main thread, and call QETApp::quitQET() there, which closes
# every editor. Not where the interrupt lands: that can be inside
# malloc(), whose lock a call into the program then waits on for ever
# (1 run in 20 timed out that way, on master too). Between two
# events the loop holds nothing, and QElectroTech's event loop
# watchdog wakes it every 50 ms, so the stop comes at once;
# 4. let it run again: the unfixed build aborts within a second, the fixed # 4. let it run again: the unfixed build aborts within a second, the fixed
# one keeps running until the test interrupts it. # one keeps running until the test interrupts it.
# #
@@ -104,14 +110,32 @@ if command -v nm >/dev/null; then
fi fi
SANDBOX="$(mktemp -d /tmp/qet-modal-quit.XXXXXX)" SANDBOX="$(mktemp -d /tmp/qet-modal-quit.XXXXXX)"
cleanup() { [ "${KEEP_LOGS:-0}" = "1" ] || rm -rf "$SANDBOX"; }
# SingleApplication keeps a System V shared memory block, keyed from a file in
# the temporary folder: 16 bits of the key vary. This run ends QElectroTech
# with gdb's kill, which leaves its block behind; blocks left so pile up, and
# once a new run's key falls on one, SingleApplication cannot start ("Unable to
# attach to shared memory block") -- 1 run in 40 with 976 of them. So the
# blocks this run leaves, attached to no process, are removed at the end, and
# a run that meets someone else's is tried again (below).
orphan_blocks() {
command -v ipcs >/dev/null || return 0
ipcs -m 2>/dev/null | awk -v me="$(id -un)" 'NR > 3 && $3 == me && $6 == 0 { print $2 }'
}
BLOCKS_BEFORE=" $(orphan_blocks | tr '\n' ' ') "
cleanup() {
if command -v ipcrm >/dev/null; then
for id in $(orphan_blocks); do
case "$BLOCKS_BEFORE" in *" $id "*) ;; *) ipcrm -m "$id" 2>/dev/null ;; esac
done
fi
[ "${KEEP_LOGS:-0}" = "1" ] || rm -rf "$SANDBOX"
}
trap cleanup EXIT trap cleanup EXIT
# A unique binary path gives this run its own SingleApplication socket, so it # A unique binary path gives this run its own SingleApplication socket, so it
# can neither be captured by nor capture a QElectroTech already running. A # can neither be captured by nor capture a QElectroTech already running. A
# symlink would not do: applicationFilePath() resolves it back to the real path. # symlink would not do: applicationFilePath() resolves it back to the real path.
TEST_BINARY="$SANDBOX/qelectrotech-modalquit"
cp "$BINARY" "$TEST_BINARY" || { echo "could not copy binary" >&2; exit 2; }
# Keep the original file name. QElectroTech decides what to open from the # Keep the original file name. QElectroTech decides what to open from the
# extension, so copying a .elmt to "project.qet" would quietly turn an # extension, so copying a .elmt to "project.qet" would quietly turn an
@@ -141,9 +165,16 @@ end
break QDialog::exec break QDialog::exec
run run
delete delete
if \$_isvoid(\$_exitcode)
printf "QET_TEST: dialog exec() entered\n" printf "QET_TEST: dialog exec() entered\n"
else
printf "QET_TEST: exited before any dialog, code %d\n", \$_exitcode
end
python qet_interrupt_later($SETTLE) python qet_interrupt_later($SETTLE)
continue continue
tbreak QEventLoop::processEvents thread 1
continue
printf "QET_TEST: at the dialog's event loop\n"
thread 1 thread 1
set \$app = ((void* (*)(void))'QETApp::instance()')() set \$app = ((void* (*)(void))'QETApp::instance()')()
call ((void (*)(void*))'QETApp::quitQET()')(\$app) call ((void (*)(void*))'QETApp::quitQET()')(\$app)
@@ -162,14 +193,29 @@ EOF
# the full watchdog period no matter how fast gdb finished: ten seconds of # the full watchdog period no matter how fast gdb finished: ten seconds of
# work reported as two minutes, a stone's throw from the CTest timeout. # work reported as two minutes, a stone's throw from the CTest timeout.
GDB_TIMEOUT=120 GDB_TIMEOUT=120
if command -v timeout >/dev/null; then # Each try under a binary path of its own: a new SingleApplication key. Tried
timeout --signal=KILL "$GDB_TIMEOUT" \ # again only when QElectroTech stopped before any dialog because
# SingleApplication met a block it could not use -- the scenario never ran,
# the code under test played no part.
for attempt in 1 2 3; do
TEST_BINARY="$SANDBOX/qelectrotech-modalquit-$attempt"
cp "$BINARY" "$TEST_BINARY" || { echo "could not copy binary" >&2; exit 2; }
if command -v timeout >/dev/null; then
timeout --signal=KILL "$GDB_TIMEOUT" \
gdb -batch -x "$SANDBOX/scenario.gdb" --args "$TEST_BINARY" "$SANDBOX_PROJECT" \
> "$SANDBOX/gdb.log" 2>&1
else
gdb -batch -x "$SANDBOX/scenario.gdb" --args "$TEST_BINARY" "$SANDBOX_PROJECT" \ gdb -batch -x "$SANDBOX/scenario.gdb" --args "$TEST_BINARY" "$SANDBOX_PROJECT" \
> "$SANDBOX/gdb.log" 2>&1 > "$SANDBOX/gdb.log" 2>&1
else fi
gdb -batch -x "$SANDBOX/scenario.gdb" --args "$TEST_BINARY" "$SANDBOX_PROJECT" \ if grep -q "QET_TEST: exited before any dialog" "$SANDBOX/gdb.log" \
> "$SANDBOX/gdb.log" 2>&1 && grep -q "SingleApplication: Unable to" "$SANDBOX/gdb.log"; then
fi echo "note: try $attempt: SingleApplication could not set up its shared memory (a block left by another run); trying again"
cp "$SANDBOX/gdb.log" "$SANDBOX/gdb-try$attempt.log"
continue
fi
break
done
log="$SANDBOX/gdb.log" log="$SANDBOX/gdb.log"
if ! grep -q "QET_TEST: dialog exec() entered" "$log"; then if ! grep -q "QET_TEST: dialog exec() entered" "$log"; then
+29 -4
View File
@@ -92,7 +92,7 @@ if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja")
--build ${CMAKE_BINARY_DIR} --binary $<TARGET_FILE:qelectrotech> --build ${CMAKE_BINARY_DIR} --binary $<TARGET_FILE:qelectrotech>
--source ${CMAKE_CURRENT_SOURCE_DIR}/lazy_folios_probe.cpp --source ${CMAKE_CURRENT_SOURCE_DIR}/lazy_folios_probe.cpp
--fixture ${CMAKE_CURRENT_SOURCE_DIR}/../../examples/industrial.qet) --fixture ${CMAKE_CURRENT_SOURCE_DIR}/../../examples/industrial.qet)
set_tests_properties(tst_lazyfolios PROPERTIES TIMEOUT 600 RUN_SERIAL TRUE set_tests_properties(tst_lazyfolios PROPERTIES TIMEOUT 1800 RUN_SERIAL TRUE
ENVIRONMENT "QT_QPA_PLATFORM=offscreen") ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
# Deleting a cable, undoing the delete twice and then editing anything # Deleting a cable, undoing the delete twice and then editing anything
# has both cable commands tearing down the same cable. Runs under the # has both cable commands tearing down the same cable. Runs under the
@@ -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(
@@ -735,6 +748,13 @@ target_link_libraries(tst_pdfpagesize PRIVATE Qt::Test Qt::Gui)
target_compile_definitions(tst_pdfpagesize PRIVATE target_compile_definitions(tst_pdfpagesize PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"") "QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
# The print window's PDF export turns each page to its folio when "Fit in
# page" is on (#1480), and keeps the chosen orientation when it is off. The
# links on a turned page are checked on examples/industrial.qet.
qet_add_app_test(tst_printpdforientation tst_printpdforientation.cpp)
target_compile_definitions(tst_printpdforientation PRIVATE
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
# --export-pdf writes the cross-reference links in the same order in every # --export-pdf writes the cross-reference links in the same order in every
# run, rather than in Element pointer order. Exports examples/industrial.qet # run, rather than in Element pointer order. Exports examples/industrial.qet
# several times, in separate processes. # several times, in separate processes.
@@ -1060,10 +1080,13 @@ endif()
add_executable( add_executable(
tst_resaveunchanged tst_resaveunchanged
tst_resaveunchanged.cpp) tst_resaveunchanged.cpp
${QET_DIR}/sources/container/qetzip.cpp
${QET_DIR}/sources/container/miniz/miniz.c)
add_test(NAME tst_resaveunchanged COMMAND tst_resaveunchanged) add_test(NAME tst_resaveunchanged COMMAND tst_resaveunchanged)
add_dependencies(tst_resaveunchanged qelectrotech) add_dependencies(tst_resaveunchanged qelectrotech)
target_link_libraries(tst_resaveunchanged PRIVATE Qt::Test) target_include_directories(tst_resaveunchanged PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_resaveunchanged PRIVATE Qt::Test SQLite3::SQLite3)
target_compile_definitions(tst_resaveunchanged PRIVATE target_compile_definitions(tst_resaveunchanged PRIVATE
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"" "QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"") "QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
@@ -1253,4 +1276,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>
+597 -13
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 (on by default, QET_EAGER_FOLIOS=1 for every folio built),
// 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
@@ -23,22 +23,99 @@
#include "../../sources/qetgraphicsitem/element.h" #include "../../sources/qetgraphicsitem/element.h"
#include "../../sources/autoNum/elementautonumschemecommand.h" #include "../../sources/autoNum/elementautonumschemecommand.h"
#include "../../sources/elementprovider.h" #include "../../sources/elementprovider.h"
#include "../../sources/container/qetzip.h"
#include <QSqlDatabase>
#include <QMap>
#include "../../sources/TerminalStrip/terminalstrip.h" #include "../../sources/TerminalStrip/terminalstrip.h"
#include "../../sources/ui/elementpropertieswidget.h" #include "../../sources/ui/elementpropertieswidget.h"
#include "../../sources/conductorautonumerotation.h" #include "../../sources/conductorautonumerotation.h"
#include "../../sources/conductornumexport.h"
#include "../../sources/scripting/qetscriptapi.h"
#include "../../sources/wiringlistexport.h"
#include "../../sources/ui/terminalnumberingdialog.h"
#include "../../sources/diagramcontent.h" #include "../../sources/diagramcontent.h"
#include "../../sources/diagramview.h" #include "../../sources/diagramview.h"
#include "../../sources/diagramevent/diagrameventaddpaste.h" #include "../../sources/diagramevent/diagrameventaddpaste.h"
#include <QKeyEvent> #include <QKeyEvent>
#include "../../sources/qetgraphicsitem/conductor.h" #include "../../sources/qetgraphicsitem/conductor.h"
#include "../../sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.h"
#include "../../sources/qetgraphicsitem/terminal.h" #include "../../sources/qetgraphicsitem/terminal.h"
#include "../../sources/undocommand/changeelementinformationcommand.h" #include "../../sources/undocommand/changeelementinformationcommand.h"
#include "../../sources/undocommand/changetitleblockcommand.h" #include "../../sources/undocommand/changetitleblockcommand.h"
#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); }
static QStringList sortedLines(QString xml, const QString &path);
//A new revision on every folio (its title block's revision index and
//author), saved lazily and with every folio built: what saving builds,
//and whether the two saves hold the same. A change to folio
//@p label_folio's label (what formulas read) when it is not -1
static void revisionEveryFolio(const QString &source, const QString &output, const QString &name,
const QString &path, int label_folio = -1)
{
QString error;
qunsetenv("QET_EAGER_FOLIOS");
QETProject revised(source);
const int unbuilt = revised.unloadedFolioCount();
QetScriptApi api(&revised);
for (int i = 0 ; i < api.folioCount() ; ++i) {
check(api.setFolioProperty(i, "indexrev", "B"), "a folio's revision set");
check(api.setFolioProperty(i, "author", "Revised"), "a folio's author set");
}
if (label_folio >= 0)
check(api.setFolioProperty(label_folio, "folio", "X%id"), "a folio's label set");
check(revised.unloadedFolioCount() == unbuilt, "setting the folios' title blocks builds none");
int with_tables = 0;
for (Diagram *folio : revised.folios())
if (!folio->isLoaded() && revised.unbuiltFolioHasTables(folio)) ++with_tables;
const bool qetz = QETProject::isContainerPath(source);
const QString lazy_path = output + "/" + name + (qetz ? ".qetz" : ".qet");
if (qetz) check(revised.writeContainerFile(lazy_path, &error), "revised project saved");
else { revised.setFilePath(lazy_path); check(revised.write().isOk(), "revised project saved"); }
const int built = unbuilt - revised.unloadedFolioCount();
std::printf("%s: new revision on every folio, %d folios built by saving, %d hold a table%s\n",
qPrintable(name), built, with_tables,
label_folio >= 0 ? ", one label changed" : "");
if (label_folio < 0)
check(built <= with_tables, "saving a new revision builds only the folios holding a table");
else
check(revised.folios().at(label_folio)->isLoaded(), "a folio whose label changed is built to save");
qputenv("QET_EAGER_FOLIOS", "1");
const QString usual_path = output + "/" + name + "-usual" + (qetz ? ".qetz" : ".qet");
{
QETProject usual(source);
QetScriptApi usual_api(&usual);
for (int i = 0 ; i < usual_api.folioCount() ; ++i) {
usual_api.setFolioProperty(i, "indexrev", "B");
usual_api.setFolioProperty(i, "author", "Revised");
}
if (label_folio >= 0) usual_api.setFolioProperty(label_folio, "folio", "X%id");
if (qetz) check(usual.writeContainerFile(usual_path, &error), "revision saved when built");
else { usual.setFilePath(usual_path); check(usual.write().isOk(), "revision saved when built"); }
}
QETProject lazy_back(lazy_path), usual_back(usual_path);
qunsetenv("QET_EAGER_FOLIOS");
//What a save says of itself (file name, path, time) left out
auto lines = [&path](QETProject &project) {
QStringList list = sortedLines(project.toXml().toString(), path);
list.removeIf([](const QString &line) { return line.contains(QLatin1String("name=\"saved")); });
return list;
};
const QStringList lazy_lines = lines(lazy_back), usual_lines = lines(usual_back);
if (lazy_lines != usual_lines) {
int shown = 0;
for (const QString &line : usual_lines)
if (!lazy_lines.contains(line) && shown++ < 8) std::printf(" when built: %s\n", qPrintable(line.left(300)));
for (const QString &line : lazy_lines)
if (!usual_lines.contains(line) && shown++ < 16) std::printf(" lazily: %s\n", qPrintable(line.left(300)));
}
check(lazy_lines == usual_lines, "a new revision saved as when built");
}
//Every symbol's numbering, in a stable order //Every symbol's numbering, in a stable order
static QStringList numbering(const QETProject &project) static QStringList numbering(const QETProject &project)
@@ -174,6 +251,55 @@ static QString saved(QETProject &project)
return QETProject::spliceKeptFolios(document.toString(4), kept); return QETProject::spliceKeptFolios(document.toString(4), kept);
} }
//@p xml's lines in order, any uuid not in the file @p path "new": a
//pasted copy's and a new wire's are random, and a folio's symbols are
//saved in uuid order
static QStringList sortedLines(QString xml, const QString &path)
{
QFile file(path); file.open(QIODevice::ReadOnly);
const QString before = QString::fromUtf8(file.readAll());
static const QRegularExpression uuid_re(QStringLiteral("\\{[0-9a-f-]{36}\\}"));
for (auto it = uuid_re.globalMatch(QString(xml)) ; it.hasNext() ; ) {
const QString uuid = it.next().captured(0);
if (!before.contains(uuid)) xml.replace(uuid, QStringLiteral("new"));
}
QStringList list = xml.split(QLatin1Char('\n'));
list.sort();
return list;
}
//The saved links of a .qetz, symbol by symbol in their order: what a
//symbol saves of its partners, as its file holds it
static QStringList savedLinks(const QString &qetz, const QString &path)
{
QFile file(qetz); file.open(QIODevice::ReadOnly);
QList<QetZip::Entry> entries;
QString error;
check(QetZip::fromBytes(file.readAll(), &entries, &error), "a .qetz read");
QTemporaryDir dir;
const QString db_path = dir.filePath(QStringLiteral("project.sqlite"));
for (const QetZip::Entry &entry : entries)
if (entry.name == QLatin1String("project.sqlite")) {
QFile db(db_path); db.open(QIODevice::WriteOnly); db.write(entry.data);
}
QMap<QString, QStringList> links; //symbol -> its partners in order
{
QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), QStringLiteral("saved_links"));
db.setDatabaseName(db_path);
check(db.open(), "a .qetz's database opened");
QSqlQuery query(db);
check(query.exec(QStringLiteral("SELECT element_uuid, linked_uuid FROM link ORDER BY element_uuid, ord")),
"saved links read");
while (query.next())
links[query.value(0).toString()] << query.value(1).toString();
}
QSqlDatabase::removeDatabase(QStringLiteral("saved_links"));
QStringList rows;
for (auto it = links.cbegin() ; it != links.cend() ; ++it)
rows << it.key() + QStringLiteral(": ") + it.value().join(QLatin1Char(','));
return sortedLines(rows.join(QLatin1Char('\n')), path);
}
//The rows of the project database's tables, in their order, read as //The rows of the project database's tables, in their order, read as
//every caller reads them (newQuery(): brought up to date first) //every caller reads them (newQuery(): brought up to date first)
static QStringList tables(QETProject &project) static QStringList tables(QETProject &project)
@@ -238,7 +364,7 @@ int main(int argc, char **argv)
qputenv("QET_LAZY_SAVE_KEPT", "1"); qputenv("QET_LAZY_SAVE_KEPT", "1");
try { try {
const QString output = QString::fromLocal8Bit(argv[3]); const QString output = QString::fromLocal8Bit(argv[3]);
qunsetenv("QET_LAZY_FOLIOS"); qputenv("QET_EAGER_FOLIOS", "1");
//A file saved by this version: its folios carry their uuids, //A file saved by this version: its folios carry their uuids,
//which opening without building them needs //which opening without building them needs
const QString path = output + "/lazy_folios.qet"; const QString path = output + "/lazy_folios.qet";
@@ -265,9 +391,9 @@ int main(int argc, char **argv)
QETProject eager(path); QETProject eager(path);
check(eager.state() == QETProject::Ok, "project opens"); check(eager.state() == QETProject::Ok, "project opens");
{ {
qputenv("QET_LAZY_FOLIOS", "1"); qunsetenv("QET_EAGER_FOLIOS");
QETProject lazy_strips(path); QETProject lazy_strips(path);
qunsetenv("QET_LAZY_FOLIOS"); qputenv("QET_EAGER_FOLIOS", "1");
auto strip_of = [](const QETProject &project) { auto strip_of = [](const QETProject &project) {
for (TerminalStrip *strip : project.terminalStrip()) for (TerminalStrip *strip : project.terminalStrip())
if (strip->name() == QStringLiteral("X99")) return strip->physicalTerminalCount(); if (strip->name() == QStringLiteral("X99")) return strip->physicalTerminalCount();
@@ -288,7 +414,7 @@ int main(int argc, char **argv)
} }
check(linked > 0, "the project has linked symbols"); check(linked > 0, "the project has linked symbols");
qputenv("QET_LAZY_FOLIOS", "1"); qunsetenv("QET_EAGER_FOLIOS");
auto same = [&](QETProject &lazy, int i, const char *what) { auto same = [&](QETProject &lazy, int i, const char *what) {
Diagram *folio = lazy.folios().at(i); Diagram *folio = lazy.folios().at(i);
lazy.buildFolioToShow(folio); lazy.buildFolioToShow(folio);
@@ -321,6 +447,24 @@ int main(int argc, char **argv)
for (const QString &title : titles) for (const QString &title : titles)
check(gaps(numbered, title) == gaps(eager, title), "a scheme's gaps as when built"); check(gaps(numbered, title) == gaps(eager, title), "a scheme's gaps as when built");
check(numbered.unloadedFolioCount() == folios.size() - strip_folios.size(), "numbering read without building"); check(numbered.unloadedFolioCount() == folios.size() - strip_folios.size(), "numbering read without building");
revisionEveryFolio(path, output, "revised-qet", path);
{
//Wire numbers exported from the database filled from the
//document, as when built (D5)
QETProject exported(path);
check(ConductorNumExport(&exported).wiresNum() == ConductorNumExport(&eager).wiresNum(),
"wire numbers of a .qet exported as when built");
const QString wiring = WiringListExport(&exported).toCsvString(), usual_wiring = WiringListExport(&eager).toCsvString();
if (wiring != usual_wiring) {
const QStringList a = wiring.split('\n'), b = usual_wiring.split('\n');
for (const QString &line : b) if (!a.contains(line)) std::printf(" wiring when built: %s\n", qPrintable(line));
for (const QString &line : a) if (!b.contains(line)) std::printf(" wiring lazily: %s\n", qPrintable(line));
std::printf(" %d lines lazily, %d when built\n", int(a.size()), int(b.size()));
}
check(wiring == usual_wiring, "wiring list of a .qet exported as when built");
check(exported.unloadedFolioCount() == folios.size() - strip_folios.size(),
"exporting a .qet's wire numbers and wiring list builds no folio");
}
//A symbol placed on a folio, numbered by a scheme others follow //A symbol placed on a folio, numbered by a scheme others follow
QString location, title; QString location, title;
int folio = -1; int folio = -1;
@@ -398,9 +542,9 @@ int main(int argc, char **argv)
} }
{ {
QETProject edited(path), usual(path); QETProject edited(path), usual(path);
qunsetenv("QET_LAZY_FOLIOS"); qputenv("QET_EAGER_FOLIOS", "1");
QETProject usual_edited(path); QETProject usual_edited(path);
qputenv("QET_LAZY_FOLIOS", "1"); qunsetenv("QET_EAGER_FOLIOS");
edit(edited, busiest); edit(edited, busiest);
std::printf("edits on folio %d (%d links to other folios): %d folios not built\n", std::printf("edits on folio %d (%d links to other folios): %d folios not built\n",
busiest + 1, most, edited.unloadedFolioCount()); busiest + 1, most, edited.unloadedFolioCount());
@@ -480,9 +624,9 @@ int main(int argc, char **argv)
//After everyday edits on a folio: as when built, others not built //After everyday edits on a folio: as when built, others not built
{ {
QETProject edited(path), usual(path); QETProject edited(path), usual(path);
qunsetenv("QET_LAZY_FOLIOS"); qputenv("QET_EAGER_FOLIOS", "1");
QETProject usual_edited(path); QETProject usual_edited(path);
qputenv("QET_LAZY_FOLIOS", "1"); qunsetenv("QET_EAGER_FOLIOS");
edit(edited, busiest); edit(edited, busiest);
edit(usual_edited, busiest); edit(usual_edited, busiest);
QFile file(path); file.open(QIODevice::ReadOnly); QFile file(path); file.open(QIODevice::ReadOnly);
@@ -512,9 +656,9 @@ int main(int argc, char **argv)
//links reach are built to be saved, the others not //links reach are built to be saved, the others not
{ {
QETProject renamed(path); QETProject renamed(path);
qunsetenv("QET_LAZY_FOLIOS"); qputenv("QET_EAGER_FOLIOS", "1");
QETProject usual_renamed(path); QETProject usual_renamed(path);
qputenv("QET_LAZY_FOLIOS", "1"); qunsetenv("QET_EAGER_FOLIOS");
for (QETProject *project : {&renamed, &usual_renamed}) { for (QETProject *project : {&renamed, &usual_renamed}) {
Diagram *folio = project->folios().at(busiest); Diagram *folio = project->folios().at(busiest);
TitleBlockProperties block = folio->border_and_titleblock.exportTitleBlock(); TitleBlockProperties block = folio->border_and_titleblock.exportTitleBlock();
@@ -529,9 +673,9 @@ int main(int argc, char **argv)
//folio is built to be saved //folio is built to be saved
{ {
QETProject changed(path); QETProject changed(path);
qunsetenv("QET_LAZY_FOLIOS"); qputenv("QET_EAGER_FOLIOS", "1");
QETProject usual_changed(path); QETProject usual_changed(path);
qputenv("QET_LAZY_FOLIOS", "1"); qunsetenv("QET_EAGER_FOLIOS");
for (QETProject *project : {&changed, &usual_changed}) { for (QETProject *project : {&changed, &usual_changed}) {
DiagramContext properties = project->projectProperties(); DiagramContext properties = project->projectProperties();
properties.addValue(QStringLiteral("lazy_test"), QStringLiteral("1")); properties.addValue(QStringLiteral("lazy_test"), QStringLiteral("1"));
@@ -567,6 +711,446 @@ 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();
qunsetenv("QET_EAGER_FOLIOS");
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");
}
//A .qetz opened lazily: no folio built but those
//the terminal strip needs, each built when shown, drawn and
//linked as when built, saved as the .qet is
{
const QString zipped = output + "/lazy_folios.qetz";
QString error;
check(eager.writeContainerFile(zipped, &error), "saved as a .qetz");
QETProject indexed(zipped);
check(indexed.state() == QETProject::Ok, "the .qetz opens");
std::printf(".qetz opened: %d of %d folios not built (strip folios %d)\n",
indexed.unloadedFolioCount(), int(folios.size()), int(strip_folios.size()));
check(indexed.unloadedFolioCount() == folios.size() - strip_folios.size(),
"the .qetz opens building only the folios of the terminal strip's terminals");
check(numbering(indexed) == numbering(eager), "symbols' numbering of a .qetz as when built");
//The drawn items, through the view made of their tables: the
//folios built for it, as for the tables (D6: none were)
{
QETProject drawn(zipped);
auto count = [](QETProject &project) {
QSqlQuery query = project.dataBase()->newQuery(QStringLiteral("SELECT COUNT(*) FROM drawing_item_view"));
return query.exec() && query.next() ? query.value(0).toInt() : -1;
};
const int lazily = count(drawn), built = count(eager);
std::printf(".qetz drawn items through their view: %d lazily, %d when built\n", lazily, built);
check(built > 0, "the project has drawn items");
check(lazily == built, "the drawn items' view as when built");
}
//Every wire's potential from the database as QElectroTech
//follows it with every folio built
{
QETProject indexed_potentials(zipped);
int same = 0, differ = 0, unknown = 0;
for (Diagram *folio : eager.folios())
for (Conductor *wire : folio->conductors()) {
QSet<QUuid> live{wire->uuid()};
for (Conductor *c : wire->relatedPotentialConductors(true)) live.insert(c->uuid());
for (QETProject *project : {&eager, &indexed_potentials}) {
const auto stored = project->dataBase()->potentialWires(wire->uuid());
if (!stored) { ++unknown; continue; }
if (*stored == live) { ++same; continue; }
if (differ++ < 3)
std::printf(" potential of %s: %d wires followed, %d from the database\n",
qPrintable(wire->uuid().toString()), int(live.size()), int(stored->size()));
}
}
std::printf("potentials from the database: %d same, %d differ, %d not answered\n", same, differ, unknown);
check(differ == 0 && same > 0, "every wire's potential from the database as followed");
check(indexed_potentials.unloadedFolioCount() > 0, "potentials read without building");
}
//The folios' rows as saved, not as reading their headers one
//by one numbers them ("1/1" for the first of three, D7)
{
auto folioRows = [](QETProject &project) {
QStringList rows;
for (const QString &row : tables(project))
if (row.startsWith("diagram")) rows << row;
return rows;
};
check(folioRows(indexed) == folioRows(eager), "the folios' rows of a .qetz as when built");
//Folio numbers that count the folios ("%id/%total")
const QString totals = output + "/totals.qetz";
qputenv("QET_EAGER_FOLIOS", "1");
{
QETProject numbered(zipped);
QetScriptApi api(&numbered);
for (int i = 0 ; i < api.folioCount() ; ++i)
api.setFolioProperty(i, "folio", "%id/%total");
check(numbered.writeContainerFile(totals, &error), "folio numbers with totals saved");
}
QETProject usual_totals(totals);
qunsetenv("QET_EAGER_FOLIOS");
QETProject lazy_totals(totals);
check(lazy_totals.unloadedFolioCount() > 0, "opened lazily");
const QStringList lazy_rows = folioRows(lazy_totals), usual_rows = folioRows(usual_totals);
if (lazy_rows != usual_rows)
for (int i = 0 ; i < lazy_rows.size() && i < 3 ; ++i)
std::printf(" lazily %s\n built %s\n", qPrintable(lazy_rows.at(i).left(140)),
qPrintable(usual_rows.value(i).left(140)));
check(lazy_rows == usual_rows, "folio numbers with totals of a .qetz as when built");
}
for (int i = 0; i < folios.size(); ++i)
same(indexed, i, "shown from the .qetz");
check(indexed.toXml().toString() == eager.toXml().toString(), "the .qetz saves as when built");
//A folio whose table continues one on another folio, shown
//first: the folios its table is built with (D5)
int continued = 0;
for (int i = 0; i < folios.size(); ++i) {
bool continues = false;
for (QGraphicsItem *item : folios.at(i)->items())
if (item->type() == QetGraphicsTableItem::Type
&& !static_cast<QetGraphicsTableItem *>(item)->previousTableUuid().isNull())
continues = true;
if (!continues) continue;
QETProject fresh(zipped);
same(fresh, i, "a continued table shown first from the .qetz");
++continued;
}
check(continued > 0, "a folio whose table continues another");
//Numbering, the searches for symbols and the unused symbols
//are answered from the database (D5)
{
QETProject asked(zipped);
check(numbering(asked) == numbering(eager), "numbering of a .qetz as when built");
for (const QString &title : eager.elementAutoNum().keys())
check(gaps(asked, title) == gaps(eager, title), "a .qetz scheme's gaps as when built");
auto locations = [](const QList<ElementsLocation> &list) {
QStringList strings;
for (const ElementsLocation &location : list) strings << location.toString();
strings.sort();
return strings;
};
check(locations(asked.unusedElements()) == locations(eager.unusedElements()),
"a .qetz's unused symbols as when built");
const ElementProvider usual_search(&eager), search(&asked);
for (const ElementData::Types kinds : {ElementData::Types(ElementData::Slave),
ElementData::Types(ElementData::Master),
ElementData::Types(ElementData::AllReport)})
check(found(search.freeElement(kinds)) == found(usual_search.freeElement(kinds)),
"free symbols of a .qetz found as when built");
const int built = int(folios.size()) - asked.unloadedFolioCount();
std::printf(".qetz asked numbering, searches, unused symbols: %d folios built\n", built);
check(asked.unloadedFolioCount() > 0, "asking a .qetz does not build every folio");
}
//A folio added, moved and removed: no other folio built, the
//folios' rows as when every folio is built (D5)
{
auto shuffle = [](QETProject &project) {
Diagram *added = project.addNewDiagram();
project.diagramOrderChanged(1, 0);
project.removeDiagram(added);
};
auto folioRows = [](QETProject &project) {
QStringList rows;
for (const QString &row : tables(project))
if (row.startsWith("diagram")) rows << row;
return rows;
};
QETProject shuffled(zipped);
const int unbuilt = shuffled.unloadedFolioCount();
shuffle(shuffled);
std::printf(".qetz folio added, moved, removed: %d folios built\n",
unbuilt - shuffled.unloadedFolioCount());
check(shuffled.unloadedFolioCount() == unbuilt, "adding, moving and removing a folio builds no other");
qputenv("QET_EAGER_FOLIOS", "1");
QETProject usual_shuffled(zipped);
qunsetenv("QET_EAGER_FOLIOS");
shuffle(usual_shuffled);
check(folioRows(shuffled) == folioRows(usual_shuffled), "folios' rows after a move as when built");
}
//The tables a table can continue (its properties editor) and
//the wiring list's count of wires left out: only the folios
//holding a table built (D5)
{
QETProject listed(zipped);
const int unbuilt = listed.unloadedFolioCount();
auto tableUuids = [](const QVector<QetGraphicsTableItem *> &list) {
QStringList uuids;
for (QetGraphicsTableItem *table : list) uuids << table->uuid().toString();
uuids.sort();
return uuids;
};
const QStringList lazy_tables = tableUuids(ElementProvider(&listed).table(nullptr, nullptr));
check(!lazy_tables.isEmpty(), "the project has tables");
check(lazy_tables == tableUuids(ElementProvider(&eager).table(nullptr, nullptr)),
"the project's tables listed as when built");
check(listed.dataBase()->excludedConductorCount() == eager.dataBase()->excludedConductorCount(),
"wires left out of the wiring list counted as when built");
int with_tables = 0;
for (Diagram *folio : folios) {
for (QGraphicsItem *item : folio->items())
if (item->type() == QetGraphicsTableItem::Type) { ++with_tables; break; }
}
std::printf(".qetz tables listed: %d folios built, %d hold a table\n",
unbuilt - listed.unloadedFolioCount(), with_tables);
check(listed.unloadedFolioCount() > 0, "listing the tables does not build every folio");
}
//A script (and the assistant's live mode) asking about the
//folios and one folio's items: that folio built, no other (D5)
{
QETProject scripted(zipped);
const int unbuilt = scripted.unloadedFolioCount();
QetScriptApi api(&scripted), usual_api(&eager);
check(api.folioCount() == usual_api.folioCount(), "a script counts the folios as when built");
for (int i = 0 ; i < folios.size() ; ++i) {
check(api.folioTitle(i) == usual_api.folioTitle(i) && api.folioUuid(i) == usual_api.folioUuid(i)
&& api.folioBorder(i, "columns") == usual_api.folioBorder(i, "columns"),
"a script reads a folio's own data as when built");
}
check(scripted.unloadedFolioCount() == unbuilt, "reading the folios' own data builds none");
//The folio with the most links to other folios
const int target = busiest;
check(!scripted.folios().at(target)->isLoaded(), "a folio not built");
QStringList lazy_uuids = api.elementUuids(target), usual_uuids = usual_api.elementUuids(target);
lazy_uuids.sort(); usual_uuids.sort();
check(lazy_uuids == usual_uuids, "a script lists a folio's symbols as when built");
QStringList lazy_wires = api.conductors(target), usual_wires = usual_api.conductors(target);
lazy_wires.sort(); usual_wires.sort();
check(lazy_wires == usual_wires, "a script lists a folio's wires as when built");
check(scripted.folios().at(target)->isLoaded(), "the folio a script reads is built");
std::printf(".qetz script on folio %d: %d folios built\n", target + 1,
unbuilt - scripted.unloadedFolioCount());
check(unbuilt - scripted.unloadedFolioCount() == 1, "a script reading one folio builds that folio only");
}
//A script changes the information of a symbol on a folio not
//built, which nothing else shows: through the project
//database, no folio built; saved as when built; undone (D5)
{
QETProject changed(zipped);
const int unbuilt = changed.unloadedFolioCount();
int folio_index = -1;
QString uuid;
for (int i = 0 ; i < folios.size() && uuid.isEmpty() ; ++i) {
if (changed.folios().at(i)->isLoaded()) continue;
for (Element *element : folios.at(i)->elements())
if (changed.dataBase()->canEditUnbuilt(element->uuid())) {
folio_index = i; uuid = element->uuid().toString(); break;
}
}
check(!uuid.isEmpty(), "a symbol to change through the database");
QetScriptApi api(&changed);
const QString before = api.elementInfo(folio_index, uuid, "comment");
const QString value = QStringLiteral("changed through the database");
check(api.setElementInfo(folio_index, uuid, "comment", value), "a script changes a symbol not built");
check(api.elementInfo(folio_index, uuid, "comment") == value, "the change read back");
check(changed.unloadedFolioCount() == unbuilt, "changing a symbol not built builds no folio");
const QString changed_path = output + "/changed.qetz";
check(changed.writeContainerFile(changed_path, &error), "saved after the change");
std::printf(".qetz symbol on folio %d changed by a script: %d folios built, %d by saving\n",
folio_index + 1, 0, unbuilt - changed.unloadedFolioCount());
qputenv("QET_EAGER_FOLIOS", "1");
const QString usual_path = output + "/changed-usual.qetz";
{
QETProject usual_changed(zipped);
QetScriptApi usual_api(&usual_changed);
check(usual_api.setElementInfo(folio_index, uuid, "comment", value), "the same change when built");
check(usual_changed.writeContainerFile(usual_path, &error), "saved after the change when built");
}
QETProject changed_back(changed_path), usual_back(usual_path);
qunsetenv("QET_EAGER_FOLIOS");
check(sortedLines(changed_back.toXml().toString(), path) == sortedLines(usual_back.toXml().toString(), path),
"a symbol changed through the database saved as when built");
check(QetScriptApi(&changed_back).elementInfo(folio_index, uuid, "comment") == value, "the change saved");
//Undone, then redone and the folio built: the symbol shows it
changed.undoStack()->undo();
check(api.elementInfo(folio_index, uuid, "comment") == before, "the change undone");
changed.undoStack()->redo();
changed.buildFolioToShow(changed.folios().at(folio_index));
Element *built_symbol = nullptr;
for (Element *element : changed.folios().at(folio_index)->elements())
if (element->uuid().toString() == uuid) built_symbol = element;
check(built_symbol && built_symbol->elementInformations().value("comment").toString() == value,
"a symbol changed before its folio was built shows the change");
}
//A new revision on every folio: saved from the folios as read,
//not built; a label change builds its folio (D5)
revisionEveryFolio(zipped, output, "revised", path);
revisionEveryFolio(zipped, output, "relabelled", path, busiest);
//A script changes a wire's cable by its uuid, on folios not
//built, the wire and its potential numbered by hand: through
//the project database, no folio built; saved as when built;
//undone; the folio built afterwards shows it (D5)
{
QETProject cabled(zipped);
const int unbuilt = cabled.unloadedFolioCount();
int folio_index = -1;
QString uuid;
QSet<QUuid> potential;
for (int i = 0 ; i < folios.size() && uuid.isEmpty() ; ++i) {
if (cabled.folios().at(i)->isLoaded()) continue;
for (Conductor *wire : folios.at(i)->conductors()) {
QSet<Conductor *> wires = wire->relatedPotentialConductors(true);
wires << wire;
bool usable = true;
for (Conductor *c : std::as_const(wires))
if (!c->properties().m_formula.isEmpty()
|| cabled.folios().at(eager.folioIndex(c->diagram()))->isLoaded())
usable = false;
if (!usable || wires.size() < 2) continue;
folio_index = i;
uuid = wire->uuid().toString();
for (Conductor *c : std::as_const(wires)) potential.insert(c->uuid());
break;
}
}
check(!uuid.isEmpty(), "a wire numbered by hand, its potential on folios not built");
QetScriptApi api(&cabled);
const QString before = api.conductorPropertyByUuid(folio_index, uuid, "cable");
const QString value = QStringLiteral("W-42");
check(api.setConductorPropertyByUuid(folio_index, uuid, "cable", value), "a script changes a wire not built");
check(api.conductorPropertyByUuid(folio_index, uuid, "cable") == value, "the wire's change read back");
for (const QUuid &wire : std::as_const(potential))
check(cabled.dataBase()->conductorProperties(wire).m_cable == value, "every wire of the potential changed");
check(cabled.unloadedFolioCount() == unbuilt, "changing wires not built builds no folio");
const QString cabled_path = output + "/cabled.qetz";
check(cabled.writeContainerFile(cabled_path, &error), "saved after the wire change");
std::printf(".qetz wire on folio %d, potential of %d wires, cable changed by uuid: 0 folios built, %d by saving\n",
folio_index + 1, int(potential.size()), unbuilt - cabled.unloadedFolioCount());
qputenv("QET_EAGER_FOLIOS", "1");
const QString usual_path = output + "/cabled-usual.qetz";
{
QETProject usual_cabled(zipped);
check(QetScriptApi(&usual_cabled).setConductorPropertyByUuid(folio_index, uuid, "cable", value),
"the same wire change when built");
check(usual_cabled.writeContainerFile(usual_path, &error), "saved after the wire change when built");
}
QETProject cabled_back(cabled_path), usual_back(usual_path);
qunsetenv("QET_EAGER_FOLIOS");
auto lines = [&path](QETProject &project) {
QStringList list = sortedLines(project.toXml().toString(), path);
list.removeIf([](const QString &line) { return line.contains(QLatin1String("name=\"saved")); });
return list;
};
check(lines(cabled_back) == lines(usual_back), "a wire changed through the database saved as when built");
cabled.undoStack()->undo();
check(api.conductorPropertyByUuid(folio_index, uuid, "cable") == before, "the wire change undone");
cabled.undoStack()->redo();
Conductor *built_wire = nullptr;
for (Conductor *wire : cabled.folios().at(folio_index)->conductors())
if (wire->uuid().toString() == uuid) built_wire = wire;
check(!built_wire, "the folio not built yet");
cabled.buildFolioToShow(cabled.folios().at(folio_index));
for (Conductor *wire : cabled.folios().at(folio_index)->conductors())
if (wire->uuid().toString() == uuid) built_wire = wire;
check(built_wire && built_wire->properties().m_cable == value,
"a wire changed before its folio was built shows the change");
}
//The wire numbers and wiring list exports, and the terminal
//numbering dialog: as when built, building only the folios
//holding a terminal (D5)
{
QETProject exported(zipped);
const int unbuilt = exported.unloadedFolioCount();
check(ConductorNumExport(&exported).wiresNum() == ConductorNumExport(&eager).wiresNum(),
"wire numbers exported as when built");
const QString wiring = WiringListExport(&exported).toCsvString(), usual_wiring = WiringListExport(&eager).toCsvString();
if (wiring != usual_wiring) {
const QStringList a = wiring.split('\n'), b = usual_wiring.split('\n');
for (const QString &line : b) if (!a.contains(line)) std::printf(" wiring when built: %s\n", qPrintable(line));
for (const QString &line : a) if (!b.contains(line)) std::printf(" wiring lazily: %s\n", qPrintable(line));
}
check(wiring == usual_wiring, "wiring list exported as when built");
check(exported.unloadedFolioCount() == unbuilt, "exporting wire numbers and the wiring list builds no folio");
{ TerminalNumberingDialog dialog(nullptr, &exported); }
int with_terminals = 0;
for (Diagram *folio : folios)
for (Element *element : folio->elements())
if (element->elementData().m_type == ElementData::Terminal) { ++with_terminals; break; }
std::printf(".qetz terminal numbering: %d folios built, %d hold a terminal\n",
unbuilt - exported.unloadedFolioCount(), with_terminals);
check(exported.folios().size() - exported.unloadedFolioCount() <= with_terminals + (folios.size() - unbuilt),
"terminal numbering builds only the folios holding a terminal");
}
//Saved as a .qetz without building the folios not touched
//(D4): their parts and rows copied as they were
QETProject untouched(zipped);
const int unbuilt = untouched.unloadedFolioCount();
const QString kept_path = output + "/kept.qetz";
check(untouched.writeContainerFile(kept_path, &error), "saved as a .qetz without building");
std::printf(".qetz saved untouched: %d of %d folios not built before, %d after\n",
unbuilt, int(folios.size()), untouched.unloadedFolioCount());
check(untouched.unloadedFolioCount() > 0, "saving an untouched .qetz does not build every folio");
qputenv("QET_EAGER_FOLIOS", "1");
QETProject kept_back(kept_path);
qunsetenv("QET_EAGER_FOLIOS");
check(kept_back.toXml().toString() == eager.toXml().toString(),
"a .qetz saved without building holds the project");
//After everyday edits on a folio
QETProject edited_z(zipped);
edit(edited_z, busiest);
const QString edited_path = output + "/edited.qetz";
check(edited_z.writeContainerFile(edited_path, &error), "edited .qetz saved");
std::printf(".qetz saved after the edits: %d folios not built\n", edited_z.unloadedFolioCount());
check(edited_z.unloadedFolioCount() > 0, "saving an edited .qetz does not build every folio");
//Against the same edits made with every folio built, saved and
//read back too: what a save and a reading do to a project is
//not what is checked here
const QString usual_path = output + "/edited-usual.qetz";
qputenv("QET_EAGER_FOLIOS", "1");
{
QETProject edited_usual(path);
edit(edited_usual, busiest);
check(edited_usual.writeContainerFile(usual_path, &error), "edits saved with every folio built");
}
QETProject edited_back(edited_path), usual_back(usual_path);
qunsetenv("QET_EAGER_FOLIOS");
check(sortedLines(edited_back.toXml().toString(), path) == sortedLines(usual_back.toXml().toString(), path),
"an edited .qetz saved without building holds what the edits made");
//Each symbol's links as saved, partners on folios not built
//included, in their order (D5a)
check(savedLinks(edited_path, path) == savedLinks(usual_path, path),
"an edited .qetz saved without building keeps every symbol's links");
} }
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) {
+1 -1
View File
@@ -54,7 +54,7 @@ subprocess.run([ninja, '-C', str(build), '-f', probe_manifest.name, target], che
output = build / 'lazy-folios' output = build / 'lazy-folios'
output.mkdir(exist_ok=True) output.mkdir(exist_ok=True)
report = output / 'results.txt' report = output / 'results.txt'
result = subprocess.run([str(probe_binary), str(fixture), str(report), str(output)], timeout=500) result = subprocess.run([str(probe_binary), str(fixture), str(report), str(output)], timeout=1500)
if report.exists(): if report.exists():
print(report.read_text(encoding='utf-8', errors='replace')) print(report.read_text(encoding='utf-8', errors='replace'))
sys.exit(result.returncode) sys.exit(result.returncode)
+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"
+203
View File
@@ -0,0 +1,203 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "qetapptest.h"
#include "print/projectprintwindow.h"
#include "qetproject.h"
#include <QFile>
#include <QPrinter>
#include <QRegularExpression>
#include <QTemporaryDir>
// The print window's PDF export turns each page to its folio (#1480). It
// used one orientation for the whole document, landscape unless changed,
// so a portrait folio was shrunk onto a landscape page.
// fixtures/pdf_page_a3.qet has an A3 landscape folio, then an A3 portrait
// one.
class tst_printpdforientation : public QObject
{
Q_OBJECT
QTemporaryDir m_dir;
struct Page
{
QSizeF size;
QList<QRectF> links;
};
// Each page's MediaBox and the Rect of each of its links, in page
// order. Qt writes every object uncompressed, a page's annotations as
// "/Annots n 0 R" pointing at an array of annotation references.
static QList<Page> pages(const QString &pdf_path)
{
QFile file(pdf_path);
if (!file.open(QIODevice::ReadOnly))
return {};
const QString pdf = QString::fromLatin1(file.readAll());
QHash<int, QString> objects;
static const QRegularExpression object(
QStringLiteral(R"((\d+) 0 obj(.*?)endobj)"),
QRegularExpression::DotMatchesEverythingOption);
for (auto it = object.globalMatch(pdf); it.hasNext();) {
const auto m = it.next();
objects.insert(m.captured(1).toInt(), m.captured(2));
}
static const QRegularExpression ref(QStringLiteral(R"((\d+) 0 R)"));
static const QRegularExpression kids(
QStringLiteral(R"(/Type\s*/Pages.*?/Kids\s*\[(.*?)\])"),
QRegularExpression::DotMatchesEverythingOption);
static const QRegularExpression box(
QStringLiteral(R"(/MediaBox \[0 0 ([0-9.]+) ([0-9.]+)\])"));
static const QRegularExpression annots(QStringLiteral(R"(/Annots (\d+) 0 R)"));
static const QRegularExpression link(
QStringLiteral(R"(/Subtype /Link\s*/Rect \[\s*([-0-9.]+)\s+([-0-9.]+)\s+([-0-9.]+)\s+([-0-9.]+)\s*\])"));
QList<Page> result;
for (auto k = ref.globalMatch(kids.match(pdf).captured(1)); k.hasNext();) {
const QString page = objects.value(k.next().captured(1).toInt());
const auto b = box.match(page);
Page p;
p.size = QSizeF(b.captured(1).toDouble(), b.captured(2).toDouble());
const QString list = objects.value(annots.match(page).captured(1).toInt());
for (auto a = ref.globalMatch(list); a.hasNext();) {
const auto l = link.match(objects.value(a.next().captured(1).toInt()));
if (l.hasMatch())
p.links << QRectF(QPointF(l.captured(1).toDouble(), l.captured(2).toDouble()),
QPointF(l.captured(3).toDouble(), l.captured(4).toDouble()));
}
result << p;
}
return result;
}
// Export @p project as the window's "Export in pdf" button does, from
// an A3 landscape printer with fixed margins.
QList<Page> exportPdf(QETProject *project, bool fit_in_page)
{
const QString out = m_dir.filePath(QStringLiteral("out.pdf"));
QFile::remove(out);
QSettings().setValue("print/default/fitinpage", fit_in_page);
auto printer = new QPrinter();
printer->setPageSize(QPageSize(QPageSize::A3));
printer->setPageOrientation(QPageLayout::Landscape);
printer->setOutputFormat(QPrinter::PdfFormat);
printer->setOutputFileName(out);
ProjectPrintWindow window(project, printer);
// After the window loaded the margins an earlier export saved.
printer->setPageMargins(QMarginsF(7, 7, 7, 7), QPageLayout::Millimeter);
if (!QMetaObject::invokeMethod(&window, "print"))
return {};
QCoreApplication::processEvents(); // the deferred PDF post-processing
return pages(out);
}
// examples/industrial.qet, its folios made portrait (taller rows), all
// of them or all but the first.
QString portraitIndustrial(bool first_too)
{
QFile file(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
if (!file.open(QIODevice::ReadOnly))
return {};
QString xml = QString::fromUtf8(file.readAll());
static const QRegularExpression diagram(QStringLiteral("<diagram [^>]*>"));
static const QRegularExpression rows(QStringLiteral(R"(\brows="\d+")"));
QString result;
qsizetype last = 0;
int n = 0;
for (auto it = diagram.globalMatch(xml); it.hasNext();) {
const auto m = it.next();
QString tag = m.captured();
if (n++ > 0 || first_too)
tag.replace(rows, QStringLiteral("rows=\"20\""));
result += xml.mid(last, m.capturedStart() - last) + tag;
last = m.capturedEnd();
}
result += xml.mid(last);
const QString path = m_dir.filePath(
first_too ? QStringLiteral("portrait.qet") : QStringLiteral("mixed.qet"));
QFile out(path);
if (!out.open(QIODevice::WriteOnly))
return {};
out.write(result.toUtf8());
return path;
}
private slots:
void initTestCase()
{
QVERIFY(m_dir.isValid());
}
void pagesFollowFolios()
{
const QString fixture = QFINDTESTDATA("fixtures/pdf_page_a3.qet");
QVERIFY2(!fixture.isEmpty(), "fixture project not found");
QETProject project(fixture);
QCOMPARE(project.state(), QETProject::Ok);
QCOMPARE(project.diagrams().size(), 2);
const QList<Page> result = exportPdf(&project, true);
QCOMPARE(result.size(), 2);
const QSize a3 = QPageSize(QPageSize::A3).sizePoints();
QCOMPARE(result.at(0).size, QSizeF(a3.height(), a3.width())); // landscape
QCOMPARE(result.at(1).size, QSizeF(a3)); // portrait
}
// Without "Fit in page" a folio is spread over several pages, all in the
// orientation chosen in the window, as before.
void tiledPagesKeepChosenOrientation()
{
const QString fixture = QFINDTESTDATA("fixtures/pdf_page_a3.qet");
QETProject project(fixture);
QCOMPARE(project.state(), QETProject::Ok);
const QList<Page> result = exportPdf(&project, false);
QVERIFY(!result.isEmpty());
for (const Page &page : result)
QVERIFY2(page.size.width() > page.size.height(), "a tiled page is not landscape");
}
// The cross-reference links on a page turned during the export land
// where they do when the export starts on that orientation: the same
// project with every folio portrait.
void linksOnTurnedPages()
{
const QString mixed_path = portraitIndustrial(false);
const QString portrait_path = portraitIndustrial(true);
QVERIFY(!mixed_path.isEmpty() && !portrait_path.isEmpty());
QETProject mixed_project(mixed_path);
QCOMPARE(mixed_project.state(), QETProject::Ok);
const QList<Page> mixed = exportPdf(&mixed_project, true);
QETProject portrait_project(portrait_path);
QCOMPARE(portrait_project.state(), QETProject::Ok);
const QList<Page> portrait = exportPdf(&portrait_project, true);
QCOMPARE(mixed.size(), portrait.size());
QVERIFY(mixed.size() > 1);
QVERIFY(mixed.first().size.width() > mixed.first().size.height());
int links = 0;
for (int i = 1; i < mixed.size(); ++i) {
QVERIFY(mixed.at(i).size.height() > mixed.at(i).size.width());
QCOMPARE(mixed.at(i).links.size(), portrait.at(i).links.size());
for (int j = 0; j < mixed.at(i).links.size(); ++j) {
const QRectF a = mixed.at(i).links.at(j);
const QRectF b = portrait.at(i).links.at(j);
QVERIFY2(qAbs(a.left() - b.left()) < 0.01 && qAbs(a.top() - b.top()) < 0.01
&& qAbs(a.right() - b.right()) < 0.01
&& qAbs(a.bottom() - b.bottom()) < 0.01,
qPrintable(QStringLiteral("page %1 link %2 moved").arg(i + 1).arg(j)));
++links;
}
}
QVERIFY2(links > 100, "too few links to test");
}
};
QET_APP_TEST_MAIN(tst_printpdforientation)
#include "tst_printpdforientation.moc"
+443 -3
View File
@@ -69,6 +69,70 @@ class tst_qetcontainer : public QObject
return QetContainer::split(*original, QStringLiteral("tst_qetcontainer"), &error); return QetContainer::split(*original, QStringLiteral("tst_qetcontainer"), &error);
} }
//@p document without what QElectroTech computes, which a .qetz
//does not keep (format 4); tst_resaveunchanged checks that
//QElectroTech gives it back
static QDomDocument withoutComputed(const QDomDocument &document)
{
QDomDocument copy = document.cloneNode(true).toDocument();
const bool own_uuids = QetContainer::ownUuids(copy.documentElement());
for (QDomElement diagram = copy.documentElement().firstChildElement(QStringLiteral("diagram"));
!diagram.isNull() ; diagram = diagram.nextSiblingElement(QStringLiteral("diagram")))
QetContainer::dropComputed(diagram, own_uuids);
return copy;
}
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(withoutComputed(original).documentElement(), back.documentElement(), QString());
}
private slots: private slots:
void sameDocumentBack_data() void sameDocumentBack_data()
{ {
@@ -100,7 +164,8 @@ private slots:
QVERIFY2(QetZip::fromBytes(QetZip::toBytes(entries, &error), &unzipped, &error), qPrintable(error)); QVERIFY2(QetZip::fromBytes(QetZip::toBytes(entries, &error), &unzipped, &error), qPrintable(error));
QDomDocument back; QDomDocument back;
QVERIFY2(QetContainer::join(unzipped, &back, &error), qPrintable(error)); QVERIFY2(QetContainer::join(unzipped, &back, &error), qPrintable(error));
const QString d = difference(original.documentElement(), back.documentElement(), QString()); const QString d = difference(withoutComputed(original).documentElement(),
back.documentElement(), QString());
QVERIFY2(d.isEmpty(), qPrintable(d)); QVERIFY2(d.isEmpty(), qPrintable(d));
} }
@@ -136,7 +201,9 @@ private slots:
// The engineering data is in project.sqlite and nowhere else: the folio // The engineering data is in project.sqlite and nowhere else: the folio
// files hold no symbol information, links, wire numbers or title-block // files hold no symbol information, links, wire numbers or title-block
// fields, and the database holds them all (industrial.qet: 50 folios). // fields, nor what QElectroTech computes from them (a wire's
// description of its ends, a symbol text showing its information), and
// the database holds them all (industrial.qet: 50 folios).
void databaseHoldsTheData() void databaseHoldsTheData()
{ {
QDomDocument original; QDomDocument original;
@@ -181,6 +248,37 @@ private slots:
sqlite3_close(db); sqlite3_close(db);
} }
// Nor does a folio file hold what QElectroTech computes (format 3): a
// wire's description of its ends, a symbol text showing its information.
// A file holding both, so that the check can fail.
void computedNotKept()
{
QDomDocument original;
QString error;
QVERIFY(QetContainer::parse(read(QStringLiteral(QET_EXAMPLES_DIR "/schema_unifilaire_voltaique2.qet")),
&original, &error));
const QDomNodeList texts = original.elementsByTagName(QStringLiteral("dynamic_elmt_text"));
bool filled = false;
for (int i = 0 ; i < texts.size() && !filled ; ++i) {
QDomElement text = texts.at(i).toElement();
if (text.attribute(QStringLiteral("text_from")) != QLatin1String("ElementInfo")) continue;
QDomElement content = text.firstChildElement(QStringLiteral("text"));
while (content.hasChildNodes()) content.removeChild(content.firstChild());
content.appendChild(original.createTextNode(QStringLiteral("COMPUTED-LABEL")));
filled = true;
}
QVERIFY(filled);
QVERIFY(original.toString().contains(QStringLiteral("element1_label=")));
const QList<QetZip::Entry> entries = QetContainer::split(original, QString(), &error);
QVERIFY2(!entries.isEmpty(), qPrintable(error));
for (const QetZip::Entry &e : entries) {
if (!e.name.startsWith(QStringLiteral("folios/"))) continue;
QVERIFY2(!e.data.contains("element1_label=") && !e.data.contains("terminalname2="),
qPrintable(e.name));
QVERIFY2(!e.data.contains("COMPUTED-LABEL"), qPrintable(e.name));
}
}
// Terminal strips move into the database, and come back exactly: // Terminal strips move into the database, and come back exactly:
// industrial.qet with two strips on its own terminal symbols, parsed as // industrial.qet with two strips on its own terminal symbols, parsed as
// a save leaves it (no whitespace text). // a save leaves it (no whitespace text).
@@ -217,7 +315,8 @@ private slots:
QVERIFY2(!e.data.contains("<terminal_strip>"), "the strips stayed in project.xml"); QVERIFY2(!e.data.contains("<terminal_strip>"), "the strips stayed in project.xml");
QDomDocument back; QDomDocument back;
QVERIFY2(QetContainer::join(entries, &back, &error), qPrintable(error)); QVERIFY2(QetContainer::join(entries, &back, &error), qPrintable(error));
const QString d = difference(original.documentElement(), back.documentElement(), QString()); const QString d = difference(withoutComputed(original).documentElement(),
back.documentElement(), QString());
QVERIFY2(d.isEmpty(), qPrintable(d)); QVERIFY2(d.isEmpty(), qPrintable(d));
} }
@@ -242,7 +341,148 @@ 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, nullptr, &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, nullptr, &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, nullptr, &format), qPrintable(error));
QCOMPARE(format, later);
const QString d = difference(withoutComputed(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; written without keys (format 4)
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, const char *columns = "m.name, p.name, p.type") {
QStringList out;
sqlite3_stmt *st = nullptr;
sqlite3_prepare_v2(db, QByteArray("SELECT ") + columns + " 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 < sqlite3_column_count(st) ; ++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);
const QStringList keys = describe(from_file, "p.pk + p.\"notnull\"");
QVERIFY(!keys.isEmpty());
QCOMPARE(keys.count(QStringLiteral("0")), keys.size());
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 +512,108 @@ 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.
void garbageDatabaseRefused()
{
//Bytes that are no database: refused, not read (crashed reading
//the first statement until stage D7)
QDomDocument original;
QList<QetZip::Entry> entries = splitFile(QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet"), &original);
bool replaced = false;
for (QetZip::Entry &entry : entries)
if (entry.name == QLatin1String("project.sqlite")) {
entry.data = QByteArray("not a database").repeated(100);
replaced = true;
}
QVERIFY(replaced);
QDomDocument back;
QString error;
QVERIFY(!QetContainer::join(entries, &back, &error));
QVERIFY(!error.isEmpty());
}
// 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("symbol row twice")
<< QByteArray("INSERT INTO element SELECT * 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 +642,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");
+71 -4
View File
@@ -8,6 +8,10 @@
#include <QRegularExpression> #include <QRegularExpression>
#include <QTemporaryDir> #include <QTemporaryDir>
#include "container/qetzip.h"
#include <sqlite3.h>
// Saving a project that was just saved must change nothing. Two things // Saving a project that was just saved must change nothing. Two things
// made the second save differ from the first, both cleanup done on save // made the second save differ from the first, both cleanup done on save
// but not on load: // but not on load:
@@ -306,6 +310,10 @@ private slots:
QVERIFY2(!zipped.isEmpty(), "--resave as .qetz failed"); QVERIFY2(!zipped.isEmpty(), "--resave as .qetz failed");
QVERIFY2(read(zipped).startsWith("PK\x03\x04"), "the .qetz is not a zip"); QVERIFY2(read(zipped).startsWith("PK\x03\x04"), "the .qetz is not a zip");
QVERIFY2(read(zipped).size() < read(straight).size() / 2, "the .qetz is not smaller"); QVERIFY2(read(zipped).size() < read(straight).size() / 2, "the .qetz is not smaller");
QCOMPARE(databaseTables(zipped), QStringList({
"conductor", "element", "element_info", "folio", "folio_property", "link", "meta",
"project_property", "strip_bridge", "strip_bridge_terminal", "strip_terminal",
"terminal_strip"}));
const QString back = resave(zipped); const QString back = resave(zipped);
QVERIFY2(!back.isEmpty(), "opening the .qetz failed"); QVERIFY2(!back.isEmpty(), "opening the .qetz failed");
@@ -317,6 +325,31 @@ private slots:
QCOMPARE(without_save_stamps(read(back)), without_save_stamps(read(straight))); QCOMPARE(without_save_stamps(read(back)), without_save_stamps(read(straight)));
} }
// The tables of the database in the .qetz @p path: the document's
// engineering data only, each fact once -- not QElectroTech's own
// project database, built from it when the project opens
static QStringList databaseTables(const QString &path)
{
QList<QetZip::Entry> entries;
if (!QetZip::fromBytes(read(path), &entries)) return {QStringLiteral("not a zip")};
QByteArray database;
for (const QetZip::Entry &entry : std::as_const(entries))
if (entry.name == QLatin1String("project.sqlite")) database = entry.data;
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 *list = nullptr;
QStringList tables;
sqlite3_prepare_v2(db, "SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name", -1, &list, nullptr);
while (list && sqlite3_step(list) == SQLITE_ROW)
tables << QString::fromUtf8(reinterpret_cast<const char *>(sqlite3_column_text(list, 0)));
sqlite3_finalize(list);
sqlite3_close(db);
return tables;
}
// QElectroTech run in a sandbox of its own with @p args, the store // QElectroTech run in a sandbox of its own with @p args, the store
// check on (QET_CHECK_ELEMENT_INFO_STORE); its output // check on (QET_CHECK_ELEMENT_INFO_STORE); its output
QString runChecked(const QStringList &args, bool eager = false) QString runChecked(const QStringList &args, bool eager = false)
@@ -328,7 +361,7 @@ private slots:
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1")); env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
env.insert(QStringLiteral("QET_CHECK_ELEMENT_INFO_STORE"), QStringLiteral("1")); env.insert(QStringLiteral("QET_CHECK_ELEMENT_INFO_STORE"), QStringLiteral("1"));
if (eager) { if (eager) {
env.remove(QStringLiteral("QET_LAZY_FOLIOS")); //every folio built on opening env.insert(QStringLiteral("QET_EAGER_FOLIOS"), QStringLiteral("1")); //every folio built on opening
} }
env.insert(QStringLiteral("HOME"), home); env.insert(QStringLiteral("HOME"), home);
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config")); env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
@@ -340,8 +373,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 +394,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()
@@ -479,6 +512,40 @@ private slots:
QCOMPARE(rowsOf(edited), rowsOf(reopened)); QCOMPARE(rowsOf(edited), rowsOf(reopened));
} }
// A project read from a .qet and saved where it is, is saved as a .qetz
// beside it, and the .qet is left as it was (stage D1).
// What the .qetz holds is what a plain save of the .qet holds.
void qetSavedAsQetzBeside()
{
#ifndef QET_HAS_SCRIPTING
QSKIP("needs --run: this QElectroTech is built without Qt Qml");
#endif
const QString qet = m_dir.filePath(QStringLiteral("beside.qet"));
QFile::remove(qet);
QVERIFY(QFile::copy(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"), qet));
const QByteArray before = read(qet);
const QString script = m_dir.filePath(QStringLiteral("beside.js"));
QFile js(script);
QVERIFY(js.open(QIODevice::WriteOnly));
js.write("qet.log('SAVED ' + qet.save(''));\n");
js.close();
const QString log = runChecked({QStringLiteral("--run"), script, qet});
QVERIFY2(log.contains(QStringLiteral("SAVED true")), qPrintable(log.right(400)));
QCOMPARE(read(qet), before);
const QString qetz = m_dir.filePath(QStringLiteral("beside.qetz"));
QVERIFY2(read(qetz).startsWith("PK\x03\x04"), "no .qetz beside the .qet");
auto without_save_stamps = [](QByteArray xml) {
static const QRegularExpression stamp(QStringLiteral(
"\\s*<property [^>]*name=\"saved(filename|filepath|filenamedir|date|date-us|date-eu|time)\"[^>]*>[^<]*</property>"));
return QString::fromUtf8(xml).remove(stamp);
};
const QString straight = resave(qet);
const QString back = resave(qetz);
QVERIFY(!straight.isEmpty() && !back.isEmpty());
QCOMPARE(without_save_stamps(read(back)), without_save_stamps(read(straight)));
}
// A title-block value that is a single space is kept through two saves // A title-block value that is a single space is kept through two saves
// (#973), and a value with accents comes back as it went in. // (#973), and a value with accents comes back as it went in.
+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"