Two people opening the same project on a shared drive could both edit it,
and the last save silently wiped the other's work. A project now has a
hidden lock file beside it (.~plant.qetz.lock, small JSON: user, host,
process, version, since, heartbeat). Opening a project another live session
holds shows who has it, since when and how long ago they were last active,
and offers Open read-only (default), Open anyway, or Cancel.
- The lock is made with an exclusive create, refreshed every minute and
deleted when the project closes. It is not a QLockFile: that treats any
lock from another machine as stale after 30 s. Here only a lock made on
this machine by a process that no longer exists is taken silently.
- A folder the lock cannot be made in, or the setting turned off
(Settings > General > Projects), opens as before.
- Save As and .qet saved as .qetz move the lock to the new file, and refuse
a file someone else has open. A project opened read-only for another's
lock does not save over that file.
- --resave and --set-titleblock refuse a file someone has open unless given
--ignore-lock; the read-only verbs never look at locks and headless runs
never take one. A script's qet.save() refuses the same way.
Checked: 17 new unit tests (5 go red when stale detection and the exclusive
create are broken), full ctest 107/107, the dialog and both choices in the
GUI under Xvfb, and --resave refused/allowed from the command line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Implement ElementTextFormatPattern to capture and reapply dynamic element text formats (font, color, alignment, rotation, frame, positions, groups) across elements.
- When applying a format, missing ElementInfo texts present on the source template are added to the target element with the template's layout and styling, matching other texts in order.
- Register actions under Edit menu, diagram context menu, and ShortcutManager (customizable via Toolbar settings).
- Bind default shortcuts Ctrl+Alt+C (copy text format) and Ctrl+Alt+V (apply text format).
- Provide full undo/redo support via ApplyElementTextFormatCommand.
- Add SVG icons format-copy and format-apply.
- Include English source strings and German/French translations.
The reader of project.sqlite knew two formats at once: every query asked
the database which column names to use (v2() ? "folio_id" : "folio_uuid"
and so on), and each new format would have added another branch to every
query. Asked in discussion #1440.
An older database is now brought up a format at a time when a .qetz opens
(readyToRead()): format 2 to 3 renames its tables' columns, adds the show
columns and puts the symbols' and wires' uuids and the symbols' type back
in the folio files; 3 to 4 has nothing to change for reading. The reader
then knows only the current format. The next format adds one step.
Checked: the format 2 test file (tests/qttest/fixtures/qetz_format2.qetz)
resaved as .qet and as .qetz gives the same files as master. format2Read
now also checks that every symbol and wire gets its uuid and type back:
it failed with the type left out of the step, which the test did not see
before. ctest 106/106.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first pass refused a pair whenever a saved terminal had two live
candidates in reach. That happens when a whole drawing moves by half the
spacing of its terminals (pins 10 apart moved 5), and when two terminals
share a place. When one offset fits every unmatched terminal, as when a
whole drawing moved, that offset now settles all pairs; terminals sharing
a place pair in order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A project saves, for every placed symbol, where each terminal docked, its
orientation and its number. When the symbol's definition is replaced by one
whose terminal sits a few units elsewhere, wires were matched by exact
position and so were dropped on the next open: legacy wires by Terminal::
fromXml(), uuid wires by the uuid derived from the terminal's new place.
Element::fromXml() now pairs each saved terminal that nothing matched
exactly with the live terminal of the same orientation whose docking point
is within 5 units in x and y, only when each is the other's only candidate.
A pair registers the saved number, and the uuid derived from the old place,
which Diagram::findTerminals() checks after the derived uuid of the current
place. Wire ends reattached this way are counted and logged once per folio.
tst_movedterminals covers a move of 3 and 5 units (number and derived
uuid), 6 units, two candidates, and an unmoved terminal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1346 rebuilt the Edit menu from a list of named actions, written before
#1354 added Horizontal mirror and Vertical mirror. The two actions ended up
in no menu and on no toolbar, so their shortcuts (M, F) never fired; only
the right-click menu reached them. Add both to the Edit menu after
"Choose texts orientation".
Also correct two labels left behind by later changes:
- Settings > General: the fine-movement step is now used with Ctrl, not
Alt (#1395).
- Terminal editor tooltip: the checkbox is now called "Function
separation" (#1456).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every other QElectroTech file type is application/x-qet-*; the zipped
project was application/x-qelectrotech-project+zip. No release has
shipped it yet, so it is renamed now.
Files written with the old name still open: the reader accepts both, and
the shared-mime-info file keeps the old name as an alias and matches
either on content.
Review of #1472 (scorpio810, point 2).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A terminal strip saved with a terminal whose symbol is gone left that
terminal out without a word. It still does, and says so in the log.
The same happens with every folio built: the database is filled from
the document, so a symbol in the file is never missing from it.
Review of #1472 (scorpio810, point 8).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
File > Open and Import numbering from a project each had their own
filter for .qet and .qetz, worded differently. Both use
QETProject::openFileFilter() now.
Review of #1472 (scorpio810, point 11).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saving a project opened from a .qet writes a .qetz beside it. In a
read-only folder that write fails, but the project had already been
switched to the .qetz path: the window then named a file that does not
exist, and the error said only "Permission denied".
The project now stays the .qet it was when the write fails, and the error
names the file: "Cannot save /path/name.qetz: Permission denied". Save as
still saves it elsewhere.
Found answering the review of #1472 (scorpio810, point 12).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The print window exported every page in one orientation, landscape
unless changed, so a portrait folio was shrunk onto a landscape page.
With "Fit in page" on, a PDF export now turns each page to portrait or
landscape as its folio is drawn. The preview keeps one orientation
(QPrintPreviewWidget lays out every page at one paper size), and a
printer keeps it too (only Qt's PDF engine accepts a new page layout
once printing has started). The orientation chosen in the window is
put back afterwards, so it is still the one saved.
The cross-reference links took their target from painter->viewport(),
which QPainter sets once when it opens, so on a turned page they missed
their text. They now use the device's size, as render() does.
tst_printpdforientation exports through the window: an A3 landscape and
an A3 portrait folio get one page each way; tiled pages keep the chosen
orientation; and the links on examples/industrial.qet's turned pages
land where they do when the export starts portrait.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The .qetz files take this branch's side: it already holds #1473, and
adds the folio parts to join().
qetscriptapi.cpp: #1456 gave setConductorProperty() the voltage that
crosses a function separated terminal element; here that function hands
its work to applyConductorProperties(), so the change goes there, with
the properties captured before the pushes. The database path for folios
not built already gives up at such a terminal, so it builds the folio and
takes the same route.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/qttest/CMakeLists.txt: this branch's tst_folioimageload and
master's tst_svgsmallpictures (#1478) were added at the same place;
both are kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
join() and readDocument() gained a parameter on both sides: master's
Pictures map (#1470) and this branch's format. Both are kept, pictures
first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An SVG keeps every picture at full resolution, whatever size it is
drawn at: a folio with 22 phone photos shown at a tenth of their size
gave a 270 MB SVG and took 65 s to write, most of it compressing the
photos again.
The export dialog gets "SVG: smaller pictures" (off by default, kept
with the other export settings), and --export-svg gets
--svg-small-pictures. With it, a picture is stored at four times the
size it is drawn at when that is smaller than the picture; smaller
pictures stay whole. Four times is about 375 dpi on a printed folio.
The exporter sets Diagram::smallPictures() for its own drawing only,
so screen, print, PDF and PNG are unchanged. It is not part of
applyProperties(), which every format uses.
On that project: --export-svg 84.9 s -> 34.5 s (opening is 20 s of
both), 270 MB -> 57 MB, peak memory 2.45 -> 1.86 GB. Rendered at twice
the folio size, the two SVGs differ by more than 32/255 on 0.23 % of
pixels, along photo detail.
tst_svgsmallpictures: an 800x600 picture drawn at 0.1 is stored at
320x240 with the option, through --export-svg and through the
dialog's QPicture route, and at 800x600 without it; a 40x30 picture
stays whole. It fails when the factor is changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each picture of a folio was decoded in turn while its item was built,
a quarter of a second for a 12 megapixel photo. Diagram::fromXml() now
reads every picture of the folio first, decodes them all at once on a
pool of its own (one thread per core, at most one per picture), then
builds the items in file order in the GUI thread as before.
The decoding (base64 text, then the picture file) moves out of
DiagramImageItem::fromXml() into decodePicture(), which touches only
its argument. QImage::fromData() then QPixmap::fromImage() is what
QPixmap::loadFromData() does, so the pixels are the same.
On a 263 MB project with 22 photos on one folio: --info 21.3 s ->
12.5 s, the folio's content 10.6 s -> 3.2 s, opening its .qetz 15.6 s
-> 9.3 s. Peak memory +220 MB (1.81 -> 2.03 GB), the pictures' text
being converted by several threads at once. A resave is byte-identical
to master's, and so is a PNG export.
tst_folioimageload: six pictures come back with their own pixels and
positions, and an <image> without a picture does not stop the others.
It fails when the decoded pictures are handed to the wrong items.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
QETZ-FORMAT.md now describes format 4: the computed text left out of the
folio files, QElectroTech's own database built on opening rather than
stored, the tables written without keys, and format 3 among those read.
Tables written without keys keep their declared column types (INTEGER
was written as INT); tst_qetcontainer checks the columns and types
against the document, and that the file has no keys.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
QETProject::derivedUuid() copied the whole file content to drop '\r',
then copied it again to prepend the name prefix, before hashing.
On a 263 MB project with embedded photos that took 2 s of a 21 s open.
Hash the content in place, skipping '\r', and set the version 5 bits
as QUuid::createUuidV5() does.
The uuid is unchanged: same result as before on the 23 example
projects, their CRLF copies, edge cases (empty, only '\r') and the
263 MB project, and a resave of that project is byte-identical.
QETZ-FORMAT.md describes format 3: the archive and its files, the
manifest and the version rules, every table and column of project.sqlite
and what it comes from, how rows are keyed, the markers a moved block
leaves, and what reading checks. Asked for in #1440: the format had no
written specification.
tst_qetcontainer checks that the schema the document gives is the one
written: the same tables and columns, in the same order, with the same
types and keys.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A folio is copied as it was only from a database whose folio_property and
element_info have the columns this writes too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The JSON column of a property row held only its show flag, in every
project QElectroTech saves: the 25 examples and a 136-folio project used
it for nothing else. Folio and project properties now store it as a
number, as symbol information does, so a saved project's database holds
no JSON; JSON is left for attributes QElectroTech does not write (an
older or hand-edited file).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A symbol changed on a folio kept as it was needs the place its
information goes back to: its part is checked for the marker
(QetContainer::split()), where the position column said it before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The database held where each moved block sat among its parent's
children (props_at, info_at, links_at), though reading only asked
whether there was one: the marker the folio file keeps says that, and
where it goes back. The columns are gone; format 2 files leave the same
markers and still open.
A .qetz of a later format whose min-reader allows this version was
still refused by the database's own format check. It opens now,
read-only, with a message: saving it here could lose what this version
does not know. Save As writes a copy in this version's format.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The checks run once every folio is read. A .qetz written by this branch
has no keys in its tables, so a row twice is caught by the counts too.
A folio is copied as it was only from a database with the columns this
writes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A folio file in a .qetz did not say which symbol each item is: the type
(embed://...) was moved to project.sqlite, so a folio read alone could
not be drawn or diffed. It stays in the folio file now; format 2 files,
which moved it, are still read.
Nothing checked that project.sqlite and the folio files agree: a row no
item has, an item's row on another folio, or information rows of no
symbol were read as far as they went. Reading now refuses such a file and
says what does not match.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A .qetz held the engineering data twice: the document's tables, and
beside them QElectroTech's own project database (stage D2/D3), which
QElectroTech also builds from the document. Two copies of one fact can
disagree, and the copy cost a third of the file. Now project.sqlite holds
the document's tables only; QElectroTech builds its own project database
from them when the project opens, as it does for a .qet.
Measured (this build, lazy folios):
industrial (50 folios): 620 -> 374 KB, open 0.37 -> 0.65 s
f2099/nuri (136 folios): 3.27 -> 2.20 MB, open 2.2 -> 3.5 s
The same projects as .qet open in 3.3 s and 23 s.
Saving still copies the file of every folio not built, with its rows
(stage D4): the project keeps the part each folio was read from. nuri
opened and saved again: 6.3 s, no folio built, the same bytes.
A symbol's terminal list is left out of the folio file when every
folio, symbol and wire has a uuid of its own, now checked on the
document (QetContainer::ownUuids()) rather than by the stored database
being written.
Checked: 342 corpus projects open, .qetz -> .qet equals the plain
resave, a .qetz saved twice gives the same bytes; tst_lazyfolios,
tst_qetcontainer, tst_resaveunchanged (which now checks project.sqlite
holds only the document's tables).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The three code paths that copied the voltage / protocol to the far
side of a function separated terminal -- the conductor properties
dialog, search/replace and the script API -- compared it against the
far wire's own properties instead of against the edit itself. The two
sides differ on that field just as often as not, so the comparison
held no matter what was edited and every change overwrote the far
side's voltage: setting a colour on a 24 V side dragged the 0 V one
along, where master left it alone. Compare the edit -- what the
dialog was filled with against what the user made of it -- so that a
change of the voltage / protocol is what crosses, which is what the
selection panel multi-edit already did. The values are read before
the undo commands are pushed, since QPropertyUndoCommand applies on
push.
An explicitly selected wire is never the far side of anything:
picking one wire on each side of a separation and editing them edits
them both, in full, however the selection happens to be walked. Nor
is a wire that belongs to a potential on any path, so reaching it
once normally and once across a separation gives it the full edit.
#1473 keys the .qetz database's rows by the items' uuids, stores
true/false as 1/0 and writes the format to PRAGMA user_version, as
format 3. This branch's own changes (computed text left out of the
folios, tables without keys, folios kept as they were) become format 4;
formats 2 and 3 are still read.
Folios kept as they were copy their rows by folio uuid, and the keys of
those rows are kept from the other items' keys. The document's tables
are named doc_<name> only beside the project database's own tables.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#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>
Answers the points arummler raised in discussion #1440 about the
project.sqlite that .qetz files carry (format 2, PR #1462):
- Folio rows were keyed "f1", "f2"... by position, and every symbol and
wire carried a separate key beside its uuid. A row's key is now the
item's own uuid, which stays in the folio file; no key attribute is
added to the folio files of a project as QElectroTech saves it. An item
without a usable uuid (none, or one another item shares, F100) gets a
uuid made from its own content, the same on every save, never from
where it is.
- freezeLabel is stored as 0/1 (INTEGER) and an element information's
"show" in its own column; a value QElectroTech would not write stays in
the folio file verbatim. The tag is kept in the JSON only when it is
not the usual one.
- The database's PRAGMA user_version holds the format.
Format 3. Format 2 files, written by development builds since #1462,
still open: tst_qetcontainer reads one (fixtures/qetz_format2.qetz,
grafcet.qet saved by master aa503aec8) and saves it again.
Checked: tst_qetcontainer 38/38, and 343/343 readable projects of the
legacy corpus round-trip exactly. Every example saved as .qetz then .qet
equals its plain resave, and every format 2 file read by this build
equals what master writes from it. Each new test goes red with the
behaviour it checks broken.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opened from its database, a .qetz's folios not built have no XML in
memory: reading each one's part only to look for wires referring to a
cable read every folio. The project database already knows which wires
carry a cable reference, and so which folios to build for the label pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The header declares projectDocument() returning a QDomDocument but did not
include it; it only built where the precompiled header pulled it in. CI
builds without one and failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
The database a .qetz holds is read whole: its tables are now written
without their primary keys and indexes, in the order of their rows, and
the file compacted; the reader indexes what it looks folios up by once
it has read the file. Keys and indexes on text uuids were half of the
database once zipped. A file written before reads as before.
f2099 (136 folios) 5.14 MB -> 3.60 MB, industrial.qet 1.03 -> 0.71 MB;
opening and showing a folio take as long as before (2.5 s, 215 ms). Gates
on 342 corpus projects: saves, exports, round trip, re-save byte-stable,
all open; hostile .qetz all refused; ctest 96/96 both ways.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>