#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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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
Two defects found while re-testing the cable management after the
upstream merge, plus one change of intent for the numbering question.
The question "define a cable numbering rule now?" kept its answer in
the program settings, so saying no once silenced every project of the
program -- the exact opposite of what it should do. The answer now
travels with the project itself, stored on the <cable_autonums>
element as ask_numbering_rule="false"; a file which does not hold the
answer asks again, and taking the rule away in the project properties
or in the numbering window brings the question back for that project
alone. Read-only projects are no longer asked at all, since there is
nowhere in them to define a rule.
The numbering window could not hand out the first rule of a project:
it drew the rule, worked the numbers out with it, and only wrote the
rule into the project together with those numbers -- while the
planning refused to run on a project which had no rule saved yet.
The counters of a cable rule are kept under its fixed name anyway, so
planning now falls back to that name when the project holds no rule
yet: a rule which is being drawn in the window counts from the moment
it is complete, and the first renumbering of a project works.
Conflicts resolved in 24 files. Notable decisions:
- ico/icon-themes.qrc: upstream's icon folder restructure kept; the 36
cable icon entries re-added under the new qet/qet-dark path scheme
(XML valid, every referenced file exists).
- Toolbar system: upstream's DiagramToolbarSettings now owns the
toolbar filling. The cable tool is registered as
diagrameditor.add_cable (generic group loop) and listed on the
"diagram" toolbar right after the auto-break conductor action,
separated by a divider, exactly like before the merge.
- Paste: the cable lines of a copy are handed to
PasteNumberingImport::push() as extra commands, so paste + cable
lines + the numbering question stay ONE undo step on both paste
paths (DiagramView::paste and DiagramEventAddPaste::commit).
- PDF links: upstream's deterministic link sorting kept together with
the cable cross-reference injector (injectLinkTo -> links list).
- Element renumbering: upstream's ElementAutoNumSchemeCommand::renumber
replaces the old formula-based body of
QETProject::renumberElementsBySchemeTitle; the cable numbering rule
API (cableAutoNum, axis priority, rule name) is kept in full.
- Auto-numbering pages: upstream's scheme management kept, the Câbles
tab added as index 4 (changeToCableTab), both entry points
changeToCable() and changeToElement() kept side by side.
- SelectAutonumW: dirty tracking and the contextEdited signal moved
into upstream's insertPartRow() row API, so single-rule mode, the
rule-removable button and "confirm without change leaves the project
alone" keep working.
- xrefpropertieswidget: upstream's offset clamping kept; the cable
branch (Texte field, Police button, hidden display options) kept;
the stack-overlapping checkbox is hidden for cables.
Verified after the merge: build green (515/515, no errors), ctest
85/86 (only the pre-existing headless tst_menubarkeyboard failure),
offscreen smoke exit 124.
Cables are drawn line objects (toolbar button next to the auto
break/reconnect button, click-move-click, right click cancels), not
components. The type comes from a CSV catalog following the material
list pattern (designation, cores, core colours, editable through the
entry dialog with "Nouvelle entrée" / "Modifier l'entrée").
Each core is bound to the wire its colour label stands on, and the
cable reference is generated per core and written into the Conductor
entry via Conductor::setCableReference(label, cable uuid, slot). The
conductor entry is where it has to live because the terminal strip
plan (Klemmenplan) consumes it in the next step.
Numbering: "Cables" rule under Programmeinstellungen/Neues
Projekt/Nummerierung auto (mirrored in project properties), literal
"W" fallback without a rule, French gate dialog when no rule exists,
Nummerierung menu with the renumber dialog (preferred axis, per-folio
counter, rule and counter state written back, hand-typed names ask,
whole-project scope).
Also included: label block and under-line texts with per-text font and
alignment plus the material-list cell margins, free-core placement and
right-click removal, claim questions for wires of other cables,
multi-selection drag as one undo step, type change with confirmation,
cross-references for type "Cable" (%f-%l%c, own font, clickable links
in exported PDFs), report propagation of the cable definition,
Listes menu (table of contents, material list, terminal strip manager,
terminal generator plugin, cable list), cable list with freely
selectable columns (installation/localisation of cable, start and end,
Blatt start/end, length, used cores...), CSV export dialog with column
pick and preview in the Projekt menu, script API (addTable kind
"cable_list", exportCableList) and a unit test for the type catalog.
A project can now keep the texts drawn in its symbols, and the names of
their terminals, horizontal when a symbol is turned: Project properties >
General, "Garder horizontaux les textes des symboles pivotés". The box of
each text turns with the symbol; the text does not, and reads as it does
in the symbol itself.
It is a project setting, saved as <symbol_texts upright="true"/> and
only when on. A new project starts with it on; a project saved without
it (every existing one) reads with it off, so it looks and saves exactly
as before, and looks the same on every computer. The MCP server's new
projects start with it on too.
It builds on the mirror of #1354, which already redraws the texts of a
mirrored symbol readable: what the symbol does to its texts is now its
mirrors and, with the setting on, its turn (Element::symbolTextsTransform()).
ElementPictureFactory caches one drawing per such transform, terminal
names undo it the same way, and the DXF export places the texts alike.
The fields of a symbol (label, comment...) already keep their angle with
"Garder la rotation visuelle" and are left as they are.
Known limit: two texts stacked in a symbol end up side by side when it is
turned, and can overlap when kept horizontal; the setting can be turned
off for such a project.
Test: tst_uprightsymboltexts turns the symbols of a folio and checks the
angle of each motor's "M" in the DXF, with the setting off and on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Schemas now get saved with a UUID and do not have to be looked up according to the form. They can be renamed and it gets reflected in existing elements. The GUI for
editing them is now much easier, e.g. forms can be reshuffled. A formula can be applied. Many options now also for conductors and sheets.
The limits were per project only. They are now set once in Settings >
General, under the master switch: the most wires per terminal and "one
wire per folio report". Every project follows them.
Project properties > "Conducteurs par borne" gets "Utiliser les réglages
de l'application", ticked by default, showing the application's values
greyed. Unticked, the project keeps its own values, saved in the project
as before, so a drawing that needs its own limit carries it to any
computer. A project's own "no limit" overrides an application limit too.
QETProject::wiringRules() now returns the rules in force (the project's
own, or the application's), so the code that checks a wire is unchanged;
projectWiringRules() returns what the project itself stores. A project
that follows the application still writes nothing, so it saves exactly as
before.
tst_wiringrules: which rules win, the save round trip of a project's own
"no rule", and through the real binary that the application's limit
refuses a wire unless the project overrides it or the master switch is
off (checked to fail with the application's rules ignored). ctest 55/55.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015FPuYPS4T7QuEwjNu22rXD
Discussion #1158: a real terminal takes one or two wires (four with
double ferrules), a folio report is a virtual point that carries one, but
QElectroTech lets any number of wires be connected anywhere.
Project properties > General gets a "Conducteurs par borne" group:
- the most wires a terminal may take (0, the default, is no limit);
- "one wire per folio report".
When set, a new wire that would go past the limit is refused: the
terminal shows red while dragging, as for any refused link, and a tooltip
says why on release. Auto-connect on placing, moving or multi-pasting a
symbol skips full terminals, and qet.addConductor() refuses with its own
message. Wires already drawn, opening, pasting and undo are never
refused.
Settings > General gets a master switch, on by default, that turns every
rule of this feature off in every project; the project group is greyed
with a note while it is off.
The rules live in wiringrules.cpp. The project setting is saved as
<wiring_rules .../> only when a rule is on, so a project that never used
it saves exactly as before; an older version drops it, which means "off".
tst_wiringrules covers the rules, the save round trip and, through the
real binary, that a wire past the limit is refused unless the master
switch is off (checked to fail with the limit check removed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015FPuYPS4T7QuEwjNu22rXD
Where two wires cross without being connected, a project can now draw a
small arc (a hop) on one of them, instead of the user inserting a jump
symbol and splitting the wire. It is a project setting, off by default:
Project properties > Général > "Croisements de conducteurs", with no
hops, hops on horizontal wires, or hops on vertical wires. Choosing the
orientation rather than following drawing order keeps a project
consistent.
Only the drawing changes: no element is added and no wire is split. A
wire that ends or bends on another one is a junction and never hops,
and a crossing too close to the end of a segment for the arc to fit is
drawn plainly. PDF, SVG and image export and printing show the hops,
since they paint through Conductor::paint(); DXF export, which writes
the segments itself, does not.
The setting is saved as <wire_crossings hop="..."/> under the project
root, next to <usage>, and only when hops are on, so a project that
never used them saves exactly as before.
The geometry is in wirehops.cpp, free of any graphics item, and tested
on its own. Conductor::paintedPath() feeds it the conductors of the
folio from a snapshot of their scene points, rebuilt only when a
conductor changes shape, moves, or enters or leaves a folio; each hop
path is cached the same way. Asking the scene for the conductors in a
rect instead strokes every candidate's shape, which made a 366-wire
folio render 0.6 s slower; with the snapshot the difference is within
measurement noise.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Discussion #598, reviving PR #654. The crash-recovery backup written
every 20 minutes went to a single file. If the project was already in a
bad state when a backup ran, that bad state replaced the only recovery
copy.
QETProject now writes the backups in turn to three KAutoSaveFile slots
(BackupGenerations), so one bad write only replaces the oldest
snapshot. Scope is crash recovery only; the opt-in autosave is
unchanged.
After a crash, the recovery prompt groups the snapshots by project and
offers one row per project with a list to pick the snapshot to reopen,
newest selected by default. The snapshots not picked are deleted.
Ported onto current master: writeBackup() keeps the "skip if nothing
changed" check (bugtracker #273) and offerBackupFiles() keeps its place
after the stale-file filter and before the crash report (#901).
Tested with the backup interval shortened to 4 s (test build only):
after three changes, master holds one recovery file, overwritten each
time; this branch holds three, with 4, 5 and 6 folios. After killing
QET, the prompt lists the project; picking the oldest snapshot reopens
4 folios, the default reopens 6. Twice each. ctest 34/34 with and
without KDE Frameworks; the carried-over KAutoSaveFile test passes in
the nokde build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
A symbol keeps its derived uuid once saved, even when it is moved. A
symbol saved without a uuid that later turns up on the spot it left -- a
hand edit, an older version, another tool writing the file -- derived the
same uuid, and the project had two symbols with one identity.
readDiagramsXml() now collects every symbol and wire uuid the file
carries, on any folio, before a folio loads; derivedItemUuid() moves to
the next counter value while a candidate is among them. The result still
depends on the file alone. Renamed from derivedUuid(), which QETProject
already has for the project's own uuid.
tst_derivedsymboluuid: newcomerOnAMovedSymbolsSpotGetsAnotherUuid fails
with the check switched off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A symbol saved without a uuid got a random one from Element::fromXml()
on every load, and the next save wrote it out: two loads of the same file
gave the same symbol two identities, and anything pointing at it by uuid
(a script, a comparison of two versions, a wire's identity) could not
follow it from one session to the next.
When a folio is loaded, such a symbol now gets a UUID v5 derived from
what it is and where it sits: its type, its position on the folio and its
orientation. Never the folio's index, so inserting or moving a folio does
not change it. Identical symbols stacked on one spot, or a copied folio,
are told apart by a counter kept per project (QETProject::derivedUuid()),
in load order among those symbols alone. A paste still renews uuids.
Symbols that have a uuid in the file keep it. All 24 example projects
already have one for every symbol, so they are unchanged; with the
symbols' uuids stripped, each saves byte-for-byte the same twice (master:
different every time).
tst_derivedsymboluuid runs --resave on a fixture with its uuids stripped:
same uuids on every load, same after a folio is inserted in front, saved
uuids kept, stacked copies differ. The first two fail without this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
writeBackup() rebuilds the whole project's XML with toXml() on the GUI
thread before handing the file write to a worker thread. On a big project
that freezes the interface for seconds (bugtracker #273, and #329, where
the interval was raised from 2 to 20 minutes to make it rarer). It ran
right after every project was opened, and then every 20 minutes whether
or not anything had changed.
Only write a backup when something changed since the last one: the undo
stack moved, setModified(true) was called, or the embedded element or
title block collections changed. A project just opened from a file starts
clean, since the file is already what a crash would restore. New projects
and projects restored from a backup are backed up at once, as before.
Measured on examples/industrial.qet (50 folios), each backup blocks the
GUI thread for about 0.28 s on a fast machine; with a 2 s test interval,
the old code backed up on every tick, the new code only after a change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
QETProject::m_uuid was created in the constructor and never written, so
a project got a new uuid every time it was opened. Inside a running
instance it is only used to name the SQLite connection, but nothing
outside the instance could tell which project a file belongs to.
Motivation
A .qet file is increasingly handled by tools outside QElectroTech: Git
repositories on GitHub or GitLab, cloud storage, key-value stores,
per-project locks. All of them need a stable key for "this project":
- The file name and path are not stable: files get renamed, moved,
checked out in different places.
- The project title is user-editable and not unique.
- Folio uuids (persisted separately) are only unique within their
project; keying folios globally needs a project identifier as well,
e.g. projects/{projectUuid}/folios/{folioUuid}.
Change
Write the uuid as an attribute of <project> and restore it in
QETProject::openFile(), right after parsing and before the project is
built from the XML. Older versions ignore the attribute, so files stay
readable in both directions.
The project database is not affected: it takes its connection name
from the uuid created at construction (m_uuid is declared before
m_data_base), before the file is read. Two open projects carrying the
same persisted uuid therefore still get distinct connections.
Files without a uuid: why not a random one
Keeping the random uuid created by the constructor and saving it
conflicts with #754 / #779: saving an unmodified project must give the
same bytes every time. Every example project predates the attribute.
Measured on the 24 example projects (resaved 3x each from the same
original, QT_HASH_SEED=0 so that QDom's attribute order is stable,
isolated HOME per run):
upstream master 23/24 byte-identical
persist, random uuid 0/24
persist, derived uuid (this) 23/24
The remaining project, schema_indus.qet, differs only in element uuids,
the known residual #779 leaves for elements; its project uuid is
stable.
Instead, a project file without a uuid gets a name-based (version 5)
uuid derived from the raw content of the file:
QUuid::createUuidV5(<fixed QET project namespace>,
"qet-project-legacy\n" + file content without CR)
- The same file always yields the same uuid, so resaving an unmodified
legacy project stays reproducible.
- Different projects practically never share a uuid, because any
difference in content gives a different one. This is unlike folios,
where only data such as title and position could be used; the raw
file bytes are stable input for the whole project.
- Carriage returns are dropped before hashing. QFile's Text mode already
strips them on Windows but not elsewhere, and git's autocrlf can
change them on checkout; either way the uuid is the same on every
platform.
- The uuid is derived once, at load time, and saved from then on. After
that it is read, never recomputed: renaming the project, editing it
or changing it in the same session as the migration does not change
it.
- Two people opening the same legacy file on different branches get the
same project uuid.
The namespace uuid is fixed in the code and must never change, or every
legacy project would get a different uuid.
Known limitations, open for discussion
- Copies share the uuid. Two byte-identical legacy files get the same
uuid (examples/cablage-eclairages_sikli-v5.qet and
câblage-éclairages-sikli-v5.qet are such a pair), and so does a
migrated file copied in the file manager or saved with "Save as".
That is what identity means for a copy, and the same happens with Git,
but a tool that treats the uuid as globally unique has to cope with
it. Regenerating the uuid on "Save as" could be a follow-up, if that
is the preferred behaviour.
- A legacy file that differs from another only in formatting (e.g.
re-indented) gets a different uuid. The two sides of a merge only
agree if they started from the same bytes, which is the normal case.
Tests (Qt 6.4, offscreen, qelectrotech --resave / --set-titleblock /
--info)
- 24 example projects, 3 resaves each from the same original: results
above; the project uuid is identical across runs. All 24 uuids are
distinct, except the byte-identical pair mentioned above.
- Resaving an already migrated file is byte-identical to the first
output.
- The same legacy file with CRLF line endings gets the same uuid as
with LF.
- Changing the project title in a migrated file keeps its uuid.
- Migrating and modifying in the same run (--set-titleblock on a legacy
file) gives the same uuid as a plain resave.
- Re-indenting a legacy file gives a different uuid (expected).
- A migrated file opened with upstream master loads normally; the
attribute is ignored and dropped on save.
- --info on a migrated file still works.
Refs #754, #779
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDyt4txaott5JyPNGQaeVp
a) using a system provided KF6
b) downloading and compiling KF6
c) using the vendored-in re-creation of the functionality
The behaviour for both Qt5 and Qt6 is steered with the same two variables which were renamed to become version agnostic:
a) BUILD_WITH_KF=ON BUILD_KF=OFF
b) BUILD_WITH_KF=ON BUILD_KF=ON
c) BUILD_WITH_KF=OFF
The version is automatically derived from the chosen Qt major version.
Reported by @scorpio810 on #626: "The field does not update automatically;
you need to list the other rules for it to update."
Two reasons, both mine:
The refresh was wired to the combo boxes' activated() signal, which Qt
emits only for user interaction. Nothing that changed a context
programmatically -- which is to say, numbering an element -- ever reached
it. Re-picking a rule from the combo was not a workaround so much as the
only code path that refreshed at all.
And there was no signal to hang it on for two of the three categories:
addElementAutoNum() emitted elementAutoNumAdded(), but addConductorAutoNum()
and addFolioAutoNum() emitted nothing, so even a listener would not have
heard a conductor counter advance.
Add QETProject::autoNumContextUpdated(), emitted by all three setters, and
have the dock re-read its three fields on it. Kept deliberately separate
from the existing *AutoNumAdded/*Removed signals: those make listeners
rebuild their rule lists, which is both heavier than needed here and would
disturb the user's current selection every time an element is numbered.
This one only says "re-read me".
The automatic refresh skips a field that has keyboard focus, so numbering
an element cannot overwrite a value half-typed under the cursor. Explicit
refreshes after a reset or an edit still write unconditionally, so the
field always ends up showing the canonical stored value.
Measured, advancing a counter the way numbering advances it and without
touching the combo box:
field before advance "5"
context after advance 6
field after advance "6" (was still "5")
Three new QUndoCommand subclasses (AddDiagramCommand, RemoveDiagramCommand,
MoveDiagramCommand) pushed onto the project's existing (already
project-scoped) undo stack, so folio structure edits are undoable
alongside every item-level edit already on that stack.
- QETProject::addDiagram()/detachDiagram() are the shared attach/detach
primitives: they mutate the diagram list, connect/disconnect the two
per-diagram signals set up at add time, and emit diagramAdded/
diagramRemoved. AddDiagramCommand and RemoveDiagramCommand call these
(via friend access) for both redo and undo, so a removed diagram is
parked rather than destroyed -- it's only actually deleted if the
command itself falls out of undo history while still detached.
- ProjectView reacts to diagramRemoved the same way it already reacted to
diagramAdded (tearing down/rebuilding the tab), so both directions of
both commands go through the same reactive path every other diagram
listener (project database, cross-references, generic panel) already
relies on.
- MoveDiagramCommand wraps a new ProjectView::setDiagramPosition(), which
performs the tab move and the project's diagramOrderChanged() list
reorder synchronously in one step, instead of relying on the queued
tabMoved connection (needed for interactive drag-and-drop) to catch up
later -- avoiding a second, redundant reorder from that queued call.
- Multi-folio delete and multi-folio move (QETDiagramEditor::removeDiagrams()
and the moveDiagram*(QList<Diagram*>) batch slots) wrap their per-diagram
loop in QUndoStack::beginMacro()/endMacro(), so a multi-select action is
one undo step, matching current UX.
- Softened the delete confirmation's "this change is irreversible" wording
now that it no longer is.
Verified headlessly (Xvfb + xdotool + scrot): add/undo/redo, delete/undo/
redo (single and multi-select, single undo step for the batch), and
move/undo/redo all behave correctly against a 7-folio project.
Adds a ProjectUsageTracker (sources/project/) that accumulates how
long a project has been the active tab, using QElapsedTimer so the
value is computed on demand rather than via polling. It's hosted on
ProjectPropertiesHandler per that class's own stated design intent
("all new properties should be managed by this class").
The accumulated time is persisted as a new <usage time_spent="N"
enabled="true|false"/> element, a sibling of <properties> in the
project XML, written/read by new QETProject::writeUsageXml()/
readUsageXml(). It rides along on the existing autosave path for
free, since writeBackup() already serializes the full project via
toXml().
QETDiagramEditor::subWindowActivated() now pauses every open
project's tracker except the one whose tab just became current, so
switching between several open projects keeps each project's tracked
time isolated.
Surfaced in the existing Project Properties "Général" page: a
"Temps passé sur ce projet" display, a "Réinitialiser" button, and
an opt-out checkbox ("uniquement enregistré localement dans ce
fichier" - this is local-only, never transmitted anywhere).
Verified beyond compiling: full CMake build, then an actual runtime
session confirming the saved XML's time_spent value, that closing
and reopening the project round-trips and resumes timing, that the
reset button works, and - the key correctness check - that with two
projects open, the inactive one's time_spent stays frozen while the
active one accumulates real elapsed time, over the same interval.
See discussion #576.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Provide a small KAutoSaveFile-compatible implementation for the no-KF5 build path and use it to keep the existing crash-recovery code active when BUILD_WITH_KF5=OFF.
The normal KF5 build still uses the KDE KAutoSaveFile implementation.
Assisted-by: pi coding agent / Mika (OpenAI GPT-5.5)
writeBackup() fires QtConcurrent::run(QET::writeToFile, ..., &m_backup_file)
fire-and-forget: the QFuture was discarded and nothing kept m_backup_file
alive until the worker finished. If the QETProject was destroyed first, the
worker wrote through the freed member -> use-after-free crash in
QET::writeToFile (intermittent; ~1/6 on short-lived CLI runs).
Store the QFuture and waitForFinished() in ~QETProject (and before
setFilePath() re-points the managed backup file). Also skip launching a new
backup while one is still running, so two threads never write m_backup_file
at once.
The Qt6 path is still a TODO stub and the QtConcurrent block is KF5-only, so
this affects only the Qt5/KF5 build that actually has the backup code.