"Position automatique" sorted labels as plain strings, so XAUZ1:10
came before XAUZ1:2. Add QETUtils::naturalLessThan (digit runs compared
by value, case insensitive; QCollator numeric mode is ignored by some
Qt backends) and use it in SortTerminalStripCommand, FreeTerminalModel
and the terminal strip tree dock.
The element editor moves the selection in fine steps with
Ctrl+arrow. The diagram editor used Alt+arrow for its fine
movement and Ctrl+arrow for aligning a text group, so the same
modifier did different things in the two editors.
Swap the two so the diagram editor matches the element editor:
- Ctrl+arrow: fine movement (unchanged step, still configurable via
diagrameditor/key_fine_Xgrid and key_fine_Ygrid)
- Alt+arrow: align the selected text group (Left/Up/Right)
Only the modifier checks change, the behaviour of each action is
untouched.
The folio grid steps, diagrameditor/Xgrid and Ygrid, are read from the
settings in six places with a plain toInt() and used as they come. The
preferences page cannot store anything below 1, but a hand-edited or
damaged settings file can hold 0, a negative number or text:
- Diagram::snapToGrid() divides by the step, so 0 is a SIGFPE on the
first click that places a symbol;
- Diagram::drawBackground() runs "while (g_x % xGrid)" and then loops
"gx += xGrid", so 0 crashes every repaint and a negative step never
ends;
- the paste, duplicate and align paths divide by it or multiply with it.
Add foliogrid.h, a header-only helper: FolioGrid::step() reads a
settings entry and returns the built-in step (Diagram::xGrid, 10) when
the entry is missing, not a number, below 1 or above what an int
holds (QVariant::toInt() wraps such a value around). Every reader of
the two keys goes through it; the settings page, which only writes the
spin box values, is unchanged.
No file-format change, and no change for any step the preferences page
can produce.
Tests: tst_foliogrid covers the helper: 1, 10, "7", int max are kept;
missing, 0, -5, "ten", "", "nan", 99999999999 (as text and as a
number) and int max + 1 fall back; 7.9 rounds to 8 as before; and the
two settings keys through a QSettings scope of the test's own. The
helper is new, so the test cannot fail on master; the six readers are
the replacements in the diff.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Beat Hangartner <beat@hangartners.ch>
Two position comparisons handed to std::sort were not strict weak
orderings, which std::sort requires; with the wrong kind of comparator
the sort is undefined behaviour (libstdc++ can read past the range,
MSVC debug builds assert "invalid comparator").
- comparPos(), used when renumbering the elements of a project, ended
with "<=" on x and y, so two elements at the same position (pasted at
the origin, placed by a script, stacked symbols) were each "before"
the other.
- The terminal numbering dialog compared positions with a 1 px
tolerance ("within 1 px counts as aligned, then compare the other
axis"), which is not transitive: terminals at x = 2, 1.1 and 0.2 give
a < b, b < c and c < a.
Move the comparisons into positionorder.h, a header-only helper:
xThenY()/yThenX() with "<", and roundedXThenY()/roundedYThenX(), which
round the positions to whole pixels first so that items a fraction of a
pixel apart still count as aligned, as the tolerance meant to, while
staying transitive. comparPos() keeps its folio and row-letter stages
and calls xThenY() for the last one.
No file-format change. Elements at distinct positions sort exactly as
before; the terminal order changes only for terminals less than a pixel
apart that straddle a half-pixel boundary.
Tests: tst_positionorder checks each order on a grid of awkward
positions by brute force (irreflexive, asymmetric, transitive, with a
transitive equivalence), the three-terminal cycle, twenty items at one
position, and that std::sort leaves the list sorted and intact. The
helper is new, so the test cannot fail on master; the call sites are
the two replacements in the diff.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Beat Hangartner <beat@hangartners.ch>
Nine messages were never passed through tr(), so every user saw them in
French: the undo entries for grouping and ungrouping terminals, for
editing an arc (3), an ellipse, a rectangle and a PLC table in the
element editor, and the error box when integrating a title block
template fails. They are now wrapped, text unchanged.
qet_en.ts also lacked English for 63 messages added since the last
translations update (Customize dialog, mouse gestures, toolbar contents,
mirror, element scale, paste element information, paper format).
update_translations was run on qet_en.ts with lupdate's guessing
heuristics off, then the 72 new entries were translated. Most of the
diff is lupdate refreshing line numbers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Windows job configured with -DPACKAGE_TESTS=OFF, so no test ever ran
on Windows. Build the tests and run ctest after the build, with the
offscreen platform. ctest prints no output for failed tests in the MSYS2
shell, so each failed test runs again with QtTest writing its log to a
file, which is printed in its own log group. The ctest log is added to
the artifact uploaded on failure.
No change to the app or its files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AssignVariables::assignSequence() replaced the sequential-number
placeholders from 1 upwards with a plain QString::replace(). "%sequ_1"
is also the start of "%sequ_10", so with ten or more sequences a
formula's %sequ_10 became the first value followed by a "0". The same
held for every family (%sequf_, %seqt_, %seqtf_, %seqh_, %seqhf_,
%seqw_, %seqa_).
Run the loop from the highest number down to 1, so the longer
placeholder is gone before the shorter one is looked for.
No file-format change. Labels with fewer than ten sequences come out
exactly as before.
Tests: tst_tensequentialnumbers runs the binary's --export-bom on a
fixture whose element has the formula %sequ_10-%sequ_1 and the unit
values A to J, and expects the label "J-A". On master the label is
"A0-A".
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Beat Hangartner <beat@hangartners.ch>
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>
BorderProperties::toXml() writes colsize and rowsize with "%1", so a
project whose default border has a width such as 60.5 saves it as
"60.5". fromXml() read both back with toInt(), which is 0 for a
decimal, and the folio clamps 0 to its 5 px minimum. New folios of such
a project got 5 px columns and rows. The folio's own reader,
BorderTitleBlock::borderFromXml(), already uses toDouble().
Read the two sizes with toDouble(&ok) and keep the previous value when
the attribute is missing, not a number, or not finite.
No file-format change: toXml() is untouched, and whole-number sizes,
which every example project has, load exactly as before.
Tests: tst_borderpropertiesxml compiles borderproperties.cpp alone and
reads 50, 60.5, 61.3 and 61.7 back, round-trips 60.5/80.25 through
toXml(), and leaves the value alone for a missing, text, nan or inf
attribute. Seven cases fail on master.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Beat Hangartner <beat@hangartners.ch>
ConductorProperties::toSettings() writes the conductor width with
QString::number(cond_size), e.g. "1.4", and fromSettings() read it back
with toInt(), which is 0 for any value that is not a whole number. A
default width of 1.4 set in the configuration became 0 on the next
start, so every new conductor was drawn with a pen of width 0.
Read it with toDouble(), and fall back to 1 when the stored value is
not a positive finite number (a hand-edited or truncated settings
file), as the other fallbacks in fromSettings() do.
No file-format change: this is the settings file only; the project
file's condsize attribute was already read with toDouble().
Tests: tst_conductorsizesetting compiles conductorproperties.cpp alone
and round-trips 2, 1.4, 0.4, 61.3 and 61.7 through toSettings() and
fromSettings() in a QSettings scope of its own; text, 0, -1, nan, inf
and a missing value give 1. Nine of the cases fail on master.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Beat Hangartner <beat@hangartners.ch>
Diagram::toXml() writes freezeNewElement and freezeNewConductor as
"true"/"false", but Diagram::fromXml() read them with toInt(), which is
0 for both words. So "freeze new elements" and "freeze new conductors"
were off again on every folio after a project was saved and reopened,
since the day the flags were added.
Compare the attribute with "true" instead. The project-level flags in
QETProject already do this.
No file-format change: the attributes are written exactly as before,
and a file without them still loads with both flags off.
Tests: tst_foliofreezeflags runs the binary's --resave on a fixture
with one folio per combination (both, elements only, none, attributes
missing) and reads the saved attributes back. The two frozen folios
fail on master.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Beat Hangartner <beat@hangartners.ch>
TitleBlockTemplateLogoManager never sets read_only_: its constructor
leaves the member uninitialised, and setReadOnly() returns early when
the new value equals the member. The template editor calls
setReadOnly() right after creating the manager, so whenever the
garbage value happened to match, the call did nothing: the add, delete
and rename buttons stayed enabled on a read-only template, and
isReadOnly() returned garbage.
Give the member a default value, false, in the header, matching the
buttons' state after construction. Only
sources/titleblock/templatelogomanager.h changes.
No behaviour change for an editable template: setReadOnly(false) on a
manager that is already editable still returns early. No file-format
change.
Tests: none of their own; an uninitialised read is what a sanitizer
finds, and this one is the same kind as the BorderTitleBlock and
ConductorProfile fixes (#1358, #1359). ctest on macOS, and all
example projects through --resave and --export-svg, unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Beat Hangartner <beat@hangartners.ch>
PartLine and PartPolygon keep their rotation in m_rot, which nothing
sets until setRotation() is called. rotation() reads it, and
setRotation() computes the difference from it, so the first rotate of
a freshly drawn line or polygon in the element editor starts from
garbage: qRound(m_rot * 100.0) on an arbitrary value is undefined
behaviour, and the angle stored afterwards is arbitrary too.
PartRectangle and PartEllipse already start at 0.
Give m_rot a default value of 0 in both headers, as PartEllipse has.
Only the two headers change.
No file-format change, and no change for a part loaded from a file or
rotated once, since both set m_rot before reading it.
Tests: none of their own; an uninitialised read is what a sanitizer
finds, and this one is the same kind as the BorderTitleBlock and
ConductorProfile fixes (#1358, #1359). ctest on macOS, and all
example projects through --resave and --export-svg, unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Beat Hangartner <beat@hangartners.ch>
Before #1310, undoing a crop left the crop rectangle in place, so a
project saved afterwards has a <crop> but shows the uncropped picture.
Since #1368 the picture shown is computed from the original and the
crop, so the first mirror or colour key of such a picture applied the
old crop again, and undoing that edit showed the cropped picture.
The picture shown is always exactly the size of the crop. fromXml() now
drops a crop that does not match it: the crop becomes the whole
original, and when the shown picture is not the original's size either,
the shown picture becomes the original. Valid files load and save as
before.
New tst_imagestalecrop: a valid crop is kept; a stale one is dropped
for a shown picture of the original's size and of another size, where a
later crop cuts the picture that was shown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lupdate over sources/ adds the 164 strings merged after the last
translations update (da39ec1c8) and revives 3 entries it had wrongly
marked vanished (the issue #1366 describes for English); the mirror
script then fills the new ones. qet_fr.qm: 3792 finished, 0 unfinished,
0 null records.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
After a lupdate refresh, the same-text heuristic fills a new string with
the translation of an existing identical one and marks it unfinished.
In the source-language file that guess is the source text itself, which
is exactly what a mirror run writes, but the script left it alone as
"work in progress": 39 such entries stayed unfinished in qet_fr.ts after
refreshing it on 2026-10-07.
The script now finishes an unfinished translation (or plural with all
forms) that is identical to its source. Unfinished translations with
other text stay untouched. Two fixture messages and a test cover it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The strings in the code are French and serve as both translation key
and displayed text. lang/qet_fr.ts was almost empty: 3425 messages and
22 plural messages had no translation, so lrelease compiled them as
null-string records (3469 in qet_fr.qm) and the French UI showed the
code text through QTranslator's null fallback. Correcting French
wording therefore meant changing the key, which orphans the translation
of that string in all 33 other language files; the "Élement" typo fix
needed a key rename in 34 files for one accent.
This copies every empty French entry's source text into its own
translation, so qet_fr.qm is complete (514 KB, 0 null records, like
the other languages) and French wording can be corrected in the .ts
alone. Translations that already had text (113 French renderings of
English-written sources, 16 identical, 54 plurals) are untouched, as
are the 10 vanished entries. The file is byte-identical outside the
rewritten <translation> elements.
misc/i18n/mirror_source_language.py does the copy and is meant to run
after update_translations; --check exits 1 when a run is needed.
Forgetting it breaks nothing: an empty entry still falls back to the
code text. INSTALL.md section 10 explains the rule and that
-removeidentical must never be added to lrelease. Tests:
misc/i18n/test_mirror_source_language.py with a before/after fixture
pair covering every message class.
No visible change in the application.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Edit > Scale element... scales the whole element about its hotspot by a
factor picked from x0.5, x1.5, x2, x2.5, x3, x4. Only the factors that
leave every terminal on the 10 px folio grid are offered, so wires to a
scaled element stay straight. An element whose terminals are off the grid
today is offered only the factors that bring them on to it; if none does,
the dialog says so and OK is disabled.
Drawn parts are scaled with their existing handleUserTransformation(), as
the resize handles do. Texts, dynamic text fields and terminals are set
directly so that font sizes (whole points, 4 pt minimum), terminal name
offsets and line end sizes scale too, which the resize handles do not do.
One undo step restores the element.
Unlike "Import an element to resize", this needs no external program and
works on the element being edited.
Grew out of issue #1338.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Issue #1375: replacing a symbol meant retyping its label, manufacturer,
part number and the rest by hand. Copy the old symbol (Ctrl+C), select
the new one(s), then Edit > Paste element information, also in the
right-click menu of a selection.
Every field filled in on the copied symbol overwrites the same field on
each selected symbol; fields it leaves empty are left alone, since empty
fields are not written to the clipboard. The label and its formula move
together, so a target whose label comes from a formula takes the copied
label rather than rebuilding its own. A numbering-scheme id is kept only
if the scheme exists in the target project. Plain slaves and folio
reports are skipped: they take their information from what they are
linked to, as in the properties dialog. One undo step.
Enabled only when the clipboard holds exactly one symbol. The clipboard
is parsed once when it changes, not on every selection change.
No default shortcut: Ctrl+Shift+V, suggested in the issue, is already
"Paste at origin". It can be assigned in the shortcut settings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The crop rectangle came back as the string "x,y,width,height", which a
script had to split and parse. elementGeometry() returns a map; the
crop now does the same, and an empty map for no such image.
Suggested in the review of #1310.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>