Commit Graph

148 Commits

Author SHA1 Message Date
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 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 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 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 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 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 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
Laurent Trinques 77e2a9bfc5 Speed up opening big projects with lazy folios 2026-10-13 11:01:21 +02:00
ispyisail fac09253a0 Open and save projects as .qetz (zipped, opt-in)
Third stage of the zipped project file (#1440). A project is saved as a
.qetz when its file name ends in .qetz -- Save As offers it as a second
file type, and --resave, --set-titleblock, the script API's save() and
the live-mode snapshot follow the name too. Saving a .qet project still
writes a .qet; nothing changes unless the user picks .qetz.

Opening recognises a .qetz by its content (a zip), not its name, and
reads it as bytes: text mode would alter it on Windows. The project
document is joined back and read by the same code as a .qet. Crash
backups stay plain XML. Drag and drop, the open dialog and the
numbering import accept .qetz; the database export names its file from
the project's name without assuming ".qet".

Parts are now written without added indentation (one line each): the
symbol definitions are kept as read, whitespace included, and parts
that gained indentation made the next save differ (five forum projects).

Tests: tst_resaveunchanged::containerSavesTheSameProject (four examples
saved as .qetz, then as .qet, equal the straight .qet; pictures checked
to fail without the fix), tst_qetcontainer::noWhitespaceAdded. Outside
ctest: 341 of 341 projects QET can open (examples, bugtracker, GitHub,
forum) give the same .qet through a .qetz; 181.6 MB of .qet became
36.9 MB of .qetz.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Wga9B92BNurDEtxxtCDxf
2026-10-11 06:24:29 +13:00
ispyisail 5e6677ebec Keep the project database current when folios, symbols and wires change
The project database is kept up to date edit by edit, and a script's
qet.query() and the folio tables read it without a full rebuild. Three
edits left it out of date until the next rebuild:

- adding or removing a folio: the folios after it kept their old
  position and folio number, and a removed folio's diagram_info row
  stayed;
- moving a symbol: its folio cell (element.pos) stayed where the symbol
  was added;
- deleting a wire: its two ends stayed in the terminal table, which
  lists the terminals a wire ends on.

Positions and folio numbers are now written again after a folio is
added, removed or moved (one function for the three), the removed
folio's diagram_info row goes, a moved symbol's cell is written when
the database is next read (as drawing items are), and a deleted wire's
ends go unless another wire ends there.

Found by comparing the tables after random scripted edits with those
a fresh open of the saved project gives. Left alone: labels and wire
numbers made from a folio formula, which the drawing itself keeps until
the project is reopened.

Tests (tst_databasefromdocument): folioAddedAndRemoved,
movedElementCell, deletedWireTerminals; each fails without its part of
the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Wga9B92BNurDEtxxtCDxf
2026-10-10 19:07:03 +13:00
ispyisail 3089408b19 Add a link table to the project database
One row per element and element it is linked to -- a coil and its
contacts, a pair of folio reports -- from each side, with the contact
group the element saved for it. Nothing reads it yet: cross-references
are the main thing one folio needs from another, and this is what lets
them be answered without building every folio.

Filled like the other tables: from the document when it can be read
exactly (a link only one side lists, between kinds that cannot link, or
giving a contact a second coil is left to the folios), from the folios
otherwise. A link made or undone afterwards rewrites that element's rows
when the database is next read, as drawing items are, since the contact
group is set after the link is made.

Test: tst_databasefromdocument compares the link table both ways on
every example; linksAreListed checks a coil and contact (with a group)
in m_000.qet, oneSidedLinkFallsBack the refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Wga9B92BNurDEtxxtCDxf
2026-10-10 17:22:56 +13:00
ispyisail 80c8e297ec Regenerate the English source conversion on master 8b5a09f3fc
Generated by tools/qet-en-source/regenerate.sh, not edited by hand. The
tree is the fresh conversion; this commit only joins it to the PR's
history so the branch is never force-pushed.
2026-10-09 05:05:49 +13:00
ispyisail f5962ecd06 Make English the source language of the interface text
Every tr() source string in the code and the forms is now the English
text, and French is a translation (qet_fr.ts) like the other languages.
Generated, not hand-edited: master ef795a21e5 converted by
qet-en-source (harness 4c4ddc9).

3144 messages get an English source, 665 already had one, 0 left French; 8 shared-wording pairs disambiguated.
C++: 2904 literals in 233 files. Forms: 809 strings in 65 files. All 34 .ts files re-keyed; qet_fr.ts filled.
lupdate check: PASS. What each of 38 languages shows, old vs new: same 118266, English instead of French 38433, comment fallback 51, differences 0.
2026-10-08 12:50:13 +13:00
Beat Hangartner e3a1a67c66 Recognise seqhf_1 when loading sequential numbers saved the old way
Before the <sequentialNumbers> element, an element's or conductor's
sequential numbers were saved as the attributes sequ_1, sequf_1,
seqt_1, seqtf_1, seqh_1 and seqhf_1. The readers check for one of them
to take the old route, and all three lists had the same slip: sequf_1
twice, seqhf_1 never (Element::fromXml, Conductor::fromXml, and
readSequence() in the project database). A file whose only old
sequence was the hundred-folio one took the new route, found no
<sequentialNumbers>, and lost it; the database built its labels from
an empty sequence instead of refusing the fast path as it does for the
other five attributes.

Name seqhf_1 in the three lists.

No file-format change: nothing is written differently, and a file with
any of the other five attributes loads exactly as before.

Tests: tst_legacysequentialattributes runs the binary's --resave on a
fixture whose element and conductor carry only seqhf_1 (and a second
pair carrying sequ_1 as a control) and reads the <sequentialNumbers>
written back. The two seqhf_1 cases fail on master.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Beat Hangartner <beat@hangartners.ch>
2026-10-07 23:38:34 +02:00
Laurent Trinques e4e8cc2892 Merge pull request #1320 from bhangart/feature/free-text-width
Let free texts keep a width they wrap to
2026-10-06 16:18:25 +02:00
Beat Hangartner 315de47a12 Keep the width of free texts in the project database
The independent_text table had the text and rotation of a free text but
not its width, and a width change did not update the row at all -- not
even its width and height, which follow the wrapped text. Add a
text_width column (NULL for the automatic width) and update the row on
textWidthChanged.

Test: tst_scriptfreetextwidth reads the table with qet.query() (fails
before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 09:50:08 +02:00
ispyisail 8434b88eb8 Add options to leave junctions and contact blocks out of the parts list (#1178)
Since #849 the parts list has a row for every contact block (slave) and
terminal-type element. Users who draw the dots and bends where wires
branch as terminal-type symbols (114_connections) get one empty row per
junction: 109 of 151 rows on one reported cabinet.

Two options, both off by default so existing exports are unchanged:
- leave out the contact blocks: --no-slaves, "no_slaves" in qet_export,
  or uncheck the new "Contacts esclaves" element type in the dialog;
- leave out the junctions: terminal-type elements with no label,
  designation, manufacturer or manufacturer reference. --no-junctions,
  "no_junctions", or "Laisser de côté les jonctions" in the dialog.
  A terminal block with a label or a part number stays.

The element type filter had no box for slaves, so every query it built
left them out: the export dialog never listed contact blocks, although
the command line has since #849. The dialog now checks the new box by
default and gives the same rows as --export-bom. The box is unchecked by
default elsewhere, so nomenclature tables keep their rows.

docs/smart-device-bom.md still said slaves and terminals were excluded;
corrected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:08:44 +13:00
Laurent Trinques e413fde272 Merge pull request #1253 from jp2images/svg-icons-b
Convert the remaining theme icons to SVG and drop unused PNGs (#1063)
2026-10-02 13:56:56 +02:00
Jeff Patterson 6753aa3ce7 Loaded icons by theme name and removed the PNGs nothing uses 2026-10-02 05:47:40 -05:00
ispyisail e345d4e5d0 Give each end of the wiring export its terminal index and uuid
Most shipped symbols leave their terminals unnamed, so --export-wiring
wrote both terminal columns empty and a reader had to work each end out
from the .qet: terminal uuid, then its place in the definition, then the
index. The terminal table now keeps the index the scripting API's
addConductor() takes, from both fills of the database (the document and
the folios give the same value: Element::parseTerminal()'s sort, top to
bottom then left to right), and wiring_list_view and the CSV gain
from/to_terminal_index and from/to_terminal_uuid as their last columns.

The index is left empty for two terminals at one point, where the sort
is not stable and the index is not defined; the uuid tells them apart.
The wiring list dialog reads named columns and is unaffected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 22:30:49 +13:00
ispyisail b1abab1241 Work out formula labels and wire texts from the file, with the folios' code
The first fill from the document stored the label and wire text saved in
the file. QElectroTech saves what it last worked out, not what the formula
gives now: K%total-%id saved as K1-1 on a 3-folio project is K3-1 on the
folios, and the same for a wire's formula. So the document fill disagreed
with the folio fill on any file whose folios had been added or moved.

AssignVariables now works from a FormulaContext: the folio's number,
index, total, plant, location, title-block and project variables, and the
element's grid cell and prefix or the wire's four properties. The Diagram
overload fills one from a built folio, the database fills one from the
file, and both call the same evaluation.

Also brought in line with Diagram::fromXml(): an element Element::valideXml()
rejects is skipped with its wires; a folio where two elements number their
terminals alike (Element::fromXml() refuses one, by geometry), a frozen
formula wire text, and the older sequential-number attributes are left to
the folios. Each folio's border now starts from the defaults a new Diagram
has, not the previous folio's values.

tst_databasefromdocument: formulasAreWorkedOut (every variable kind, stale
saved values), unbuiltElementIsLeftOut, clashingTerminalIdsFallBack. Each
fails with its fix removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 16:08:45 +13:00
ispyisail a0d5c4fa9c Fill the project database from the file as it is read
The project database -- behind parts lists, summary tables and the wiring
list -- was filled by walking the built folios: every element and
conductor object of every scene. Filling it from the document the project
was just read from instead is what the database needs before a project
could be opened without building every folio (qelectrotech-docker
DB-FROM-XML-SCOPE.md).

updateDB(document), called by readProjectXml(), fills diagram,
diagram_info, element, element_info, terminal and conductor from the
document, through the code the folios use: a BorderTitleBlock read from
each folio's XML gives the title-block values and each element's grid
cell, the embedded collection gives each definition, and the binders are
shared with the live path. Shapes, texts and pictures still come from the
folios (their boxes need fonts and pens).

It falls back to the folios, saying why in the log, when the document
does not carry what that needs: an item without a saved uuid (older files
-- the folios derive them on load), a conductor naming its ends the older
way, a %autonum folio number, a terminal showing its master's label, a
missing or unbuildable definition. A symbol label computed from a formula
is the one saved in the file, which QElectroTech writes as it computes it
on every save. QET_DATABASE_FROM_FOLIOS=1 forces the folio path.

tst_databasefromdocument saves every example once, then fills both ways
and requires identical tables (24/24, filled from the document each
time), and checks that an older file falls back with its reason. Red when
the document path is made to write a wrong grid cell. Database phase of
loading unchanged: industrial.qet 0.146 s vs 0.160 s, Polonez 0.040 s vs
0.038 s (median of 5).

QETProject::projectWideProperties() is split out of
updateDiagramsFolioData() so both fills use the same title-block context.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 15:38:20 +13:00
ispyisail 3139fa54f7 Fix folio dates a century early in the project database
diagram_info.date -- and project_summary_view.date, which the summary
table dialog queries -- held the folio date read back from the title
block's text: QLocale::system().toDate() of the string
updateDiagramContextForTitleBlock() writes in the locale's short format.
Where that format has a two-digit year (en_US "M/d/yy") Qt reads the year
back as 19xx, so 741.qet's 2010-09-21 became 1910-09-21, and so did every
dated folio of the 24 examples (109 of 109).

bindDiagramInfoValues() now binds the folio's own date,
BorderTitleBlock::date(), the value that text was made from. A folio set
to show the current date or none gives the same date as before.

tst_dbfoliodate runs --run on examples/741.qet under LC_ALL=en_US.UTF-8
and expects 2010-09-21 from project_summary_view: 1910-09-21 without this
change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:14:15 +13:00
Laurent Trinques 6637127910 Merge branch 'master' into feat/1070-item-groups 2026-09-27 20:15:27 +02:00
ispyisail adcf11270c Upgrade old element type names in saved query presets too
A query saved to nomenclature.json (Load/Save in the query editor, since
2020) before June 2022 still says element_type = 'Simple'. Loading it
ticked none of the type boxes and kept the old query, so the new table
came out empty. Pass it through LegacyElementTypes::upgradeQuery() as
ProjectDBModel::fromXml() now does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 05:55:20 +13:00
ispyisail 166ee34f52 Fix parts lists saved before June 2022 losing rows and tables on open
A nomenclature table saved by an older version filters on the element
type names the project database used then: element_type = 'Simple',
'Terminale', 'Master'. Commit 2e70d2e59 (June 2022) changed the database
to "simple", "terminal", "master", and SQLite compares text
case-sensitively, so such a table silently lost every row of that type
on open. Its continuation tables were then empty, and
removeUselessNextTable() deleted them: opening and saving the example
industrial.qet removed seven of its ten parts-list tables (folios 44-50),
and the remaining three listed 76 of 258 parts.

ProjectDBModel::fromXml() now rewrites old names in element_type = '...'
comparisons to the current ones (LegacyElementTypes::upgradeQuery()),
which also lets the query editor tick the right boxes again. A query
saved by a current version is unchanged, and so is any other text that
happens to contain "Simple".

Checked in the GUI on industrial.qet, open then save: master keeps
tables on 3 of folios 41-50, this keeps all 10, with 258 rows (the last
table 24 of 26) and the query saved as 'simple'. tst_legacyelementtypes
fails when a name maps wrongly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-27 21:46:30 +13:00
ispyisail f7591e0c9d Add grouping of folio items: Group and Ungroup (#1070)
Symbols, free texts, shapes and pictures can be grouped from the Edit
menu, the selection's context menu or the command search. Clicking one
member selects the group, so moving, copying and deleting act on all of
it; Ctrl+click on a member deselects the group; a rubber band touching
part of a group selects all of it when released. No default shortcut:
Ctrl+G is "jump to element".

A group is not an object in the scene. Each member keeps its place and
carries the group's uuid (QGraphicsItem::data()), saved as a "group"
attribute written only when set: a project without groups saves exactly
as before, and older versions open a grouped one and ignore the groups.
Re-parenting under a QGraphicsItemGroup would have made every member's
position group-relative; ElementTextItemGroup already needs nine special
cases for that.

- Selection is completed on clicks and at the end of a rubber band, not
  on every selectionChanged(): export, search and Tab select items
  themselves and must not have groups pulled back in.
- Project database: group_uuid on element, shape, independent_text and
  image, kept in step by projectDataBase::itemGroupChanged().
- Paste and folio duplication give each source group one new uuid.
- Undo of Ungroup restores each item's exact group.

Builds on #1065 (uuids and database rows for texts, shapes and images).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-27 21:14:47 +13:00
ispyisail 29fe731728 Fix read-only queries stopping after the first row
Since #1046, QETSql::execReadOnly() runs a query with PRAGMA query_only
set and switches it off before returning. Switching it off aborts a
statement SQLite is still stepping through ("abort due to ROLLBACK"), and
QSQLITE has already stepped to the first row by then. A query that
produces its rows as it goes -- a UNION ALL without ORDER BY -- therefore
came back with its first row only and no error. A sorted query was not
affected, because SQLite has read every row before returning the first.

Every query from the SQL box of a table, a saved <graphics_table> query
and the scripting qet.query() goes through here.

The checked run is now finished before query_only is switched off, and a
query that passed is run again for the caller. SQLite refuses a write at
its first step, so passing that step is what proves a statement reads
only; the second run is of a statement already shown to be read-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-27 14:00:16 +13:00
ispyisail 3a50cd9b0e Project database: tables for shapes, independent texts and images
Now that they carry a uuid, the folio's drawing furniture gets rows of its
own: shape, independent_text and image, plus drawing_item_view, which finds
any of them by uuid without knowing its kind first and says which folio it
is on.

Rows follow edits, not just loads. The script API's query() and every
other reader go through newQuery() without a rebuild, so an item's row is
queued on each change (moves, restyles, text edits, uuid renewal) and the
queue is flushed by newQuery() and updateDB() -- a queued write is a set
insertion, which matters for a drag that moves hundreds of items per mouse
step.

A pasted copy joins its folio still carrying its source's uuid and is only
renewed afterwards, so a flush in between must not let the copy overwrite
its source's row. Each row remembers the item that wrote it; another item
with the same uuid waits until uuidChanged() says it has its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 11:16:37 +13:00
ispyisail 45aec7735b Fix #1045: crash on selecting an element with the online-installer Qt
Since #983, projectDataBase::newQuery() checked a query with
sqlite3_prepare_v2() and sqlite3_stmt_readonly() on the handle of the
QSQLITE driver. Those calls go to the libsqlite3 QElectroTech links. The
QSQLITE plugin of the Qt online installer does not use that library: it
carries its own copy of SQLite, so the handle belongs to another library
and the call crashes. #1021 then put newQuery() on every element
selection, which is where #1045 hits it.

The check now runs the query with PRAGMA query_only set, through the
driver. SQLite refuses a write itself, before touching a row, so the CTE
prefix #983 closed ("WITH x AS (SELECT 1) DELETE FROM element") stays
closed. A refused or failed query comes back empty, because several
callers call exec() again on what newQuery() returns, after query_only
is off.

QElectroTech no longer calls the SQLite C API anywhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:06:07 +12:00
Magnus Hellströmer 9786006ca4 fix(database): align sqlite3 forward declarations 2026-09-23 21:45:16 +02:00
Laurent Trinques f10ef3ded7 Merge pull request #994 from ispyisail/revive/664-element-info-orphan
Fix element_info orphan row causing UNIQUE constraint errors on undo
2026-09-23 18:33:05 +02:00
Laurent Trinques d889a384aa Revert "fix: harden project database export" 2026-09-23 13:50:49 +02:00
ispyisail 3eea3059bf Fix element_info orphan row causing UNIQUE constraint errors on undo
Reviving #664, closed 2026-09-18 purely to clear a maintainer review
backlog (#630), not on merit; not superseded. Rewritten fresh against
current master rather than merged from the old branch -- that branch
predates the Qt6-only switch and much of dataBase/projectdatabase.cpp's
later rewrite, and the two had diverged too far for a textual merge to
be trustworthy.

projectDataBase::removeElement() only ran DELETE FROM element WHERE
uuid=:uuid. It never touched element_info, even though every element
also has a row there (element_uuid is its PRIMARY KEY, with a FOREIGN
KEY back to element.uuid that isn't enforced by this connection -- no
ON DELETE CASCADE in effect). So deleting an element left its
element_info row orphaned.

Re-adding an element with that same uuid later -- undo of that same
deletion, or a redo replaying it -- goes through addElement(), which
INSERTs into both tables. The element insert succeeds (that row really
was removed). The element_info insert hits the orphaned row's primary
key and fails, silently: the error is logged and swallowed, so the
element re-enters the scene with no element_info row at all, and
nothing later re-syncs it.

removeDiagram() already cascades this cleanup when a whole folio is
removed (a later, unrelated addition) -- confirmed on current master --
but that path never runs for a single element removed on its own,
which is the case this fixes.

Verified on the built binary, not just read: placed an element, deleted
it (Ctrl+A, Delete), undid the deletion (Ctrl+Z). Reverting just this
fix and repeating the identical sequence reproduces the exact reported
error:

  Debug: projectDataBase::addElement insert element info error :
  QSqlError("1555", "Unable to fetch row",
  "UNIQUE constraint failed: element_info.element_uuid")

With the fix, the same sequence produces nothing. Qt 6.10.2, ctest
13/13.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 16:13:04 +12:00
ispyisail 28acbadaf0 Merge remote-tracking branch 'upstream/master' into pr983-rebase-check
# Conflicts:
#	sources/dataBase/projectdatabase.cpp
#	tests/qttest/CMakeLists.txt
2026-09-23 13:16:48 +12:00
Laurent Trinques 1f0ed43006 Merge pull request #985 from ispyisail/fix/query-row-cap
A query stored in a project file can hang QElectroTech for ever
2026-09-23 02:19:28 +02:00
Magnus Hellströmer 13a37e1f59 fix: harden project database export
Use a bound VACUUM INTO path and remove the stale SQLite
handle declaration. Fix shell continuations in Windows CI and
Debian installation instructions.
2026-09-22 19:05:05 +02:00
ispyisail 3fa5e0a475 Stop a query in a project file from hanging QElectroTech for ever
A <graphics_table>'s <query> is stored in the .qet and executed when the
project loads. SQLite produces rows lazily, so the cost of that query is
not bounded by anything the project contains -- it is bounded by how long
the loop reading the rows is willing to run. A recursive CTE takes one line
to make that forever:

  WITH RECURSIVE c(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM c) SELECT n ...

Put that in the <query> of any project's summary table and opening the file
pins a core at 100% and grows ProjectDBModel::m_record until memory runs
out. Measured on examples/industrial.qet with the query swapped, built from
master:

  clean     --export-bom   3.6 s, 396 rows, exit 0
  poisoned  --export-bom   killed at 90 s, still going, no output

No scripting, no MCP, no flag beyond an ordinary export. Opening the file in
the editor is the same code path.

QetScriptApi::query() has the identical loop, and the script engine's own
30 s interrupt does not reach it: that aborts JavaScript, and this is C++
inside a single call. Left alone it hung a --run for 45 s until the harness
killed it.

Both loops now stop at projectDataBase::MaxResultRows (100000) and say so.
That is a backstop, not a page size: the largest table in the shipped
examples is 396 rows, and a caller that reaches 100000 has been handed
something it should not run to completion. It is not silent either way --
the model logs the offending query text, and qet.query() sets queryError(),
so a truncated result is never mistaken for a complete one.

  clean     --export-bom   3.6 s, 396 rows, exit 0   (unchanged)
  poisoned  --export-bom   20.2 s, 396 rows, exit 0, warning names the query
  qet.query(recursive CTE) 3.8 s, 100000 rows, queryError() set

Reverting each cap restores the hang, so both checks discriminate.

Related to #983, which fixes a different flaw reachable through the same
stored query. Neither depends on the other.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 04:16:21 +12:00
ispyisail f777be05b4 Enforce read-only SQL with SQLite, not with a first-word check
projectDataBase::isReadOnlySelect() decides whether a query only reads
by looking at its first keyword and rejecting internal semicolons.
SQLite has allowed a CTE prefix in front of a data-modifying statement
since 3.8.3, so

    WITH x AS (SELECT 1) DELETE FROM element

begins with WITH, contains no semicolon, passes the check, and deletes
every row. UPDATE and INSERT go through the same way.

This is not only reachable from the custom-query box. ProjectDBModel::
fromXml() reads a <graphics_table>'s saved <query> straight out of the
.qet and fillValue() executes it, so a project file can carry the
statement. Reproduced against a build of this branch's parent, with no
scripting and no CLI flag beyond the export itself: a project whose
stored table query was replaced with the DELETE above exported a bill
of materials of 0 rows instead of 14, exit code 0, nothing logged. A
silently empty or -- with UPDATE -- silently altered BOM is the kind of
output someone orders parts from.

Fixed by asking SQLite about the statement it actually compiled.
sqlite3_prepare_v2() compiles without running, sqlite3_stmt_readonly()
reports on the compiled statement rather than on how it was spelled,
and the prepare tail catches a second statement structurally. The same
project now exports its 14 rows again and logs a reason for the
refusal, while an ordinary WITH ... SELECT in a project file still runs
untouched -- the fix is not "ban CTEs".

isReadOnlySelect() stays in front of it rather than being replaced:
SQLite considers ATTACH, BEGIN and several PRAGMAs read-only too, since
none of them change the contents of the database, so dropping the
statement-type allowlist would have widened what is accepted while
fixing what is executed.

The check lives in its own translation unit depending on nothing but
QString and SQLite, so tests/qttest/tst_sqlreadonly.cpp can link it
alone and exercise the security property without standing up a
QETProject: 18 assertions covering the three CTE-prefixed writes named
in the review, bare writes, trailing statements, comment-only input
(which compiles to a null statement sqlite3_stmt_readonly() must not be
handed) and a null connection (refused, not waved through). Confirmed
the suite discriminates by deliberately disabling the new check and
watching exactly the nine write-refusal assertions go red while the
accept cases stayed green.

ctest 13/13, qet-coherence-check and qet-pdflink-check clean on the
example corpus.

Reported in PR #980's review thread by @elevatormind and confirmed
against this code by @scorpio810; fixed here on its own because the
flaw is in already-released code and needs none of that branch to
reach.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 03:23:40 +12:00
Magnus Hellströmer 27dea3ffab refactor: use VACUUM INTO for database export
Remove the native SQLite backup API and direct SQLite library
dependency.
2026-09-20 20:49:02 +02:00
Jeff Patterson 8ef130a59d Add a "qet" icon theme and load icons by name
QET had no icon theme: the 446 entries of the icon table and the 116
iconsets in .ui files each named a resource path, so an icon could only
ever be one file, and a variant for another palette or a vector source
had nowhere to go (GitHub #466, #690, #870). This adds the theme layout
without changing a single pixel; a dark variant comes in a follow-up.

The theme "qet" follows the freedesktop layout Qt's icon loader
understands. misc/make_icon_themes.py generates ico/icon-themes.qrc,
which aliases the existing ico/<size>/<name>.png files into
themes/qet/<size>/<name>.png, and ico/themes/qet/index.theme. No file
moves. The four table entries that paired a 16 pixel file with a 22
pixel file of another name (ConductorSettings, DiagramAdd,
DiagramDelete, DialogInformation) get the 22 pixel file aliased under
the 16 pixel name.

QETApp::initIconTheme() registers the theme before initIcons() and makes
it current on every platform, so a desktop icon theme cannot replace
QET's icons. Icons are then looked up by name: QIcon::fromTheme() in
qeticons.cpp and in the few places that built a QIcon from a resource
path directly, and theme="..." on the iconsets in .ui files, with the
resource path kept as fallback. Flags, color swatches, application and
MIME icons stay on their paths.

One entry does not go through the theme. The elements panel draws the
project root with ProjectFileGP in the 50 pixel slot it reserves for
element previews, and the name "project" also carries the 128 pixel
file the configuration dialog uses. On a 2x display Qt's loader picks
that file for a 50 pixel request and fills the slot. ProjectFileGP
loads the 16 and 22 pixel files directly, as before.

tests/qttest/tst_qeticons: every name in the theme resolves, the four
aliases resolve at 22 pixels, a Fusion tool button shows its icon at
3:1 with disabled weaker than enabled, and the project root icon stays
at 22 pixels or less when asked for 50 at 2x while the configuration
dialog still gets its 128 pixel file. The rendering helpers shared
with tst_qetpalette moved to tests/qttest/inkcontrast.h.
2026-09-17 05:41:50 -05:00
ispyisail 12ecf6b28f Add read-only enforcement, preview, import/export to custom SQL reports
Refs discussion #886: a saved-report manager built on custom SQL and
nomenclature.json. Most of what was asked for already existed --
Projet > Exporter au format CSV already builds/saves/reuses named
SELECT queries via ElementQueryWidget and nomenclature.json, and
Projet > Ajouter une nomenclature already inserts one into a folio.
This fills the three real gaps.

- projectDataBase::isReadOnlySelect() rejects anything that isn't a
  single SELECT/WITH statement. Checked in newQuery() itself, the one
  choke point every query path already goes through -- including a
  query loaded from a saved nomenclature/summary table's <query>
  element on project open, not just the dialog's own custom-SQL box.
  ElementQueryWidget shows the same check live as you type, and
  BOMExportDialog surfaces it before running or exporting anything.
- BOMExportDialog gains a Preview button + table (QSqlQueryModel),
  so a report can be checked on screen before committing to a CSV file.
- ElementQueryWidget gains Importer.../Exporter... buttons that
  read/write nomenclature.json's saved reports as a JSON file, so a
  report can be handed to a colleague or another install. Import asks
  before overwriting a locally-saved report of the same name.

Verified: Qt 6.10.2, builds clean, ctest 8/8. Drove the real dialog
through Xvfb: typed "DROP TABLE element" into the custom-SQL box and
got the inline warning immediately, then confirmed Preview also
refuses it with a "Requête refusée" dialog rather than running it.
Preview against the real default query returned live column headers
and a row. Export opens a save dialog without crashing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 10:09:14 +12:00
ispyisail a756bc0722 Fix bugtracker #885: preserve custom summary-table SQL on accept
setQuery() restored the loaded SQL text into the line edit but never
restored the "Edit SQL query" checkbox, so a custom query (a join, a
subquery, a view other than project_summary_view) displayed correctly
while the widget stayed in built-in mode. queryStr() only consults that
checkbox, so accepting the dialog without touching anything silently
replaced the custom query with a freshly generated one.

Detect it instead of trusting a flag that was never set: after parsing
columns from the loaded query, rebuild the query those columns would
produce and compare it against what was loaded. A mismatch means the
widget cannot reconstruct it, so it must be user-written -- check the
box and keep the literal text.

Verified against both cases this has to get right, not just the one
in the report: a genuinely custom query (join) now round-trips through
an untouched accept+save byte-for-byte, and a plain built-in query
written before the #238 pos-ordering fix (examples/industrial.qet, no
ORDER BY clause) is classified as custom rather than silently gaining
an ORDER BY it didn't have -- it round-trips unchanged rather than
being corrupted, though its column picker is now disabled until a user
rebuilds it by hand.

Based on the patch attached to #885.
2026-09-16 13:09:48 +12:00