Review of #1109 (scorpio810):
- tst_resaveunchanged now resaves every .qet in examples/ twice instead
of two of them (24 rows, ~40 s). Dropping the load-time trim now also
fails affuteuse_250h.qet, which the two-file version missed.
- New case singleSpaceValueKept: a title-block property set to one
space survives two saves (#973), and an accented property comes back
unchanged. Fails if qetproject.cpp stops parsing with
PreserveSpacingOnlyNodes.
- New tst_diagramcontext: the QDom reader (projects) and the pugixml
reader (element definitions in the collection) return the same value
for plain, stray-spaced, accented and non-Latin text. Fails if the
pugixml path decodes as Latin-1 or either reader stops trimming.
The pugixml reader still reads a single-space value as "": pugixml drops
whitespace-only text unless parse_ws_pcdata is set, as it did before
this PR. That reader only sees element definitions, never a project, so
#973's title-block values do not go through it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- An end conductorEnds() reports as "?" is never picked.
- An empty "conductor" says why: an older project's conductors have no
saved uuid, so qet_conductors reports it empty.
- The op description and README say set_conductor still changes the
whole potential when given a uuid, and that older projects' conductors
are named by element + terminal until #1103.
- test_conductor_by_uuid covers move_conductor_segment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The anonymous namespace sat between the comment and the function, so
Doxygen attached the comment to describeEnd().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Discussion #1070 proposed that rotate, like move, copy and delete, works on
the whole group once one of its items is clicked. Rotate (Space) turned
each member on its own spot instead, so rotating a group pulled it apart:
two grouped texts side by side ended up each turned in place, no longer
side by side.
When the selection is exactly one whole group -- wires aside, which follow
their symbols -- Rotate now turns it as one piece around its centre, as
"Pivoter le groupe" (Shift+Space) already does
(ItemGroups::soleWholeGroup()). Any other selection, including a single
member picked out of its group, rotates as before.
In the GUI, on two grouped texts selected by one click: Space on master
leaves both where they were, turned; here it gives exactly what
Shift+Space gives on both (both texts swung around the group's centre).
tst_itemgroups: 4 new checks; without the whole-group condition, a
picked member counts as a group and fails. ctest 24/24.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clicking an item of a group selects the whole group (#1070). Clicking
again on one of its items is meant to select just that item, to edit it
on its own -- what discussion #1070 proposed -- but the second click
selected the whole group again: Qt left only the clicked item selected on
release, and the group completion pulled the others back in.
A press on a member of a group that is selected whole now notes that
member (ItemGroups::memberToPick(), which also finds the member when the
click lands on a symbol's own text). If the click ends without a drag and
Qt has left only that member selected, the selection stays so. A drag
still moves the whole group; Ctrl+click keeps its meaning; a group of one
is not picked from.
In the GUI, on two grouped texts: one click then Delete removes both
(master and this); click, click again, Delete removes only the clicked
text here, both on master; dragging after one click moves both texts by
the same amount on both. tst_itemgroups: 5 new checks; removing the
whole-group or the group-of-one condition fails one each. ctest 24/24.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test runs a script through --run, which a build without the Qt Qml
module does not have (QET_HAS_SCRIPTING). Linux CI installs no Qml package,
so the binary took the script for a project to open and the test waited
out its 60 s. The test is now built only when scripting is: a configure
with Qt6Qml disabled lists 24 tests, without it; with Qml it runs and
passes as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saving a project that had just been saved changed it again in 18 of the
24 example projects, so a project kept in version control showed changes
nobody made. Both causes were cleanup done on save but not on load:
- Symbol information whose values were all empty was written as an empty
<elementInformations/> block (DiagramContext::toXml() skips empty
values, Element::toXml() wrote the block anyway). The next load read it
as no information and the next save dropped it. The block is now written
only when something went into it.
- Information values were trimmed on save but not on load, so a label with
stray spaces (" PRISE") kept them in memory and in its displayed copy
until the project was opened again. The same rule, kept in one place,
now applies when reading: stray whitespace around real content trimmed,
a value that is only whitespace kept (#973).
All 24 examples now save identically a second time (master: 6), and each
one's first save is byte-for-byte what master wrote only on its second.
A title-block property set to a single space keeps it through two saves.
tst_resaveunchanged runs --resave twice on Projet_vierge.qet and
m_000.qet; both fail without this change.
Co-Authored-By: Claude Opus 5.5 <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>
set_conductor, move_conductor_segment and delete_conductor named a
conductor by one of its terminals, which had to carry exactly one
conductor: where two meet at a terminal, neither could be named from it.
Each now also takes "conductor": "{uuid}" (as qet_conductors reports it)
in place of element + terminal. The generated script asks
qet.conductorEnds() for the conductor's two ends and passes the one whose
terminal carries only that conductor. Where both ends are shared, or no
conductor has that uuid, the op fails and its "note" says which. The
lookup is required only when a uuid is used; giving both forms is an
error.
Tests: the script generated for each form and the argument errors; on a
folio where one terminal carries two conductors, deleting either by uuid
leaves exactly the other; an unknown uuid is reported. With the end chosen
without checking its terminal carries only that conductor, the terminal
test fails. 248/248 with a build carrying qet.conductorEnds().
Stacked on the conductorUuids()/conductorEnds() scripting change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A script could name a conductor only by one of its ends, "the conductor on
terminal N of element X", which fails where two conductors meet at one
terminal and cannot follow a conductor that is re-connected.
qet.conductorUuids(folio) lists the folio's conductor uuids, in the order
qet.conductors() lists them. qet.conductorEnds(folio, uuid) returns that
conductor's two ends as "{element uuid} terminal N" -- the form
conductors() prints and the conductor calls take -- or an empty list if
the folio has no such conductor. The end formatting conductors() already
did is shared rather than copied.
Conductors of older projects have no saved uuid yet, so theirs change
from one load to the next until that is settled (discussion #1103); new
conductors keep theirs.
tst_scriptconductoruuid runs a script through --run on a fixture: every
conductor has a distinct uuid, and its ends match the conductors() line
at the same position; an unknown uuid, a malformed one and a folio that
does not exist give empty lists. It fails with the two ends swapped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Complete Spanish translations
Complete 1,383 pending entries in lang/qet_es.ts, using terminology appropriate for electrical schematics, wiring and control panels.
Translate missing entries and review unfinished drafts.
Preserve placeholders, plural forms, links and markup.
Leave previously completed entries unchanged.
Validation: XML structure checked and no pending translations remain in the submitted file. Not tested in the running application.
Conflict in _extras(): keep master's tables and folio uuids and _angle(),
and read only the folio's own inputs/shapes/images as this branch does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
schema_indus.qet was saved by QElectroTech 0.3, where each placed symbol
kept its own values for its definition's old text fields, as
<element><inputs><input text="T1" .../>. Loading those was deliberately
dropped in 1f53c3929 ("Remove retro compatibility of element text item
prior to qet 0.7"), so since 2021 this shipped example has shown none of
its per-symbol texts: 122 texts on 44 of its 48 symbols, including every
device label (Q1, KM1, KM2, T1, M1, S1-S5, H1, H2, the X terminals) and
ratings such as "24VAC" and "F0 am 0,5A".
This commit changes the example file only, not the loader. It was
converted by loading it once with a local build that had 1f53c3929
reverted (and the converted label kept when the symbol's own label was
empty, as that code intended), and saving it. That build is not proposed.
Checked with a build of current master: every one of the 44 symbols shows
exactly the texts its <inputs> held; the 48 symbols (type, position,
rotation), the 69 wires (ends and numbers) and the folio are unchanged; the
two free texts only gain their font written out, as any save does. Like any
save, the file also gains uuids and the current version number.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
set_table_position and delete_table took a table's index, and
set_element_text / delete_element_text a text field's index. An index
shifts when an earlier item is deleted, so a run that deletes table 0 and
then moves "table 1" moved the wrong table.
Each now also takes the item's uuid, resolved at run time through
qet.tableIndex() and qet.elementTextIndex() (the previous commit), as
texts, shapes and pictures already are through textIndex() and friends. A
text field's lookup is scoped to the op's element, since copies of a symbol
share their fields' uuids. An index still works, and a build without the
lookups is refused with the usual missing-methods hint only when a uuid is
actually used.
Tests: the generated script (no binary), and end to end: delete one table
then move the other, both by uuid; and edit one copy's shared field by uuid,
leaving the other copy's untouched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A field with a uuid on a symbol with none cannot be keyed by
(symbol, field); the mutation audit showed either side's guard could be
dropped unnoticed. Now tested with the symbol uuid missing on one side,
then the other.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qet.textIndex(), shapeIndex() and imageIndex() turn a free text's, shape's
or picture's uuid into the index the other calls take. Tables and symbol
text fields had no such lookup, so a script could only name them by index,
and an index shifts when an earlier item is deleted: a script that deletes
table 0 and then moves "table 1" moves the wrong table.
- qet.tableIndex(folio, uuid): the table's current index in tables(folio),
or -1.
- qet.elementTextIndex(folio, elementUuid, textUuid): the field's current
index in elementTexts(folio, elementUuid), or -1. The element is part of
the address because a field's uuid is unique only within its element:
copying an element keeps its fields' uuids (2612_ats_singlephase.qet has
one field uuid on 20 copies).
Tests in misc/qet-mcp's integration suite, which drives these through
--run: a table followed across a deletion of the one before it, and the
same field uuid resolving on two copies to each copy's own field. Both
fail against a build without this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qet_diff's texts, shapes and pictures sections collected every <input>,
<shape> and <image> under a folio with iter(). Symbols in older files carry
their own <inputs><input> texts, so those were counted as the folio's free
texts: schema_indus.qet folio 1 showed 124 where QElectroTech has 2, and
editing one of those symbol texts would have been reported as a free text
changing.
Only the folio's direct <inputs>, <shapes> and <images> children are read
now. Shapes and pictures have no nested copies in the shipped examples;
they are changed too so the three stay alike. Found by comparing the
tools' answers with QElectroTech's own lists over the shipped examples.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qet_edit addresses free texts, shapes, pictures and tables by uuid, and
qet_diff reports them by uuid, but no tool listed them: the only way to
learn an item's uuid was to read the .qet. qet_items lists every free
text, shape, picture, table and symbol text field per folio (counted from
1, as qet_elements), with its uuid and main fields; filter by folio and
kind; default limit 500.
Also a test that every tool argument holding a data path is in the
workspace policy (_DATA_PATHS). Nothing checked that direction: a new tool
left out of the policy would have read or written anywhere with every test
passing, as removing qet_items' entry showed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A symbol text field's uuid is unique only within its symbol: copying a
symbol keeps them, so 7 of the 24 shipped examples repeat one, up to 20
times (2612_ats_singlephase.qet). Keyed on the field uuid alone, those
fields merged and an edit to one copy could be reported on another. They
are now keyed on (symbol uuid, field uuid).
More generally, uuids are used as keys only when present on every item and
unique on each side; otherwise the old position/ends matching is kept, for
texts, shapes, pictures, tables, conductors and folios alike. Found by the
seeded-edit invariants (an untouched symbol's field reported changed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
QElectroTech now saves a uuid on conductors (those created since, or
loaded with one), folios, symbol text fields and tables, but qet_diff still
matched them by position or by ends:
- conductors: a rewire read as one wire removed and another added; by
uuid it is that conductor with changed "ends".
- folios: a reorder read as every later folio changing its fields; by uuid
it is one "reordered" entry, plus "added"/"removed" folios.
- symbol text fields: deleting the first of two read as the second
changing; by uuid it is that field removed.
- tables (<graphics_table>) were not compared at all; now a section of
their own.
Each is matched by uuid only when every item of that kind on both sides
has one; otherwise the old matching is kept, and each section says which
in "keyed_by", since older files and their first re-save mix the two.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rotating a symbol changes only its orientation attribute (quarter turns),
which qet_diff did not read, so a pure rotation reported no change in any
section. The elements section now has "rotated": each symbol whose
orientation changed, with the value before and after.
Rotating and undoing leaves QElectroTech writing text-field rotations as
"-270" where they were "90" (or "-90" for "270"); compared as strings that
read as a change. Rotations of texts, shapes, pictures and element text
fields are now compared reduced to [0, 360).
Found by a seeded-edit invariant run over the shipped examples: 51
rotations unreported across 20 projects, and 7 undo sequences reported as
changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>