One row per element and element it is linked to -- a coil and its
contacts, a pair of folio reports -- from each side, with the contact
group the element saved for it. Nothing reads it yet: cross-references
are the main thing one folio needs from another, and this is what lets
them be answered without building every folio.
Filled like the other tables: from the document when it can be read
exactly (a link only one side lists, between kinds that cannot link, or
giving a contact a second coil is left to the folios), from the folios
otherwise. A link made or undone afterwards rewrites that element's rows
when the database is next read, as drawing items are, since the contact
group is set after the link is made.
Test: tst_databasefromdocument compares the link table both ways on
every example; linksAreListed checks a coil and contact (with a group)
in m_000.qet, oneSidedLinkFallsBack the refusal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Wga9B92BNurDEtxxtCDxf
On opening a project, a coil and its contacts are linked in the order
the folios' items are visited. When contacts are on an earlier folio
than the coil they link first, so the coil's list followed the folio
order instead of the file's, and contacts on one folio swapped places
from run to run. Saving the same project twice could then give two
different files.
After linking, Element::initLink() now puts the linked elements back in
the order the file listed them. Elements linked but not in that list
keep their place after it.
Test: tst_resaveunchanged::savedLinkOrderKept reverses a coil's two
contacts in m_000.qet and checks a resave keeps that order; it fails
without the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Wga9B92BNurDEtxxtCDxf
edgeCases() expected an empty device to be exactly MinHeight (30) tall.
The sizing rule is max(MinHeight, 2 x label height) rounded up to the
grid, so with a taller fallback font the body is 40 and the test fails.
The Linux CI image has no Liberation Sans; fontconfig substitutes Nimbus
Sans (fonts-urw-base35), whose line height gives 40.
Compute the expected height from the label font's metrics, as the
layout does.
Reproduced locally with a fontconfig file listing only the urw-base35
fonts: the old test fails with the CI's exact message (40 vs 30), the
new one passes. Doubling the label factor to 3 in genericdevice.cpp
still makes the new check fail, with Liberation and with Nimbus.
Reported by bhangart on #1418.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DiagramImageItem::fromXml() read the size attribute with a plain
toDouble(). A picture whose size was missing or not a number got size
0, so it was invisible on the folio, left out of the SVG export, and
a resave wrote size="0" back, which then stayed 0 on every load. "nan"
was kept and written back as such. The <transform> child, which newer
files carry, already defaults its scale to 1.
Read the size with an ok flag and use 1, the size a picture is added
with, when it is missing, not a number, not finite, or 0.
No file-format change for usable sizes. A picture that an earlier save
left at size="0" shows again, at size 1, after this change.
Tests: tst_imagemissingsize runs the binary's --resave and --export-svg
on a fixture with six small pictures: size missing, "0", "nan", "big",
"0.5" and "2". The first four fail on master (saved as 0, 0, nan, 0)
and the export draws only 3 of the 6 pictures there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Beat Hangartner <beat@hangartners.ch>
"nan", "inf" and "-inf" parse as numbers, and IndependentTextItem::
fromXml() used them as they came for x, y and rotation. A project with
such a value kept it: --resave wrote x="nan" back, and the SVG export
drew the text at matrix(1,0,0,1,nan,95), which is not valid SVG.
Conductors and element texts already load a non-finite position as 0.
Load a non-finite x, y or rotation of a free text as 0, the same way.
No file-format change: finite values load and save as before.
Tests: tst_freetextnonfinite runs the binary's --resave and --export-svg
on a fixture with x="nan", y="inf", x="-inf", rotation="nan" and a
finite text (61.7, 61.3, 37 degrees, kept as they are). The four
non-finite cases and the export check fail on master.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Beat Hangartner <beat@hangartners.ch>
The texts added with dropping picture files on a folio (#1416) had no
entry in the translation files yet, so the French interface showed them
in English, and the English one wrote "Add 1 image(s)".
- qet_fr.ts: the French of all nine texts (the eight of #1416 and the
"%1: %2" of the list of refused files), "%1 : %2" with the French
space before the colon.
- qet_en.ts: the singular and plural of "Add %n image(s)".
The entries are lupdate's own, inserted where lupdate places them, so a
later refresh of the .ts files moves nothing; lupdate run over sources/
finds them all as existing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The message listing the files a drop could not add wrote each line as
"name : reason", with the space French puts before a colon, and as a
fixed string that no translation could change. It is now a tr() text,
"name: reason", so each language can write it its own way.
Suggested in the review of #1416.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tst_menubarkeyboard sends Alt+F and F10 right after the test window is
exposed, then sleeps 300 ms and checks. Menu mnemonics and the F10
shortcut (a Qt::WindowShortcut) only fire in the active window, and
being exposed does not mean being active: on a loaded Xvfb, or on macOS
with another application in front, activation arrives later and the key
is dropped. The sleep comes after the key and cannot help. This made
shortcutOpensMenuBar fail on #1420's Linux CI ("F10 did not activate the
menu bar") while the same base passed on #1421 and #1422, and made the
Alt+F control fail in 1 of about 33 local runs with the cocoa platform.
Every test now waits for the window to be active before pressing a key
(qWaitForWindowActive, with its own failure message), and the two tests
that expect a menu poll for it with QTRY_VERIFY2 instead of a fixed
sleep. plainF10DoesNothingInQt keeps its 300 ms: it checks that nothing
happens, which cannot be polled for; with the window now active first,
that check can no longer pass merely because the key went nowhere.
Test-only change. Checked: with a second window active, F10 sent to the
exposed but inactive window does not activate its menu bar (offscreen
and cocoa); once it is active, it does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lupdate over sources/ adds the 32 strings merged since the conversion to
English (#1390); with the entries already empty on master, 59 had
no Polish text and 2 only a lupdate guess. All 61 are translated with the
file's terms; "cable" is
"przewód" and its wires "żyły".
Corrections to existing entries:
- "master" (master element) is "główny", not "nadrzędny", in the 11
entries that still used it (properties, cross-references, search and
replace, terminal editor); "nadrzędny" stays where it means a parent
folder or collection.
- the help text for %folio in two widgets still had a French sentence
("Les variables suivantes sont utilisables :") inside the Polish text.
- "Komentaż PLC" typo, now "Komentarz PLC".
- 6 colons with a French space before them ("Marginesy :").
Most of the diff is lupdate moving line numbers. Checked with lrelease
(3847 translations, no warning) and the placeholder check for every
changed entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lupdate over sources/ adds the 32 strings merged since the conversion to
English (#1390), among them picture drop, element scaling, paste element
information, the terminal strip cable fields and the settings-loading
dialog. With the 29 entries that were already empty on master, 61
entries had no German text, so the German UI showed
English there. All 61 are translated with the terms the file already
uses (Bauteil, Anschluss, Klemme, Raster, Kabel/Adern, Spiegeln).
As arummler noted in #1381, "Programmable Logic Controller (PLC)" read
"SPS (PLC)"; it is now "Speicherprogrammierbare Steuerung (SPS)" in both
places, and "PLC-Querverweis" becomes "SPS-Querverweis" like every other
PLC string in the file.
Most of the diff is lupdate moving line numbers. Checked with lrelease
(3847 translations, no warning) and the placeholder check for every
changed entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
25 of the 79 plural messages in qet_fr.ts used the old "(s)" marker in
both forms, e.g. "Ce document PDF contient %n page(s)" for one page and
for many. They come from the time French was the source language: the
French source text had "(s)", and when French became a translation in
#1379 and #1390 that text was copied into both forms. Reported by
arummler in #1381.
Each of them now has a singular form (also used for 0, as Qt's French
rule does) and a plural form, with the agreements that follow ("%n
borne de cet élément n'est pas sur la grille" / "%n bornes ... ne sont
pas"). Only <numerusform> lines change; no other message is touched.
Checked with lrelease (3815 finished, no warning); placeholders checked
for every form.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7c6a0411d set PACKAGE_TESTS=OFF in windows-build.yml to make the job
green again. The job had turned red because three tests merged on
2026-10-07 (tst_toolbarsettings, tst_diagramtoolbarsettings,
tst_gesturesettings) set no settings scope, so QSettings on Windows had
no registry key and they failed. #1411 gives them their scope. With
PACKAGE_TESTS=OFF the "Run the unit tests" step finds no tests and
still succeeds, so the job checks nothing.
Set PACKAGE_TESTS back to ON.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Beat Hangartner <beat@hangartners.ch>
Edit > Add > Generic device (also on the Add toolbar, and found by the
command search as diagrameditor.add_generic_device) opens a three-page
wizard for devices used too rarely to draw a symbol for, such as a VSD
or a power supply:
1. Layout: terminals per side (0 for none), a gap every N terminals,
divider marks on gaps, groups lined up across opposite sides, pitch
and extra size.
2. Terminals: a table per side and the same list as one line, where
In{4} is In1..In4, Q{z4} is Q0..Q3, X{3-6} is X3..X6 and an empty
entry is a gap; number pins 1..n counter-clockwise; paste rows of
side, name, function from a spreadsheet or a CSV file.
3. Information: name, label, manufacturer, reference, description.
A preview beside every page shows the box as it will be placed. Finish,
available from the first page, writes an ordinary symbol to the folder
"Generic devices" of the project's collection and puts it on the cursor.
The box sizes itself to its terminals and their names; every terminal
lands on the grid and gets a uuid. No new XML: any QElectroTech reads it.
The layout is pure (sources/genericdevice/genericdevice.cpp) and tested
by tst_genericdevice. ElementPictureFactory gains pictureFromDefinition()
to draw the preview with the same code as a placed element.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Symbols copied in old versions can share one uuid on a folio. Up to
0.100 that was harmless: a wire on a terminal without uuid was saved by
terminal number. Since #1118 every terminal has a uuid, so every wire was
saved by symbol uuid and terminal uuid, and on reopening findTerminal()
took the first symbol carrying it: all the copies' wires landed on it.
- Save: a wire end on a symbol whose uuid another symbol of the folio
also carries is written by terminal number again.
- Load: when a wire's uuid names several symbols, pickEnds() takes the
pair whose distance matches the wire's saved path, then the symbols
whose labels match the ones saved with the wire, then two terminals no
wire joins yet; otherwise the first, as before. This repairs a file a
build with #1118 saved once.
tst_sharedsymboluuid gives the three lamps of
Habitat-Schemas_developpes.qet one uuid: master loses a net after a
save (8 of 9) and reopens the file it saved with 8; with this, 9 both
times. The 25 examples save the same as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Dialog sizes and positions (dialoggeometry/...) describe this computer,
like the other window layout keys: no longer saved to a settings file,
and kept when one is loaded.
- The trackpad gestures setting (diagramview/gestures) is in the part
for toolbars, shortcuts and gestures rather than all other settings.
- Loading only the defaults for new projects now says it is done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mirroring a symbol on a folio keeps its centre in place and then snapped
the new position to the grid. A symbol that was not on the grid was
pulled onto it, so a second mirror the same way left it a few pixels
away from where it started. In perceuse.qet that is 171 symbols, each
way. tst_scriptmirror::mirrorBackAndSave() picks its symbol in an order
that changes from run to run, so it failed only some of the time (three
runs in eight on macOS, and on the Windows CI).
The move is now snapped to the grid instead of the position. A symbol on
the grid stays on it, one off the grid keeps its offset, and because
qRound() rounds halves away from zero both ways, a second mirror moves
it exactly back.
The new tst_scriptmirror::mirrorTwiceComesBack mirrors every symbol of
perceuse.qet twice, horizontally and vertically, and checks that every
terminal is back in place. It fails on master (171 symbols moved).
No change to the file format.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tst_toolbarsettings, tst_diagramtoolbarsettings and tst_gesturesettings
never set an organization or application name. On macOS and Linux,
QSettings then falls back to an "Unknown Organization" file, so the
tests pass there. On Windows it has no registry key: every write is
dropped and every read gives the default, and nine checks fail.
Each test now sets its own organization and application name in
initTestCase(), as tst_scriptingsetting and tst_sheetbackgroundsetting
already do. That also keeps the tests away from the settings of the
account that runs them.
Tests only; no change to the app or its files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1379 added misc/i18n/mirror_source_language.py and an INSTALL.md rule
to run it on lang/qet_fr.ts after update_translations, while French was
the source language. Since #1390 the code is English and qet_fr.ts is an
ordinary translation, so following the rule would copy English text into
empty French entries. The repository test still checked qet_fr.ts and
kept passing.
The rule, the script's description and the repository test now name
lang/qet_en.ts. The script refuses a file whose <TS> header names a
language different from its sourcelanguage (qet_fr.ts says fr_FR / en),
exits 2 and leaves it untouched; files without a sourcelanguage, like
the fixtures, are accepted as before. The 6 entries master added since
the conversion are mirrored, so qet_en.ts is complete again.
Tests: 19 in misc/i18n/test_mirror_source_language.py. The repository
test fails on master (6 entries to mirror); with the check_languages()
call removed, the two refusal tests fail and a run on qet_fr.ts goes
through.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Save settings as... now asks which parts to save: toolbars, keyboard
shortcuts and mouse gestures; defaults for new projects (folio, title
block, wires, numbering); everything else. All three ticked writes the
same complete file as before (format 1, collection folders included).
A file with only some parts is format 2 and lists its parts. Loading it
replaces those parts and keeps every other setting. The folders of the
collections are never in such a file: they rarely exist on another
computer. A QElectroTech that only knows format 1 refuses it rather than
loading it as complete and losing every other setting.
A file holding only the defaults for new projects loads without closing
QElectroTech, since QETProject reads them again for every new project.
Which part a key belongs to is ConfigProfile::partOf(); a key it does
not list falls in "everything else", so a new setting can never be left
out of every part. The test checks that what BorderProperties writes for
the folio defaults lands in the new-project part.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pictures dropped together were cascaded 20 px apart from the drop
point; once scaled to fit the folio they lay almost on top of each
other. They are now laid out side by side in a roughly square grid
over the folio's drawing area, inside the same 20 % margin, each
shrunk into its cell with its proportions kept. A single dropped
picture still lands on the drop point.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dropped picture too large for the folio is now scaled to leave 20 %
of the drawing area free on every side, instead of being fitted to
the visible part of the view, and every dropped picture is kept inside
the frame -- a scaled-down one also off that margin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Picture files (png, jpg, bmp, svg) dragged from the file manager onto a
folio are added there: the first centred on the drop point, the others
cascaded from it, and one undo step removes them all. A picture larger
than the visible part of the folio is scaled down to fit it. Files that
cannot be used are listed once after the others have been placed, and a
drop holding only other files (a .qet project) still reaches the main
window, which opens it.
The checks a picture must pass before it is embedded in the project
move into ImageDrop::load and are now shared by the drop, the add image
dialog and the script API: a regular file of at most 10 MB, and at most
64 megapixels, read from the header before any pixel is allocated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A shape remembers whether its pivot was placed by hand: a hand-placed
pivot is saved and no longer follows the shape's centre when the shape
is resized. Dragging the pivot handle marked it as hand-placed, but the
undo step restored only the pivot and the position. After Ctrl+Z the
pivot was back at the centre and still marked as hand-placed, so it
stayed behind on the next resize and was saved as a <transform>.
pivotIsCustom becomes a property and goes into the pivot drag's undo
step, the same fix as #1367 for pictures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pressing Escape while a handle of a picture was dragged went to
DiagramView, which clears the selection. That removed the handles under
the mouse, so the drag stopped where it was: the picture kept the new
size, angle or pivot, and since the release never arrived, no undo step
was recorded and the change could not be undone.
Diagram gets a keyboardItem: an item in a state Escape must end. While
a resize, rotate, skew or pivot handle is dragged, the picture is that
item; DiagramView sends each key to it first, and only keys it does not
take reach the view's own shortcuts. Escape puts the picture back as it
was when the handle was pressed (position, transform, and whether the
pivot was placed by hand) and ignores the rest of the gesture, so no
undo step is recorded. The drag also ends when the handles go away.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rotate action (Space) turned a picture through QGraphicsItem's
plain rotation, which DiagramImageItem::toXml() does not save -- it
saves the rotation of the picture's own transform. The picture came
back unrotated once the project was reopened. Turn it through
"rotationAngle" instead, around its own pivot, and when a selection
is turned as a group, move the picture so that its pivot, not its
top-left corner, orbits the group's centre.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>