A dragged shape put its pos() on the grid. pos() cannot be seen, and it
is off the drawn corner whenever the shape was drawn or resized with
Ctrl held, or rotated, so such a shape stayed off the grid however it
was dragged. Worse, Snap to grid (previous commit) moves pos() off the
grid to put the corner on it, so the next drag undid the snap.
QetShapeItem now overrides setPos(), which only the drag calls through
the virtual: dragged alone or with other shapes only, the top-left
corner of the drawn outline goes on the grid. Dragged together with
anything else it snaps by pos() as before, because the rest of the
selection follows this shape's movement and a corner correction would
take the symbols off the grid. Ctrl still drags freely: the snap goes
through Diagram::snapToGrid(), which reads it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The seven Edit > Align commands (Snap to grid and the six align
commands, discussion #1069) had no icons: the menu, the context menu,
command search and the shortcut bar showed them as bare text.
They are pixel-grid SVGs in ico/scalable/ drawn like the folio and
Add PDF icons: 24 pixel canvas, art on the inner 22 pixel grid, one
currentColor ink so misc/make_icon_themes.py writes the dark copies.
Each align icon is a guide line with two boxes of different lengths
against it; the centre ones show the guide only between the boxes.
Snap to grid is a faint grid with one box sitting exactly on a cell.
The boxes have a solid outline and a 35 % tinted inside, which keeps
the guide the strongest mark at 16 pixels.
Names follow the freedesktop icon names (align-horizontal-left, ...).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Rectangles, ellipses, lines and polygons were left out of the Align
submenu: with only shapes selected every command was greyed out, and a
shape outside a group was ignored when aligning it with a symbol.
Shapes now take part like pictures. Their edges are the shape as drawn
(the new QetShapeItem::sceneOutlineRect(), without the pen, the 6 px
selection margin or the wider hover outline; the old code used
sceneBoundingRect() for grouped shapes and so aligned them 6 px off).
The point that goes on the grid is the top-left corner of that box: a
rectangle's corner, an ellipse's box. pos() is not used, because a
shape drawn with Ctrl held or rotated has its corners off the grid
while pos() is on it.
The menu's enable rule now counts what the command counts, a group as
one. Before, two symbols in one group enabled the six align commands,
which then did nothing and only said so in the status bar.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Grouping two symbols and then locking one of them (Lock position in its
properties) left the group in a broken state: dragging the unlocked symbol
pulled it away while the locked one stayed, and dragging the locked one
did nothing. The move simply dropped locked items, so the rest of the
group went without them.
A group with a locked member now does not move at all, whichever member
is dragged, and the status bar says why. This is the rule the item-groups
proposal (discussion #1070) set out for this case. The same rule applies
to the arrow keys and to the Align commands, which share
DiagramContent::removeNonMovableItems().
Also fixed on the way, for a plain selection with a locked symbol: a wire
between the locked symbol and one being dragged kept its user-placed text
moving with the dragged end. Such a wire is now redrawn only, as a wire to
an unselected symbol already is.
Checked in the GUI on two symbols joined by a wire (grafcet example),
master against this branch, positions read from the saved file:
- drag the unlocked member: master moves it 190 px, this branch moves
nothing and shows the message
- arrow keys on the selected group (3 runs each): master moves the
unlocked member, this branch nothing
- the same two symbols ungrouped: both move the unlocked one, as before
- user-placed wire text: master shifts it 190 px, this branch keeps it
ctest: 34/34.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Group and Ungroup are enabled in slot_updateComplexActions(), which ran
only when the selection changed. Grouping, ungrouping and undoing either
leave the selection as it is, and so does right-clicking an item that is
already selected, so the actions kept the state from before. The
right-click menu hides disabled actions, so it went on showing Group on a
selection that was now one group (and Ungroup after ungrouping), and the
one that would work was not in the menu at all.
Diagram::setItemGroup() is the one place an item's group changes (group,
ungroup, undo, redo, paste), so it now emits itemGroupChanged() and the
editor refreshes its actions on it, beside the selectionChanged
connection.
Checked in the GUI on two free texts, saving after each step: right-click
> Group, right-click again, Ctrl+Z, right-click again. Before, the second
and third right-clicks offered Group again and did nothing (both saves
still grouped). After, they offered Ungroup and ungrouped (0 grouped
texts in both saves); Group, Ungroup, Group also round-trips.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Windows portable ZIP ships "Lancer QET_qt6.bat", but the script only
looked for "Lancer QET.bat", so it stopped at its first check and never
registered anything. It now tries the ZIP's name first and falls back to
the installer's.
It also wrote to HKEY_CLASSES_ROOT, which a standard (non-admin) Windows
account cannot create keys in. It now writes to
HKEY_CURRENT_USER\Software\Classes: per-user, no elevation needed, and
Windows merges it into HKEY_CLASSES_ROOT for that user.
qet_uninstall_file_associations.reg removes the HKCU keys first, then the
old HKEY_CLASSES_ROOT ones as before.
Tested under Wine 10 on the git10275 nightly ZIP: the old script aborts
("Lancer QET.bat ... n'a pas ete trouve"), the new one registers .qet,
.elmt and .titleblock under HKCU with nothing under HKLM, the uninstall
file removes them, and the "Lancer QET.bat" fallback works. Wine does not
enforce admin rights, so the non-admin case itself is not proven there.
Reported in #1148.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The first fill from the document stored the label and wire text saved in
the file. QElectroTech saves what it last worked out, not what the formula
gives now: K%total-%id saved as K1-1 on a 3-folio project is K3-1 on the
folios, and the same for a wire's formula. So the document fill disagreed
with the folio fill on any file whose folios had been added or moved.
AssignVariables now works from a FormulaContext: the folio's number,
index, total, plant, location, title-block and project variables, and the
element's grid cell and prefix or the wire's four properties. The Diagram
overload fills one from a built folio, the database fills one from the
file, and both call the same evaluation.
Also brought in line with Diagram::fromXml(): an element Element::valideXml()
rejects is skipped with its wires; a folio where two elements number their
terminals alike (Element::fromXml() refuses one, by geometry), a frozen
formula wire text, and the older sequential-number attributes are left to
the folios. Each folio's border now starts from the defaults a new Diagram
has, not the previous folio's values.
tst_databasefromdocument: formulasAreWorkedOut (every variable kind, stale
saved values), unbuiltElementIsLeftOut, clashingTerminalIdsFallBack. Each
fails with its fix removed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A project saved as "my.project.qet" showed "my" in every title block
cell using %{projectfilename}, and "my" again in %{savedfilename} after
a save. QETProject used QFileInfo::baseName(), which stops at the first
"."; completeBaseName() strips only the ".qet" suffix. Same fix as
#725 made for the PDF export's file name.
Verified headlessly on examples/industrial.qet copied to
my.project.qet, with a title block cell set to %{projectfilename}:
--export-svg shows "my" on master and "my.project" with this change,
on all 50 folios. For %{savedfilename}, a --run script calling
qet.save("") twice writes "my" on master and "my.project" with this
change. ctest 32/32.
Not changed: write() updates the saved* variables after the file is
written, so the values stored in a file are those of the save before.
Co-Authored-By: Claude Opus 5.5 (1M context) <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>
The configuration names "python" or "python3", or the Python the Windows
installer can add. When none of those can run, the assistant fails with
an error far from the cause, so the dialog now says so in bold, with what
to do: tick "Python pour l'assistant IA" in the installer or install
Python (Windows), or install Python 3 from the system's packages.
Windows 10 and 11 put a python.exe in WindowsApps that only opens the
Microsoft Store, so a PATH search alone reports Python where there is
none. A result there is reported as possibly that shortcut. Backslashes
are converted explicitly, not with QDir::fromNativeSeparators(), which
leaves them alone off Windows and so could not be tested here.
tst_aiassistantsetup: 17 cases (8 new: the bundled Python is taken
without looking on PATH; none, python.org, the Store shortcut with either
separator, and a Linux folder named WindowsApps). Each of the 4 new rules
was removed in turn and the test failed. ctest 32/32. In the GUI, the
same install layout shows the warning with no python3 on PATH and no
warning with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows every tool that starts QElectroTech (qet_export, qet_edit,
qet_query, qet_check, qet_continuity, qet_project_new) failed, for two
reasons:
- The server ran a copy of the executable from a temporary folder, to get
its own SingleApplication key. A Windows program loads its DLLs from its
own folder, so the copy died before main() with 0xC0000135 (DLL not
found). Every flag the server passes is a CLI export flag or --run, and
main.cpp handles both before it constructs SingleApplication, so on
Windows the original is now run. The copy stays elsewhere.
- It set QT_QPA_PLATFORM=offscreen. The Windows packages ship only the
qwindows platform plugin, so Qt found none and stopped at a message box
nobody could close: every call hung until its timeout. Windows now keeps
its default platform; the export flags and --run open no window.
Checked under Wine (qet-wine-smoke) on the fork's CI Windows build, run
through python.org's embeddable Python: before, qet_export ended with
exit 3221225781; after, a PDF export, a qet_query (98 elements, as on
Linux) and a qet_edit placing a common:// element all succeed. The hang
was isolated by launching the same export from bash (works) and from
Python with one change at a time: only dropping QT_QPA_PLATFORM made it
work. Four unit tests pin both choices per platform; each fails with its
fix removed. Suite 262/262 none skipped on Linux.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
diagram_info.date -- and project_summary_view.date, which the summary
table dialog queries -- held the folio date read back from the title
block's text: QLocale::system().toDate() of the string
updateDiagramContextForTitleBlock() writes in the locale's short format.
Where that format has a two-digit year (en_US "M/d/yy") Qt reads the year
back as 19xx, so 741.qet's 2010-09-21 became 1910-09-21, and so did every
dated folio of the 24 examples (109 of 109).
bindDiagramInfoValues() now binds the folio's own date,
BorderTitleBlock::date(), the value that text was made from. A folio set
to show the current date or none gives the same date as before.
tst_dbfoliodate runs --run on examples/741.qet under LC_ALL=en_US.UTF-8
and expects 2010-09-21 from project_summary_view: 1910-09-21 without this
change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mutation audit of the functions no audit had covered (tests needing no
QElectroTech): these 13 caught 153 of 337 planted bugs (45 %). With these
tests, 322 (96 %). qet_check was at 26/59 and qet_continuity at 24/37
even with the binary tests, since those look at a finding or two.
- qet_check / qet_continuity: _run_qet replaced by a stub returning chosen
log lines, so the whole answer is compared -- summary counts, "ok",
passed, check_failures, each finding's count, note and sampled rows,
sorting by severity, folio_number, the launch hint carried through,
the folio argument in the script, folio bounds, and lines that only
look like ours.
- qet_element_build / qet_element_info: the written header, names, kind
information and terminals, and the reported result, key for key; the
refusals; geometry worked out by hand; every part kind's extent and
written attributes; number formatting.
- qet_element_search and its index: the index entry key for key, the
cache, ranking (exact name, then first word, then length), the default
and given limits.
The 15 left are equivalent: timeouts and output limits, a ranking
constant that only has to exceed 0, and a containment check the 5-unit
margin keeps from ever failing. Tests only; 282/282.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dialog showing the configuration an AI assistant needs to start
QElectroTech's MCP server (misc/qet-mcp), with this installation's paths
filled in: pick the assistant (Claude Desktop, Claude Code, GitHub
Copilot in VS Code, Cursor, Gemini CLI, Codex CLI, LM Studio), the
drawings folder the assistant may use, and whether it may edit (off by
default). It says where the text goes, warns that text inside a project
from someone else can steer an assistant, and copies the text. It writes
nothing and starts nothing.
AiAssistantSetup finds the server beside the running program, in the two
places the packages put it (<root>/mcp beside <root>/bin on Windows,
<prefix>/share/qelectrotech/mcp beside <prefix>/bin otherwise), and the
Python the Windows installer can add. When there is none, the dialog says
the server is not installed with this version and links the guide.
The configuration is written with QJsonDocument, so Windows paths are
escaped correctly; Codex CLI gets TOML.
tst_aiassistantsetup (9 cases): both layouts, no server, the bundled
Python on Windows only, valid JSON for every JSON client with paths that
parse back unchanged, the "servers" key for VS Code, "type" only where
wanted, editing off unless allowed, escaped TOML. Each of 8 rules was
removed in turn and the test failed. ctest 32/32.
Checked in the GUI (Xvfb): Help menu opens it; from a build tree it
reports the server missing and disables Copy; from an install layout it
fills the paths, and the text Copy put on the clipboard, used unchanged
as a Claude Code configuration, ran a real qet_project_info call.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Most Windows users have no Python, which the MCP server needs. The
installer now offers "Python for the AI assistant", unticked by default:
the official embeddable package from python.org (3.14.7, ~12 MB, no
registry, removed with QElectroTech), in <folder>\mcp\python. The
portable folder and the MSI, which pack all of files/, carry it.
The workflow downloads it pinned by version and by python.org's own
sha256 for the file (checked against python.org's download API), and
fails the build on a mismatch. The script stays plain text beside it.
Checked under Wine (64-bit, qet-wine-smoke image), on an installer built
with makensis 3.10 (0 warnings, strings in all 29 languages):
- a silent default install puts mcp\qet_mcp.py in place and no Python;
the same installer with the section ticked by default installs it, so
the check tells the two apart;
- the installed Python runs the installed server: 15 tools listed,
qet_project_info on an example answers as on Linux, and the server
finds bin\QElectroTech.exe and elements\ by itself.
Not checked: a real Windows machine; the CI download step (fork run).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The MCP server (misc/qet-mcp) was only reachable from a source checkout.
It now ships with every package, next to the program, and finds that
QElectroTech and its element collection from where it sits:
- make install (Linux distributions, snap, flatpak):
<prefix>/share/qelectrotech/mcp/qet_mcp.py, executable, with its README.
- Windows installer: a new "AI assistant (MCP)" component (on by default,
~200 KB), installed to <folder>\mcp. The workflow stages files/mcp, so
the portable folder and the MSI, which packs all of files/, carry it too.
The server learns the Windows layout (<root>/mcp beside <root>/bin, whose
program is QElectroTech.exe, and <root>/elements), in addition to
<prefix>/share/qelectrotech/mcp. A copy saved anywhere else, even beside
some bin/ folder, is not taken for an installation.
The installer strings are given in all 29 installer languages: French
translated, the others in English until translated, which is what NSIS
would fall back to anyway but without its warning 6040 per language.
Checked: make install into a scratch prefix, then from the installed
script with nothing configured and no qelectrotech on PATH, an SVG export
and a qet_edit placing a common:// element both succeeded. makensis 3.10
compiles the installer with 0 warnings before and after (removing the
French description gives warning 6040), and the installer contains
mcp/qet_mcp.py. Suite 274/274 none skipped; each layout check was
removed in turn and a test failed. Snap/flatpak: installed by the same
CMake rule; launching their QElectroTech from outside is untested.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qet_export, qet_edit, qet_query, qet_continuity, qet_check and
qet_project_new took the QElectroTech executable as a per-call argument
and ran whatever executable file it named, with the call's own paths as
arguments. The workspace policy exempted it as configuration, but it is
chosen by the model on every call, so text inside a project could steer
an assistant into starting another program.
The server now finds QElectroTech itself: QET_BINARY, then the install it
ships in (<prefix>/share/qelectrotech/mcp/), then PATH. "binary" becomes
optional; when given it must be that same file (after resolving
symlinks) or one listed in QET_MCP_BINARIES. QET_MCP_ALLOW_ANY_BINARY=1
restores the old behaviour, as QET_MCP_ALLOW_ANY_PATH does for paths.
"elements_dir" defaults to the installed collection and, when given, must
be in the workspace, that collection, or QET_MCP_ELEMENTS.
Both checks live in enforce_path_policy(), the one place tool arguments
enter. test_configuration_paths_are_exempt asserted the old exemption and
is replaced by BinaryPolicy (13 tests) and a stdio test of the original
reproduction. Each new check was removed in turn and the tests failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tools/qet-mcp-audit/mutate.py over the code changed since #1095
(_build_script, the diff helpers, _parse_script_output, tool_items and
the wire-end keys of #1118), tests needing no QElectroTech only: master
f530b7636's suite noticed 531 of 578 planted bugs (92 %). With these
tests, 568 (98 %). The 10 left are equivalent: rsplit("/", 1) vs 2, a
1e-6 tolerance compared with < or <=, a one-letter orientation sliced
[:1] or [:2], branches that only touch parts without terminals, and the
placeholder terminal 0 a conductor uuid overwrites.
- Every argument kind refuses what it cannot take, before any launch:
malformed indices, bool, points and nodes, search_and_replace with an
unknown kind or conductor field or an empty element_info field,
"conductor" given with "element" or "terminal" alone, an op that is not
an object -- and the same kinds accept what they should.
- _parse_script_output reports capabilities (none logged = unknown, not
"nothing missing"), notes on the right op, save and stopped_early;
ignores a line without the marker even when it would parse, and a line
of a kind it does not know.
- A uuid wire end in a column of terminals (same x) keys like the
numbered one: the match is on x, y and orientation together.
Tests only; 264/264.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "$id" of an add_folio or insert_folio held the index the folio had
when it was made, so a later insert_folio or remove_folio in the same
qet_edit run shifted it, and ops naming "$id" edited the wrong folio --
the case #1115 fixed for folios given by uuid, left open for these.
The script now also keeps such a folio's uuid (qet.folioUuid()) and
resolves "$id", where an op takes a folio, through qet.folioIndex() at
the moment it is used. On a build without folioUuid() it falls back to
the stored index, as before, and nothing new is required of the binary.
The op's reported result is still the index.
Test: add_folio "$f" (index 1), insert_folio at 0, set_folio_title
"$f": the title lands on the third folio -- on the second without this
change. Two script-generation tests updated for the new expression.
255/255.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A web chat in a browser cannot start a local program, so it cannot run
this server. Two ways round that, documented in the README:
- the Claude desktop app runs stdio servers; a step-by-step setup;
- a chat that can execute Python can upload qet_mcp.py and run
`--call <tool> '<json>'` (or `-` to read the arguments from stdin).
--call goes through the same dispatcher as the stdio server, so the
workspace policy applies unchanged. Exit status 0 success, 1 the tool
reported an error, 2 the call was malformed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
updateConductorPath() fits a stored profile to the new terminal
positions by sharing the horizontal difference over the profile's
horizontal segments and the vertical one over its vertical segments.
When a profile has no segment of non-zero length along an axis, the
difference along that axis was dropped and the last point joined the
terminal diagonally. On save that diagonal was written as one
axis-aligned segment, so on reopen pathFromXml() found the lengths
incoherent and rerouted the wire.
Every straight wire with a stored path hits this after a reload, as a
zero-length segment is saved as horizontal. Moving one end at a right
angle to the wire, in the direction that keeps its path type, showed it.
Generate a new path in that case, as is already done when there is no
profile for the path type.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Translation text only; no source string, no other language touched.
"folio" is kept, as decided in #935, and the two "on the current sheet"
strings whose source says folio now say folio too.
- Broken wording: "N° scheme", "Export fully folio", "Title of folio",
"Label folio", "N° of folio", "Dimensions of folio", "Folio Untitled",
"Move up this folio", "Title block informations", the table
"lines are missing %1" warning.
- Folio report elements get one name, "folio reference", which
DiagramView already used (was: folio referencing(s), reference folio
following, previous reference folio, folio reports).
- Machine-translation damage: "</ b>" broke the folio-reference error
message's HTML, and two variable help texts listed "% F", "% l" etc.,
which are not variables.
- Plurals still showing French or wrong: conductor colour undo entry
(empty, so French was shown), "%n erreur", "%n forme", "%n item placed"
for both forms, "redesigned" for redrawn, and "bornes" rendered as
"boundaries" in the element-reload message.
- Cross-reference spelled one way; "Réf." and "Numéro : %1" translated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of the previous commit: an add_conductor with both terminal uuids
wrong logged two notes under one op index and the second replaced the
first, and neither said which end it was about. Notes of one op are now
joined, and a terminal note starts with its argument ("from_terminal:",
"to_terminal:", "terminal:"). The integration test's both-ends case fails
with the old replacing behaviour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of the previous commit: the line was built with chained arg()
calls, so a "%3" in a terminal's name was replaced by the conductor
count -- as before -- and now a "%4" by the uuid as well. One
multi-argument arg() substitutes each placeholder of the pattern once.
percentInNameKept renames a terminal of perceuse.qet "x%3y%4": listed as
"x1y{uuid}" before, "x%3y%4" now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"terminal", "from_terminal" and "to_terminal" take the terminal's uuid
(as qet_element_info lists it) in place of its index. It names a
terminal of the op's own element -- for add_conductor, of that end's
element, "$name" references included -- and is turned at run time into
the index the call takes by qet.terminalIndex(); if the element has no
such terminal the op fails with a note. Unlike the index, a sort by
position, it is defined between two terminals at the same point. The
lookup is required only when a uuid is used, so index-only edits still
run on older builds.
README: the terminal uuids; wires of older projects now get a lasting
uuid (#1107) instead of staying unnamed.
Tests: script generation for every terminal-taking op, "$name" and folio
uuid resolution, the index still passed through; through the binary,
bobine_ka_a_remanence (file order A2, A1; index order A1, A2) wired A2 to
A1 by uuid lands on exactly those ends, and an unknown uuid stops the run
with its note. 255/255 against a build with qet.terminalIndex(); a build
without it is reported as missing it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The conductor calls name a wire end as element uuid + terminal index.
The index is the terminal's place in Element::terminals(), a sort by
position that is undefined between two terminals at the same point, and
the documentation ruled terminal uuids out as "empty for most of the
installed base". Since #1118 every terminal of an opened project has one.
- elementTerminals() ends each line with the terminal's uuid
(Terminal::stableUuid()); the text before it is unchanged.
- terminalIndex(folio, elementUuid, terminalUuid) returns the index the
calls take, or -1 if the element or terminal is not there, or if two of
the element's terminals carry that uuid.
- The class documentation says what does address a terminal: its uuid
together with its element's.
tst_scriptterminaluuid runs --run on perceuse.qet (552 elements, two
terminals at one point in some): every terminal listed with a uuid,
distinct within its element, found again at its own index; -1 for an
unknown or malformed uuid, an unknown element and a bad folio. Red when
terminalIndex() returns the wrong index. qet-mcp suite 253/253.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of the previous commit: "Import scaled element" and "Import DXF"
add a definition to an open symbol through OpenElmtCommand, which, unlike
a paste, kept the imported terminals' uuids. With derived uuids, an old
symbol imported into itself (or into another with a terminal at the same
point) gave two terminals one uuid; importing any symbol whose terminals
already carried uuids did the same before this series. OpenElmtCommand now
renews the imported terminals' uuids, as PastePartsCommand does.
Checked in the editor with a stand-in scaler at scale 1: open
6es7_212-1ae40-0xb0__p3.elmt, import it into itself, save -- 6 terminals,
3 distinct uuids without this commit, 6 with it, the original 3 keeping
their derived values.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A symbol file whose terminals have no uuid got a random one for each
terminal when opened in the element editor (PartTerminal's constructor),
written on save. Every copy of the same old symbol therefore ended up
with different terminal uuids, none of them the one a project gives the
same terminals on opening (TerminalUuids::fillMissing()).
ElementScene::loadContent() now reads a copy of the definition filled by
TerminalUuids::fillMissingInDefinition(), the same rule as a project,
including the next occurrence for the second of two terminals at one
point. Terminals that have a uuid keep it; a paste still renews them all
(PastePartsCommand).
Checked in the editor: 6es7_212-1ae40-0xb0__p3.elmt (no terminal uuids
in the collection) and tm3saf5r_layout.elmt with its uuids stripped (two
terminals at one point), opened, nudged back and forth, saved: this build
writes exactly the derived values (computed independently in Python),
the previous one random ones. Select all, copy, paste, save: 6 terminals,
6 distinct uuids.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two problems with a custom variable newly added to a title-block template:
1. Find/Replace > Folio > Custom showed an empty table, so the user had to
know and type each variable name. It now lists every custom variable the
folios already carry plus those their templates use, with empty values.
An empty value now means "leave unchanged", like every field of the main
tab; only the variables actually filled in are written to the folios.
2. The title block showed the variable's own name ("%doc-type") until the
folio's properties were opened, because interpreteVariables() only
replaces names present in the context. Placeholders in the template text
that no key resolves now render blank, as auto-added unset ones already
did since #973. Only the template's own text is considered, so a value
that contains "%something" is never touched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saving an element from the element editor calls
ElementsCollectionWidget::locationWasSaved(), which runs clearData() on
the panel item: the icon is set to a null QIcon. The icon only comes
back through FileElementCollectionItem::setUpIcon(), but since the
#633 recursion guard that returns for good once m_icon_initialized is
set, and nothing ever reset it. The row stayed without an icon until
the whole collection was reloaded.
#1008 refreshed the picture caches on save, but no one asked them for
the new picture, so it could not fix this. Resetting the flag in
clearData() lets the next paint rebuild the icon, which then comes
from those refreshed caches, so the panel shows the new drawing.
The flag is reset after the base clearData(): its setIcon() emits
dataChanged(), which re-enters setUpIcon() and must still return early.
setUpIcon() sets the flag before its own setIcon(), so the #633 guard
is unchanged.
The same reset should also bring back a folder's icon after editing its
properties (editDirectory() calls clearData() too); read, not tested.
Project collection items were never affected: their setUpIcon() guards
on icon().isNull().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up to #1088, covering its review comments and one behaviour
change found while testing the feature:
- MaterialList::load() names the columns from the machine key line when
the file has one, instead of from the translated label line above it.
A catalogue written in another language fills the element fields
again, and saving no longer replaces the key line with labels.
- The entry picked after "New entry" is found by comparing the columns
one by one instead of comparing the two maps as a whole: the entry
form leaves the empty columns out, so the record never matched the
line that had just been written and the search was cleared for
nothing. The search is now only given up when it really hides the new
line.
- Applying a catalogue entry pushes an undo command only when the live
edit is on. In the properties window, where there is none, the fields
wait for "Apply", so "Cancel" gives the element its own values back
instead of leaving the picked part in place.
- Cells a file holds past the header are kept in MaterialRecord::extra
and written back, so appending an article never shortens a line.
- An empty cell of a column describing the article itself (MaterialList::
isArticleBound: description, designation, manufacturer, order number,
supplier, model, ratings, dimensions, auxiliary block) clears the
field, so an element never keeps the manufacturer of the part picked
before. An empty cell of any other column (function, comment, notes,
plant, location, quantity, unity), and any column the file does not
hold at all, leaves the field alone.
- Comments left where the review asked for them: the corner button
lookup, the ';' separator fallback, the search filter cost.
Review of the previous commit:
- The load fallback compared a saved uuid with occurrence 0 only, so a
wire on the second of two terminals at one point of a symbol was lost
once the definition was replaced, and one on the first could go to
either of the pair (Element::m_terminals is sorted, not in definition
order). Element::parseTerminal() now records each terminal's rank among
the terminals of the definition at the same point, derivedUuid() uses
it, and fillMissing() starts from the same rank.
derivedUuidFoundAfterReplacement runs on perceuse.qet and industrial.qet
too: 154/156 and 670/671 wires without the rank, all with it.
qet-mcp: the first save of an older project now rewrites its wires from
the numbered form to the uuid form, and qet_diff keyed the two forms
differently, so an untouched resave showed every wire removed and added
(4 failures in test_qet_mcp.py). A uuid end is now resolved to the same
key as a numbered one: the terminal's definition position, moved to where
the wire docks, is the placed symbol's <terminal> record.
test_conductor_key_same_in_both_forms fails without it; 253/253 pass on
this build and on the previous stage's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Most symbols stored in older projects have no uuid on their terminals
(706 of the 900 in the 24 examples), so a terminal's identity is worked
out from where it sits in its symbol on every load (stableUuid()). That
is only sound while nothing keyed on it is kept between loads.
- On opening a project, every terminal of its embedded symbols without a
uuid gets that same derived value (TerminalUuids::fillMissing(), from
XmlElementCollection's loading constructor, before any folio is
built). The next save writes it, and the wires on it in the form that
names terminals by uuid, which QElectroTech reads since 0.8.0.
- The recipe moves to TerminalUuids::derived(), which stableUuid() now
calls, so the two cannot drift apart. A second terminal at the same
point of a symbol gets the next occurrence, and no value is given
twice within a symbol.
- findTerminal(): a wire whose terminal uuid is not found is matched to
the terminal whose derived value it is, so a saved uuid still finds its
terminal after the symbol's definition was replaced by one whose
terminals carry other uuids.
The project database's terminal and conductor tables are identical before
and after on all 24 examples except the 3 terminals that share a point
with another in their symbol (industrial.qet 1, perceuse.qet 2), which
now have an identity of their own. Every example keeps every wire through
a resave, and a second save changes nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of the previous commit:
- keep() could give an old uuid to a new terminal while another terminal
of the new definition already carried it (a moved terminal), leaving
two terminals with one uuid. A terminal carrying any old uuid is now
left alone, and an old uuid already in use is never handed out.
- copyDirectory() replaced a whole category of the embedded collection
(drag a folder onto the project's folder of the same name) without
carrying terminal uuids over. keepInDirectory() walks both trees by
name and calls keep() on each symbol.
- The "wire(s) not loaded" log line repeated the folio's list on every
paste; it is now written only when a folio is loaded.
tst_terminaluuids: 3 new cases, each red on the previous keep().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A wire whose terminals have uuids is saved against them, and on load it
is reattached to a terminal with that uuid or dropped, with only a qDebug
line. Re-importing a changed symbol and choosing "replace" swapped the
project's definition for one whose terminal uuids differ; the placed
symbols kept the old ones until the project was reopened, so every wire
on them was lost at the next open, silently, and gone for good at the
next save.
- XmlElementCollection::copyElement(), where an embedded definition is
overwritten, carries each old terminal uuid onto the new terminal at
the same place and orientation (TerminalUuids::keep()). Terminals that
moved, and new ones, keep their own.
- Diagram::fromXml() records wires it could not reattach, logs them, and
the editor lists them in one warning after opening a project.
Measured on 2612_ats_singlephase.qet with the stored splice's terminal
uuids made to differ from the collection's: replace, save, reopen loads
34 of 131 wires on master, 131 with this change (GUI, both arms).
tst_terminaluuids covers keep() and runs the real loader.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qet_edit takes a folio as its index, and the index shifts when an earlier
op in the same run adds, inserts or removes a folio: remove folio 0, then
edit "folio 2", and the edit lands on the folio that was 3. Every other
item qet_edit addresses (elements, texts, shapes, pictures, tables, symbol
text fields) can already be named by uuid; folios could not.
- "folio" and "to_folio" take the folio's uuid as well as its index,
turned into the current index at run time by qet.folioIndex(). The
lookup is required only when a uuid is used, so an index-only edit still
runs on a build without it.
- qet_project_info lists each folio's uuid. A folio saved without one
(132 of the 133 in the shipped examples) shows it empty until the
project is saved once, and an empty "folio" says so.
Tests: the generated script for a uuid folio, on its own and inside a
table lookup and link_elements' to_folio; and a real run that removes
folio 0 and then retitles the old folio 2 by uuid, on a file saved without
folio uuids. The run fails against a build without qet.folioIndex().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every scripting call names a folio by its index, and the index shifts when
a folio is added, removed or moved: a script that removes folio 0 and then
edits "folio 2" edits the wrong one. Texts, shapes, pictures, tables and
symbol text fields already have a uuid lookup for the same reason; folios
had none.
- qet.folioUuid(index): the folio's uuid, or "".
- qet.folioIndex(uuid): the folio's current index, or -1.
A folio saved without a uuid (132 of the 133 in the shipped examples) is
given one on load, derived from the file, so it is the same on every load
and is written on the next save. No two folios share one: a clash is
renewed on load.
Tests in misc/qet-mcp's integration suite, which drives these through
--run: a folio followed across the removal of the one before it, on a file
saved without folio uuids, and a new folio's uuid found in the saved file.
Both fail against a build without this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 wire saved without a uuid got a random one on every load, never saved
(#754): it had no identity from one session to the next, so a script
could only name it as "the wire on terminal N of symbol X", and a
comparison of two versions could not tell a moved wire from a new one.
When a folio is loaded, such a wire now gets a UUID v5 derived from its
two ends -- the symbol and terminal at each, sorted so the direction it
was drawn in does not matter -- and it is written on save. Never its
place in the file or its folio's index: inserting or moving a folio, or
saving the wires in another order, does not change it. Once saved the
uuid no longer depends on the ends, so re-connecting the wire keeps it;
QETProject::derivedItemUuid() never hands out a uuid the file already
carries, so a wire later drawn on the ends it left gets another one.
Wires that have a uuid keep it; a paste still renews them.
The 24 example projects: 3,189 wires, none with a uuid before, all 3,189
after one save, none lost, no uuid used twice in any project; two saves
of the same file are identical, and a second save keeps every wire's
uuid. Discussion #1103 has the measurements behind the recipe.
tst_derivedwireuuid runs --resave on a fixture naming ends by uuid and on
examples/tremie_vibrante.qet (ends by terminal number): every wire gets a
distinct uuid, the same on every load, read back after a save, kept per
wire when a folio is inserted, the wires are reordered or a wire is drawn
the other way, and a newcomer on a re-connected wire's old ends gets
another uuid. Without this change 14 of the 18 fail; with the uuid taken
from folio index and file order instead, the folio-insert and reorder
tests fail.
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>
A mutation audit -- planting one small bug at a time in the server's read
and diff code (a flipped comparison, a skipped branch, a dropped output
field, a list cap off by one) and running this suite -- caught 78 of 241
planted bugs with the tests that need no QElectroTech binary. qet_elements,
qet_conductors and their row builders caught none: a filter that never
applied, a missing field or a wrong default limit all passed.
Two new classes pin exact output on small hand-made projects:
ReadToolContracts (qet_project_info, qet_elements, qet_conductors: every
field, every filter, limit and truncation, legacy and current conductor
keys) and DiffContracts (every qet_diff section: moves, relabels, info,
conductors and the unstable-key warning, texts/shapes/pictures by uuid and
by position, element text fields, folio and project fields, terminal
strips, and every list cap). The same audit now catches 240 of 241; the
one left is equivalent (rsplit("/", 1) vs rsplit("/", 2) then [-1]).
Tests only; no change to the server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A folio's conductor auto-numbering rule is saved as
<autonum><conductor><part .../></conductor></autonum>. qet_project_info
and qet_conductors counted every <conductor> tag under the folio, so the
rule appeared as a wire with no ends and no number: schema_indus.qet
folio 1 reported 70 conductors where QElectroTech holds 69.
Only children of <conductors> are wires now. A new corpus test compares
each folio's element and conductor counts with QElectroTech's own
elementCount()/conductorCount() after loading the file, over every shipped
example; it failed only on schema_indus.qet before this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qet.checkContinuity() answers a folio index it has no folio for with an
empty list, so qet_continuity returned "0 findings" for it -- the same
answer as a clean folio. The index counts from 0 while qet_elements numbers
folios from 1, so passing the last folio's number checked nothing and said
so cleanly; any other folio's number checked the next folio instead.
An index with no folio is now refused before QElectroTech is launched, with
the valid range and the counting rule. Each finding also carries
folio_number (counted from 1) beside the existing folio index. The
qet_continuity and qet_conductors descriptions and the README say how each
tool counts. Nothing changes for a valid index.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The editor's bar row and command list mix element previews with command
icons. The command icons already follow the palette through the qet-dark
icon theme, and running them through ElementPreviewDelegate again
flattened them to one lightness (up to 16/255 per pixel off master; a
two-tone icon would have had its tones swapped).
Adapt the element icons once, where the items are built, instead of
installing the delegate on those lists. This also covers an element
dragged from the element list onto the bar, which copies the item's icon
and so previously kept the dark one until the editor was reopened.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The collection tree draws element previews through ElementPreviewDelegate,
which inverts the black line art on a dark palette. The ranked search list
(#1051) and the Insert element picker (#1052) are separate views and never
installed it, so their icons stayed black on a dark background.
Install the delegate on the ranked list, the picker's list and the shortcut
bar editor's lists, and adapt the pinned-element buttons' icons directly.
Coloured icons and light palettes are unchanged.
Reported in #1083.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qet_elements and qet_project_info number folios from 1, as the application
does; qet_edit passes "folio" straight to the scripting API, which counts
from 0. Using the number qet_elements showed addresses the next folio, and
the op fails with nothing but "returned False".
Nothing changes for a call that works. When an op fails and the element it
names is in the project on another folio, the hint now says which index to
use. The qet_edit description and the README say how folios are counted.
Counting from 1 instead was not done: it would silently move every existing
caller's edits to another folio.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
QElectroTech writes an empty label as an empty <elementInformation> and
drops it on the next save. qet_diff compared the raw information bags, so
re-saving grafcet.qet with no edit reported 9 of its 37 elements as
info_changed ({"label": ""} -> {}), and Projet_vierge.qet 4.
An empty field and a missing one now compare equal, and info_changed lists
only fields that hold a value. The resave test now also requires an empty
info_changed; it failed on grafcet.qet and Projet_vierge.qet before this
change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A group whose only unlocked member is a shape (its other members
locked) kept a centre of (0,0), so Centrer horizontalement and
Centrer verticalement pulled every item toward the folio origin and
the shape itself landed off the line. The rule that turns a group's
members into one item now lives in Alignment::combined(), where every
member, shapes included, brings its own centre, and tst_alignment
covers it.
Also comments why unitFor()'s returned reference is safe, as asked in
the review of #1087.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
The element description dialog only offered typing, so every article of
a catalogue had to be copied in by hand into the Artikelbeschreibung
row of each block, over and over.
Settings > General now keeps the path of a CSV material list (Créer...
writes a template with the header lines), and the Artikelbeschreibung
row of every block of the Bauteil dialog shows a "..." button opening a
searchable listing of that file. "Appliquer" fills the fields of that
block only -- the main block gets everything but label and formula, an
auxiliary block gets description, number, manufacturer, references,
supplier, quantity, unit and additional info -- and a cell that is
empty in the file never erases a value already in the element. The
symbol editor is left alone.
- sources/materiallist/: CSV reading and writing (UTF-8 BOM, ';', every
field quoted, atomic write through QSaveFile), two header lines
(translated labels then the canonical English keys), the selection
window and the new entry form.
- The selection window reads the file again every time it opens, keeps
its size, column widths, column order and sort between openings,
shows every column of the file, scrolls all four directions with the
wheel (Shift + wheel moves the columns) and puts back the order of
the file when the corner above the row numbers or a row number is
clicked.
- German translations for the new strings in lang/qet_de.ts.
conductorpropertieseditorwidget.cpp, from the #500 work, included
<KColorButton> unconditionally, so a build with BUILD_WITH_KF=OFF stopped
at "KColorButton: No such file or directory". Include the nokde stand-in
there instead, as dynamictextfieldeditor.h and terminaleditor.h do. It
has the same changed() signal the file connects to.
Checked on Ubuntu 22.04 (Qt 6.2.4, BUILD_WITH_KF=OFF): the file fails on
master and compiles with this change; together with #1085 the whole tree
builds and links. Builds with KDE Frameworks compile the same line as
before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
QDomDocument::ParseOption::PreserveSpacingOnlyNodes, used since 84add7b3e
to keep a title-block variable set to a single space through a reload,
only exists from Qt 6.5. On older Qt the project file is parsed as it was
before that commit: the build works again, and such a value reloads as
empty there.
Checked on Ubuntu 22.04's Qt 6.2.4: qetproject.cpp fails on master with
"'QDomDocument::ParseOption' has not been declared" and compiles with
this change. On Qt 6.5 and later the code is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Edit > Aligner gains six commands, also in the selection's context menu
and the command search: Aligner à gauche, Centrer horizontalement,
Aligner à droite, Aligner en haut, Centrer verticalement, Aligner en bas.
One undo step; wires follow their symbols. Second stage of discussion
#1069, on top of "Aligner sur la grille".
Left/right/top/bottom line the edges up on the outermost one. The two
centre commands line the items up on the mean of their centres, and a
symbol's centre is its origin point, not the middle of its drawing: in
the collection, vertical two-terminal symbols almost always have their
terminals on the origin's axis, so this puts their wires on one line.
A picture is aligned by the picture itself, without its caption
(imageRect() becomes public for this).
A group (#1070) lines up as one piece: its edges are its members'
together, and every member moves by the same amount, so the group keeps
its shape; shapes inside a group come along.
Each item moves only across the line it is aligned on, and lands on the
grid its drag uses, so aligning never takes a symbol off the grid. Two
symbols whose edges sit at different distances from their origins
cannot both be exactly on the line and on the grid; they end up within
half a grid step of it.
The commands need two items (Aligner sur la grille still needs one).
Locked items stay put and the status bar says so. If nothing moves, no
undo step is pushed and the status bar says the selection is already
aligned as far as the grid allows. The geometry is in alignment.h,
tested by tst_alignment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous commit changed only the bar that appears beside the
cursor after a click. The S shortcut bar builds its own tooltips, so
its "Orienter les textes" button still said only that. Its tooltips
now carry the status tip on a second line as well, after the keyboard
shortcut.
Checked in the GUI: the S bar's second button shows "Orienter les
textes (Ctrl+Space)" over "Pivote les textes sélectionnés à un angle
précis".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "Orienter les textes" dialog always opened at 0, so turning a text
at 90 degrees by a little meant typing the angle in again. It now opens
at the angle every selected text and text group shares, and at 0 as
before when they differ.
The shortcut bar showed only a command's short name as its tooltip,
which for this one ("Choose texts orientation") does not say what it
does. It now adds the command's status tip on a second line: "Rotate
selected texts to a specific angle".
Checked in the GUI on a folio holding one text at 90 degrees: master
opens the dialog at 0.00, this at 90.00.
Fixes#1082
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A query saved to nomenclature.json (Load/Save in the query editor, since
2020) before June 2022 still says element_type = 'Simple'. Loading it
ticked none of the type boxes and kept the old query, so the new table
came out empty. Pass it through LegacyElementTypes::upgradeQuery() as
ProjectDBModel::fromXml() now does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1051 replaced the filtered tree with a ranked flat list. A new checkbox
in Settings > General, "Afficher les résultats de recherche sous forme de
liste triée" (elementscollection/search-flat-list, default on), keeps the
ranked list; unticked restores the pre-#1051 filtered tree search.
The Insert picker and shortcut bar call rankedSearch() directly and are
unaffected. Down/Enter from the search field only apply to the flat list.
Requested by scorpio810 on #1051.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Both branches added an undo command, an include and a test target at
the same spots; kept both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every wire was written in the default colour, whatever its colour on the
folio: 723 of the 3,190 wires in the example projects (23 %, in 10
projects) lost theirs. The wire outline now takes the wire's colour, and
its number the wire's text colour, mapped to the nearest DXF colour as
the free texts already are. Black still comes out as BYLAYER.
A two-colour wire gets its main colour, and dashed wires (46 in the
examples) stay continuous: DXF line types are not written yet.
Checked on the 24 example projects, 133 folios: exactly 723 wire
outlines now carry a colour of their own, the only change from the
previous commit; ezdxf reads all files with no audit errors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Most entities were written with colour 0, BYBLOCK. Outside a block that
falls back to the default colour, so it shows black on white or white on
black, and no layer colour could change it: recolouring QET_WIRES in a
CAD program left the wires as they were. Black itself maps to 0 too
(RGBcodeTable[0]).
Colour 0 is now written as 256, BYLAYER. The layers are colour 7, so a
file looks the same on opening, and recolouring a layer now recolours
its contents. Real colours (free texts, terminal markers) are kept.
Checked on the 24 example projects, 133 folios: the only change from
the previous commit is 110,283 colour codes 0 -> 256, no entity is left
on 0, and ezdxf reads all of them with no audit errors. Rendered with
QET_WIRES set to red and QET_SYMBOLS to blue: before, 0 red and 0 blue
pixels; after, the wires and symbols take the layer colours.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Everything in an exported DXF was on layer 0, and the file had no LAYER
table: in a CAD program the border, title block, symbols, wires and texts
could not be hidden, printed or recoloured separately. Each kind of
content now has its own layer, declared in a LAYER table (white/black,
continuous, so the file looks the same on opening):
QET_BORDER, QET_TITLEBLOCK, QET_SYMBOLS, QET_SYMBOL_TEXTS,
QET_TERMINALS, QET_WIRES, QET_WIRE_NUMBERS, QET_JUNCTIONS, QET_TEXTS,
QET_XREFS, QET_SHAPES, QET_TABLES, QET_IMAGES
Fixed, untranslated names, so layer filters and scripts work in any
language. Createdxf gets a current layer beside its xScale/yScale;
DxfExport sets it before each kind of content, and BorderTitleBlock
switches to the title block layer itself. No DXF version change: layers
exist in R10.
Checked on the 24 example projects, 133 folios: with the layer values
set back to 0 and the LAYER table removed, every file is byte-identical
to before; no entity is on an undeclared layer or left on 0; ezdxf reads
all of them with no audit errors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
qelectrotech --export-dxf <project.qet> <output_dir> [--show-terminals]
writes one DXF per folio, named <NN>_<title>.dxf like the PNG and SVG
exports. Discussion #1072.
The DXF code moves out of ExportDialog into DxfExport, unchanged except
that its options come from an ExportProperties argument instead of the
dialog. The dialog and the command line both call it, and the command
line uses the dialog's default options (the preferences' export
settings), so both write the same entities.
- Createdxf answers a file it cannot open with a message box and
exit(0); the command line checks the file first and fails with a
message and exit code 1 instead.
- A note is printed when pictures become outline boxes, as the dialog
warns.
- Scripting: qet.exportDxf(outDir, showTerminals). MCP: format "dxf".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
A nomenclature table saved by an older version filters on the element
type names the project database used then: element_type = 'Simple',
'Terminale', 'Master'. Commit 2e70d2e59 (June 2022) changed the database
to "simple", "terminal", "master", and SQLite compares text
case-sensitively, so such a table silently lost every row of that type
on open. Its continuation tables were then empty, and
removeUselessNextTable() deleted them: opening and saving the example
industrial.qet removed seven of its ten parts-list tables (folios 44-50),
and the remaining three listed 76 of 258 parts.
ProjectDBModel::fromXml() now rewrites old names in element_type = '...'
comparisons to the current ones (LegacyElementTypes::upgradeQuery()),
which also lets the query editor tick the right boxes again. A query
saved by a current version is unchanged, and so is any other text that
happens to contain "Simple".
Checked in the GUI on industrial.qet, open then save: master keeps
tables on 3 of folios 41-50, this keeps all 10, with 258 rows (the last
table 24 of 26) and the query saved as 'simple'. tst_legacyelementtypes
fails when a name maps wrongly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
A title block cell holding a line break (tableau_domestique.qet's
"DESSINE" + CR LF + "%{author}") was written into the DXF as one TEXT
value with the break inside it. A DXF value ends at the line break, so
every group code after it was off by one line: ezdxf refuses all five
folios of that example with DXFStructureError, and other readers take
values for codes.
- TitleBlockTemplate::renderTextCellDxf() writes each line of a cell as
its own TEXT, stacked by the cell's vertical alignment as the screen
shows them, 1.6 text heights apart as the diagram texts' DXF export
does. A single-line cell is written exactly as before.
- Createdxf's text writers turn any line break still in a value into a
space, so no other caller can break the file either.
Checked in the Export dialog: tableau_domestique.qet's five folios, which
ezdxf refused on master, now read with no audit errors; grafcet.qet, which
has no line break, keeps exactly the same entities. (Byte comparison waits
on the DXF entity-order fix: until then every export reorders its entities.)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Exporting the same folio to DXF twice gave files with the same entities
in a different order, so two exports never compared equal and a DXF could
not be diffed to spot a change. The export walked diagram->items(), whose
order follows memory addresses; saving had the same fault until
bugtracker #343.
Items are now taken in stacking order (z, then insertion order, which a
load makes the file's order), from the same rect query Diagram::toXml()
uses. Anything the query misses keeps its items() place after the rest.
Checked in the Export dialog on grafcet.qet, three folios, two exports
per build: master's differ every time, these are byte-identical, and both
contain exactly the same entities, only reordered.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Symbols, free texts, shapes and pictures can be grouped from the Edit
menu, the selection's context menu or the command search. Clicking one
member selects the group, so moving, copying and deleting act on all of
it; Ctrl+click on a member deselects the group; a rubber band touching
part of a group selects all of it when released. No default shortcut:
Ctrl+G is "jump to element".
A group is not an object in the scene. Each member keeps its place and
carries the group's uuid (QGraphicsItem::data()), saved as a "group"
attribute written only when set: a project without groups saves exactly
as before, and older versions open a grouped one and ignore the groups.
Re-parenting under a QGraphicsItemGroup would have made every member's
position group-relative; ElementTextItemGroup already needs nine special
cases for that.
- Selection is completed on clicks and at the end of a rubber band, not
on every selectionChanged(): export, search and Tab select items
themselves and must not have groups pulled back in.
- Project database: group_uuid on element, shape, independent_text and
image, kept in step by projectDataBase::itemGroupChanged().
- Paste and folio duplication give each source group one new uuid.
- Undo of Ungroup restores each item's exact group.
Builds on #1065 (uuids and database rows for texts, shapes and images).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Edit > Aligner > Aligner sur la grille, also in the selection's
context menu and the command search, puts each selected item back where
dragging it would have left it: symbols and pictures on the folio grid,
free texts on the text grid. One undo step; wires follow their symbols.
Symbols leave the grid through the fine nudge (Alt+arrow, 1 px), and
nothing put them back: 508 of the 3,678 symbols in the example projects
are off the 10 px grid. First stage of discussion #1069.
The status bar says how many items moved, or that the selection was
already on the grid, and how many locked items were left in place.
Shapes are left out: they are made of several points and no single one
is the obvious one to snap.
The snap never reads the keyboard, unlike Diagram::snapToGrid(), so a
user who binds the action to a shortcut with Ctrl gets the grid, not
pixel rounding. The geometry is in alignment.h, tested by tst_alignment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
DiagramImageItem::toXml() PNG-encoded every picture on every save,
autosave and copy, whether or not it had changed. That is most of the
cost of pictures in a project: resaving one holding 60 of them took
6.2 s and now takes 3.8 s.
The PNG bytes are now kept and reused while QPixmap::cacheKey() still
matches, so any edit (replace, crop, mirror, transparency) re-encodes
without each of those functions having to invalidate anything. On load
the cache is filled with the file's own bytes, so the first save
encodes nothing either.
Output is byte-identical to before on the example projects. A picture
whose PNG came from another encoder now keeps its original bytes
instead of being re-encoded; the pixels are identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
A picture can now carry a caption, set from its properties panel
("Libellé"). It is drawn centred under the picture at the folio's
normal text size whatever the picture's scale, turns with it, and
moves, copies and prints with it because the picture itself paints it.
Clicking the caption selects the picture.
Saved as a "label" attribute on <image>, written only when non-empty:
a project without labels saves byte-for-byte as before, and older
versions open a labelled project and simply ignore the caption.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
A file written before drawn items carried a uuid gets them from the folio
uuid, the kind of item and its order in the file. Three integration tests
pin what that promises: opening the same file twice gives the same uuids,
the first save writes exactly those and a reopen keeps them, and a uuid
repeated in a hand-edited file ends up naming one item only.
Each fails when the derived uuid is replaced by a random one. They read
drawing_item_view unsorted, which is what found the first-row-only bug
in QETSql::execReadOnly() fixed by the previous commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Since #1046, QETSql::execReadOnly() runs a query with PRAGMA query_only
set and switches it off before returning. Switching it off aborts a
statement SQLite is still stepping through ("abort due to ROLLBACK"), and
QSQLITE has already stepped to the first row by then. A query that
produces its rows as it goes -- a UNION ALL without ORDER BY -- therefore
came back with its first row only and no error. A sorted query was not
affected, because SQLite has read every row before returning the first.
Every query from the SQL box of a table, a saved <graphics_table> query
and the scripting qet.query() goes through here.
The checked run is now finished before query_only is switched off, and a
query that passed is run again for the caller. SQLite refuses a write at
its first step, so passing that step is what proves a statement reads
only; the second run is of a statement already shown to be read-only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Since #1046, QETSql::execReadOnly() runs a query with PRAGMA query_only
set and switches it off before returning. Switching it off aborts a
statement SQLite is still stepping through ("abort due to ROLLBACK"), and
QSQLITE has already stepped to the first row by then. A query that
produces its rows as it goes -- a UNION ALL without ORDER BY -- therefore
came back with its first row only and no error. A sorted query was not
affected, because SQLite has read every row before returning the first.
Every query from the SQL box of a table, a saved <graphics_table> query
and the scripting qet.query() goes through here.
The checked run is now finished before query_only is switched off, and a
query that passed is run again for the caller. SQLite refuses a write at
its first step, so passing that step is what proves a statement reads
only; the second run is of a statement already shown to be read-only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
- qet_edit: every op taking a text, shape or image "index" also takes its
uuid, resolved at run time with textIndex()/shapeIndex()/imageIndex().
Those are only required when a uuid is used.
- qet_element_build writes a uuid on every part (the caller's, or a new
one) and returns them in part_uuids; qet_element_info lists part and
terminal uuids.
- README and tests: drawing_item_view, uuid addressing, part uuids.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lines, rectangles, ellipses, arcs, polygons, PLC tables and static texts
now save a uuid, as terminals and dynamic texts already did, so a tool can
name one part of a symbol. The graphic parts keep it next to their styles,
since all of them save through stylesToXml(); a part read from a
definition that predates this gets one on the next save.
Paste renews them, as it already did for terminals -- and now for dynamic
texts too, which were pasted with their source's uuid. A placed element
gives its dynamic texts fresh uuids anyway, so only the definition's own
identity changes. PartTerminal::uuid() returns the terminal's saved uuid
rather than inheriting the graphic-part one it never writes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Now that they carry a uuid, the folio's drawing furniture gets rows of its
own: shape, independent_text and image, plus drawing_item_view, which finds
any of them by uuid without knowing its kind first and says which folio it
is on.
Rows follow edits, not just loads. The script API's query() and every
other reader go through newQuery() without a rebuild, so an item's row is
queued on each change (moves, restyles, text edits, uuid renewal) and the
queue is flushed by newQuery() and updateDB() -- a queued write is a set
insertion, which matters for a drag that moves hundreds of items per mouse
step.
A pasted copy joins its folio still carrying its source's uuid and is only
renewed afterwards, so a flush in between must not let the copy overwrite
its source's row. Each row remembers the item that wrote it; another item
with the same uuid waits until uuidChanged() says it has its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The constructor built QRegularExpression("[^A-Z]") on every call, and
compiling it cost more than the rest of Diagram::convertPosition() put
together -- about 95k instructions a call under callgrind. Every element
row of the project database pays it on each rebuild, as does anything
else that turns a scene point into a folio cell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Elements, conductors, terminals and tables already carry a uuid. The
drawing furniture beside them did not, so a script or the MCP server could
only name a line, a box or a note by its index in a position-sorted list,
which shifts whenever one is added or removed.
- QetShapeItem, IndependentTextItem and DiagramImageItem get uuid(),
newUuid() and setUuid(), read from and written to a "uuid" attribute.
- A folio loaded from a file written before this (or carrying a duplicate
uuid) derives one from the folio uuid, the item kind and its order in the
file, so the same file gives the same uuids on every load and a re-save
is stable -- the #754 lesson for conductors.
- Paste and folio duplication renew them, as they already do for elements
and conductors.
- Scripting: texts(), shapes() and images() end each line with the uuid;
textIndex(), shapeIndex() and imageIndex() turn one back into an index.
- misc/qet-mcp: qet_diff keys texts, shapes and images on uuid when both
sides have one, so a move or edit reads as a change to that item.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Showing a selected conductor's properties in the selection-properties
dock is now a preference, on the General page under Appearance:
"Afficher les propriétés d'un conducteur sélectionné dans le panneau
Propriétés de la sélection". It is off by default, so selecting a
conductor does what it did before unless the user turns it on.
This replaces the View-menu toggle, which defaulted to on; that entry
is dropped in the merge with master. Same setting key
(diagrameditor/conductor_properties_panel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After customising, the bar now opens at the cursor like every other
time, instead of where the customising window was.
A size grip in the bar's corner sets its width. The tiles are laid out
in rows that wrap at that width, so a bar with many commands and pinned
elements becomes a block rather than one long strip; the height follows
the rows, and the width is saved. Until the user sets one, the tiles
stay on one row, as before. The grip is driven by hand: QSizeGrip asks
the window manager to resize, and on X11 a popup is not managed, so
nothing would happen. The hint line wraps rather than being cut off at
narrow widths.
The customising window remembers the size it was left at.
Discussion #1033.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bar's customising window gets an element search next to the command
list. Type part of a name, then drag a hit onto the bar, double-click
it, or press Enter to pin the best one. A pinned element shows as its
icon among the commands, and clicking it places the element, as the
picker does.
Pinned elements are saved in the same list as the commands, by
collection path (common://, custom://, company://), so the row keeps
the user's order. Elements embedded in a project are not offered: their
path names the project as loaded now. Elements are offered for the
empty-folio bar only; with something selected the bar is for acting on
it. Once any element is pinned there, the palette folder grid under the
bar is hidden, and comes back while typing a search.
Dragging a pinned element off the bar onto the commands, or a double
click, removes it. The Preferences page shows pinned elements by name
and icon, and removing one there drops it instead of listing it as a
command. The customising window is now kept on screen, since it is
taller than before.
Discussion #1033.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a collection search is active the tree is replaced by the flat
ranked list, and that list had no drag support: dragging a result only
moved the selection. Anyone used to searching and then dragging lost the
drag as soon as they typed.
The tree's drag is moved into a static ElementsTreeView::execElementDrag()
taking the source widget, and the results list uses it from the path each
row already carries, so the drag content and pixmap are the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Holding Ctrl+Shift over a folio pans it, and only a key release seen by
the view ends that. A Ctrl+Shift shortcut that opens a window -- the
command search, Ctrl+Shift+M -- takes the keyboard before Ctrl and Shift
are released, so the view never sees the release. On Windows, where the
Shift press itself already reports Ctrl+Shift, the folio then stayed in
pan mode after a command was chosen: a hand cursor, and clicks ignored,
so a drawing tool picked from the search did nothing. On Linux the Enter
key's release happened to reach the view and end it.
The view now remembers that it pans because of Ctrl+Shift (not because
the hand tool was chosen) and stops when it loses the focus, or at a
click made without Ctrl+Shift, instead of waiting for a release it may
never get.
Reproduced on Linux by holding back Enter's release, as Windows does:
the line drawn after "Ctrl+Shift+M, une ligne, Enter" is not saved on
master and is with this change. Ctrl+Shift+drag and the hand tool still
pan (a dragged step does not move), as on master.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Most commands on the empty-folio ring start a drawing tool, and the view
left the right button alone while a tool ran, so the gesture after one
that started a tool only cancelled the tool: every other gesture opened
the context menu instead of the ring.
Now a right drag always shows the ring. A running tool is ended when the
drag starts, as picking another tool from the toolbar would. A plain
right click still goes to the tool, which cancels or finishes it as
before.
Discussion #1033.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Hold the right button on a folio and drag: a ring appears around the
point where the button went down, showing up to eight commands, and the
one in the mouse's direction is highlighted with its name underneath.
Releasing runs it; releasing near the centre cancels. These are
SolidWorks' mouse gestures.
The commands are the shortcut bar's for the selection, placed clockwise
from the top, so customising the bar customises the ring. As with the
context menu, a right press selects what is under the mouse first.
A plain right click still opens the context menu, now on release on
every platform. The view tracks the right button and opens the menu
itself; the platform's own right-click event (sent on press on X11, on
release on Windows) is ignored while gestures are on, so the menu is
never opened twice. The keyboard menu is unchanged.
The view keeps out of the way while a tool or a placement is running,
since a right click cancels or finishes those, and while a text is
being edited. The new General option "Gestes de la souris avec le
bouton droit" (diagrameditor/mouse_gestures, on by default) turns it
off and restores the previous right button exactly.
Discussion #1033.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
(cherry picked from commit a625142f36e619bb4d4f3551b6086f2dd8eabc86)
Enter on a folio runs the last drawing or placing command again, as it
does in SolidWorks: the last tool from the add-item family (text, line,
rectangle, terminal strip plan...) or the last element placed, however
it was placed -- dragged, double-clicked, picked, or from the shortcut
bar.
Enter is handled in DiagramView::keyPressEvent, not bound as a shortcut,
so it keeps working everywhere else: search fields, the collection
tree, dialogs. On the folio it is left alone while a tool is running or
an item has the focus (a text being edited), and with any modifier held.
The same command is in Édition, named after what it will do --
"Répéter : Ajouter une ligne", "Répéter : insérer « Diode »" -- and
registered with ShortcutManager (diagrameditor.repeat_last_command, no
default key), so it can be bound or put on the shortcut bar.
Discussion #1033.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
(cherry picked from commit 1e06ce97961ee18734ddaaa9b72b0303a169b9b2)
After a click that selects something on a folio, a small row of commands
appears just above and to the right of the cursor, as the SolidWorks
context toolbar does. It fades as the mouse moves away and is gone past
200 pixels; a click elsewhere, the wheel, a key press or an emptied
selection hide it too. Clicking a command leaves it up, so rotate can be
clicked again.
The commands are the shortcut bar's for that selection -- elements or
conductors -- at most eight, so customising the bar customises this as
well. It is a child of the view's viewport, never a window, and never
takes the focus.
It is not shown after a drag (moving items, a rubber band), while
placing or drawing (Diagram::eventInterfaceIsRunning()), on
a read-only folio, or when switched off with the new General option
"Afficher les commandes près de la sélection" (diagrameditor/
context_toolbar, on by default).
ShortcutBarSettings::contextFor() now decides the context for both the
bar and this toolbar.
Discussion #1033.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
(cherry picked from commit 78f3c1a2bc5c3cf1f7beae4bd60aefec20847d6d)
Right-click the shortcut bar, or click the "…" button at its end, and it
turns into a small window holding two lists: the bar's commands, left
to right, and every other command. Drag a command onto the bar, off
it, or to another place on it; a double click moves it to the other
list. Terminé saves and shows the bar again where it was, with the
result; Annuler, Esc or closing the window leaves it as it was.
"Valeurs par défaut" puts back the defaults for this context.
A Qt::Popup closes on a press outside it and holds the mouse grab, so
the bar is re-shown as a Qt::Tool window for the time of the edit.
Only QSettings is written, through ShortcutBarSettings, never the
project's undo stack.
The popup now looks the commands up itself (popUpShortcutBar(pos,
context)) instead of being handed actions, since it has to rebuild
them after an edit. ShortcutBarSettings::availableIds() lists what can
go on the bar, shared with the configuration page. An empty bar still
shows its "…" button, so it can be filled again.
Discussion #1033.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
(cherry picked from commit 4145d4bec1d403126b2a6587105aea47092db8a2)
Pressing S on a folio opens the element picker at the cursor with a row
of commands above it, chosen by what is selected, like the SolidWorks
shortcut bar:
nothing selected insert last element, element picker, text, line,
rectangle, terminal strip plan, paste, folio
properties
elements selected rotate, rotate texts, edit, copy, cut, delete
only conductors reset path, edit, delete
Each row is a list of ShortcutManager ids, so any registered command can
go on it and the bar carries no command list of its own. The lists are
in QSettings (diagrameditor/shortcut_bar/<context>); a context the user
has not changed follows the defaults. A disabled command keeps its
place, greyed, so a row looks the same each time. Clicking a button
closes the bar and triggers the action.
A new configuration page, "Barre de raccourcis", edits the three lists:
add, remove and reorder any diagram editor command.
To make that possible:
- ShortcutManager::action(id, owner) returns the action a given window
registered under an id, since each editor window registers its own.
- The add-item actions (text, image, shapes, terminal strip plan) are
registered as diagrameditor.add_<kind>, with no default key. They also
appear in the Shortcuts page and can now be bound.
Discussion #1033.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
(cherry picked from commit 53c1e213282b9f7226ca1c09e81703113236bb88)
Insert opens a small picker where the mouse is, with its search field
focused. Type to search the whole collection, Enter to place the best
hit (it is preselected), Up/Down to choose another, Esc to close. The
chosen element goes into the usual placement mode, so it can be placed
several times and repeated with A.
With the field empty the picker shows a palette: the elements of a
folder, as an icon grid. The palette is a folder rather than a setting
or a file format. Subfolders are read in, the 01_/02_ filename prefixes
the shipped collection already uses give the order, and sharing it is
putting it in the company collection. Only the path is stored,
"elementscollection/palette-path", defaulting to the user collection.
It is read each time the picker opens, capped at 60 entries and three
folder levels.
The picker builds no second collection model. It asks the Collections
panel's rankedSearch(), so both give the same results in the same order
and startup is unchanged.
"Insérer un élément…" is in the Édition menu, registered with
ShortcutManager on Insert, which nothing else uses, and disabled with no
folio open or on a read-only project.
Discussion #676.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
(cherry picked from commit 01e1e0303b96380e710da8d4bb2300b21d01e938)
The Collections search hid non-matching rows, so the hits stayed spread
through the expanded folders. Searching "diode" against the shipped
collection left five levels of tree open, and the first visible hit was
"Avalanche diode bidirectional".
The same matches are now shown as a flat list that takes the tree's
place while a search is active, best match first:
exact name 1000
name starts with 800
name contains 600, less the match position
info field only 300
less the name length, so "Diode" precedes "Diode Zener bidirectional"
"diode" now gives Diode, diode-tube, Diode bridge, Diodes Module Pilot
Wire, Photodiode. No new index: this ranks what match() already returns
against Qt::UserRole+1, the string built at startup from the name and
every element-info field. An element matching several "+" terms is
listed once, and each row's tooltip is its folder, so similar names can
be told apart.
Double click or Enter on a result places it, as in the tree. Down from
the search field moves into the results, so searching and placing needs
no mouse. Emptying the field brings the tree back.
The query and ranking live in rankedSearch(), which returns hits without
model indexes, so another list can reuse them.
Discussion #676.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
(cherry picked from commit 3d6a280f9608758527ef0ca2de7223a19dfbfa45)
Right-clicking an empty folio now also offers:
- "Insérer le dernier élément": the same action as Édition and the A
key, shown once something has been placed.
- "Renvoi de folio": the folio report elements the project already
uses, read from its embedded collection, with dated copies of the same
element listed once. A project that has none yet gets the coming and
going arrows of the common collection. An entry places its element
where the menu was opened.
The submenu is rebuilt each time the menu opens, from the embedded
collection's XML, not from the folios' scenes. It is left empty, and so
hidden, on a read-only folio.
Discussion #1033.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
(cherry picked from commit 94e3f61bf93689066b3964f2316e3c8f1ee7b673)
Ajouter une ligne, un rectangle, une ellipse... had no ShortcutManager
id, so the command search could not list them. Give each one an id with
no default key; they can also be bound in the Shortcuts page now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ctrl+Shift+M (Édition → "Rechercher une commande…") opens a small
search box at the cursor listing every command of the diagram editor
window, as SolidWorks' "Search Commands" and the command palette of
many editors do. Typing narrows it, best match first: name starting
with the text, then a word starting with it, then containing it.
Matching ignores case, accents and mnemonic "&", so "editer" finds
"Éditer l'item sélectionné". Each row shows the command's key when it
has one, which also teaches the keys. Disabled commands are listed,
greyed, and cannot be run. Enter runs the highlighted one after
closing the box; Esc closes.
The list is ShortcutManager's registry, restricted to the actions this
window owns (ShortcutManager::action(id, owner)), so a second editor
window's commands never appear and nothing has to be listed by hand.
Ctrl+Shift+P, the usual key for this, is already the autonumbering
dock's.
tst_commandsearch covers the folding, the ranking, that another
window's commands are left out and that a disabled command does not
run; both behaviours were checked to fail the test when broken.
Discussion #1033.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
BorderTitleBlock::draw() leaves the row header's room before the first
column even when the row header is hidden; insideBorderRect() does not,
so the top ruler was one header width out of line on such a folio. Take
the first cell's position from the header sizes, as draw() does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Affichage › "Afficher les limites des cases" draws a faint dashed line
at every column and row limit of the folio border, so that zoomed into
the middle of a folio you can see where a cell ends, not only which one
the headers name. Off by default, remembered once set
(diagrameditor/cell_lines).
Drawn by DiagramView::drawBackground(), under every item and only in
the view: printing and export render the Diagram, never the view, so
they cannot pick the lines up. PaletteGraphicsView gains scenePainter()
so a subclass can draw there on the inverted (dark palette) path too.
The lines follow the border's own cell positions, and each direction is
hidden when the folio hides that header.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Windows 11 style gives QPalette::Button a translucent colour
(#FFFFFFB3). The rulers filled their background with it, and since they
paint with WA_OpaquePaintEvent nothing clears them first: every zoom
step blended the new labels over the old ones, leaving a fading trail.
Fill with the button colour composed over the window colour instead,
which is always opaque.
Each ruler is now also hidden while the folio's own column (or row)
header is wholly in sight, so zoomed out only the folio's headers show,
not both. When a ruler comes or goes the drawing keeps its place on
screen: the ruler covers or uncovers the edge of the viewport.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since #983, projectDataBase::newQuery() checked a query with
sqlite3_prepare_v2() and sqlite3_stmt_readonly() on the handle of the
QSQLITE driver. Those calls go to the libsqlite3 QElectroTech links. The
QSQLITE plugin of the Qt online installer does not use that library: it
carries its own copy of SQLite, so the handle belongs to another library
and the call crashes. #1021 then put newQuery() on every element
selection, which is where #1045 hits it.
The check now runs the query with PRAGMA query_only set, through the
driver. SQLite refuses a write itself, before touching a row, so the CTE
prefix #983 closed ("WITH x AS (SELECT 1) DELETE FROM element") stays
closed. A refused or failed query comes back empty, because several
callers call exec() again on what newQuery() returns, after query_only
is off.
QElectroTech no longer calls the SQLite C API anywhere.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Info.plist template shipped LSMinimumSystemVersion=12.3.0, but
the CMake build embedded the host SDK's deployment target (minos
26.0 on recent build machines) into the binary's LC_BUILD_VERSION.
Launch Services relies on the binary's minos, not the plist, so it
refused to launch the app on macOS < 26 even though it ran fine
when invoked directly.
- Set CMAKE_OSX_DEPLOYMENT_TARGET=14.0 explicitly in the cmake
configure step, matching the real minimum imposed by the
Homebrew Qt6 toolchain used for this build.
- Sync misc/Info.plist's LSMinimumSystemVersion to 14.0.0, and have
MacQetDeploy_arm64_cmake.sh set it via PlistBuddy at bundle
install time so it can't drift from the build target again.
Reported-by: guillaume.ruivo (forum)
DiagramEventAddElement is already a good placement mode: the element
follows the cursor on the grid, a left click drops it, Space rotates it,
and it stays loaded for a run of the same symbol. Its only caller was
DiagramView::handleElementDrop(), so it could be reached only by
finishing a drag. Double-clicking a symbol in the Collections dock
opened the element editor instead.
- DiagramView::startElementPlacement() is split out of
handleElementDrop(). defaultPlacementPos() uses the cursor when it is
over the view and the centre of the visible area otherwise.
- ElementsCollectionWidget emits insertElementRequested() on double
click, or Enter on the highlighted item. The host decides which view
receives it, so the widget can later be reused outside the editor.
- When there is nowhere to place it (no folio open, read-only project),
the element editor opens, as a double click did before.
- "Insérer le dernier élément" (Édition menu, default key A) places the
last element again. DiagramView reports every placement it starts, so
an element dropped by drag counts too. Macros are not remembered.
A rather than Space: Space rotates the pending element inside placement
mode and is bound three more times in this editor. The key is a
ShortcutManager default and can be changed in the Shortcuts page.
Double click placing is a behaviour change, so it has a preference,
"elementscollection/double-click-inserts" (default true), shown in
Configuration as an opt-out: "Double-cliquer dans la collection ouvre
l'éditeur d'élément au lieu de l'insérer".
Discussions #676 and #1033.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Right-clicking an empty folio offered Paste here, Folio properties and
four row/column actions. Four of six entries change the folio's layout,
which is rarely wanted and easy to hit by mistake, while nothing in the
menu helps draw.
The empty-folio menu now holds the "Ajouter" submenu (text, image,
shapes, terminal strip plan -- the same actions as the Edition menu and
toolbar), then Folio properties, then the row and column actions in a
"Lignes et colonnes" submenu. The selection menu is unchanged.
A submenu whose actions are all disabled is dropped, the same way
disabled actions already are.
Discussion #1033.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
ElementTextsMover::endMovement() returned early when the text had not
moved without clearing m_movement_running, so every later
beginMovement() on that folio was refused. The dragged text still
moves, because it positions itself, but the mover no longer tracks the
drag and builds no undo step for it.
To reproduce: Shift+drag one element text a couple of pixels so it
snaps back where it was, then Shift+drag another text of the element
and press Ctrl+Z. The second text stays where it was dropped; with this
change it goes back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
ElementTextsMover::endMovement() returned early when the text had not
moved without clearing m_movement_running, so every later
beginMovement() on that folio was refused. The dragged text still
moves, because it positions itself, but the mover no longer tracks the
drag and builds no undo step for it.
To reproduce: Shift+drag one element text a couple of pixels so it
snaps back where it was, then Shift+drag another text of the element
and press Ctrl+Z. The second text stays where it was dropped; with this
change it goes back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Drop 1:2.5: on a grid of 10 it steps by 4, which misses 10, so texts on
two elements 10 apart could never line up. Every remaining divisor is a
whole number, which tst_textgrid checks.
The divisor list and the snapping arithmetic move to the header-only
textgrid.h so the toolbar menu, the preferences page and the test share
them. The preferences page gets the same choice under Grille + Clavier;
QETApp::textGridChanged keeps every editor's toolbar button in step.
While element texts are dragged, the status bar names the text grid and
says to release Shift and hold Ctrl for free placement -- Ctrl+Shift
together is the pan shortcut.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Texts snapped to the folio grid, so the first drag of an off-grid label
pulled it sideways by up to half a grid step (discussion #1020). Texts
now snap to a fraction of the folio grid, chosen from a "Textes 1:N"
button in the View toolbar and a menu under Affichage: Off, 1:1, 1:2,
1:2.5, 1:5, 1:10. Because the step divides the folio grid, texts on
different elements still line up. Ctrl still places a text freely.
1:1 is the default and matches the previous behaviour. The setting is
stored in QSettings; nothing changes in saved projects.
All five text movers switch together: element texts, text groups,
texts moved with a selection, conductor texts and independent texts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
The popup now also takes a folio position before the cell, the way
cross references and folio reports write it by default (%f-%l%c): 3-B13
offers "Folio 3 (<title>), case B13", and Enter shows that folio and
zooms onto the cell. Case and a leading "p" are ignored, and the
separator is optional: 3-b13, 3b13, P3B13 and p3-b13 all work. A plain
B13 still means the current folio, and P3 alone is still row P,
column 3: a folio always needs a cell after it. A folio position past
the last folio, or a cell outside that folio's border, offers nothing.
The zoom on another folio is queued, so it runs once the tab switch
has been handled. Checked on the ATS example: jumping to 1-C5 from
folio 3 gives the same view, pixel for pixel, as C5 typed on folio 1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Affichage > "Garder les en-têtes visibles" adds a bar along the top and
the left of the diagram view that repeats the folio's column numbers and
row letters, aligned with the cells at any zoom, so they stay in sight
when the folio's own headers are scrolled away. Off by default; the
choice is stored as diagrameditor/cell_rulers.
The bars are CellRuler widgets in the view's margins
(setViewportMargins), not scene items, so printing and PDF/PNG/DXF
export never see them, and they paint with the application palette
outside of the dark-palette inversion. They keep a constant thickness;
when cells get narrower than their labels, only every 2nd, 5th, 10th...
label is written. A bar is hidden when the folio hides that header.
Showing or hiding them keeps the centre of the view where it was.
The labels come from BorderCellLabels, now also used by
BorderTitleBlock::draw(), so the bars and the border cannot disagree.
PNG export of all 133 folios of the examples is pixel-identical to
master, with border-columns_0 true and false.
Known limits: changing border-columns_0 repaints the bars at the next
scroll or zoom; the menu toggle updates the views of its own editor
window only, like the grid toggle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Typing a cell reference into the "Atteindre un élément" popup now offers
"Case B13"; Enter zooms the view onto that cell with one cell of margin.
The cell is read the way the border labels it: row letter(s) then column
number, honouring the "columns start at 0" setting and multi-letter rows
(AA, AB...). Cells outside the folio are not offered.
BorderTitleBlock::cellRect() is the reverse of convertPosition(); a
round trip over every cell of a 30x23 folio, under both column-numbering
settings, returned the same cell for all 1380.
When an element is labelled exactly like the cell (K1), the element stays
first so Enter keeps its old meaning; the cell is listed after it.
DiagramView::zoomToRect() re-centres from a queued call: zooming in makes
the scroll bars appear, and the viewport resize that follows is anchored
under the mouse (setResizeAnchor(AnchorUnderMouse)), which otherwise
scrolls the view away from the cell straight after the zoom.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Windows reads the device through hid.dll and SetupAPI with ctypes, as
hidapi's Windows backend does: shared, so 3DxWare can keep running, and
dropping the 0 report ID Windows adds, as hidapi does, so the bytes are
the ones QET decodes. Windows does not give out the report descriptor,
so a Windows recording has none; tst_spacemousehid then uses its
fallback layout.
The docstring, printed in full by --help, now has step-by-step
instructions for Linux, macOS and Windows, download included.
Tested: Windows Python 3.12 (embeddable) under Wine 10, against the
virtual uhid 3D mouse: --list finds it, a push sent during `right`
lands in `right`, and a button press and release land in `buttons`,
with the same bytes the Linux path records for the same push. Linux
and the mocked macOS path still pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The recorder only read /dev/hidraw, so it could not run on a Mac. It now
also reads through IOKit via ctypes, using the calls hidapi's mac backend
makes: a shared open (as #1028 does) and the input report callback.
hidapi passes those bytes through unchanged, so a recording is exactly
what QET decodes on macOS. Standard library only, and no sudo.
If 3DxWare holds the device, or Input Monitoring is not granted, it says
which instead of recording nothing. The JSON adds "backend" and
"other_readers" (any 3DxWare or spacenavd process that was running).
Reports that queue up while it waits for Enter are now dropped, so each
step holds only its own movement. The Linux path is otherwise unchanged.
Tested: the Linux path against the virtual uhid device
(tools/hid-capture/fake-spacemouse.py in the docker harness). A stale
report was dropped and the step's own push was kept. The macOS path has
only been run against a mocked IOKit, not on a Mac.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SpacePilot PRO recording on #599 has a weak `right` step: pushed
too lightly to read above the cap's normal cross-axis noise. A longer
window makes it easier to hold a firm push and gives the decoder more
samples to average over.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Measured on a macOS 15 runner with 3DxWare 10.8.13: an ad-hoc signed
test binary with the hardened runtime loads 3DconnexionClient with or
without the entitlement, so ad-hoc signing does not enforce library
validation and CI cannot prove the entitlement is needed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On macOS, 3DxWare installs a driver extension that takes the 3D mouse
over. With it installed, HidBackend opens the device but receives
nothing, so the mouse did nothing in QElectroTech until 3DxWare was
uninstalled (discussion #599, PR #1028). Most Mac owners of a 3D mouse
have 3DxWare installed.
ConnexionBackend asks 3DxWare for the motion instead, through
3DconnexionClient.framework, as Blender does. SpaceMouseListener tries
it first. When 3DxWare is not installed, or is installed but its driver
is not running, it falls back to HidBackend, so the device works in
both setups. Take-over mode stops 3DxWare's own actions in QET, so the
view does not move twice.
The library is loaded at run time from where 3DxWare installs it:
nothing is linked or bundled, and the build needs no SDK. The few
declarations are written here, from Blender's
GHOST_NDOFManagerCocoa.mm, because 3Dconnexion's SDK headers may not be
redistributed. 3DxWare's axes are y up and z away from the user; they
are mapped to QET's raw USB convention by comparing Blender's 3DxWare
and spacenavd code paths.
The release script signs with the hardened runtime, which refuses a
library another team signed. misc/qelectrotech.entitlements adds
com.apple.security.cs.disable-library-validation (Blender's notarized
build carries the same one), and MacQetDeploy_arm64_cmake.sh now passes
it to all four signings of the app, including the re-sign inside the
DMG.
Tested: tst_spacemouseconnexion runs the backend on every platform
against fakeconnexion, a stand-in library that answers from its own
thread as 3DxWare does: registration, the axis mapping, buttons, other
clients' messages, 3DxWare not installed or not running, deletion
with a message in flight. Flipping an axis sign or dropping the client
check turns it red. realLibrary() loads the real framework when
3DxWare is installed. Linux Qt 6 build with the 3D mouse enabled: all
18 tests pass.
Not tested: on a Mac with a real device. The axis signs and whether
buttons arrive as a bitmask with current 3DxWare are unverified.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a crash, QElectroTech offers to reopen its recovery files. If one
cannot be read, answering OK crashed the program instead of showing the
"could not open" warning: QETProject(KAutoSaveFile *) takes ownership of
the file and deletes it on failure, and openBackupFiles() then read the
file name from the deleted object to build the warning. Read the name
first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Both issues from the review, preview of empty contact comb slots only:
- Slice the flat label list over the declared poles in drawAsContacts
(terminals per pole from labels.size()/contactCount) instead of a
fixed stride of 2 (3 for a switch) inside drawContact: terminalCount
and contactCount are edited independently, so with 3 poles and the
default terminal count of 2 the fixed stride starved every pole but
the first. Available labels are now distributed across the poles.
- Map changeover labels of a slot to their own position always
(common=stored[0] right, NC=stored[1] bottom-left, NO=stored[2]
top-left, missing entries stay empty) instead of falling back to the
raw stored order, which put the numbers on the wrong contact halves
when the terminal count was below three.
- Element editor: changing the contact count now keeps the terminal
count in step (same terminals per contact, type default 2/3 for
inconsistent data, minimum 3 for a switch), so the mismatch cannot
be created anymore; legacy mismatched data is handled by the new
slicing.
- Drop the now redundant elmt check before is_power_ctc (it already
includes it) - the dead null check from the review.
Diagram::toXml() writes texts, images, shapes and tables in items()
order. The diagram scene uses NoIndex, and Qt's linear index sorts its
item list by pointer address the first time any item is removed from
the scene, which the editor does constantly (selection handles, for
one). From then on the order in the saved file follows memory
addresses, so moving one element reshuffles unrelated blocks and a
version-control diff of the project becomes unreadable.
Write those blocks in stacking order instead, read from a rect query,
which Qt sorts by z and insertion order even with NoIndex. Reloading a
file rebuilds exactly that order, so a resave is stable and the drawing
does not change. Elements and conductors were already sorted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Qt 6 declares the PDF/X namespace (pdfxid) in the XMP metadata of every
PDF it writes, even when the file is not PDF/X. Adobe Acrobat then draws
small text too bold at some zoom levels. Qt only needs the declaration
for PDF/X-4 output, which QElectroTech never asks for.
Blank the declaration out after export, in place with spaces so no
offsets move, in both the export window and the --export-pdf path.
Diagnosis and the Acrobat testing by Alf, bugtracker #340.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
hidapi's macOS backend opens every device with
kIOHIDOptionsTypeSeizeDevice unless told otherwise (hid_init() calls
hid_darwin_set_open_exclusive(1) for backward compatibility). When
3DxWare is running it already holds the SpacePilot/SpaceMouse, so the
seize fails, hid_open_path() returns NULL and HidBackend::scan() keeps
retrying every 3 s without ever finding the device.
Reported in discussion #599: a SpacePilot Pro (046d:c629) is listed by
hid_enumerate() and works in 3DxWare, but has no effect in QET's macOS
build. Enumeration never opens a device, so it did not exercise this.
Guarded on HID_API_VERSION >= 0.12, where the setter first appeared.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The root of a project's embedded collection showed "Projet sans titre"
whenever the project had no title, while the project panel shows the
file name. Fall back to the file name the same way, and only use
"Projet sans titre" for a project that has neither.
The name was also computed once, so changing the project title or saving
it under a new name left the pane stale until the collections were
reloaded. Update it on projectTitleChanged and projectFilePathChanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Installs MSYS2's hidapi and turns on QET_ENABLE_SPACEMOUSE with the
hidapi backend, so the Windows package reads a 3Dconnexion SpaceMouse
directly over USB, with no 3Dconnexion driver (discussion #599).
The existing transitive DLL scan copies libhidapi-0.dll into the package,
and both installers take everything in bin/. Two checks make a silent
loss fail the build instead: CMake only warns when hidapi is missing, so
the exe must link against it, and the DLL must be deployed, or
QElectroTech.exe would not start.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For device owners on Linux: guided movements, with every raw report and
the device's report descriptor saved to one JSON file. Dropped into
tests/qttest/fixtures/spacemouse/, a recording is checked by
tst_spacemousehid against what the user was asked to do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 3D mouse only worked on Linux, through spacenavd. This adds a second
backend that reads the device directly over USB through hidapi, with no
3Dconnexion driver or SDK: the route to Windows and macOS (discussion
#599), and usable on Linux without spacenavd.
SpaceMouseHid decodes the raw reports from the device's own report
descriptor -- where each axis and button sits, its range, absolute or
relative -- so no per-model table is needed, with the classic report
1/2/3 layout as a fallback when the descriptor cannot be read and the
0x1c button list newer devices send. Absolute axes are rescaled to
+-500 exactly as spacenavd does, so both backends give QET the same
values. HidBackend polls from the main thread (fast while moving, slow
when still), emits one sample per poll, and looks for a device every 3 s
so plugging one in or back in needs no restart.
QET_SPACEMOUSE_BACKEND (auto, spnav, hid) picks the backend; auto keeps
libspnav on Linux when it is found and uses hidapi otherwise. hidapi is
found through pkg-config as hidapi-hidraw (Linux) or hidapi (MSYS2,
Homebrew).
A sample arriving in the same millisecond as the previous one now counts
for no time instead of a full period, so a burst of queued samples no
longer moves the view further than the time it covers.
Tested without a device: tst_spacemousehid (descriptor parsing, broken
and hostile descriptors, every report form, recordings from real devices
once they are added to fixtures/spacemouse), and end to end on Linux
through a virtual USB device created with /dev/uhid: the same moves give
byte-identical screenshots through the hidapi and libspnav backends, an
absolute axis is rescaled as spacenavd does, buttons trigger their
bound action, and unplugging and replugging while QET runs (including
with a dialog open that a device button opened) reconnects cleanly.
Not tested on Windows, macOS or real hardware.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 3D mouse's pan and zoom speeds were fixed guesses, and each sample
was applied as it came, so the speed on screen depended on how often
the driver sends samples -- different for every platform and device.
Motion now goes through SpaceMouseMotion::map(), which scales each
sample by the time since the previous one, and applies the user's
settings from a new "Mouvement" section of Configuration > Souris 3D:
pan and zoom speed, a dead zone, inverting each axis, and zooming by
push/pull (as before) or by twisting the cap. The defaults keep the
previous behaviour. Zoom is now exponential in the deflection, so the
factor stays positive however hard the cap is pulled (1 + z/1000 went
negative past z = -1000) and an equal push and pull cancel out. Sub-
pixel pan is carried over between samples instead of being rounded
away. The backend now reports all six axes.
tst_spacemousemotion covers the mapping without a device and is built
whether or not QET_ENABLE_SPACEMOUSE is on. The new behaviour was also
checked end to end with tools/spnav-shim (qelectrotech-docker): twist
with a dead zone of 10 ignores push/pull and small drift, and a twist of
60 gives the same frame as a push of 50 with the defaults.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 3D mouse only acted on the diagram editor: SpaceMouseListener
ignored every other window, so the element editor did not move at
all (reported by scorpio810 in PR #635 with a SpacePilot Pro).
applyMotion() now also handles a QETElementEditor, driving its
ElementView through the same scrollbar pan and a new
ElementView::zoom(factor), which keeps the wheel zoom's clamping.
The element editor's scene rect only covers what is on screen, so it
is grown before each pan sample, as its middle-button pan does;
without that the scrollbars have no range and the pan does nothing.
Verified under Xvfb with an LD_PRELOAD stand-in for libspnav feeding
recorded motion samples: element editor zooms 1.63x for ten z=50
samples and pans; diagram editor screenshots are byte-identical to
master's for the same input.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each information field of the element properties now offers, as a
drop-down while typing, the values the other elements of the project
already carry for it: supplier, manufacturer, reference...
The values come from the project database's element_info table, not
from a walk over the folios. Spellings that differ only by case are
offered once, as most elements spell them, and the edited element's
own value is left out. The field key is checked against
elementInfoKeys() before it becomes part of the SQL text.
Browsing the list with live edit on applies each highlighted value,
as typing applies each keystroke; ChangeElementInformationCommand
merges them, so this still leaves one undo entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Call updateLabel() explicitly when the xref is created in itemChange for a master that must show its configured contact groups without slaves (same pattern as the PLC branch above).
- Register the hover/click hit rect of a contact independently of its position text again, as before this feature: only the drawing stays guarded by !str.isEmpty(), and the map insert is now keyed on elmt so free slots (nullptr) never enter the map.
- Revert is_power_ctc to the original element-type test (with a null guard): the Power-flag term was redundant for every caller that passes an element, so no linked contact changes classification.
- Clarify the label-order comment: single pole NO/NC are swapped, changeover labels are rotated per pole inside drawContact() (multi pole included), multi pole NO/NC groups keep the master order.
5b0785fcc routed most reads of auto_num_locked, potential_isolating and
exclude_from_bom through QET::infoFlagIsTrue(), which accepts the same
spellings as the parts-list query (true/1/yes/on, trimmed, any case).
Four checkbox reads still compared against a literal lowercase "true":
the "exclude from parts list" box in the folio properties panel, and all
three flags in the element editor.
A value such as "True" or "1" from a hand-written .elmt/.qet was
therefore left out of the parts list, but shown unticked in both panels,
and pressing Apply there wrote back "false" and flipped the flag.
Revives the unconverted part of PR #785.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RotateTextsCommand called QDialog::exec() from inside its constructor, so
the command could not be built without a human answering a dialog. That
made it untestable headlessly, undrivable from any script or test harness,
and it is why bugtracker #312 (PR #707) shipped with its save/reload
round-trip unverified -- the symptom could not be reproduced without a GUI.
The command now takes the angle as a parameter and does no asking. Two
statics carry the interactive half:
hasSelectedTexts(diagram) -- is there anything to rotate
askRotation(rotation) -- open the dialog, false if cancelled
The single call site in QETDiagramEditor asks first, then builds the
command, so the user-visible behaviour is unchanged: same dialog, same
title, same no-dialog-on-empty-selection. Keeping askRotation() in this
class also keeps the QObject tr() context, so existing translations of
"Orienter les textes sélectionnés" are not invalidated.
Also guards undo()/redo() against a null m_anim_group. When nothing is
selected the constructor calls setObsolete(true) without ever creating the
animation group, and QUndoStack::push() calls redo() before discarding an
obsolete command -- a latent null dereference on that path.
Verified headlessly, which was the point: driving the command through a
scratch --test-ops op on examples/741.qet (67 conductors), rotation
attributes written on save go 0 -> 67 with the #707 fix present and stay
at 0 with it reverted, while the reverted build instead writes userx on
all 67. That is bugtracker #312 reproduced and fixed under test for the
first time.
QETApp's constructor ends with checkBackupFiles(), which opens modal
dialogs -- the "restore these files?" prompt, and the crash report offer.
Those dialogs run their own event loop, so the constructor does not return
until the user answers them.
main() still has work to do at that point. In particular this, a few lines
later:
QObject::connect(&app, &SingleApplication::receivedMessage,
&qetapp, &QETApp::receiveMessage);
While the prompts are up that connection does not exist yet. A second
instance launched during the window -- double-clicking a project, or
xdg-open, while the first copy is still asking about restore files --
hands its file names over, SingleApplication accepts and delivers them,
and nothing is listening. The message is discarded and the second process
has already exited, so the file is simply lost with no error.
Deferring checkBackupFiles() to the event loop lets the constructor return
promptly. main() finishes wiring up, and the prompts appear immediately
afterwards exactly as before.
Verified with a stale restore file present, sending a project to the
running instance while the restore prompt is displayed: before, the file
was dropped and never appeared, even after answering the prompt; after, it
opens. The restore and backup prompts still appear and still work.
Note this is only observable together with the fix for bugtracker #248 --
before that, no file passed to a running instance was opened under any
circumstances.
Three bugs stacked to produce the symptom in issue #409 (move button does
nothing):
1. selectionChanged() was declared in freeterminaleditor.h but had no body
and was never connected to the selection model, so the move button had no
awareness of whether a terminal was selected. The button could appear
enabled with nothing selected, then silently return early in
on_m_move_pb_clicked() at the real_t_vector.isEmpty() guard.
2. The dataChanged→setDisabledMove(true) connection was a one-way trap: any
cell edit (including accidentally opening and closing a type/function
combo, or toggling LED back to its current value) permanently disabled the
move button until reload() was called. There was no tooltip explaining why
the button was greyed out, so the user had no way to recover.
3. FreeTerminalModel::setData() for LED_CELL had no change guard, unlike
LABEL_CELL which checks label != value. Clicking the LED combo while it
was already at the same value still emitted dataChanged and triggered the
disable.
Fix:
- Implement selectionChanged() to enable the move controls only when at
least one row is selected AND there are no pending (yellow) edits.
- Connect it to both selectionModel::selectionChanged and model::dataChanged
so the button state is always consistent with actual UI state.
- Route reload()'s re-enable through selectionChanged() instead of calling
setDisabledMove(false) directly, so the selection state is respected
immediately after a move.
- Add a tooltip to m_move_pb explaining the disabled state.
- Add the missing change guard to LED_CELL in setData().
Co-Authored-By: Claude Sonnet 4.6 <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>
"MS Shell Dlg 2" is a Windows font alias, not a font, and projects and
settings saved on Windows carry it. Qt 5's GDI backend let Windows resolve
it to Tahoma; Qt 6's DirectWrite backend does not know the alias and falls
back to Arial, so those texts render heavier on screen and in exported PDFs.
Register the substitutions Windows itself uses, before any application
object exists so the headless export and scripting paths get them too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Picking a sheet (folio) background colour in the diagram editor was lost
on every restart. Diagram::background_color is a static initialised to
white and PaletteGraphicsView's s_custom_bg a static bool, and neither
was ever written anywhere -- Diagram::toXml() carries no colour attribute
either -- so closing and reopening a project always came back on the
default and the choice had to be made again.
Store it in QSettings under diagrameditor/sheet_background_* as a pair of
values rather than one: the colour, and whether it was picked explicitly.
Both halves are needed. "#ffffff, follow the system" and "#ffffff, always
white" are the same colour and two behaviours -- the first is what the
views invert on a dark palette -- so keeping only the colour would
silently turn one into the other on the next start, which is the reported
problem one step removed.
The colour is written as HexRgb on purpose. The SVG export gives
Diagram::background_color an alpha of 0 to render a transparent
background and never puts it back, and that transient value must not be
persisted as a permanently transparent sheet.
Applied from main() after the headless export and scripting branch -- those
return before reaching it and must keep rendering on plain white, the rule
ProjectPrintWindow already enforces for printing -- and before QETApp is
constructed, since that constructor already loads the projects given on
the command line. The GUI export dialog is left alone: it renders through
drawBackground(), so what you see is what you export, as it already was
within a session.
Saved at the moment the colour is applied rather than at shutdown, so
neither the print window's temporary white nor the SVG export's alpha can
reach it. The button's constructor now mirrors the stored state instead of
always claiming "system colour", and the "recently used" list is stored
alongside it.
Covered by tst_sheetbackgroundsetting, which pins the custom flag and the
dropped alpha -- the two rules a single stored colour would lose.
Add a new cross reference setting per xref type (coil, protection,
commutator, PLC), labelled "Afficher tous les esclaves definis par le
maitre" and persisted as showallconfiguredslaves. When it is enabled,
the contacts display is selected and the master declares contact
groups, the contact comb draws every contact group of the master in the
master's own order, even when no slave is linked to it yet. Masters
without declared contact groups and the option turned off keep the
previous behaviour exactly: linked slaves only, sorted by position.
- XRefProperties: new property stored in the settings and in the
project XML (attribute showallconfiguredslaves, absent means false so
old files are unaffected), included in operator==.
- XRefPropertiesWidget: new checkbox placed after the terminal names
one, enabled only while "Afficher en contacts" is selected; its
enabled state is now also set explicitly when a type is loaded (a
radio button that does not change emits no toggled()).
- For the PLC type the contacts/cross radios, the two display
checkboxes and the cross options group are hidden: a PLC master is
always drawn as its IO table, those settings have no effect there.
Positioning and label settings, which the table really uses, stay.
- CrossRefItem: free slots draw the symbol of the group plus the
terminal names the master defines (pairs swapped for a single pole
NO/NC contact, labels of a changeover contact rotated one step
counter-clockwise), without position text and without hover/click.
Linked slaves keep drawing from their own data at their assigned
group position; links without a group are appended at the end in
position order.
- Xref lifecycle: the item is created and kept without linked slaves
for snap-to-bottom (MasterElement::mustShowXrefWithoutSlave) and for
snap-to-label (DynamicElementTextItem::updateXref and
ElementTextItemGroup::updateXref, which now also run when the element
lands on the scene and re-establish their project connection), so a
freshly placed master shows its comb immediately instead of only
after the next settings change. updateLabel() resets its geometry
when the option is turned off again, so no stale ghost stays.
The two width-resize handles added in #591 were free scene items, moved
with setPos() from inside DynamicElementTextItem::paint(). Moving an item
while the view is painting is outside what QGraphicsView's partial
repaint tracks: the handle was drawn only where the current repaint band
overlapped it, and its old position was not reliably cleared. Moving or
zooming a selected element left green slivers on the folio (#1002).
Since 29d16c333 the handles show on every text of a selected element,
so any ordinary element move triggered it.
Make the handles children of the text instead. Qt then moves and
repaints them together with the element, in local coordinates, and
their position only needs updating when the text's size changes, which
documentSizeChanged reports (text, font, width, undo of a resize).
This also takes paint() out of the handle logic entirely.
Reproduced on Linux (Xvfb), so the issue is not Windows-specific.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Editing an element already in the user collection, saving, and closing
the editor left the old thumbnail in the elements panel until the
whole collection was reloaded. ElementsLocation::icon() serves the
preview from two caches keyed by path+uuid -- ElementPictureFactory's
in-memory picture cache and ElementsCollectionCache's on-disk SQLite
cache -- and neither was ever told the file changed.
QETElementEditor::toLocation() writes the new XML and returns;
ElementsCollectionWidget::locationWasSaved() then refreshes the panel
item, but it reads the icon through the same two stale caches, so the
refresh was a no-op. slot_reloadElementDrawings() already shows the
correct invalidation call for ElementPictureFactory; this wires the
same pattern, plus a matching refresh of ElementsCollectionCache's
row, into the save path itself.
Fixes the preview half of #1004. The paste-cursor-jump half of that
report is a live design disagreement between two recent commits from
a different contributor and is written up separately rather than
fixed here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fills lang/phrase/qet_fr_xx.qph for every language QET ships, following
on from discussion #873. A phrase book is never compiled into what
ships -- a translator opens it in Qt Linguist and sees a suggestion
already filled in, to accept or correct.
Only ever appends. phrasebook.write() skips any source string already
in the file (case-insensitive), so no existing entry -- human or
machine -- is touched. 24 languages had no phrase book at all before
this; the other 5 (da, de, nl, ru, sv) keep every line they had.
Built and run with tools/qet-i18n in the qelectrotech-docker harness
(DeepSeek Flash). Companion to qelectrotech-elements#82, which does the
same for the element collection.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ConductorColorToolButton::applyColor() only recoloured the selected
Conductor objects. A wire drawn across a junction is several separate
Conductor segments sharing one electrical potential, so selecting one
segment and picking a colour left the rest of the wire its old colour
-- visible only by falling back to F2/double-click, which already
expands to relatedPotentialConductors() for exactly this reason
(ConductorPropertiesDialog, and QetScriptApi::setConductorProperty).
Expand each selected conductor to its potential before building the
undo command, matching that existing pattern.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Element::editProperty() gave PropertiesEditorDialog no size of its own,
so the dialog fell back to its sizeHint, which is sized for the compact
general-purpose editors it usually hosts. A PLC master instead shows a
six-column IO table that grows horizontally, and the dialog came out
too narrow to read those columns in.
For a master whose type is PLC, resize the dialog to three times its
natural width before exec(), keeping the natural height. The width is
clamped to the available screen so it cannot run off the display, and
every other element type opens exactly as before.
The IO table of MasterPropertiesWidget -- the panel that opens when a
PLC master placed on a schematic is edited -- set every section to
QHeaderView::Stretch. Stretch spreads the sections evenly over the
widget and disables section dragging altogether, so the column
boundaries were permanently fixed: they could be neither widened nor
narrowed, and only the width of the whole panel had any effect.
Use QHeaderView::Interactive instead, with movable sections and a set
of default widths, so the columns can be dragged as they are everywhere
else in the application. The layout reached this way is written to
QSettings under masterpropertieswidget/plc-table-header-state on every
sectionResized and sectionMoved, and restored the next time the table
is built -- the same header-state trick the free and linked element
trees of this widget already use, except saved automatically rather
than only from the context menu.
sources/ElementsCollection/fileelementcollectionitem.cpp had a German
tr() source string ("Makros") in a project whose source language is
French/English elsewhere. Renamed to "Macros" (identical in both
languages, so no translation catalog change is needed). Translated
three German-language comments in elementscollectionmodel.cpp and
diagramview.cpp to English.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PR #983 added a hard SQLite3 system dependency to CMakeLists.txt, but
the workflow only installed libqt6sql6-sqlite (Qt's runtime driver
plugin), not the C headers/library find_package(SQLite3) needs. Every
PR based on current master has been failing CI with "Could NOT find
SQLite3" since #983 merged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reviving the still-relevant part of #785, closed 2026-09-10 purely to
clear a review backlog, not on merit. Investigated fresh against
current master -- one of the original PR's three targets turned out
to already be fixed independently: element_nomenclature_view's SQL
predicate for exclude_from_bom already does
"COALESCE(LOWER(TRIM(ei.exclude_from_bom)), '') NOT IN ('true', '1',
'yes', 'on')" (projectDataBase::createElementNomenclatureView()).
auto_num_locked and potential_isolating had no equivalent: five call
sites across terminal.cpp, terminalnumberingdialog.cpp and
elementinfowidget.cpp compared the raw stored string against the
literal "true" with QString::operator==, silently treating "True",
"TRUE", a trailing space, or any value written by something other
than this app's own checkbox as off -- with no error and no visible
difference from the checkbox being genuinely unticked.
Added QET::infoFlagIsTrue(), matching the same accepted spellings
("true"/"1"/"yes"/"on", case-insensitive, trimmed) the SQL predicate
already uses, and switched all five call sites to it.
Verified the exact comparison logic in isolation, outside any QET
build: 15 cases including "True", "TRUE", padded whitespace, "1",
"yes", "on", and their false counterparts -- all correctly
discriminated. Qt 6.10.2, ctest 13/13.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reviving #659, closed 2026-09-10 purely to clear a review backlog
(#630), not on merit. Rebuilt fresh against current master rather than
merged from the old branch (elementspanelwidget.cpp had drifted enough
that a textual merge risked silently losing content, as it did earlier
in this same session for a different revival). Builds discussion #607.
Cutting/copying a linked group of elements -- a relay coil with its
contacts, a PLC master with its slave I/O elements -- dropped the
master/slave link entirely. Traced end to end: Element::toXml() writes
each partner's uuid into <link_uuid>, Element::fromXml() reads it back
into a deferred, unresolved buffer (tmp_uuids_link), and the only code
that ever resolves that buffer is initLink(QETProject *) -- called
only from Diagram::refreshContents(), itself only called from full
project load and macro-block insertion. Neither DiagramView::paste()
nor ElementsPanelWidget::duplicateDiagram() ever call it, so
tmp_uuids_link is populated correctly and never resolved: the link is
silently dropped. duplicateDiagram() already knew this and worked
around it by calling clearPendingLinks() -- correct to not link back
to a stale source, but it meant folio duplication never preserved a
link either.
Added Element::initLink(const QList<Element *> &candidates) --
resolves against a caller-supplied list instead of a project-wide
search. The scoping is the subtle part: right after the XML round-trip
and before uuids are renewed, a pasted/duplicated element's
tmp_uuids_link still holds its source's original partner uuid, which
at that exact moment still equals the not-yet-renewed uuid of that
partner's own copy, if it was carried along in the same batch.
Resolving only within the batch is what stops a linked pair pasted
together from matching an original element left elsewhere that
happens to still carry that same soon-to-be-replaced uuid. If only one
half of a linked group is in the batch, its entry finds no match and
is dropped -- the same "leave it unlinked" outcome as before.
Wired into PasteDiagramCommand::redo(), before the existing newUuid()
loop and gated by the same first_redo flag. Wired into
duplicateDiagram() the same way, replacing its clearPendingLinks()
call (initLink() clears tmp_uuids_link internally, matched or not).
Verified live -- the original PR's own test plan left both of these
unchecked, so this closes that gap rather than repeating it. Built a
project with a linked PLC master/slave pair (qet-mcp's link_elements),
then drove the real interaction under Xvfb:
Ctrl+A, Ctrl+C, Ctrl+V:
originals 95ad58fc <-> e728632c (unchanged)
pasted 513e6bf8 <-> 29aa60b4 (linked to each other)
Right-click folio > "Copier et coller":
originals 95ad58fc <-> e728632c (unchanged)
duplicated 0a33ccb4 <-> 3264fe66 (linked to each other)
Neither copy links back to an original or comes in unlinked. Qt 6.10.2,
ctest 13/13.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reviving the still-relevant third of #682 (closed 2026-09-18 purely to
clear a review backlog, not on merit). Investigated fresh against
current master rather than merged wholesale -- two of the original
PR's three findings turned out to already be resolved independently:
- Element::valideXml() and Terminal::valideXml() already reject a
non-finite x/y (qIsFinite checks, with comments citing this exact
class of bug) -- added by someone else since #682 was written.
Verified live: a project with x="nan" on an element loads and
exports cleanly on current master, 3.1s, no hang.
- The illegal-XML-control-byte segfault in QDomDocument::setContent()
does not reproduce either. Tested both bytes from the original
report (0x00, 0x0E) against a real Qt6 build: both are now refused
cleanly (XmlParsingFailed, exit 0), no crash. Qt6's QDom parses
differently to the Qt5 one #682 was written and tested against.
What's still genuinely open: QET::attributeIsAReal() itself --
QString::toDouble()'s output parameter reports success for "nan"/
"inf"/"-inf", and this shared helper (26+ call sites across the
codebase, per #682's own count) had no finiteness check independent of
element.cpp/terminal.cpp's own since-added ones. Confirmed several
call sites are not behind either of those two gates -- notably
elementpicturefactory.cpp's line/rect/ellipse/circle/arc parsing for a
symbol's own drawing (a corrupted .elmt, not just a corrupted project
file), which was and remains reachable through this helper alone.
Qt 6.10.2, ctest 13/13.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reviving #713, closed 2026-09-21 purely to clear a maintainer review
backlog (#630), not on merit; not superseded. The crash #713 was
originally named after (bugtracker #291) was already fixed separately
by 39ac5716c, merged 14 Aug -- confirmed still on master. What's left,
and what this revives, is the one-line follow-up #713 itself narrowed
to after that: m_future.cancel() before the wait.
Without it, ~ElementsCollectionModel()'s wait runs the whole queued
QtConcurrent::map() to completion, so cancelling the open-element
dialog blocks until every remaining item has been processed -- a
visible hang on the button pressed precisely to stop the work.
cancel() drops the not-yet-started items so the wait is short, while
still waiting for whatever item is already in flight (needed so it
can't dereference this object after it's gone).
Qt 6.10.2, ctest 13/13. The responsiveness gain itself is reasoned
from QFuture's documented cancel()/waitForFinished() semantics rather
than timed -- same as the original PR's own stated verification.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reviving #664, closed 2026-09-18 purely to clear a maintainer review
backlog (#630), not on merit; not superseded. Rewritten fresh against
current master rather than merged from the old branch -- that branch
predates the Qt6-only switch and much of dataBase/projectdatabase.cpp's
later rewrite, and the two had diverged too far for a textual merge to
be trustworthy.
projectDataBase::removeElement() only ran DELETE FROM element WHERE
uuid=:uuid. It never touched element_info, even though every element
also has a row there (element_uuid is its PRIMARY KEY, with a FOREIGN
KEY back to element.uuid that isn't enforced by this connection -- no
ON DELETE CASCADE in effect). So deleting an element left its
element_info row orphaned.
Re-adding an element with that same uuid later -- undo of that same
deletion, or a redo replaying it -- goes through addElement(), which
INSERTs into both tables. The element insert succeeds (that row really
was removed). The element_info insert hits the orphaned row's primary
key and fails, silently: the error is logged and swallowed, so the
element re-enters the scene with no element_info row at all, and
nothing later re-syncs it.
removeDiagram() already cascades this cleanup when a whole folio is
removed (a later, unrelated addition) -- confirmed on current master --
but that path never runs for a single element removed on its own,
which is the case this fixes.
Verified on the built binary, not just read: placed an element, deleted
it (Ctrl+A, Delete), undid the deletion (Ctrl+Z). Reverting just this
fix and repeating the identical sequence reproduces the exact reported
error:
Debug: projectDataBase::addElement insert element info error :
QSqlError("1555", "Unable to fetch row",
"UNIQUE constraint failed: element_info.element_uuid")
With the fix, the same sequence produces nothing. Qt 6.10.2, ctest
13/13.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Split from #913's second suggestion. There was no shortcut for the common
"duplicate with offset" convention; the nearest existing feature,
"Collage multiple", is a different workflow (a dialog for repeating a
paste in a grid pattern, not a one-shot duplicate).
Ctrl+D copies the selection and places it immediately, offset by a
configured spacing and direction -- no interactive follow-the-cursor
step, unlike Ctrl+V. The first press (or after the setting is explicitly
reopened) shows DuplicateOffsetDialog: spacing in grid steps, direction
up/down/left/right. Every later press reuses whatever was confirmed then,
silently, so a row of copies is one key held down and tapped, not a
dialog every time -- unattended, repeatable stamping is the actual point
of a duplicate shortcut, which a dialog or an interactive placement step
on every press would defeat. A separate "Configurer la duplication..."
entry reopens the dialog on demand to change the setting later. Cancel
leaves the diagram untouched -- verified, not assumed: qet_diff against
the saved file shows 0 added.
Chaining ("keep tapping to lay out a row") needs no special handling:
QET already reselects whatever a paste just added
(PasteDiagramCommand::redo()), so the next Ctrl+D naturally continues
from the copy just placed rather than the original.
The offset is applied by hand rather than by asking paste()/fromXml() to
place the copy at a target position. Both of those feed the position
through Diagram::snapToGrid(), which reads
QApplication::keyboardModifiers() and rounds to the nearest PIXEL instead
of the grid whenever Ctrl is held -- and Ctrl is always held here, this
action's own shortcut being Ctrl+D. Measured before settling on this:
routing the offset through paste() first produced copies off-grid on both
axes, by an amount that tracked the selection's own bounding-box geometry
rather than being a fixed error -- caught by qet-mcp's qet_elements
against the saved file, not by eye. fromXml() is instead called with no
position argument at all (leaves every item at its source coordinates,
landing the copy on top of the originals -- (0,0) is not a position, this
is "keep the source coordinates"), and the offset is added directly with
setPos(). A plain addition cannot be off by a rounding rule that never
runs.
Conductors are not in the hand-translated set: fromXml() itself does not
reposition them either -- they load after elements are already in their
final place and take their geometry from their terminals, which have
already moved with the elements that own them. Verified this holds: drew
a conductor by hand between two elements (drag, not click-click),
selected both, Ctrl+D, and the new conductor correctly joins the two new
elements via qet_conductors -- not the originals, not a mix.
Verified end-to-end on a built binary via qet-mcp, not by eye:
before L2 (303,207) L9 (512,196) -- deliberately off-grid
spacing=2, down (303,227) (512,216) -- +0,+20 exactly
same again, 2nd (303,247) (512,236) -- +0,+20 again, chained
Both elements land exactly the configured offset from their immediate
source regardless of the selection's own alignment. Qt 6.10.2, ctest
11/11.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A property value that is entirely whitespace -- reported in #973 as a
workaround (setting a title-block custom variable to a single space, the
only way to give it a value other than blank before that bug was fixed in
#989) -- did not survive a save/reload cycle. Two independent causes, both
needed for the round trip to actually work:
1. DiagramContext::toXml() called .trimmed() on every stored value before
writing it, unconditionally. For ordinary content this only strips
accidental leading/trailing whitespace, but for a value that IS
whitespace it collapses the entire thing to "", indistinguishable from
a value that was never set.
2. QDomDocument::setContent(), used to parse the project file, discards a
text node that is entirely whitespace by default. Confirmed in
isolation, outside any QET code: parsing "<a> </a>" with the default
ParseOptions gives QDomElement::text() == ""; adding
ParseOption::PreserveSpacingOnlyNodes gives " ". So even once (1) stops
destroying the value on save, the very next load throws it away again.
Fix (1) only trims when the trimmed result isn't empty, i.e. leaves an
all-whitespace value untouched. Fix (2) adds PreserveSpacingOnlyNodes to
the one setContent() call that parses a project file
(QETProject::readProjectXml()) -- not the other ~19 call sites in the
codebase (clipboard paste, element/macro loading, translations, autonum
context), which read different, narrower documents and are not implicated
in this report.
Blast radius of (2): every place that walks a QDomNode's children already
filters on isElement() (see QET::findInDomElement()), so the extra
whitespace-only text-node siblings this keeps around are inert wherever
current code already expected only elements. The one place it isn't inert
is exactly the bug -- calling .text() on an element whose entire content
is whitespace.
Verified end-to-end, not just at one stage: a single-space title-block
variable now survives two successive --resave cycles unchanged (confirmed
byte-for-byte in the saved XML), and renders as blank space rather than
literal placeholder text or a vanished value. Re-saved all 24 shipped
examples with and without this change and diffed: 23 byte-identical, the
one that differs (schema_indus.qet) differs only in element uuids -- and
resaving it twice with the SAME unpatched binary produces that same kind
of diff, confirming it is pre-existing non-determinism in files that
predate persisted uuids, unrelated to this change. Qt 6.10.2, ctest 11/11.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
BorderTitleBlock::updateDiagramContextForTitleBlock() skips merging a
page-level custom variable into the title block's render context whenever
its value is empty -- added by PR #572 to fix#531, where an empty
page-level value was shadowing a real project-level one of the same name.
But skipping the merge removes the key from the context entirely, and
TitleBlockTemplate::interpreteVariables() only replaces "%name"/"%{name}"
when "name" is an actual key in that context -- anything absent is left as
its own literal placeholder text. Folio Properties auto-adds every one of
a template's custom variables to the Custom tab with an empty value (#271/
#495) precisely so the user only has to fill in what's missing; until they
do, that variable now renders as e.g. "%label1" instead of blank.
Reproduced two ways: a synthetic fixture, and examples/2612_ats_singlephase.qet
itself, which already carries three such auto-added-but-unset properties
("label1", "label2", "label3") and renders all three literally on current
master.
Fix: skip the empty page-level value only when a project-level one already
exists to show through (preserving #531's guarantee); otherwise still merge
it in empty, so the placeholder resolves to blank rather than falling out
of the context altogether.
Verified against the shipped example (--export-png, before/after crop of
the rendered title block): "%label1"/"%label2"/"%label3" now blank. A
variable never added to the Custom tab at all ("%client", also present in
the same example) is unaffected -- nothing was ever configured for it, and
that is a separate, narrower case. Qt 6.10.2, ctest 11/11.
A related but distinct issue -- DiagramContext::toXml() trims a stored
value before saving, so an all-whitespace value is written as empty --
explains a second symptom from the same report (a single-space "workaround"
value vanishing after the project is reopened) but touches every
context-backed property, not just title blocks, and is left for a separate
fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Use a bound VACUUM INTO path and remove the stale SQLite
handle declaration. Fix shell continuations in Windows CI and
Debian installation instructions.
#984 switches JavaScript scripting off by default, and five tools here
drive QElectroTech through --run: qet_query, qet_continuity, qet_check,
qet_project_new, qet_edit. Against such a build they all stop working, and
what came back was exit code 3 and a paragraph of French naming a settings
dialog nobody driving an MCP server is looking at.
Nothing needed building to make them work again -- _run_qet() inherits its
environment, so QET_ENABLE_SCRIPTING=1 in the "env" block of the client's
own configuration already reaches QElectroTech. Verified both ways against
a #984 binary: without it qet_query returns ok=false exit=3, with it
ok=true and the rows.
So this is about saying so. The refusal is now recognised and answered with
an instruction the caller can act on, keyed on QElectroTech naming the
variable with exit 3 as a fallback for a future build that words it
differently. Two older hints fitted the same symptom and were overwriting
it -- "the binary never ran the script ... is it a build with --run
support?" sends the reader to check the one thing that is fine -- so both
now yield to whatever the launch already reported. qet_check builds its
answer fresh rather than layering onto the launch result, so it carries the
reason across explicitly; without that every check read "no result came
back", which is true and tells nobody why.
The server does not set the variable itself, on purpose. A switch a program
turns on for itself is not a switch: whoever configured this server and
pointed it at a QElectroTech binary made that choice, and their interactive
QElectroTech keeps whatever its own setting says. README says this, and the
registration example now shows the env block with both variables in it.
Six tests, faking subprocess.run so they cost no launch. Two are structural
rather than behavioural: one fails if either older hint goes back to
assigning over the specific one, the other reads which tools actually pass
script= to _run_qet and fails if the hint's list of them drifts. Both were
mutation-checked by reintroducing exactly those mistakes.
176 tests pass with QET_BINARY, QET_ELEMENTS, QET_EXAMPLES and
QET_ENABLE_SCRIPTING set.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A <graphics_table>'s <query> is stored in the .qet and executed when the
project loads. SQLite produces rows lazily, so the cost of that query is
not bounded by anything the project contains -- it is bounded by how long
the loop reading the rows is willing to run. A recursive CTE takes one line
to make that forever:
WITH RECURSIVE c(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM c) SELECT n ...
Put that in the <query> of any project's summary table and opening the file
pins a core at 100% and grows ProjectDBModel::m_record until memory runs
out. Measured on examples/industrial.qet with the query swapped, built from
master:
clean --export-bom 3.6 s, 396 rows, exit 0
poisoned --export-bom killed at 90 s, still going, no output
No scripting, no MCP, no flag beyond an ordinary export. Opening the file in
the editor is the same code path.
QetScriptApi::query() has the identical loop, and the script engine's own
30 s interrupt does not reach it: that aborts JavaScript, and this is C++
inside a single call. Left alone it hung a --run for 45 s until the harness
killed it.
Both loops now stop at projectDataBase::MaxResultRows (100000) and say so.
That is a backstop, not a page size: the largest table in the shipped
examples is 396 rows, and a caller that reaches 100000 has been handed
something it should not run to completion. It is not silent either way --
the model logs the offending query text, and qet.query() sets queryError(),
so a truncated result is never mistaken for a complete one.
clean --export-bom 3.6 s, 396 rows, exit 0 (unchanged)
poisoned --export-bom 20.2 s, 396 rows, exit 0, warning names the query
qet.query(recursive CTE) 3.8 s, 100000 rows, queryError() set
Reverting each cap restores the hang, so both checks discriminate.
Related to #983, which fixes a different flaw reachable through the same
stored query. Neither depends on the other.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A script reaches the whole project and, through the export calls, the
filesystem. That is a capability most people installing an electrical CAD
program never asked for, and leaving it on by default hands it to them
anyway. So QET_HAS_SCRIPTING builds now ship with it switched off.
QetSettings::scriptingEnabled() is the single answer, read by all three
places that need it, with QET_ENABLE_SCRIPTING=1 overriding the stored
value. The override is not decoration: a CI job or a batch run has no
dialog to tick, and a machine whose HOME is created fresh for each run has
nowhere to keep the setting either. It beats a stored "false" on purpose,
so a box unticked once cannot lock a build server out of --run for good.
Only the exact value "1" counts.
--run refuses with exit 3 and a message naming both ways in.
Projet > Exécuter un script... asks once, and turns the setting on if
the answer is yes. Asking beats grey: a disabled menu
entry says something exists and nothing about how to have
it, and this is the pattern people already know from
macro security in office software.
Configurer QElectroTech > Général > Projets has the checkbox, for
turning it back off. While the environment forces
scripting on, the box is disabled and says why, and
applyConf() then leaves the stored value alone rather
than quietly overwriting it.
runOnProject() checks as well, after both callers have. It is the one
function that actually evaluates JavaScript, so it is the one place a
future caller cannot forget to ask; the callers check first only to give a
better answer than it can.
Verified on the built binary, all four states, with an isolated HOME:
stored env result
absent - refused, exit 3
true - script runs, exit 0
false - refused, exit 3
false 1 script runs, exit 0
tst_scriptingsetting covers the same matrix hermetically, in its own
QSettings scope, and was mutation-checked: flipping the default to true
turns defaultsToOff() red.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two security reviews of #980 landed on the same gap: every path in a tool
call is chosen by the model, and nothing checked where those paths pointed.
That made the server a read/write primitive for anything the process could
reach -- read any project on the disk, export one somewhere else, overwrite
an unrelated file, embed an arbitrary local image or PDF. The sandboxed HOME
each QElectroTech launch gets isolates settings, not the filesystem.
Data paths are now confined to a workspace: QET_MCP_WORKSPACE (os.pathsep
separated), defaulting to the directory the server was started in, which is
what an MCP host normally sets anyway. QET_MCP_ALLOW_ANY_PATH=1 turns the
check off; it exists so that is a visible choice rather than the default.
Paths are resolved before comparison, so a symlink planted inside the
workspace is judged by where it points -- the case a string-prefix check
gets wrong.
Two arguments are deliberately exempt: "binary" and "elements_dir". Those
are configuration, chosen once by whoever runs the server, and both normally
live in /usr or a build tree. Confining them would reject the ordinary case
while stopping nothing -- they are not where a model gets to point the
server at /etc.
Enforcement sits at the dispatcher, where model-supplied arguments enter,
not inside each tool. Importing the module and calling tool_export() from
Python stays unconfined and is meant to: that is the caller's own code with
the caller's own paths.
Separately, an existing "output" is now refused unless the call passes
"overwrite": true. qet_project_new already worked this way; qet_export,
qet_edit and qet_element_build now match it. Replacing a file is the one
step this server cannot undo.
17 tests cover it, including the symlink escape, the traversal, the
overwrite gate and the operation-level file paths that add_image and
add_pdf_page carry one level down. Two of them compare the policy table
against the tool schemas, because a write tool missing from either list
fails silently in opposite directions. Two more drive a real server process
over stdio, which is the only thing that shows a call is gated rather than
merely gate-able. Mutation-checked: removing the confinement fails 8, and
desynchronising the two lists fails the drift pair.
170 tests pass, with QET_BINARY, QET_ELEMENTS and QET_EXAMPLES set so none
are skipped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
projectDataBase::isReadOnlySelect() decides whether a query only reads
by looking at its first keyword and rejecting internal semicolons.
SQLite has allowed a CTE prefix in front of a data-modifying statement
since 3.8.3, so
WITH x AS (SELECT 1) DELETE FROM element
begins with WITH, contains no semicolon, passes the check, and deletes
every row. UPDATE and INSERT go through the same way.
This is not only reachable from the custom-query box. ProjectDBModel::
fromXml() reads a <graphics_table>'s saved <query> straight out of the
.qet and fillValue() executes it, so a project file can carry the
statement. Reproduced against a build of this branch's parent, with no
scripting and no CLI flag beyond the export itself: a project whose
stored table query was replaced with the DELETE above exported a bill
of materials of 0 rows instead of 14, exit code 0, nothing logged. A
silently empty or -- with UPDATE -- silently altered BOM is the kind of
output someone orders parts from.
Fixed by asking SQLite about the statement it actually compiled.
sqlite3_prepare_v2() compiles without running, sqlite3_stmt_readonly()
reports on the compiled statement rather than on how it was spelled,
and the prepare tail catches a second statement structurally. The same
project now exports its 14 rows again and logs a reason for the
refusal, while an ordinary WITH ... SELECT in a project file still runs
untouched -- the fix is not "ban CTEs".
isReadOnlySelect() stays in front of it rather than being replaced:
SQLite considers ATTACH, BEGIN and several PRAGMAs read-only too, since
none of them change the contents of the database, so dropping the
statement-type allowlist would have widened what is accepted while
fixing what is executed.
The check lives in its own translation unit depending on nothing but
QString and SQLite, so tests/qttest/tst_sqlreadonly.cpp can link it
alone and exercise the security property without standing up a
QETProject: 18 assertions covering the three CTE-prefixed writes named
in the review, bare writes, trailing statements, comment-only input
(which compiles to a null statement sqlite3_stmt_readonly() must not be
handed) and a null connection (refused, not waved through). Confirmed
the suite discriminates by deliberately disabling the new check and
watching exactly the nine write-refusal assertions go red while the
accept cases stayed green.
ctest 13/13, qet-coherence-check and qet-pdflink-check clean on the
example corpus.
Reported in PR #980's review thread by @elevatormind and confirmed
against this code by @scorpio810; fixed here on its own because the
flaw is in already-released code and needs none of that branch to
reach.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two issues on the interactive paste path:
1. Stall: DiagramEventAddPaste's constructor called Diagram::fromXml()
with no database batching, so every addItem() emitted dataBaseUpdated()
and each connected table model re-ran its full SQL query. A typical
paste (~40 elements + ~40 conductors) triggered ~77 rebuilds of the
table models -- measured at ~2.1 s of pure fromXml time on a large
project. Project loading already batches this via
setUpdateBlocked()/blockSignals() (QETProject::readProjectXml); the
paste path now does the same: block during fromXml, one updateDB()
after. Measured fromXml: 2114 ms -> 143 ms.
2. Cursor jump: m_initial_cursor was set to the group origin but the
physical cursor stayed at the Ctrl+V press location, so the first
mouseMoveEvent computed a large delta and the items jumped on first
touch. Warp the cursor to the group origin after placement so the
baseline and the actual cursor position match.
Commit dd0c194a3 (#913) moved the pasted group to the cursor position
at construction time. The desired behaviour is that items appear at
their original XML coordinates (where they were copied from) so the
user starts from the origin. The grid-snapped movement baseline and
the context-menu restoration from that commit are kept.
LinkElementCommand::redo() already had a check meant to catch exactly
this -- two report-linked conductors whose properties disagree -- and
ask the user which to keep via PotentialSelectorDialog. It never
worked: it built ONE combined list from three unrelated fields
(tension_protocol, wire_color, wire_section) and tested that whole
list for string equality, so a tension-protocol value could never
equal a wire-colour value even when every field individually matched
across every conductor. Worse, "wire_color"/"wire_section" are
ConductorProperties::m_wire_color/m_wire_section, a separate free-text
documentation pair that says nothing about how the wire is actually
drawn -- that's "color"/"style" -- so the one field #974 is actually
about was never compared at all.
Fixed by comparing each relevant field (text/num, function, tension
protocol, colour, line style) separately. Downloaded the reporter's
actual project, confirmed the mismatched wire reads color="#0000ff" on
one side of a "Folio suivant"/"Folio precedent" link and
color="#55aa00" on the other, with the link's other four conductors
matching correctly (ruling out a rendering artifact) -- see PR #980's
checkContinuity() extension, which now flags this class of mismatch on
sight.
Extracted the comparison into its own static
reportLinkNeedsPotentialChoice(), for the same reason
ConductorCreator::needsPotentialChoice() already exists as its own
method: a caller with nobody there to answer a modal dialog needs to
check first and decline, and the condition must not drift away from
the one redo() actually applies.
Fixing the comparison surfaced a real, previously-latent hang in this
session's own qet.linkElements(): PotentialSelectorDialog::exec() is a
plain QDialog::exec(), not routed through QET::QetMessageBox, so
headless --run has nobody to answer it. Measured directly -- hung
until killed with the property-comparison fix alone, clean refusal
after adding the guard. linkElements() now calls
reportLinkNeedsPotentialChoice() before constructing the command and
declines with a clear reason, the same choice addConductor() already
makes about ConductorCreator's own equivalent dialog.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
link_elements() now correctly refuses a mismatched report link instead
of allowing it (see the two preceding commits), so the
report_link_mismatch reproduction can no longer be built by linking
two already-differently-coloured conductors -- that path is refused
before it happens. Updated to patch a saved file's XML directly
instead (the same technique test_continuity_detects_a_tampered_
potential already uses), since a file QElectroTech's own edits produce
can no longer end up in this state at all. Adds
test_link_elements_refuses_a_mismatched_report_link to cover the
refusal itself.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixing LinkElementCommand's property comparison (previous commit)
means it now correctly detects a report-link colour/style mismatch --
which means it now correctly pops PotentialSelectorDialog for one,
same as the GUI. Under headless --run there is nobody to answer a
plain QDialog::exec(), so this hangs forever; confirmed directly with
a timeout before adding this guard.
qet.linkElements() now checks LinkElementCommand::
reportLinkNeedsPotentialChoice() before constructing the command and
declines with a clear reason pointing at checkContinuity() and
setConductorProperty(), the same choice addConductor() already makes
about ConductorCreator's own equivalent ambiguous-potential dialog.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
LinkElementCommand::redo() already had a check meant to catch exactly
this -- two report-linked conductors whose properties disagree -- and
ask the user which to keep via PotentialSelectorDialog. It never
worked: it built ONE combined list from three unrelated fields
(tension_protocol, wire_color, wire_section) and tested that whole
list for string equality, so a tension-protocol value could never
equal a wire-colour value even when every field individually matched
across every conductor. Worse, "wire_color"/"wire_section" are
ConductorProperties::m_wire_color/m_wire_section, a separate free-text
documentation pair that says nothing about how the wire is actually
drawn -- that's "color"/"style" -- so the one field #974 is actually
about was never compared at all.
Fixed by comparing each relevant field (text/num, function, tension
protocol, colour, line style) separately. Downloaded the reporter's
actual project, confirmed the mismatched wire reads color="#0000ff" on
one side of a "Folio suivant"/"Folio precedent" link and
color="#55aa00" on the other, with the link's other four conductors
matching correctly (ruling out a rendering artifact) -- see PR #980's
checkContinuity() extension, which now flags this class of mismatch on
sight.
Extracted the comparison into its own static
reportLinkNeedsPotentialChoice(), for the same reason
ConductorCreator::needsPotentialChoice() already exists as its own
method: a caller with nobody there to answer a modal dialog needs to
check first and decline, and the condition must not drift away from
the one redo() actually applies.
Fixing the comparison surfaced a real, previously-latent hang in this
session's own qet.linkElements(): PotentialSelectorDialog::exec() is a
plain QDialog::exec(), not routed through QET::QetMessageBox, so
headless --run has nobody to answer it. Measured directly -- hung
until killed with the property-comparison fix alone, clean refusal
after adding the guard. linkElements() now calls
reportLinkNeedsPotentialChoice() before constructing the command and
declines with a clear reason, the same choice addConductor() already
makes about ConductorCreator's own equivalent dialog.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Brings misc/qet-mcp up to date with checkContinuity()'s new
report_link_mismatch finding: updated qet_continuity's description,
and two new tests reproducing #974 with the shipped 02going_arrow.elmt/
01coming_arrow.elmt pair (no custom fixtures needed -- this element
type already ships).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New report_link_mismatch finding (severity "warning", not "error" the
way potential_mismatch is): a next_report/previous_report folio-jump
pair whose conductors disagree on colour, style, num, or any of the
other checked properties. Unlike potential_mismatch, this one is not
proof of external tampering -- LinkElementCommand::isLinkable() only
ever checks type and freedom (see its own doc comment), never conductor
properties, so nothing in QElectroTech copies one side's colour onto
the other when a report link is made or keeps them in sync afterwards.
This is a real, unenforced gap reachable through completely ordinary
use, not a defect a script or the GUI could introduce.
Reproduces qelectrotech/qelectrotech-source-mirror#974 exactly:
downloaded the reporter's actual project, traced the mismatched wire to
a "Folio suivant"/"Folio precedent" link pair, and confirmed via query
that the two sides read color="#0000ff" and color="#55aa00" while the
link's other four conductors (0V/Low/High/Ground) matched -- ruling out
a rendering artifact. Verified fresh with a synthetic reproduction
(tests in misc/qet-mcp) using the shipped 02going_arrow.elmt/
01coming_arrow.elmt pair, giving exactly one finding, not one per
folio-link conductor.
Also fixes a real gap in the existing potential_mismatch check while
here: checked_properties was missing "color" and "style" entirely,
checking only "conductor_color" (ConductorProperties::m_wire_color, a
separate free-text documentation field, typically empty) -- meaning
the same-folio version of this exact bug class would have gone
undetected too.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Brings misc/qet-mcp up to date with the scripting API additions in
this branch: qet_edit gains ops for tables, PLC master IO tables and
PLC-slave linking, manual conductor segment routing, polygon and path
shapes, PDF page import, and project-wide search & replace; a new
qet_continuity tool exposes the electrical continuity/ERC-style checks.
Adds the test suite that did not exist here before (150 tests: unit
validation, JSON-RPC protocol, and Integration/PlcIntegration/
CorpusIntegration runs against a built binary) plus the two minimal
PLC fixture .elmt files it needs (no shipped element has masterType/
slaveType "plc" to test against).
Also removes a __pycache__/*.pyc that had been committed by mistake,
and ignores __pycache__/*.pyc going forward.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
qet.checkContinuity(folioIndex) runs two structural checks against the
live Terminal/Conductor object graph -- Terminal::conductors() and
Conductor::relatedPotentialConductors(), the same primitive
setConductorProperty() already uses -- rather than a heuristic read of
the saved XML:
- unconnected_terminal (info): a terminal with no conductor at all.
Deliberately low severity -- routine (a spare relay contact, an
unused optional pin), not necessarily a mistake.
- potential_mismatch (error): two conductors electrically on the same
potential (following bridged terminal strips and linked report
elements, matching setConductorProperty()'s own scope) disagreeing
on num/conductor_color/conductor_section/function/bus/cable.
QElectroTech's own edits always keep every member of a potential
identical, so any divergence found here came from hand-edited XML,
a legacy file, or an external tool -- verified with a test that
patches a saved file's XML directly to introduce exactly that.
Documented plainly what this does NOT check and why: pin electrical
direction/power conflicts and No/Nc/Common contact shorts, since
QElectroTech's terminal data model (Generic/Inner/Outer/No/Nc/Common --
contact role within one relay, not signal direction) does not carry
the information either would need. This is continuity/consistency
checking, not full ERC.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
qet.searchAndReplace(kind, field, pattern, replacement, useRegex,
caseSensitive) finds and replaces a substring or regular expression
within one text field across every folio, as a single undo step --
kind is element_info, conductor or text. Unlike a script loop over
elementInfo()/setElementInfo() (or the conductor/text equivalents)
doing the same thing one item at a time, each pushing its own undo
entry, this wraps the whole run in one macro.
This is deliberately NOT a wrapper around QET's own "Search and
replace" panel (SearchAndReplaceWorker): that one is a batch
overwrite-with-sentinel template built for picking items from an
interactive tree, a poor fit for a script that can already say
precisely which items it means. This does what the name plainly says
instead -- an actual substring/regex replace within each item's
current value.
Found and fixed while writing the first conductor-kind test: a hub
topology (several conductors sharing one terminal, e.g. a star wiring)
made the conductor branch pick terminal1 unconditionally to address a
Conductor object through setConductorProperty() -- for a hub member
conductor, terminal1 is the shared, ambiguous hub terminal itself
(findConductor() correctly refuses to address a conductor through a
terminal carrying more than one), so every conductor touching that hub
silently failed to update, returning a changed count of 0 with no error
for a genuinely matching project. Fixed by preferring whichever of
terminal1/terminal2 carries exactly that one conductor.
Also caught during testing: an empty macro (a run that matches
nothing) still gets pushed onto the undo stack by QUndoStack::endMacro()
-- it is not silently discarded the way the earlier revision assumed --
leaving a confusing no-op "Rechercher et remplacer" undo entry. Fixed
by counting matches in a dry run first and never touching the undo
stack at all when that count is zero.
textContent() is a new small getter alongside the existing
setTextContent(), filling a gap this needed (reading an independent
text's current content) that is independently useful too.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
qet.addPdfPage() renders one page of a PDF file to an image and
places it, through the same QPdfDocument::render() call, white-
background compositing (a transparent page would otherwise show
whatever is under it, unlike every other placed image) and
DiagramImageItem/AddGraphicsObjectCommand underneath as the "add PDF"
toolbar action's own file/page-selection dialogs.
Only reachable in a build with the QtPdf module (Qt >= 6.4) -- some
Qt6 distributions omit it entirely (see diagrameventaddpdf.h). The
method is still always declared and compiled, guarded internally
instead of with the class itself: a script asking whether qet.addPdfPage
exists must never get "not a function" for a reason it has no way to
discover. Refuses with a clear reason when the module is missing, the
page number is out of range, the file cannot be loaded, or the
resulting render is degenerate.
Verified against a real 2-page PDF (this session installed qt6-pdf-dev,
which was missing here) that the two rendered pages differ in content
and that dpi scales the rendered pixel size linearly, not just that the
call returns something.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
addShape()'s own "polygon" only ever produces the degenerate two-point
form -- it shares addShape()'s p1/p2 constructor and nothing else.
addPolygon()/setShapePolygon() take an arbitrary point list through
QetShapeItem's public setPolygon(), pushed via the existing "polygon"
Q_PROPERTY the same way a point-handle drag would.
addPath()/setShapePathNodes() add the Path shape type: a polygon's
points plus, per node, a kind (corner/smooth/symmetric) and optional
bezier in/out handles, the same model the pen tool and node-edit mode
build. PathNode holds std::optional<QPointF> members and isn't
Q_PROPERTY-friendly, so setShapePathNodes() reuses PromoteShapeCommand's
before/after XML snapshot mechanism instead -- the same fallback
QetShapeItem::associatedUndoCommand() already uses for the identical
reason on a PathAnchor/PathControlIn/PathControlOut handle drag.
setShapeClosed() opens or closes a polygon or path through the existing
"close" Q_PROPERTY. shapePolygon()/shapePathNodes() read a shape's
current geometry back in scene coordinates, refusing (empty) on the
wrong shape type.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Conductor::moveSegment(index, dx, dy) is the same primitive
handlerMouseMoveEvent()/handlerMouseReleaseEvent() apply on a drag --
move both axes on the target segment (each of ConductorSegment's
moveX()/moveY() silently no-ops on the wrong axis or a static,
terminal-anchored segment), recompute the path, and push one
ChangeConductorCommand undo step via the existing saveProfile().
Caught while writing the first test for it: moveSegment() never set
modified_path, so Conductor::toXml() skipped writing <segment> children
and a manually rerouted conductor silently reverted to auto-routing on
the very next save -- the change took effect in the running scene but
never reached disk. Fixed by setting the flag, the same as every other
path-modifying call site already does.
qet.conductorSegments() lists a conductor's segments (endpoints in
scene coordinates, orientation, static/movable) so a script can find
the index it wants; qet.moveConductorSegment() applies the move and
refuses a static segment or an out-of-range index.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
addPlcIO/setPlcIO/removePlcIO edit a PLC master's IO table (type,
address, function text, comment) directly through setElementData(),
the same as MasterPropertiesWidget's own PLC IO editor -- and, like it,
these are not undoable: MasterPropertiesWidget::associatedUndo()
deliberately returns nullptr for PLC masters, since their linking is
managed through the IO table rather than the link-tree widget it would
otherwise build an unlink-all command from.
linkElements() gains an optional groupIndex so a PLC slave can be
linked onto one specific IO row instead of leaving the row
unspecified. LinkElementCommand only reads the group index it is given
when the command's own element is the Slave -- when built from the
Master side (a=master, b=slave, the usual call shape) it looks in a
per-slave map this call never populates, and setGroupIndex() is
silently a no-op. Fixed by building the command from whichever of the
two elements is actually the Slave, matching what PlcLinkWidget does.
elementLinkGroupIndex() reads the result back.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
QetGraphicsTableFactory::create() only reads settings already set on an
AddTableDialog and never depended on the dialog being shown, so make it
public alongside setTableName()/setAdjustTableToFolio()/
setAddNewTableToNewDiagram() on AddTableDialog -- this lets the scripting
API build and configure a dialog headlessly instead of exec()'ing one.
qet.addTable() requires a non-empty query: ElementQueryWidget and
SummaryQueryWidget both default to zero selected columns, so an empty
query silently produced a table with no rows rather than a sensible
default. qet.tables()/deleteTable() list and remove by a position-sorted
index. qet.setTablePosition() repositions one, since newTable() always
places a new table at a fixed (50, 50) and a folio with more than one
needs to move all but the first itself.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
stripRealTerminals(strip) index, owning element, current
physical position, neighbours
groupTerminals(strip, indices) merge onto one physical position
bridgeTerminals(strip, indices) wire together without merging
sortTerminalStrip(strip) canonical physical order
Each goes through the same command the terminal strip editor's own
group/bridge/sort buttons push (GroupTerminalsCommand,
BridgeTerminalsCommand, SortTerminalStripCommand), so a script's changes
undo like the editor's.
groupTerminals() replicates the editor's own receiver-selection heuristic
line for line rather than picking the first terminal named: the physical
position that already carries the most real terminals receives the
others, not necessarily the one at index 0. Verified with a case built to
distinguish the two: three terminals grouped first (one position, three
real terminals), then a fourth, previously-alone terminal grouped with
one of those three, named first in the call -- the alone terminal moved
onto the three-terminal position, ending at four, not the other way
around.
bridgeTerminals() refuses through TerminalStrip::isBridgeable() itself,
the same check the editor's bridge button applies, rather than
re-deriving what "the same level" means. Real terminals are addressed by
index into stripRealTerminals(), the strip's own order; grouping shifts
later physical-position indices down, so the header says to re-list
after a change that adds or removes one, the same rule already
documented for texts, shapes and images.
Verified end to end on four placed terminal elements: added to a strip,
grouped two, refused a group of one and an out-of-range index, bridged
the remaining two, sorted, undo restoring order without disturbing the
grouping (sort doesn't touch it, so it shouldn't), and a bad strip index
refused on all three operations. Qt 6.10.2, build clean, ctest 12/12,
coherence gate clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
titleBlockTemplates() embedded + common/company/custom, by name
embedTitleBlockTemplate(name) copy one into the project's own collection
setFolioProperty(f,"template",name) embed-if-needed, then apply
folioProperty(f,"template")
Not the trivial addition to the existing title-block-field list it looked
like at first. Diagram::setTitleBlockTemplate() resolves a name only
against QETProject::embeddedTitleBlockTemplatesCollection() -- the exact
same copy-into-the-project step addElement() already goes through for
elements, and for the same reason: a project opened on another machine
must not depend on files only this one has. embedTitleBlockTemplate()
does that copy through get/setTemplateXmlDescription(), the same round
trip the template editor itself uses to save one -- not scripting-specific
code, and unlike defining an auto-numbering context, not undoable, for the
same reason that isn't: the application does both through direct
collection/project calls with no undo command of their own.
Two things found only by testing, not by reading:
- "default" is a real template name in the common collection, and setting
a folio's template to it is legitimate -- but
BorderTitleBlock::titleBlockTemplateName() normalises a template
literally named "default" back to "", indistinguishable from no
override, since that is genuinely what "no override" renders with. The
first version compared the raw name and reported success as failure;
fixed by comparing against that same normalised form, which folioProperty()
now also documents.
- QElectroTech resolves the common template collection from a compiled-in
path (here, an absolute /usr/share/qelectrotech/titleblocks, not
relative to the binary), and --common-tbt-dir, the CLI override, is
read by QETApp::parseArguments() -- which the --run headless path never
reaches, confirmed by the CLI itself swallowing the flag as a stray
positional argument. There is no QSettings fallback the way
commonElementsDir() has. So testing this at all needed the path to
genuinely exist; no environment trick from inside the process reaches
it.
Verified: 10 common templates listed; DIN_A4 embedded and applied,
folioProperty reading it back; re-applying the same name a no-op success;
an unknown name refused; "default" applied and correctly read back as ""
per the note above; both folios exported to PNG and visually compared --
plain default rendering vs. DIN_A4's logo, revision table and field
layout, genuinely different, not just an API call returning true. The
choice survives a save and reload. Qt 6.10.2, ctest 12/12, coherence gate
clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
elementGeometry(folio, uuid) x, y, rotation, left, top, right, bottom
insertFolio(position)
The API could set an element's position but never read it, so a script
could not lay one thing out relative to another, or check that a move had
landed; verification had to go through the saved file. elementGeometry
returns the origin (what setElementPosition sets), the rotation, and the
box the element occupies on the folio -- its drawn extent, which sits at
the element's hotspot from the origin and, once rotated, is the rotated
extent.
Measured on a coil whose hotspot is (17, 32): placed at (200, 300) the box
is 183..223 by 268..328, exactly that far from the origin; a move of
(+50, -20) shifts both together; a 90 degree turn swaps the box to 60 by
40 about an unchanged origin and 180 turns it back; and the origin agrees
with the saved file (x=250 y=280, orientation 2 for 180 degrees).
insertFolio puts a folio at a position (0 first, folioCount() last) through
QETProject::addNewDiagram(pos), undoable. The position is checked in the
binding: QETProject::addDiagram() hands it straight to QList::insert(),
which is undefined past the end, so -1 and anything above the count are
refused with a reason. Verified: first, middle and last insertions land in
the right order, and undo and redo of an insertion restore the order.
Folio reordering itself still needs the application's project view.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
setProjectTitle(title)
folioBorder(folio, prop) setFolioBorder(folio, prop, value)
The frame is the grid of columns and rows around a folio: columns,
column-width, display-columns, rows, row-height, display-rows -- the six
fields the folio properties panel offers -- through ChangeBorderCommand,
so it undoes like a hand edit. The title block's header sizes, which the
panel does not offer, are left alone. Changing the project title is not
undoable, because the application sets it directly too.
Counts are 1 to 99 and sizes 1 to 1000. The panel's upper limits are the
same; its lower limit is 0, which is not offered: a grid with no columns
has no use here and 0 was not tested, so it is refused rather than
assumed safe. The extremes that are offered (99 x 99 cells, widths from 1
to 1000) were exported to PNG without a hang or crash. Fractions, out of
range values, an unknown property and a bad folio are refused with a
reason.
Verified: ten columns of 40 and four rows of 100, undo of the last change,
and the folio and the renamed project read back after a reload. My first
reload check read the wrong folio and briefly looked like the border was
not persisted; the file itself had the right values.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
duplicateElements(fromFolio, [uuids], toFolio, x, y)
selectedElements(folio)
What Ctrl+C and Ctrl+V do: the named elements are selected, serialised
with Diagram::toXml(false, true), the previous selection is put back, and
the copy is pasted with Diagram::fromXml() and a single
PasteDiagramCommand. A conductor is copied only if both its ends are among
the copied elements. As on a paste in the application, the copies come
without labels or their conductors' wire numbers (measured: empty on
both). One undo removes the elements and the conductor together.
Three properties found by measuring rather than assuming:
- Position is the top left of the pasted group's bounding rectangle, so an
element's own origin ends up offset by its hotspot: +20, +30 for a coil,
identical across two trials. (0, 0) is not a position; Diagram::fromXml
treats the origin as "keep the source coordinates".
- The application's pasted list is in scene order, not the order the
elements were named. Asking for the elements at x = 700, 100, 900 returned
the copies of 100, 700, 900, so a caller pairing copies with sources by
index was wired to the wrong ones with no error. A paste is a pure
translation, so sorting sources and copies by position pairs them, and
the result is returned in the order asked. Checked with a scrambled
request over a zig-zag layout: every copy is the identical translation
(-30, -20) from the source at its index. Two elements at one point cannot
be told apart; if the counts disagree it says so and returns the
unpaired list rather than guess.
- Copying works by selecting, so the previous selection is given back;
selectedElements() exists to make that checkable.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
useElementAutoNum(name) select the current element numbering context
numberElement(folio, el) give one element its label from it
numberElement calls Element::setUpFormula(), the call the "add element"
tool makes right after placing an element, so a script gets the same
numbering: three coils numbered in turn are K1, K2, K3, and the counter
persists in the project (after a reload the next element is K3).
It is a separate call rather than a change to addElement(), which is
merged code: numbering what it places would change what an existing
script produces the moment its project happens to have a context selected.
A slave or a report is refused, since it takes its label from its master,
and so is a project with no context selected, instead of reporting a
success that did nothing.
setUpFormula() has a hazard for an element that is already placed. It
writes the label straight into the element's information and pushes only
the counter's advance onto the undo stack. Placing a new element hides
that, because undoing the placement removes the element; for an existing
one, a single undo rolled the counter back and left the label, so c3 stayed
"K3" while the counter went back to expecting K3 and the next numbering
would repeat a label it had forgotten. So the label it computed is taken,
the information put back, and the change pushed as a command inside the
same macro as the counter: one undo now reverts both, and renumbering c3
afterwards yields K3 again. Redo and the database agree.
Folio auto-numbering is deliberately not offered: in the application it
spawns whole new folios from a context, which is a different operation
from labelling. "Renumber existing conductors" has no equivalent to bind --
conductor numbering is applied when a conductor is created or moved, and
QElectroTech has no renumber-all action.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
elementTexts() addElementText() setElementTextProperty()
elementTextProperty() deleteElementText()
A symbol arrives with the fields its definition gives it -- coil
bobine_ka_a_remanence has A2, A1 and a label -- and until now a script
could fill the value the label shows (setElementLabel) but not control
the fields themselves: where each sits, its size, whether it is framed,
what it is bound to, or add and delete one.
A field's source is "text" (a fixed string), "info" (follows one of the
element's information keys, so it follows setElementInfo and
setElementLabel) or "composite" (a formula). Changes go through
QPropertyUndoCommand on the item's own properties, as the element-texts
editor does, and adding through AddElementTextCommand. An unknown source,
a key that is not an element information key, a non-positive size and a
bad index are refused with a reason.
Two things called text differ for an information-bound field. The "text"
property is the stored string, an unused placeholder (empty, or "Texte"
for a newly added field); "shows" is what is drawn. I first believed the
displayed text was stale in-session and wrote a helper to read it from the
element's information instead. That was wrong: comparing toPlainText()
against elementInfo() at seven points across relabel, rebinding, setting
and undo found no difference. It was the stored string that looked stale,
and the helper and the comment claiming the item "refreshes lazily" are
removed. The header now says which is which.
Fields are addressed by index in the element's own list, which follows the
definition and shifts on delete; undoing a deletion puts the field back at
the end. Consecutive info/label changes on one element merge into one undo
step (ChangeElementInformationCommand::mergeWith), so one undo can revert
several -- noted, not a bug.
Verified on a coil: the label field moved, enlarged to 14 pt and framed, a
new field bound to "comment" showing "24VDC coil", all through the
project, exported to PNG and looked at. The saved file carries the moved
position, size, frame and bound value, and a reload reads them back.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
setFolioProperty(f, "version", "V9-USER") returned true and changed
nothing. TitleBlockProperties::version is the file-format stamp
QElectroTech writes on every save, so a value set through the API reads
back as "0.200.1-dev" and is what ends up in the file.
A property that reports success and does nothing is the failure this API
is careful to avoid everywhere else, so it is removed rather than
documented. It went in untested: the earlier verification set author and
plant and never tried the other six names.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The comment on elementTerminals() said the order of Element::terminals()
"also comes from the definition", which reads as file order and is
wrong. Element::parseTerminal() re-sorts the list on every insertion, top
to bottom then left to right on each terminal's local position, so index 0
is the topmost terminal whatever order the .elmt lists them in.
bobine_ka_a_remanence.elmt writes A2 (y=20) before A1 (y=-20) and index 0
is A1. Measured by placing 400 shipped elements and reading the real order
back: a top-to-bottom, left-to-right prediction matched all 400, while
file order matched only the 100 where the two happen to coincide. Of the
837 shipped elements with distinct named terminals, 619 list them in a
different order than QElectroTech indexes them.
Comment only. Two terminals at one point tie and the sort is not stable;
the comment says so.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
images() addImage() setImageScale() setImageRotation() deleteImage()
addImage reads a file the way the "add image" tool does after its file
dialog and pushes the same AddGraphicsObjectCommand. The pixels are
copied into the project, which DiagramImageItem::toXml writes inline, so
the saved file does not refer to the original path: verified by moving the
source file away and reloading, where the image and its scale came back.
The price is that the project grows by about the size of the image, so
files over 10 MB are refused; so are unreadable files, non-images and
missing paths, each with its own reason.
Scaling sets both axes as one undo step. Images are addressed by index in
a position-sorted listing like texts and shapes, by the on-screen
bounding box -- so because an image turns and scales about its centre,
scaling or rotating one can change where it sorts, and the header says to
re-list after either. Observed: a 2x scale moved a listed top-left from
(100,100) to (68,84), which is that, not a displacement.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
shapeProperty()/setShapeProperty() color, fill, width, line-style, rotation
Through QPropertyUndoCommand on the pen, brush and rotation properties
the shape's own style editor changes, so it undoes like a hand edit.
fill takes a colour or "none" for no fill; line-style is solid, dashed,
dotted or dashdot. A colour that does not parse, a non-positive width,
an unknown style or property and a bad index are refused with a reason.
Verified on a rectangle: all five set and read back, fill "none" reads
back as none, undo restores the previous fill, and the whole look
survives save and reload.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
setConductorProperty()/conductorProperty() now also take the properties
that control how a conductor looks, under the names the .qet file uses:
style normal, dashed or dashdotted
bicolor true/false, with color2 as the second colour
dash-size positive integer
condsize positive number (line width)
numsize positive integer (text size)
displaytext true/false
Values are validated rather than stored: a boolean other than
true/false, a non-positive size, an unparseable colour or a line style the
file format cannot express is refused, because ConductorProperties would
write the latter back as a solid line and silently lose it. The change is
still applied to the whole potential.
Verified: all eight set, read back, written to the file in its own form
(style as "line-style: dashed;", displaytext as 0) and read back
again after a reload. Four invalid values decline.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
autoNums(kind) list contexts and their formulas
addAutoNum(kind, name, parts) define one; parts are "type[:value[:increase]]"
removeAutoNum(kind, name)
useConductorAutoNum(folio, n) new conductors on a folio take it
Kinds are conductor, element and folio. Part types are the ones the
auto-numbering dialog offers; anything NumerotationContext rejects, an
unknown kind, a non-numeric increase or a missing context is refused
with a reason rather than dropped.
Defining and removing a context is not undoable, because the
application does it through direct project calls; only the counter
advance is on the undo stack. useConductorAutoNum sets both the folio's
name and the project's current name, because the numbering code reads
the context by one and writes the advanced counter back under the other.
Verified: a "W" + unit context selected on a folio, two conductors
wired, W1 and W2 in the saved file, and the context survives reload.
Depends on the preceding ConductorCreator fix for the database to agree
with the drawing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ConductorCreator inserts a conductor and only afterwards calls
refreshText(), which resolves the auto-numbering formula into
properties.text. The project database inserted its row while text was
still the raw formula, and refreshText() writes the resolved text
without emitting propertiesChange -- the signal the database listens
for -- so nothing corrects the row.
Measured: with a conductor auto-numbering "W%sequ_1" selected, two
wired conductors read W1 and W2 on the live objects and in the saved
file, but "W%sequ_1" and "W%sequ_1" in conductor.text and in
wiring_list_view.wire_number. A full updateDB() corrects it, so the
data was right and only the cache was stale. Anything that reads the
database between creating a conductor and the next rebuild -- the
wiring list, a BOM export, a custom query -- sees the formula, not the
number.
Update the row after refreshText(). The row change deliberately emits no
dataBaseUpdated(), as updateConductor() already documents, so this adds
no model re-queries.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
terminalStrips() the project's strips, in its own order
addTerminalStrip() AddTerminalStripCommand, as the creation dialog does
removeTerminalStrip() RemoveTerminalStripCommand
addTerminalToStrip() AddTerminalToStripCommand
Only terminal-type elements can be added, the restriction the editor
enforces by construction; anything else, or a terminal already on a
strip, or a bad index, declines with a reason. Verified: two terminal
elements placed and added, listing shows 2 terminals, removal and undo,
and the strip with its terminals survives save and reload.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
deleteConductor() the conductor on a terminal, addressed as elsewhere
removeFolio() RemoveDiagramCommand, the GUI's delete-folio command
setFolioProperty() title, author, filename, plant, locmach, indexrev,
folioProperty() version, folio -- via ChangeTitleBlockCommand
deleteConductor removes only the named conductor; unlike a property
change it is not potential-wide, and DeleteQGraphicsItemCommand rebuilds
the rest of the potential so it stays connected, as when a user selects
one conductor and presses Delete. Verified: deleting one leaf of a
three-terminal potential leaves the other conductor, with its number.
removeFolio skips the GUI's confirmation box, which nobody could answer
headlessly. Undo and redo both work; later folio indexes shift down.
Undoing a removal prints a UNIQUE-constraint warning from
projectDataBase::addDiagram. That is inside RemoveDiagramCommand::undo(),
which the GUI runs too, so it predates this change.
The date and template are not offered as folio properties: the date has a
use-current-date mode a plain string cannot express honestly.
Verified headlessly, including save/reload of the title block fields.
Qt 6.10.2, build clean, ctest matches master.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add a KColorButton next to the 'Utiliser les couleurs du système'
checkbox in the Apparence settings tab:
- When the checkbox is active (default), system colors are used
and the color button is disabled
- When the checkbox is inactive, the color button becomes active
and lets the user pick any color for the entire application
- useCustomPalette() builds a full QPalette from the chosen color
with proper light/dark text contrast, button shading, and icon
theme switching
- The chosen color is persisted in QSettings as
'customapplicationcolor' and restored on next startup
Files changed:
- sources/ui/configpage/generalconfigurationpage.ui: HBoxLayout
with checkbox + KColorButton, customwidget declaration
- sources/ui/configpage/generalconfigurationpage.h: new slot
- sources/ui/configpage/generalconfigurationpage.cpp: load/save
custom color, enable/disable logic, toggled slot
- sources/qetapp.h: useCustomPalette() declaration
- sources/qetapp.cpp: useCustomPalette() implementation,
startup restore of custom color
DynamicElementTextItem::updateLabel() resolved %{label} in composite
text using the stale value from elementInformations()["label"], which
is only set once at load time and never updated when the folio/page
number changes.
Use element->actualLabel() instead, which resolves the label formula
(including %F, %f, %id) against the current folio at call time.
Two bugs reported by @arummler on #591 after merge:
"It works but to select the text field one has to right click on
it...I think there are competing handlers or something."
"the drag elements should be on the border of the box. In the
moment they appear directly left and right from the text."
Both reproduced headlessly (scripts/qet-gui-dialog.sh) against a fresh
build of current master and root-caused before touching anything.
Selection: DynamicElementTextItem::mousePressEvent() forwards a plain
click (no Shift) straight to parentElement()->mousePressEvent(), by
design and pre-existing -- it's what lets dragging a symbol by its own
label move the whole symbol rather than just the label. That's correct
and untouched here. But it means a plain click leaves the *parent*
selected, not the text, and #591's handles were wired only to the
text's own ItemSelectedHasChanged -- so they were only reachable via
Shift+click or a right-click's context menu (which happens to select
the item under the cursor for its own context menu, unrelated to the
Shift path), neither of which anyone reaches for to resize a text.
Confirmed with screenshots at each step, including that Shift+click
already reached the existing (if misplaced) handles correctly.
Fix: DynamicElementTextItem::refreshResizeHandlesVisibility() shows the
handles when either the text itself or its parent element is selected,
and Element gets an itemChange() override (it had none) that calls it
on each of its own texts when the element's own selection changes. Both
sides driven from itemChange(), Qt's own hook for exactly this and the
same one already used for the text's own selection.
First attempt drove this from paint() instead, since the PR's own
updateResizeHandlesPos() already runs there. That crashed reproducibly
(SIGABRT) on deselecting a text: paint() runs while QGraphicsScene
iterates its item list to draw it, and addResizeHandles()/
removeResizeHandles() mutate that list via QGraphicsScene::addItem()/
removeItem(), which cannot safely happen mid-iteration. Caught it with
the same headless repro before it went anywhere near a PR, moved the
logic to itemChange(), and re-ran the full sequence -- select, resize,
undo, deselect, twice through -- clean.
Position: updateResizeHandlesPos() placed the handles on frameRect(),
which is a box sized to the text's natural (idealWidth()) content and
then re-centred inside boundingRect() -- it does not grow with
textWidth(). Once a text has been widened, frameRect() stays tight
around the glyphs while boundingRect() -- the box QGraphicsView actually
outlines as the selection, and the box a user drags relative to -- grows
around it, leaving the handles stranded well inside the visible
selection border. Fix: position them on boundingRect() instead, which
does track textWidth(); confirmed by widening a text and checking the
handle lands exactly on the new edge rather than partway across it.
Verified headlessly end to end on the original report's own element
("motor off" on grafcet.qet, folio 1): a single plain left-click (no
Shift, no right-click) now shows both handles at the true box border;
dragging resizes correctly and the handle tracks the growing edge;
Ctrl+Z restores the -1 auto-width sentinel and the handles stay at the
reverted position; clicking away removes them; repeated twice with no
crash. Qt 6.10.2, ctest 12/12, no new warnings in either changed file.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every structural question this API could answer, it answered by walking
live objects. The project builds a SQLite database that already knows
most of them, and nothing outside the application could reach it.
tables() what is queryable, tables and views
query(sql) rows, one object per row
queryError() why the last one returned nothing
This is not a new door. QElectroTech already ships a "Requête SQL
personnalisée" box in the element-query dialog where a user types
arbitrary SQL, guarded by projectDataBase::isReadOnlySelect(); query()
goes through projectDataBase::newQuery(), which applies that same rule
and returns the same rejection message. A script gets what a user
already has, and neither can write: DELETE, UPDATE and a chained
"SELECT 1; DROP TABLE" are all refused before reaching SQLite.
An empty result and a failure are told apart. query() returns no rows
for both, so queryError() carries the reason -- a refusal, or SQLite's
own message for a bad column -- and is empty when the query simply
matched nothing. Conflating those is how a silent typo in a column name
becomes "there are no such elements".
No updateDB() before querying, and that is a measured decision rather
than an omission. A script that has just edited something is the
expected caller, so a stale cache was the obvious hazard; but
projectDataBase maintains itself incrementally through addElement(),
elementInfoChanged(), addConductor() and the rest, which the undo
commands behind every edit already call. Tested both ways on the cases
most likely to go stale -- an element added and labelled, a conductor
property changed -- each queried immediately afterwards through both the
table and the view. Identical counts with the rebuild and without it,
and updateDB() repopulates every table, so calling it per query would
have been real cost for no benefit. The comment says so, so it is not
added back on the assumption it must be needed.
The views are the surface to depend on: element_nomenclature_view,
project_summary_view and wiring_list_view exist to be queried. The
tables are how the cache is arranged today and a column may move --
which is why tables() lists both and the header says which is which.
Verified against examples/industrial.qet, the largest shipped project:
618 elements counted, the busiest wire numbers ranked (0VDC 93 times,
24V2 64), and duplicate element labels found by GROUP BY ... HAVING --
V6 seven times, V5 six -- which is a design-rule question no tool here
could previously ask. Qt 6.10.2, ctest matches master.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The drawing furniture beside the circuit: a free-standing note, a line,
a rectangle, an ellipse, a polygon.
texts() addText() setTextContent() setTextColor()
setTextRotation() deleteText()
shapes() addShape() deleteShape()
Added with the same AddGraphicsObjectCommand the corresponding GUI tools
use, and changed through the plainText/color/rotation properties those
items already publish, so a script's note undoes like a hand-placed one.
These are addressed by index into a listing sorted by position, reading
order, because they have no better identity: unlike an element they carry
no uuid, and unlike a conductor they have no terminal to be named by.
Position is what they have and it persists, so the ordering survives a
save and reload -- verified by listing before and after, including a
rotated text whose bounding box moves. It does not survive adding or
deleting one: indexes after that point shift the way a list's do, which
is why texts() and shapes() exist rather than a caller keeping a handle.
The sort is on sceneBoundingRect(), not pos(). A QetShapeItem keeps its
geometry in its line/rect/polygon and leaves pos() at the origin, so
sorting on pos() put three shapes drawn in three different places all at
(0, 0) and made every shape index refer to whichever the set yielded
first -- which is what the first version of this did, and the test that
caught it was asking for three shapes and getting index 0 three times.
Path is deliberately not offered: it is built by successive clicks and
has no two-point form to give here.
Verified headlessly: three texts added bottom-up and listed in reading
order, edited, recoloured, rotated, one deleted; three shapes added,
listed with their real geometry, the middle one deleted and the right one
gone; unknown shape name, invalid colour and out-of-range index all
decline with a reason. Saved, reloaded, both listings identical.
Qt 6.10.2, build clean, ctest matches master, qet-lint clean on the
generated project, qet-coherence-check clean on the example corpus.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two gaps left over from the drawing verbs. A script could create a
conductor but not say what it was -- no number, colour, section or
formula -- and could not link a master to its slave, although
LinkElementCommand has been there all along and nothing bound it.
conductors() what is on this folio, and how to address it
conductorProperty()
setConductorProperty() num, formula, function, bus, cable,
tension_protocol, conductor_color,
conductor_section, color, text_color
elementLinkType() simple / master / slave / next_report / ...
linkedElements()
linkElements() two folio indices: a master and its slave are
normally on different folios
unlinkElement()
The property names are the ones the .qet file uses for the same fields,
so what a script sets is what a reader of the file sees rather than a
third spelling invented here.
A property is applied to every conductor of the same electrical
potential, not to the one conductor named. That is the rule the
application already follows -- SearchAndReplaceWorker pushes one
QPropertyUndoCommand per conductor of relatedPotentialConductors()
inside a macro -- because a wire number describes a potential, not one
drawn segment; setting it on one and leaving the rest of the potential
disagreeing would produce a file no GUI action could have produced.
Linking asks LinkElementCommand::isLinkable() rather than re-deriving
its rules, so a script cannot make a link the GUI would refuse: master
to master, a PLC master to a non-PLC slave, a next-report to another
next-report, or anything to an already-taken target.
A conductor is addressed as "the conductor on terminal i of element U".
It has no identity of its own to use instead: conductors carry no
persisted uuid, and the terminal1/terminal2 ids in the file are
folio-scoped integers QElectroTech renumbers on every save. Since the
change is potential-wide, any terminal of the potential names it equally
well, so in practice a potential is addressed from one of its leaves; a
terminal carrying several conductors names none of them and is refused
rather than guessed at.
Verified headlessly. Conductor: num, section and colour set from one end
of a potential and read back from the other, saved and reloaded, present
in the XML. Propagation shown to discriminate, which took two tries --
the first attempt wired A.0-B.0 and B.1-C.0 and saw no propagation,
correctly, because a coil's two terminals are opposite ends of the coil
and not one potential. Wiring a real hub at A.0 instead, a number set
via the B leaf appears on the C conductor too, in memory and in the
saved file. Cross-reference: a master on one folio linked to a slave on
another, linkedElements() agreeing from both ends, surviving save and
reload with link_uuid written on both folios; master-to-master,
self-link, unlink and relink all behave. Unknown property, invalid
colour, bare terminal and ambiguous terminal all decline with a reason.
Qt 6.10.2, build clean, ctest matches master, qet-coherence-check clean
on the example corpus, qet-lint clean on the generated projects.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
qet_diff keyed each conductor on its raw terminal1/terminal2 pair, with
a comment claiming that pair was "stable within a folio". It is stable
within a folio; it is not stable across a save. QElectroTech reassigns
those folio-scoped integer ids on every write, in whatever order it
serialises the elements, so one untouched conductor of ArduinoLCD.qet
goes from terminal1="1" terminal2="16" to terminal1="34" terminal2="15".
Diffing a project against a re-saved copy of itself therefore reported
29 of its 47 conductors as removed and 29 as added, with nothing
changed. That is the main thing this tool is for, so the conductor half
of the answer was noise in exactly the case it was wanted.
The format has two addressing schemes and a file can hold both at once.
Older conductors use the integer ids with no element1/element2; current
ones use terminal uuids from the .elmt definition plus element1/element2
naming the placed instances. A terminal uuid alone is not an identity --
it belongs to the definition, so two coils of one type share it and a
conductor between them keys as a self-loop -- so an end is identified by
the (instance, terminal) pair, taken from the conductor where it carries
one and resolved through the folio's elements where it does not.
Where an element predates persisted uuids there is nothing stable to key
on. Keying those on terminal geometry alone collapsed nine distinct
conductors of schema_indus.qet onto a single key, which is worse than
the instability it was meant to fix, so such ends stay unresolved, keep
a "#"-marked key, and the diff reports unstable_keys and says in words
that added/removed may not mean what they look like.
Measured over the 24 shipped example projects, 3190 conductors: 0
colliding keys, against 8 for the geometry-only key. On a re-saved but
otherwise untouched project: 0 added, 0 removed, against 29 and 29
before this change. A project with two conductors genuinely added still
reports exactly two added and none removed, so the check still
discriminates.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The scripting API (bugtracker #162) could place an element and move it,
and could count conductors but not make one. So a script could put a
coil and a motor on a folio and had no way to connect them, which is
most of what drawing is. This adds the missing verbs:
addConductor() wire terminal i of one element to terminal j of another
rotateElement()
setElementInfo() any information key
setElementLabel() the label key, by name, since it is the one people want
addFolio()
setFolioTitle()
elementUuids() what is on this folio
elementName()
elementTerminals() which terminal index is which, before wiring it
Each goes through the command the GUI already uses, so a script's edits
undo like manual ones and reach the project database the same way:
ConductorCreator (the drag-a-rectangle-over-terminals path, which is
what makes a new conductor inherit an existing potential's properties
and join auto-numbering), ChangeElementInformationCommand,
QETProject::addNewDiagram(), ChangeTitleBlockCommand. rotateElement()
pushes the same QPropertyUndoCommand on "rotation" that
RotateSelectionCommand pushes for an Element, rather than
RotateSelectionCommand itself, which works on the diagram's selection
and would mean rewriting the user's selection to rotate one element.
Terminals are addressed by index, not uuid. Terminal::uuid() is a
property of the catalog .elmt definition: empty for most of the
installed base, and where present, identical across every instance of
that element -- two coils of the same type placed side by side have
byte-identical terminal uuids, so a uuid cannot say which coil's A1 is
meant. elementTerminals() exists so a script can see the indexing
instead of guessing it.
The one real hazard is that ConductorCreator asks the user which
potential to inherit from when the two terminals sit on two different
existing ones, and it asks with a plain modal QDialog that
QET::QetMessageBox's non-interactive mode does not cover -- so under
headless --run there is nobody to answer and the call never returns.
Measured: with the check removed, that one call hangs until killed;
with it, it declines in 0.4 s. addConductor() therefore refuses that
case, the same way and for the same reason addElement() already refuses
the import-conflict dialog.
To make that check without duplicating the condition, existingPotential()
becomes static over an explicit terminal list and ConductorCreator gains
a public needsPotentialChoice() predicate. Behaviour of the GUI path is
unchanged; setUpPropertieToUse() passes m_terminals_list to the same code
it called before.
Verified headlessly against a copy of examples/ArduinoLCD.qet: new folio
titled, two coils placed, wired, labelled, an info key set and the
element rotated; saved, reloaded, and the conductor, label, title and
rotation (persisted as orientation="1") all read back. Re-saving the
result is byte-identical. qet-lint clean on the generated project;
qet-coherence-check clean on it and on the 24-project example corpus,
and shown to report 9 findings on a deliberately broken copy of the same
file, so the clean result discriminates. Qt 6.10.2, ctest identical to
master (the 61 failures are the vendored KDE ECM suite, present on both).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A small stdio MCP server that lets an assistant read a project, ask what
an edit actually changed, and sweep a corpus. Standard library only --
Python 3.9+, no third-party dependencies, and the MCP SDK is not
required. Nothing in the build or the application refers to it; it sits
in misc/ beside make_icon_themes.py and is inert unless run.
It exists because verifying a change by screenshot is unreliable, and
that unreliability produced two wrong conclusions in a single review
session. A drag of a multi-element selection looked like it had left the
symbols behind and detached their labels; diffing the saved file showed
all four elements had moved by an identical (0,-80) and no label had
moved at all. An Apply button looked like it did nothing; it was
disabled because a required field was empty. Both times the pixels
misled and the file told the truth, so these tools read the file.
Seven tools: qet_project_info, qet_elements, qet_conductors, qet_diff,
qet_scan, qet_element_info and qet_export. Only qet_export launches
QElectroTech; everything else parses the .qet or .elmt directly, which
needs no display and cannot be confused by a dialog.
Two behaviours of QElectroTech are carried inside the tool rather than
left for the caller to rediscover. SingleApplication keys its socket on
applicationFilePath(), so a second launch of the same path forwards its
request to a running instance and returns that process's answer with no
error; qet_export therefore copies the binary to a unique temporary
path, gives it a private HOME and runs it offscreen. A symlink would not
do, because applicationFilePath() resolves it back. And the CLI matches
its export flags by exact string (cli_export.cpp:828) with the project
and output as positional arguments (:862, :882), so --export-bom=out.csv
is not recognised as an export at all and the run starts the interface
and hangs headless; the tool uses the positional form.
Worth recording for anyone extending this: the project database would be
a better query surface than the XML, but it is not reachable from
outside the application. projectDataBase::newQuery() and
isReadOnlySelect() are C++-internal and the JavaScript scripting API
exposes no SQL binding. A --query CLI verb, or a scripting binding,
would let this expose the guarded read-only SELECT surface instead.
Verified against the shipped examples: qet_scan reports 3190 conductors
across the 24 example projects with no cable value, and qet_diff
reproduces the four-element move above from the two saved files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replace the white/grey toggle (m_grey_background) with a full color
picker widget (DiagramBgColorToolButton) in the Affichage toolbar,
matching the existing ConductorColorToolButton UX:
- Preset colors: White, Off-white, Light grey, Grey, Dark grey, Black
- Recently used colors section
- "Autre couleur..." opens QColorDialog for any custom color
- "Couleur système" restores the default dark-mode inverted background
New features:
- Diagram::m_custom_background_color flag: when the user picks a
custom color, PaletteGraphicsView skips lightness inversion so the
chosen color is displayed as-is
- Border and titleblock text/lines automatically switch between black
and white based on Diagram::background_color.lightness(), so a dark
background always shows a visible light border and titleblock content
- "Couleur système" restores Qt::white + re-enables inversion
Files changed:
- New: sources/ui/diagrambgcolorbutton.h/.cpp
- sources/diagram.h/.cpp: added static m_custom_background_color flag
- sources/palettegraphicsview.cpp: skip inversion when custom bg active
- sources/bordertitleblock.cpp: adaptive border pen color
- sources/titleblocktemplate.cpp: adaptive ink color for cell borders/text
- sources/qetdiagrameditor.h/.cpp: replace toggle with new widget
- cmake/qet_compilation_vars.cmake: register new source files
ApplyForEqualAttributes() was missing the 'style' attribute, causing
dashed/dash-dotted line styles to be lost when potentials are merged
via folio reports. Only color and other properties were copied.
Add style copy in single-element case and equality check in
multi-element case, matching the existing pattern for other attributes.
Fix macro elements jumping to upper-left corner instead of being placed
at the correct drop position.
Root cause: The preview offset used itemsBoundingRect() (all items
including children), while Diagram::fromXml() computed its translation
offset from top-level items only. This mismatch caused fromXml to
translate elements to the wrong position.
Changes:
- Compute top-level-only bounding rect in dummy diagram constructor
to get the correct m_items_top_left reference point
- Pass final_pos + m_items_top_left to fromXml() so the internal
translation yields the intended final position
- Add braces around single-statement for-loop in fromXml
- Remove empty else block leftovers from debug cleanup
QETStyle::hoverColor() lightened the highlight color until it read at
3.5:1 against the Light role. On a dark face that is the way to go; on a
light face lightening only fades the ink, so with a pale platform accent
that QET keeps (macOS's green selection color, black selection text) the
loop ran to white and every hovered line-art icon vanished. The ink now
moves away from the face, darker on a light face, lighter on a dark one,
and falls back to the button text color if twenty steps are not enough.
The hover test gets a row with that accent on each palette, and a new
test sweeps accents across hues and lightness on both palettes and
requires the hover ink to read at 3:1 on the face.
Fixes#962
sources/qetsbom.cpp and sources/qetsbom.h were untracked local files
that a directory-wide add swept into the rebuilt #944 commit. Nothing
references them; the build does not compile them.
Laurent found that moving an element on a #954 build left its terminals'
help lines behind at every step, on both palettes. The view listened to
QGraphicsScene::changed() so that render() would keep the scene's updates
flowing, and any receiver on that signal puts the scene on its Qt 4.4
compatibility path, which erases a moved item's own old rect only:
children bigger than their parent stay on screen. Master hides that with
FullViewportUpdate, which repaints the whole viewport on every change.
Paint the inverted folio through QGraphicsView::paintEvent() instead, with
IndirectPainting set for that call and the draw hooks painting into a
viewport-sized image, and hand the scene the viewport when the items are
drawn so it records where each item was painted, including a child whose
geometry is set while its parent paints. The listener and the
full-viewport update go. Two tests move a parent with a sheet-wide child,
read the backing store, and require the repaint to be a partial one.
Switching the system between light and dark while QET runs changed
the palette of every plain widget but left widgets that carry a style
sheet in the colors they were created with: the folio tab bar stayed
light in dark mode, and after a dark-to-light switch its Add folio and
chevron buttons hovered as a near-black box with the icon lost inside
it. QApplication::setPalette() does not reach a widget with a style
sheet; QStyleSheetStyle resolved its palette once, when the sheet was
applied, and keeps it. Seventeen call sites set a sheet on a widget and
five .ui files carry one, so any of them could show the stale palette.
QET::Palette::refreshStyleSheets() re-applies each such widget's own
sheet, which makes QStyleSheetStyle resolve it against the palette now
in force. QETApp::useSystemPalette() calls it after installing the
palette, so both the OS color scheme change and the "use system
colors" setting are covered.
tests/qttest/tst_qetpalette: a tab widget with the folio tab bar's
sheet is still drawn in the old colors after setPalette(), which is the
defect, and follows the palette after refreshStyleSheets(), in both
directions.
Fixes#943.
The settings and project dialogs list their pages with 64 or 128 pixel
icons, and two pages had theirs at 22 pixels only: the terminal-strip
page and the shortcuts page, which borrowed configure-toolbars. Both get
a 128 pixel icon drawn in the style of the other page icons, the
shortcuts page under its own name, configure-shortcuts. The SVG sources
sit beside the PNGs.
On a dark palette the Printing and Export pages were small too: their
128 pixel icons exist in the light theme only, and Qt inherits by name,
not by size, so the dark theme's small copies were scaled up instead.
make_icon_themes.py now aliases the light files of the sizes a dark name
lacks, when they read on the dark window at 3:1.
A test asks the theme for every page icon at 128 pixels, on both
palettes.
Fixes#960
The pinning comment stated that a tag is mutable but not what an attacker
does with that, so the trade-off was hard to judge for anyone reviewing or
later undoing the pins. Spell out the mechanism: a tag is a name pointing
at a commit, anyone with push access upstream can force-push it elsewhere,
and FetchContent resolves it at build time, so a stolen maintainer account
or CI token makes every fresh build compile the attacker's code while
nothing changes here and the tag name still reads correctly. A commit hash
is derived from the content and cannot be moved that way.
Name the two cases where this was actually exploited: tj-actions/changed-
files in March 2025 (CVE-2025-30066), where tags v1 through v45.0.7 were
retargeted to a commit leaking CI secrets into build logs across more than
23,000 repositories, and aquasecurity/trivy-action in March 2026
(CVE-2026-33634), where 76 of 77 version tags were force-pushed to a
credential stealer for about twelve hours. Both were GitHub Actions rather
than CMake dependencies, which the comment says, because the point is the
shared mechanism of resolving a tag at build time.
Also document how to upgrade a pin, including that git ls-remote reports
the tag object for an annotated tag and the commit on the "^{}" line.
The note lives in fetch_pugixml.cmake, which fetch_kdeaddons.cmake and
fetch_singleapplication.cmake already refer to. Comments only; no build
behaviour changes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CMake fetches pugixml, SingleApplication and the three KDE Frameworks
modules by git tag. A tag is a mutable pointer that its owner can move,
so two builds of the same QElectroTech commit can silently get different
third-party sources, and a compromised upstream account can change what
every builder downloads without anything changing in this repository.
Pinning each dependency to the commit its tag currently points at closes
that, while keeping the tag name in a trailing comment so the intended
version stays readable.
No versions change. Every pinned commit is the one its tag resolves to,
checked with git ls-remote and confirmed by fetching each one and
verifying that git describe reports exactly the tag. The three KDE
modules live in separate repositories and therefore need separate
commits, so the single KF_GIT_TAG variable becomes three per-module
variables; passing -DKF_GIT_TAG=<ref> still selects one ref for all
three, unpinned, exactly as before, and KF_GIT_TAG stays defined so the
build summary in define_definitions.cmake is unaffected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add a 'Numérotation auto' tab to the global settings page (Settings >
Nouveau projet) where users can define default auto-numbering rules
for Conducteurs, Eléments, and Folios. These rules are automatically
transferred to every new project created.
Changes:
- Add NumerotationContext::saveToSettings()/loadFromSettings() static
helpers for persisting named numerotation contexts via QSettings
- Add 'Numérotation auto' tab to NewDiagramPage with three sub-tabs
using SelectAutonumW widgets (same UI as project properties)
- Add save/remove/persist slots for conductor, element, and folio
contexts with immediate QSettings persistence on every change
- NewDiagramPage::applyConf() saves autonum settings when editing
global defaults (no project)
- QETProject constructor loads global autonum settings from QSettings
for new empty projects
Dragging an element's text -- its label, article number, any of its
information fields -- moved it in free one-unit steps while everything
else in the editor snapped to the grid. Reported by pki791 in #923 for
labels moved with Shift.
QET moves a text with the mouse along five paths. Four snap and let Ctrl
place freely:
DiagramTextItem::mouseMoveEvent an independent text
ElementTextItemGroup::mouseMoveEvent a group of element texts
ElementTextsMover::continueMovement every OTHER selected element text
QetGraphicsItem::setPos elements, images, shapes
DynamicElementTextItem::mouseMoveEvent, the text actually under the
cursor, ended "setPos(new_pos)" with no grid and no modifier check. It is
otherwise the same function as the group's, which is why this reads as an
omission rather than a decision: the line this adds is that function's,
character for character.
The inconsistency was visible in one gesture. With two element texts
selected and one of them dragged, ElementTextsMover skips the driver item
and snaps the rest, so the text under the cursor was the only one on the
folio that did not land on the grid.
Verified on a virtual display (Xvfb + openbox) against a two-lamp fixture,
grid 10, reading the saved positions rather than the screen:
Shift+drag the label before (32.95, -11.55) -> (7.95, 23.45) off-grid
after -> (10, 20) on-grid
the co-selected label (10, -10) -> (50, 20) on-grid, before and after
Shift to grab, then Ctrl -> (7.95, 23.45) off-grid, free placement kept
The last line matters: moving an element text needs Shift at press, and
the modifier is read at move time, so Ctrl still places freely -- press
with Shift, hold Ctrl to drag. Holding both from the press is a different
gesture, reserved by DiagramView::isCtrlShifting() for the view's mode
switch, and does not move the text at all. Nothing that was possible
before is lost.
Worth knowing when reviewing: 470 of the 492 element texts in the 24
example projects (95.5 %) sit off the grid today, because element
definitions place their default text at fractional offsets. The first
drag of almost any existing label will pull it onto the grid, by at most
half a grid step.
ctest 12/12, Qt 6.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Discussion #599's own scope explicitly deferred this ("Related, not
proposed here... a natural follow-up once basic pan/zoom motion works").
Basic pan/zoom now works (previous commits on this branch), so this adds
it -- generically, for whichever SpaceMouseBackend is in use, not tied to
libspnav specifically, matching the seam the previous commit built.
## Reuses ShortcutManager instead of inventing a second action registry
ShortcutManager is already an app-wide registry of every named, rebindable
action -- undo, redo, rotate selection, cut/copy/paste, autonum configure,
and dozens more -- each carried by a live QAction or QAbstractButton. A
device button binding to one of *those* ids, rather than to a bespoke
QET-3D-mouse-only action list, means the discussion's own examples
(rotate/mirror/undo) are available for free, and any action added to the
app in the future is automatically bindable too.
Added ShortcutManager::trigger(id): find the first still-alive target for
an id and call QAction::trigger() or QAbstractButton::click(), whichever
it is. Deliberately not disambiguated by which window is currently active,
unlike SpaceMouseListener's own pan/zoom dispatch -- a target's owning
top-level window isn't reliably discoverable from a bare QAction. Correct
in the overwhelming common case of one open editor window; documented in
the header as a known simplification, not silently assumed correct.
## The binding itself: SpaceMouseButtonMap
A thin QSettings-backed button-number -> action-id map, unbound by default
for every button on every device -- nothing happens on any button press
until the user opens Configuration > Souris 3D and binds something,
matching this whole feature's "silent until asked for" default.
## Backend side: SpaceMouseBackend::buttonPressed(int)
Added to the platform interface alongside the existing motion() signal.
SpnavBackend now handles SPNAV_EVENT_BUTTON (previously explicitly
ignored) and emits on press only -- release is not reported, since nothing
downstream has a use for it. A future non-spnav backend implements the
same signal and gets button support for free through
SpaceMouseListener::applyButton(), without that logic being duplicated or
re-verified per backend -- the same reasoning the previous commit's seam
was built around.
## Configuration UI: SpaceMouseConfigPage
Modelled directly on the existing ShortcutsConfigPage -- same QTableWidget
shape, same "persist on applyConf(), not live" contract -- one row per
binding: button number (spin box, unbounded, since button count and
numbering genuinely vary from 2 to 30+ across real devices and this could
not be checked against hardware) and action (combo box populated from
ShortcutManager::instance().allShortcuts(), the exact same live registry
the Shortcuts page itself lists). Only added to the Configuration dialog
when QET_SPACEMOUSE_SUPPORT is compiled in.
## Verified, including the one thing that doesn't need hardware to prove
Rebuilt from scratch both ways: option off adds zero new object code
(confirmed via a forced rebuild of the one unconditionally-changed file,
shortcutmanager.cpp, which alone picked up new warning-free code); option
on compiles all four new/changed files warning-free and links clean.
The backend's button *detection* (SPNAV_EVENT_BUTTON -> buttonPressed
signal) still cannot be verified without a real device or daemon -- same
limitation as the motion path from the previous commits, stated plainly
rather than glossed over.
What *is* fully verified, because none of it needs hardware:
- SpaceMouseButtonMap: unbound by default, set/read-back, clearing via an
empty id, enumeration -- all confirmed via a standalone harness linked
against the real compiled objects.
- ShortcutManager::trigger(): registered a real QAction, confirmed
trigger() fires it exactly once and returns true; confirmed it returns
false (not a crash) for an unknown id.
- SpnavBackend: constructs safely with no daemon present (isAvailable()
false, as it must be), and both its motion and buttonPressed signals
are correctly wired per Qt's own metaobject data (QSignalSpy).
- The configuration page end-to-end, via a real Xvfb session: opened
Configuration > Souris 3D, confirmed the action combo box lists the
live, real ShortcutManager registry (undo, rotate, cut/copy/paste,
dozens more -- not a mock), added rows, edited the button number,
removed rows, selected "Éditeur de schémas — Pivoter" (Rotate -- the
discussion's own example) for button 3, clicked OK, and confirmed via
the actual settings file that it persisted exactly as
"buttons\3=diagrameditor.rotate_selection". Reopened the dialog and
confirmed it read back correctly. This is a full, real round trip
through the UI, not a claim.
The user asked for phase 2 (Windows/macOS via 3Dconnexion's proprietary
3DxWare SDK) on top of #635. This sandbox has no 3DxWare SDK, no Windows
toolchain, and no macOS toolchain -- nothing to compile, link, or run a
single line of platform code against, unlike the Linux/libspnav path,
which was built and actually tested here for real. Writing 3DxWare
integration code that has never even built would be a materially weaker,
unverifiable thing sitting in this PR, so it is not in this commit.
What is: the structural seam that makes adding it later a contained,
reviewable change instead of a rewrite of code that already works.
## Before
SpaceMouseListener did three unrelated things in one class: own the
libspnav connection, read spnav events, and apply motion to the active
DiagramView. A Windows/macOS backend would have had to either duplicate
all of the DiagramView-facing logic (the pan/zoom calls, the Z-to-zoom-
factor mapping, the "which view is active" lookup -- all already verified)
or bolt onto the same class with a maze of #ifdefs. Either way, touching
that file again would put the already-tested Linux path back in scope for
review.
## After
- SpaceMouseBackend: a tiny interface (isAvailable(), a motion(dx,dy,dz)
signal). A backend's only job is owning one platform's connection to the
driver/daemon and translating its native event into this one signal.
- SpnavBackend: the libspnav code from the previous commit, moved behind
that interface with no behaviour change -- still spnav_open() in the
constructor, still a QSocketNotifier on spnav_fd(), still silent when no
daemon/device is present.
- SpaceMouseListener: now backend-agnostic. Owns whichever backend the
platform provides, applies its motion to the active DiagramView exactly
as before. The DiagramView-facing code (pan/zoom calls,
zoomFactorForZAxis) did not need to change at all -- only its input
changed from a spnav_event_motion struct to three plain ints.
A future 3DxWare backend implements SpaceMouseBackend, is selected in
SpaceMouseListener's constructor behind its own
QET_SPACEMOUSE_BACKEND_3DXWARE guard (see the comment marking exactly
where), and never has to touch SpnavBackend or SpaceMouseListener's
DiagramView-facing half.
## CMake: one user option, one define per backend
QET_ENABLE_SPACEMOUSE is unchanged as the single option a user sets.
Internally, find_spacemouse.cmake now decides *which* backend (if any)
that resolves to: on Linux with libspnav found, QET_SPACEMOUSE_BACKEND_SPNAV
plus the umbrella QET_SPACEMOUSE_SUPPORT. Turning the option on anywhere
else today downgrades cleanly with a warning naming discussion #599,
instead of trying (and failing) to find libspnav on a platform that
doesn't ship it. Adding 3DxWare later means adding one more branch here,
not restructuring this file.
## Verified this is a pure refactor, not just "still compiles"
Reconfigured and rebuilt both ways from scratch:
- option off: unchanged from before -- no new source files compiled, zero
new object code.
- option on: both new files compile with zero warnings, binary still
links against libspnav.so.0 (confirmed via ldd), and run to completion
in this environment (which has no spacenavd) with zero crashes and zero
spnav-related output -- identical to before the refactor.
- zoomFactorForZAxis re-linked and re-run in isolation: identical output
to the pre-refactor commit (z=0 -> exactly 1.0, z=+-350 -> 1.35/0.65),
confirming the math moved unchanged rather than being reimplemented.
Implements discussion #599's phase 1 (Linux, libspnav): a 3Dconnexion
6-DOF device pans and zooms the active diagram view, via spacenavd.
## Off by default, zero cost when off
QET_ENABLE_SPACEMOUSE (cmake/developer_options.cmake) is OFF. Verified in
two separate build directories from a clean configure: with it off, the
new cmake/find_spacemouse.cmake step runs and does nothing (no library
lookup, no definition, no new source files compiled), and qetapp.cpp/.h
produce zero new object code -- both are entirely #ifdef'd out. The
default build is byte-for-byte the same shape as before this commit.
With it on, libspnav is located via its pkg-config file (spnav.pc, shipped
by libspnav-dev on Debian/Ubuntu and equivalent packages elsewhere). If the
option is on but the library isn't found, this does not hard-fail
configure: it downgrades back to off with a warning, so an opt-in feature
never blocks a developer who doesn't have the library installed.
## No new navigation logic -- a new input source for the existing one
DiagramView::wheelEvent() already turns a physical wheel's delta into
horizontalScrollBar()/verticalScrollBar() calls for pan and a
zoom(1 + value/1000) call for zoom -- see diagramview.cpp:661-687.
SpaceMouseListener calls exactly those same primitives from spnav motion
events instead of wheel events. It does not reimplement panning or
zooming.
## Safe by construction even when compiled in
The overwhelming majority of users of a build with the option on still
won't have spacenavd running or a device attached -- that must never
surface as an error dialog or a startup warning. SpaceMouseListener::
isAvailable() reflects this: spnav_open() failing is treated as the
ordinary case, not an error, and the object then does nothing at all.
Verified for real in this environment, which genuinely has no spacenavd
running: built with the option on, ran the resulting binary to completion,
and confirmed zero crashes and zero spnav-related output of any kind --
the silence is the point.
Motion is read via a QSocketNotifier on spnav_fd() (event-driven, no
polling loop, no idle cost) and applied to whichever DiagramView is
currently active, found via qApp->activeWindow() -> QETDiagramEditor ->
currentProjectView() -> currentDiagram(): a 6-DOF device is one ambient
input source for the whole application, not something tied to a
particular window, so there is exactly one listener, owned by QETApp.
## What could not be verified without hardware
The Z-axis-to-zoom-factor mapping (SpaceMouseListener::zoomFactorForZAxis)
is a pure function specifically so it could be tested without a live
device: confirmed a centered device (z=0) yields exactly 1.0 (an exact
no-op, not an epsilon-off value that could trip DiagramView::zoom()'s
>=1 branch), and that push/pull produce symmetric zoom-in/out factors.
What genuinely cannot be checked in this environment: which physical axis
is "left/right" vs "up/down" vs "push/forward", their sign, and whether
the ZOOM_DIVISOR/PAN_SCALE constants feel right on real hardware. Both are
named constants specifically so recalibrating them is a one-line change
once someone with a device tries it -- flagged plainly in the PR rather
than presented as verified.
## Not in this commit
Windows/macOS (proprietary 3DxWare SDK, materially bigger lift) and
device button mapping are both explicitly out of scope for this phase per
the discussion.
Adds two QetGraphicsHandlerItem grip handles at the left/right edges of a
selected DynamicElementTextItem's frameRect(), reusing the exact same
handle class, scene-event-filter wiring, and live-drag-then-undo-on-release
pattern QetShapeItem already uses for its own diagram-level resize handles
(sources/qetgraphicsitem/qetshapeitem.cpp).
- Handles are created/destroyed on ItemSelectedHasChanged, matching
QetShapeItem's convention (and ElementPrimitiveDecorator's, for the
element editor's own primitives).
- Position is recomputed in paint() rather than hooked to specific
mutators, since textWidth/font/text/rotation can all move frameRect()
and there's no single itemChange notification that covers all of them.
- The drag delta is resolved through mapFromScene() into the item's own
local coordinates, so a rotated text box still resizes along its own
baseline rather than along the scene's x-axis.
- setTextWidth() is called live during the drag for immediate visual
feedback (matching how QetShapeItem's handlerMouseMoveEvent live-updates
geometry); only on release is a QPropertyUndoCommand pushed -- the exact
same command the properties-panel width spinbox already uses
(sources/ui/dynamicelementtextmodel.cpp), so no new undo-command class
or XML was needed.
- The original textWidth() value is preserved as-is (including -1, the
"auto" sentinel) for the undo command's old_value, separately from the
concrete baseline used for the live drag's delta math -- otherwise an
undo would replace "auto width" with a synthesized fixed width instead
of actually restoring the auto-sizing state.
Scoped to DynamicElementTextItem per the discussion's phase 1 (the
buildable, no-new-XML piece); IndependentTextItem and the element editor's
PartText/PartDynamicTextField have no serialized width property to resize
yet and are left as explicitly out-of-scope follow-ups.
Verified headlessly (Xvfb + xdotool + scrot): selecting an element's
label text shows the two handles, dragging one live-resizes the text
(confirmed via the properties panel's width field updating in real time),
and undo/redo correctly restores the exact original width including the
auto-width (-1) case.
The dock editor snapshotted the conductor once and never refreshed, so a
change made via the modal Edit-conductor dialog left the dock holding stale
values; on deselection apply() wrote that stale snapshot back, overwriting
the dialog's change. Subscribe to Conductor::propertiesChange and refresh via
updateUi() so the dock mirrors external edits and apply() becomes a no-op
when nothing changed in the dock.
Add English translations (qet_en.ts) for the new conductor-panel strings
so non-French users don't see untranslated source text: the panel title,
the two undo captions, the apply-to-all checkbox, and the View-menu
toggle + status tip.
The apply-to-all checkbox now reuses the modal dialog's exact wording
("Appliquer les propriétés a l'ensemble des conducteurs de ce potentiel")
for consistency and so it shares the existing translation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port the modal dialog's "apply to all conductors" option into the dock
panel, resolving the open propagation question. A persisted checkbox,
pinned above the tabs, makes each edit propagate to every conductor on
the same potential in one undo step - identical semantics to
ConductorPropertiesDialog, but the choice is remembered across edits.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses the testing feedback on the conductor selection-properties
prototype:
1. Edits were never applied. The dock drives every editor through
setLiveEdit(true), which each editor overrides to connect its field
changes to apply(); ConductorPropertiesEditorWidget didn't override it,
so the call was a no-op. Implement setLiveEdit() to connect the hosted
ConductorPropertiesWidget's controls (commit-style signals) to apply().
A m_updating guard suppresses the signals emitted while the widget is
loaded programmatically, so a partial mid-load state is never committed.
2. Clicking a conductor's text label showed nothing, because the label is
a ConductorTextItem, not a Conductor. Map it to its parentConductor() in
the editor factory, mirroring the double-click-the-label dialog behaviour.
3. The panel sat at a small size hint with empty space below it. Give the
editor an Expanding vertical size policy (and a minimum height) so it
fills the dock like the other editors.
Refs #500
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LangStringvar3${LANG_ENGLISH}"Examples of cartridges"
LangStringvar4${LANG_ENGLISH}"Examples of diagrams"
LangStringvar5${LANG_ENGLISH}"Fonts"
LangStringMcp${LANG_ENGLISH}"AI assistant (MCP)"
LangStringvar6${LANG_ENGLISH}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_ENGLISH}"Python for the AI assistant"
LangStringvar7${LANG_ENGLISH}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringuninstFailed${LANG_ENGLISH}"Uninstallation of the previous version failed.$\nPlease uninstall ${SOFT_NAME} manually before continuing."
@@ -43,6 +47,10 @@
LangStringvar3${LANG_KOREAN}"표제란 예제"
LangStringvar4${LANG_KOREAN}"도면 예제"
LangStringvar5${LANG_KOREAN}"글꼴"
LangStringMcp${LANG_KOREAN}"AI assistant (MCP)"
LangStringvar6${LANG_KOREAN}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_KOREAN}"Python for the AI assistant"
LangStringvar7${LANG_KOREAN}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringuninstFailed${LANG_KOREAN}"이전 버전을 제거하지 못했습니다.$\n계속하기 전에 ${SOFT_NAME}을(를) 수동으로 제거해 주세요."
LangStringvar6${LANG_POLISH}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_POLISH}"Python for the AI assistant"
LangStringvar7${LANG_POLISH}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringuninstFailed${LANG_POLISH}"Odinstalowanie poprzedniej wersji nie powiodło się.$\nPrzed kontynuowaniem odinstaluj ręcznie program ${SOFT_NAME}."
LangStringvar6${LANG_GREEK}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_GREEK}"Python for the AI assistant"
LangStringvar7${LANG_GREEK}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringuninstFailed${LANG_GREEK}"Η απεγκατάσταση της προηγούμενης έκδοσης απέτυχε.$\nΠαρακαλώ απεγκαταστήστε χειροκίνητα το ${SOFT_NAME} πριν συνεχίσετε."
LangStringvar6${LANG_CZECH}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_CZECH}"Python for the AI assistant"
LangStringvar7${LANG_CZECH}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringuninstFailed${LANG_CZECH}"Odinstalování předchozí verze se nezdařilo.$\nPřed pokračováním prosím odinstalujte ${SOFT_NAME} ručně."
@@ -139,6 +159,10 @@
LangStringvar3${LANG_SPANISH}"Ejemplos de cartelas"
LangStringvar4${LANG_SPANISH}"Ejemplos de esquemas"
LangStringvar5${LANG_SPANISH}"Fuentes"
LangStringMcp${LANG_SPANISH}"AI assistant (MCP)"
LangStringvar6${LANG_SPANISH}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_SPANISH}"Python for the AI assistant"
LangStringvar7${LANG_SPANISH}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringuninstFailed${LANG_SPANISH}"La desinstalación de la versión anterior ha fallado.$\nPor favor, desinstale ${SOFT_NAME} manualmente antes de continuar."
LangStringvar6${LANG_GERMAN}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_GERMAN}"Python for the AI assistant"
LangStringvar7${LANG_GERMAN}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringuninstFailed${LANG_GERMAN}"Die Deinstallation der vorherigen Version ist fehlgeschlagen.$\nBitte deinstallieren Sie ${SOFT_NAME} manuell, bevor Sie fortfahren."
@@ -187,6 +215,10 @@
LangStringvar3${LANG_RUSSIAN}"Примеры штампов"
LangStringvar4${LANG_RUSSIAN}"Примеры схем"
LangStringvar5${LANG_RUSSIAN}"Шрифты"
LangStringMcp${LANG_RUSSIAN}"AI assistant (MCP)"
LangStringvar6${LANG_RUSSIAN}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_RUSSIAN}"Python for the AI assistant"
LangStringvar7${LANG_RUSSIAN}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringuninstFailed${LANG_RUSSIAN}"Удаление предыдущей версии завершилось с ошибкой.$\nПожалуйста, удалите ${SOFT_NAME} вручную перед продолжением."
@@ -211,6 +243,10 @@
LangStringvar3${LANG_ARABIC}"أمثلة على كتل العنوان"
LangStringvar4${LANG_ARABIC}"أمثلة على المخططات"
LangStringvar5${LANG_ARABIC}"الخطوط"
LangStringMcp${LANG_ARABIC}"AI assistant (MCP)"
LangStringvar6${LANG_ARABIC}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_ARABIC}"Python for the AI assistant"
LangStringvar7${LANG_ARABIC}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringvar6${LANG_CATALAN}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_CATALAN}"Python for the AI assistant"
LangStringvar7${LANG_CATALAN}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringuninstFailed${LANG_CATALAN}"La desinstal·lació de la versió anterior ha fallat.$\nSi us plau, desinstal·leu ${SOFT_NAME} manualment abans de continuar."
@@ -259,6 +299,10 @@
LangStringvar3${LANG_ITALIAN}"Cartigli di esempio"
LangStringvar4${LANG_ITALIAN}"Schemi di esempio"
LangStringvar5${LANG_ITALIAN}"Caratteri"
LangStringMcp${LANG_ITALIAN}"AI assistant (MCP)"
LangStringvar6${LANG_ITALIAN}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_ITALIAN}"Python for the AI assistant"
LangStringvar7${LANG_ITALIAN}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringuninstFailed${LANG_ITALIAN}"La disinstallazione della versione precedente non è riuscita.$\nSi prega di disinstallare ${SOFT_NAME} manualmente prima di continuare."
@@ -283,6 +327,10 @@
LangStringvar3${LANG_PORTUGUESE}"Exemplos de legendas"
LangStringvar4${LANG_PORTUGUESE}"Exemplos de esquemas"
LangStringvar6${LANG_PORTUGUESE}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_PORTUGUESE}"Python for the AI assistant"
LangStringvar7${LANG_PORTUGUESE}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringuninstFailed${LANG_PORTUGUESE}"A desinstalação da versão anterior falhou.$\nPor favor, desinstale ${SOFT_NAME} manualmente antes de continuar."
@@ -307,6 +355,10 @@
LangStringvar3${LANG_ROMANIAN}"Exemple de cartușe"
LangStringvar4${LANG_ROMANIAN}"Exemple de scheme"
LangStringvar5${LANG_ROMANIAN}"Fonturi"
LangStringMcp${LANG_ROMANIAN}"AI assistant (MCP)"
LangStringvar6${LANG_ROMANIAN}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_ROMANIAN}"Python for the AI assistant"
LangStringvar7${LANG_ROMANIAN}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringuninstFailed${LANG_ROMANIAN}"Dezinstalarea versiunii anterioare a eșuat.$\nVă rugăm să dezinstalați ${SOFT_NAME} manual înainte de a continua."
LangStringvar6${LANG_CROATIAN}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_CROATIAN}"Python for the AI assistant"
LangStringvar7${LANG_CROATIAN}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringuninstFailed${LANG_CROATIAN}"Deinstalacija prethodne verzije nije uspjela.$\nMolimo deinstalirajte ${SOFT_NAME} ručno prije nastavka."
@@ -355,6 +411,10 @@
LangStringvar3${LANG_DUTCH}"Voorbeelden van titelblokken"
LangStringvar4${LANG_DUTCH}"Voorbeelden van schema's"
LangStringvar5${LANG_DUTCH}"Lettertypen"
LangStringMcp${LANG_DUTCH}"AI assistant (MCP)"
LangStringvar6${LANG_DUTCH}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_DUTCH}"Python for the AI assistant"
LangStringvar7${LANG_DUTCH}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringuninstFailed${LANG_DUTCH}"Het verwijderen van de vorige versie is mislukt.$\nVerwijder ${SOFT_NAME} handmatig voordat u verdergaat."
LangStringvar6${LANG_DANISH}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_DANISH}"Python for the AI assistant"
LangStringvar7${LANG_DANISH}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringuninstFailed${LANG_DANISH}"Afinstallation af den tidligere version mislykkedes.$\nAfinstaller venligst ${SOFT_NAME} manuelt, inden du fortsætter."
LangStringvar3${LANG_FRENCH}"Exemples de cartouches"
LangStringvar4${LANG_FRENCH}"Exemples de schémas"
LangStringvar5${LANG_FRENCH}"Polices"
LangStringMcp${LANG_FRENCH}"Assistant IA (MCP)"
LangStringvar6${LANG_FRENCH}"Permet à un assistant IA d'ouvrir, vérifier et modifier vos schémas. Nécessite Python : le vôtre, ou celui proposé ci-dessous"
LangStringMcpPython${LANG_FRENCH}"Python pour l'assistant IA"
LangStringvar7${LANG_FRENCH}"Python de python.org, utilisé uniquement par le composant Assistant IA. Inutile si Python est déjà installé (environ 12 Mo)"
LangStringuninstFailed${LANG_FRENCH}"La désinstallation de la version précédente a échoué.$\nVeuillez désinstaller ${SOFT_NAME} manuellement avant de continuer."
LangStringvar6${LANG_HUNGARIAN}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_HUNGARIAN}"Python for the AI assistant"
LangStringvar7${LANG_HUNGARIAN}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringuninstFailed${LANG_HUNGARIAN}"Az előző verzió eltávolítása nem sikerült.$\nKérjük, távolítsa el manuálisan a ${SOFT_NAME} programot, mielőtt folytatná."
@@ -52,6 +56,10 @@
LangStringvar3${LANG_JAPANESE}"表題欄の例"
LangStringvar4${LANG_JAPANESE}"回路図の例"
LangStringvar5${LANG_JAPANESE}"フォント"
LangStringMcp${LANG_JAPANESE}"AI assistant (MCP)"
LangStringvar6${LANG_JAPANESE}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_JAPANESE}"Python for the AI assistant"
LangStringvar7${LANG_JAPANESE}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringvar6${LANG_MONGOLIAN}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_MONGOLIAN}"Python for the AI assistant"
LangStringvar7${LANG_MONGOLIAN}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringvar6${LANG_NORWEGIAN}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_NORWEGIAN}"Python for the AI assistant"
LangStringvar7${LANG_NORWEGIAN}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringuninstFailed${LANG_NORWEGIAN}"Avinstallasjon av forrige versjon mislyktes.$\nVennligst avinstaller ${SOFT_NAME} manuelt før du fortsetter."
@@ -143,6 +159,10 @@
LangStringvar3${LANG_PORTUGUESEBR}"Exemplos de legendas"
LangStringvar4${LANG_PORTUGUESEBR}"Exemplos de esquemas"
LangStringvar6${LANG_PORTUGUESEBR}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_PORTUGUESEBR}"Python for the AI assistant"
LangStringvar7${LANG_PORTUGUESEBR}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringuninstFailed${LANG_PORTUGUESEBR}"A desinstalação da versão anterior falhou.$\nPor favor, desinstale ${SOFT_NAME} manualmente antes de continuar."
@@ -170,6 +190,10 @@
LangStringvar3${LANG_SERBIAN}"Примери заглавља"
LangStringvar4${LANG_SERBIAN}"Примери шема"
LangStringvar5${LANG_SERBIAN}"Фонтови"
LangStringMcp${LANG_SERBIAN}"AI assistant (MCP)"
LangStringvar6${LANG_SERBIAN}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_SERBIAN}"Python for the AI assistant"
LangStringvar7${LANG_SERBIAN}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringuninstFailed${LANG_SERBIAN}"Деинсталација претходне верзије није успела.$\nМолимо деинсталирајте ${SOFT_NAME} ручно пре наставка."
LangStringvar6${LANG_SLOVENIAN}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_SLOVENIAN}"Python for the AI assistant"
LangStringvar7${LANG_SLOVENIAN}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringuninstFailed${LANG_SLOVENIAN}"Odstranitev prejšnje različice ni uspela.$\nPred nadaljevanjem ročno odstranite ${SOFT_NAME}."
@@ -251,6 +283,10 @@
LangStringvar3${LANG_SWEDISH}"Exempel på ritningshuvuden"
LangStringvar4${LANG_SWEDISH}"Exempel på scheman"
LangStringvar5${LANG_SWEDISH}"Teckensnitt"
LangStringMcp${LANG_SWEDISH}"AI assistant (MCP)"
LangStringvar6${LANG_SWEDISH}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_SWEDISH}"Python for the AI assistant"
LangStringvar7${LANG_SWEDISH}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringuninstFailed${LANG_SWEDISH}"Avinstallationen av den föregående versionen misslyckades.$\nAvinstallera ${SOFT_NAME} manuellt innan du fortsätter."
LangStringvar6${LANG_TURKISH}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_TURKISH}"Python for the AI assistant"
LangStringvar7${LANG_TURKISH}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringuninstFailed${LANG_TURKISH}"Önceki sürümün kaldırılması başarısız oldu.$\nDevam etmeden önce lütfen ${SOFT_NAME}'i manuel olarak kaldırın."
LangStringvar6${LANG_UKRAINIAN}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_UKRAINIAN}"Python for the AI assistant"
LangStringvar7${LANG_UKRAINIAN}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
LangStringuninstFailed${LANG_UKRAINIAN}"Видалення попередньої версії завершилося помилкою.$\nБудь ласка, видаліть ${SOFT_NAME} вручну перед продовженням."
LangStringvar6${LANG_SIMPCHINESE}"Lets an AI assistant open, check and edit your drawings. Needs Python: your own, or the one offered below"
LangStringMcpPython${LANG_SIMPCHINESE}"Python for the AI assistant"
LangStringvar7${LANG_SIMPCHINESE}"Python from python.org, used only by the AI assistant component. Not needed if Python is already installed (about 12 MB)"
# In order to do so, uncomment the following line.
#add_definitions(-DTODO_LIST)
# Build with KDE Frameworks.
# Build with KDE Frameworks.
option(BUILD_WITH_KF"Build with KDE Frameworks"ON)
# Precompiled headers for the Qt umbrella headers.
@@ -47,3 +47,8 @@ option(BUILD_WITH_KF "Build with KDE Frameworks" ON)
# compile for everyone else. Leaving it off keeps CI and contributors on the
# strict behaviour, and only developers who opt in trade that for the speed.
option(QET_ENABLE_PCH"Use precompiled headers (developer build speed; may mask missing #includes)"OFF)
# Discussion #599: 3Dconnexion SpaceMouse/SpacePilot pan/zoom support. Off by
# default -- see cmake/find_spacemouse.cmake for the backends and what happens
# when it is on but no library is found.
option(QET_ENABLE_SPACEMOUSE"Build with 3D mouse (3Dconnexion SpaceMouse) support for pan/zoom; needs libspnav or hidapi, see QET_SPACEMOUSE_BACKEND"OFF)
ico/128x128/diagram.png by the QElectroTech team (License CC BY-ND 3.0)
ico/128x128/document-export.png by the QElectroTech team (License CC BY-ND 3.0)
ico/128x128/project.png by the QElectroTech team (License CC BY-ND 3.0)
ico/128x128/terminalstrip.png and configure-shortcuts.png by Jeff Patterson from the QElectroTech team (License CC BY-ND 3.0), rendered from the .svg files beside them
ico/scalable/pdf-import.svg by Jeff Patterson from the QElectroTech team (License CC BY-ND 3.0), laid out like ico/22x22/insert-image.png
ico/scalable/diagram.svg, folio-new.svg, folio-delete.svg, folio-properties.svg, label.svg by Jeff Patterson from the QElectroTech team (License CC BY-ND 3.0), the folio icons redrawn in the same style
ico/256x256/* by Nuri from the QElectroTech team (License CC BY-ND 3.0)
<pathstyle="fill:currentColor;fill-opacity:0.35;stroke:none"d="M 4 5 h 14 v 4 h -14 Z M 7 13 h 8 v 4 h -8 Z"class="ColorScheme-Text"/>
<pathstyle="fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none"d="M 10 1 h 2 v 3 h -2 Z M 10 10 h 2 v 2 h -2 Z M 10 18 h 2 v 3 h -2 Z M 3 4 h 16 v 6 h -16 Z M 4 5 h 14 v 4 h -14 Z M 6 12 h 10 v 6 h -10 Z M 7 13 h 8 v 4 h -8 Z"class="ColorScheme-Text"/>
<pathstyle="fill:currentColor;fill-opacity:0.35;stroke:none"d="M 5 5 h 13 v 4 h -13 Z M 5 13 h 8 v 4 h -8 Z"class="ColorScheme-Text"/>
<pathstyle="fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none"d="M 2 1 h 2 v 20 h -2 Z M 4 4 h 15 v 6 h -15 Z M 5 5 h 13 v 4 h -13 Z M 4 12 h 10 v 6 h -10 Z M 5 13 h 8 v 4 h -8 Z"class="ColorScheme-Text"/>
<pathstyle="fill:currentColor;fill-opacity:0.35;stroke:none"d="M 4 5 h 13 v 4 h -13 Z M 9 13 h 8 v 4 h -8 Z"class="ColorScheme-Text"/>
<pathstyle="fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none"d="M 18 1 h 2 v 20 h -2 Z M 3 4 h 15 v 6 h -15 Z M 4 5 h 13 v 4 h -13 Z M 8 12 h 10 v 6 h -10 Z M 9 13 h 8 v 4 h -8 Z"class="ColorScheme-Text"/>
<pathstyle="fill:currentColor;fill-opacity:0.35;stroke:none"d="M 5 4 h 4 v 13 h -4 Z M 13 9 h 4 v 8 h -4 Z"class="ColorScheme-Text"/>
<pathstyle="fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none"d="M 1 18 h 20 v 2 h -20 Z M 4 3 h 6 v 15 h -6 Z M 5 4 h 4 v 13 h -4 Z M 12 8 h 6 v 10 h -6 Z M 13 9 h 4 v 8 h -4 Z"class="ColorScheme-Text"/>
<pathstyle="fill:currentColor;fill-opacity:0.35;stroke:none"d="M 5 4 h 4 v 14 h -4 Z M 13 7 h 4 v 8 h -4 Z"class="ColorScheme-Text"/>
<pathstyle="fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none"d="M 1 10 h 3 v 2 h -3 Z M 10 10 h 2 v 2 h -2 Z M 18 10 h 3 v 2 h -3 Z M 4 3 h 6 v 16 h -6 Z M 5 4 h 4 v 14 h -4 Z M 12 6 h 6 v 10 h -6 Z M 13 7 h 4 v 8 h -4 Z"class="ColorScheme-Text"/>
<pathstyle="fill:currentColor;fill-opacity:0.35;stroke:none"d="M 5 5 h 4 v 13 h -4 Z M 13 5 h 4 v 8 h -4 Z"class="ColorScheme-Text"/>
<pathstyle="fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none"d="M 1 2 h 20 v 2 h -20 Z M 4 4 h 6 v 15 h -6 Z M 5 5 h 4 v 13 h -4 Z M 12 4 h 6 v 10 h -6 Z M 13 5 h 4 v 8 h -4 Z"class="ColorScheme-Text"/>
<pathstyle="fill:currentColor;fill-opacity:0.35;stroke:none"d="M 2 0 h 1 v 22 h -1 Z M 8 0 h 1 v 22 h -1 Z M 14 0 h 1 v 22 h -1 Z M 20 0 h 1 v 22 h -1 Z M 0 2 h 22 v 1 h -22 Z M 0 8 h 22 v 1 h -22 Z M 0 14 h 22 v 1 h -22 Z M 0 20 h 22 v 1 h -22 Z M 9 9 h 5 v 5 h -5 Z"class="ColorScheme-Text"/>
<pathstyle="fill:currentColor;fill-opacity:1;fill-rule:evenodd;stroke:none"d="M 8 8 h 7 v 7 h -7 Z M 9 9 h 5 v 5 h -5 Z"class="ColorScheme-Text"/>
<pathstyle="fill:#dcdcdc;fill-opacity:0.35;stroke:none"d="M 4 5 h 14 v 4 h -14 Z M 7 13 h 8 v 4 h -8 Z"class="ColorScheme-Text"/>
<pathstyle="fill:#dcdcdc;fill-opacity:1;fill-rule:evenodd;stroke:none"d="M 10 1 h 2 v 3 h -2 Z M 10 10 h 2 v 2 h -2 Z M 10 18 h 2 v 3 h -2 Z M 3 4 h 16 v 6 h -16 Z M 4 5 h 14 v 4 h -14 Z M 6 12 h 10 v 6 h -10 Z M 7 13 h 8 v 4 h -8 Z"class="ColorScheme-Text"/>
<pathstyle="fill:#dcdcdc;fill-opacity:0.35;stroke:none"d="M 5 5 h 13 v 4 h -13 Z M 5 13 h 8 v 4 h -8 Z"class="ColorScheme-Text"/>
<pathstyle="fill:#dcdcdc;fill-opacity:1;fill-rule:evenodd;stroke:none"d="M 2 1 h 2 v 20 h -2 Z M 4 4 h 15 v 6 h -15 Z M 5 5 h 13 v 4 h -13 Z M 4 12 h 10 v 6 h -10 Z M 5 13 h 8 v 4 h -8 Z"class="ColorScheme-Text"/>
<pathstyle="fill:#dcdcdc;fill-opacity:0.35;stroke:none"d="M 4 5 h 13 v 4 h -13 Z M 9 13 h 8 v 4 h -8 Z"class="ColorScheme-Text"/>
<pathstyle="fill:#dcdcdc;fill-opacity:1;fill-rule:evenodd;stroke:none"d="M 18 1 h 2 v 20 h -2 Z M 3 4 h 15 v 6 h -15 Z M 4 5 h 13 v 4 h -13 Z M 8 12 h 10 v 6 h -10 Z M 9 13 h 8 v 4 h -8 Z"class="ColorScheme-Text"/>
<pathstyle="fill:#dcdcdc;fill-opacity:0.35;stroke:none"d="M 5 4 h 4 v 13 h -4 Z M 13 9 h 4 v 8 h -4 Z"class="ColorScheme-Text"/>
<pathstyle="fill:#dcdcdc;fill-opacity:1;fill-rule:evenodd;stroke:none"d="M 1 18 h 20 v 2 h -20 Z M 4 3 h 6 v 15 h -6 Z M 5 4 h 4 v 13 h -4 Z M 12 8 h 6 v 10 h -6 Z M 13 9 h 4 v 8 h -4 Z"class="ColorScheme-Text"/>
<pathstyle="fill:#dcdcdc;fill-opacity:0.35;stroke:none"d="M 5 4 h 4 v 14 h -4 Z M 13 7 h 4 v 8 h -4 Z"class="ColorScheme-Text"/>
<pathstyle="fill:#dcdcdc;fill-opacity:1;fill-rule:evenodd;stroke:none"d="M 1 10 h 3 v 2 h -3 Z M 10 10 h 2 v 2 h -2 Z M 18 10 h 3 v 2 h -3 Z M 4 3 h 6 v 16 h -6 Z M 5 4 h 4 v 14 h -4 Z M 12 6 h 6 v 10 h -6 Z M 13 7 h 4 v 8 h -4 Z"class="ColorScheme-Text"/>
<pathstyle="fill:#dcdcdc;fill-opacity:0.35;stroke:none"d="M 5 5 h 4 v 13 h -4 Z M 13 5 h 4 v 8 h -4 Z"class="ColorScheme-Text"/>
<pathstyle="fill:#dcdcdc;fill-opacity:1;fill-rule:evenodd;stroke:none"d="M 1 2 h 20 v 2 h -20 Z M 4 4 h 6 v 15 h -6 Z M 5 5 h 4 v 13 h -4 Z M 12 4 h 6 v 10 h -6 Z M 13 5 h 4 v 8 h -4 Z"class="ColorScheme-Text"/>
<pathstyle="fill:#dcdcdc;fill-opacity:0.35;stroke:none"d="M 2 0 h 1 v 22 h -1 Z M 8 0 h 1 v 22 h -1 Z M 14 0 h 1 v 22 h -1 Z M 20 0 h 1 v 22 h -1 Z M 0 2 h 22 v 1 h -22 Z M 0 8 h 22 v 1 h -22 Z M 0 14 h 22 v 1 h -22 Z M 0 20 h 22 v 1 h -22 Z M 9 9 h 5 v 5 h -5 Z"class="ColorScheme-Text"/>
<pathstyle="fill:#dcdcdc;fill-opacity:1;fill-rule:evenodd;stroke:none"d="M 8 8 h 7 v 7 h -7 Z M 9 9 h 5 v 5 h -5 Z"class="ColorScheme-Text"/>
<source> (Ctrl pendant le glissement = position libre, sans accrochage à la grille)</source>
<target>(Ctrl tijdens het slepen = vrije positie, zonder vastklikken aan het raster)</target>
</phrase>
<phrase>
<source> -- %1 : mode %2</source>
<target>-- %1 : modus %2</target>
</phrase>
<phrase>
<source> ; point rouge : glisser pour repositionner le centre de rotation</source>
<target>; rood punt: slepen om het draaipunt te verplaatsen</target>
</phrase>
<phrase>
<source> ; point turquoise : arc</source>
<target>; turquoise punt: boog</target>
</phrase>
<phrase>
<source> ; un bord : inclinaison</source>
<target>; een rand: kanteling</target>
</phrase>
<phrase>
<source> °</source>
<target>°</target>
</phrase>
<phrase>
<source> — Cliquer : mode %1</source>
<target>— Klikken: modus %1</target>
</phrase>
<phrase>
<source>, Alt = créer des poignées</source>
<target>, Alt = handgrepen creëren</target>
</phrase>
<phrase>
<source>, Alt = détacher en polyligne</source>
<target>, Alt = losmaken als polylijn</target>
</phrase>
<phrase>
<source>Ajoute une courbe de Bézier sur le folio actuel</source>
<target>Voegt een Béziercurve toe op het huidige blad</target>
</phrase>
<phrase>
<source>Ajouter la borne %1</source>
<target>Klem %1 toevoegen</target>
</phrase>
<phrase>
<source>Ajouter un point à une courbe</source>
<target>Een punt aan een curve toevoegen</target>
</phrase>
<phrase>
<source>Ajouter une courbe</source>
<target>Een curve toevoegen</target>
</phrase>
<phrase>
<source>Angle</source>
<target>Hoek</target>
</phrase>
<phrase>
<source>Anguleux</source>
<target>Hoekig</target>
</phrase>
<phrase>
<source>Aperçu</source>
<target>Voorbeeld</target>
</phrase>
<phrase>
<source>Arrondir les coins d'%1</source>
<target>Hoeken van %1 afronden</target>
</phrase>
<phrase>
<source>Borne 1</source>
<target>Klem 1</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Borne 2</source>
<target>Klem 2</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Catégorie</source>
<target>Categorie</target>
</phrase>
<phrase>
<source>Ce format ne prend pas en charge la transparence : l'image sera enregistrée telle qu'elle était avant l'application de la couleur transparente. Continuer ?</source>
<target>Dit formaat ondersteunt geen transparantie: de afbeelding wordt opgeslagen zoals ze was vóór het toepassen van de transparante kleur. Doorgaan?</target>
</phrase>
<phrase>
<source>Champ de texte</source>
<target>Tekstveld</target>
</phrase>
<phrase>
<source>Clic : positionner à la taille d'origine. Cliquer-glisser : positionner et redimensionner. Clic droit : pivoter de 90°. Ctrl+molette : ajuster la taille.</source>
<target>Klik: op oorspronkelijke grootte plaatsen. Klikken en slepen: plaatsen en formaat wijzigen. Rechtsklik: 90° draaien. Ctrl+muiswiel: formaat aanpassen.</target>
</phrase>
<phrase>
<source>Clic gauche : point suivant ; double-clic ou Entrée : terminer ; clic droit : annuler le dernier point</source>
<target>Linkerklik: volgend punt; dubbelklik of Enter: beëindigen; rechtsklik: laatste punt annuleren</target>
</phrase>
<phrase>
<source>Clic gauche : positionner le coin opposé (Maj = carré, Ctrl = depuis le centre + position libre, Ctrl+Maj = carré centré) ; clic droit : annuler</source>
<source>Clic gauche : positionner le coin opposé (Maj = cercle, Ctrl = depuis le centre + position libre, Ctrl+Maj = cercle centré) ; clic droit : annuler</source>
<source>Clic gauche : positionner le point de départ (Ctrl = position libre)</source>
<target>Linkerklik: startpunt plaatsen (Ctrl = vrije positie)</target>
</phrase>
<phrase>
<source>Clic gauche : positionner le point final (Ctrl = position libre) ; clic droit : annuler</source>
<target>Linkerklik: eindpunt plaatsen (Ctrl = vrije positie); rechtsklik: annuleren</target>
</phrase>
<phrase>
<source>Clic gauche : positionner le premier coin (Ctrl = point central, position libre)</source>
<target>Linkerklik: eerste hoek plaatsen (Ctrl = middelpunt, vrije positie)</target>
</phrase>
<phrase>
<source>Clic gauche : positionner le premier point (Ctrl = position libre)</source>
<target>Linkerklik: eerste punt plaatsen (Ctrl = vrije positie)</target>
</phrase>
<phrase>
<source>Clic: point anguleux. Cliquer-glisser: point courbe. Clic sur le premier point: fermer. Échap/Entrée: terminer. Clic droit: annuler le dernier point.</source>
<target>Klik: hoekpunt. Klikken en slepen: curvepunt. Klik op het eerste punt: sluiten. Esc/Enter: beëindigen. Rechtsklik: laatste punt annuleren.</target>
</phrase>
<phrase>
<source>Cliquer</source>
<target>Klikken</target>
</phrase>
<phrase>
<source>Cliquer : mode %1</source>
<target>Klikken: modus %1</target>
</phrase>
<phrase>
<source>Cliquez pour choisir une couleur</source>
<target>Klik om een kleur te kiezen</target>
</phrase>
<phrase>
<source>Cliquez sur l'image pour ajouter une couleur. Ajustez la tolérance de chaque couleur avec son curseur, ou cliquez sur × pour la retirer.</source>
<target>Klik op de afbeelding om een kleur toe te voegen. Pas de tolerantie van elke kleur aan met de schuifregelaar, of klik op × om ze te verwijderen.</target>
</phrase>
<phrase>
<source>Cliquez sur l'image pour choisir une couleur</source>
<target>Klik op de afbeelding om een kleur te kiezen</target>
</phrase>
<phrase>
<source>Coins arrondis</source>
<target>Afgeronde hoeken</target>
</phrase>
<phrase>
<source>Composant 1</source>
<target>Component 1</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Composant 2</source>
<target>Component 2</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Convertir %1 en courbe de Bézier</source>
<target>%1 omzetten naar Béziercurve</target>
</phrase>
<phrase>
<source>Convertir %1 en polyligne</source>
<target>%1 omzetten naar polylijn</target>
</phrase>
<phrase>
<source>Convertir en courbe de Bézier</source>
<target>Omzetten naar Béziercurve</target>
</phrase>
<phrase>
<source>Convertir en polyligne</source>
<target>Omzetten naar polylijn</target>
</phrase>
<phrase>
<source>Couleur transparente</source>
<target>Transparante kleur</target>
</phrase>
<phrase>
<source>Couleur transparente...</source>
<target>Transparante kleur...</target>
</phrase>
<phrase>
<source>Courant nominal</source>
<target>Nominale stroom</target>
</phrase>
<phrase>
<source>Deplacer le centre de rotation</source>
<target>Draaipunt verplaatsen</target>
</phrase>
<phrase>
<source>Description</source>
<target>Beschrijving</target>
</phrase>
<phrase>
<source>Distance in pixels between the label and the slave cross reference</source>
<target>Afstand in pixels tussen het label en de slave-kruisverwijzing</target>
</phrase>
<phrase>
<source>Distance label - slave :</source>
<target>Afstand label - slave:</target>
</phrase>
<phrase>
<source>Définir une couleur transparente</source>
<target>Een transparante kleur instellen</target>
</phrase>
<phrase>
<source>Déformer une courbe</source>
<target>Een curve vervormen</target>
</phrase>
<phrase>
<source>Déplacer le centre de rotation d'une image</source>
<target>Het draaipunt van een afbeelding verplaatsen</target>
</phrase>
<phrase>
<source>Déverrouillé : largeur et hauteur peuvent être modifiées indépendamment. Cliquer pour verrouiller.</source>
<target>Ontgrendeld: breedte en hoogte kunnen onafhankelijk worden gewijzigd. Klik om te vergrendelen.</target>
<source>Glisser le point violet : arrondir les coins</source>
<target>Sleep het paarse punt: hoeken afronden</target>
</phrase>
<phrase>
<source>Glisser un coin : pivoter (Maj = par pas de 15°) ; glisser un bord : incliner (Maj = par pas de 15°) ; point rouge : déplacer le centre de rotation</source>
<target>Sleep een hoek: draaien (Shift = stappen van 15°); sleep een rand: kantelen (Shift = stappen van 15°); rood punt: draaipunt verplaatsen</target>
</phrase>
<phrase>
<source>Glisser un coin/bord : redimensionner (Ctrl = depuis le centre, Maj = conserver les proportions)</source>
<target>Sleep een hoek/rand: formaat wijzigen (Ctrl = vanuit het midden, Shift = verhoudingen behouden)</target>
</phrase>
<phrase>
<source>Glisser un coin/bord : redimensionner (Ctrl = depuis le centre, Maj = proportions, Alt = détacher en polyligne)</source>
<target>Sleep een hoek/rand: formaat wijzigen (Ctrl = vanuit het midden, Shift = verhoudingen, Alt = losmaken als polylijn)</target>
</phrase>
<phrase>
<source>Glisser un point : le déplacer</source>
<target>Sleep een punt: het verplaatsen</target>
</phrase>
<phrase>
<source>Glisser une extrémité : la déplacer</source>
<target>Sleep een uiteinde: het verplaatsen</target>
</phrase>
<phrase>
<source>Glisser une poignée ou la courbe : déformer (Alt = briser la tangente) ; Alt+glisser un point anguleux : créer des poignées ; clic droit : menu du nœud le plus proche</source>
<target>Sleep een handgreep of de curve: vervormen (Alt = de raaklijn breken); Alt+sleep een hoekpunt: handgrepen creëren; rechtsklik: menu van het dichtstbijzijnde knooppunt</target>
<source>Images non incluses dans l'export DXF</source>
<target>Afbeeldingen niet inbegrepen in de DXF-export</target>
<definition>message box title</definition>
</phrase>
<phrase>
<source>Impossible d'enregistrer l'image à cet emplacement.</source>
<target>Kan de afbeelding niet op die locatie opslaan.</target>
</phrase>
<phrase>
<source>Impossible d'enregistrer la nomenclature dans %1.
%2</source>
<target>Kan de stuklijst niet opslaan in %1.
%2</target>
</phrase>
<phrase>
<source>Impossible de charger l'image.</source>
<target>Kan de afbeelding niet laden.</target>
</phrase>
<phrase>
<source>Inclinaison X</source>
<target>Kanteling X</target>
</phrase>
<phrase>
<source>Inclinaison Y</source>
<target>Kanteling Y</target>
</phrase>
<phrase>
<source>Incliner %1</source>
<target>%1 kantelen</target>
</phrase>
<phrase>
<source>Incliner une image</source>
<target>Een afbeelding kantelen</target>
</phrase>
<phrase>
<source>La limite fixée pour cet élément maître est atteinte (Limite: %1).
Voulez-vous tout de même lier ce contact esclave ?</source>
<target>De limiet voor dit hoofdelement is bereikt (Limiet: %1).
Wilt u dit hulpcontact toch koppelen?</target>
</phrase>
<phrase>
<source>Largeur</source>
<target>Breedte</target>
</phrase>
<phrase>
<source>Le format DXF utilisé ici (AC1006) ne permet pas d'inclure d'image. Les images seront représentées uniquement par un rectangle de contour (position, taille, rotation et inclinaison conservées), sans le contenu de l'image.</source>
<target>Het hier gebruikte DXF-formaat (AC1006) laat niet toe afbeeldingen op te nemen. Afbeeldingen worden enkel voorgesteld door een omtrekrechthoek (positie, grootte, rotatie en kanteling behouden), zonder de inhoud van de afbeelding.</target>
<definition>message box content</definition>
</phrase>
<phrase>
<source>Lisse</source>
<target>Vloeiend</target>
</phrase>
<phrase>
<source>Liste de câblage</source>
<target>Bekabelingslijst</target>
<definition>window title</definition>
</phrase>
<phrase>
<source>Liste de câblage (base de données)</source>
<source>Ce format ne prend pas en charge la transparence : l'image sera enregistrée telle qu'elle était avant l'application de la couleur transparente. Continuer ?</source>
<target>Este formato não suporta transparência: a imagem será salva como estava antes da aplicação da cor transparente. Continuar?</target>
</phrase>
<phrase>
<source>Champ de texte</source>
<target>Campo de texto</target>
</phrase>
<phrase>
<source>Composant 1</source>
<target>Componente 1</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Composant 2</source>
<target>Componente 2</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Convertir %1 en courbe de Bézier</source>
<target>Converter %1 em curva de Bézier</target>
</phrase>
<phrase>
<source>Convertir en courbe de Bézier</source>
<target>Converter em curva de Bézier</target>
</phrase>
<phrase>
<source>Courant nominal</source>
<target>Corrente nominal</target>
</phrase>
<phrase>
<source>Distance in pixels between the label and the slave cross reference</source>
<target>Distância em pixels entre o rótulo e a referência cruzada do escravo</target>
<source>Images non incluses dans l'export DXF</source>
<target>Imagens não incluídas na exportação DXF</target>
<definition>message box title</definition>
</phrase>
<phrase>
<source>Impossible d'enregistrer l'image à cet emplacement.</source>
<target>Não foi possível salvar a imagem neste local.</target>
</phrase>
<phrase>
<source>Impossible d'enregistrer la nomenclature dans %1.
%2</source>
<target>Não foi possível salvar a lista de materiais em %1.
%2</target>
</phrase>
<phrase>
<source>Impossible de charger l'image.</source>
<target>Não foi possível carregar a imagem.</target>
</phrase>
<phrase>
<source>Incliner %1</source>
<target>Inclinar %1</target>
</phrase>
<phrase>
<source>Incliner une image</source>
<target>Inclinar uma imagem</target>
</phrase>
<phrase>
<source>La limite fixée pour cet élément maître est atteinte (Limite: %1).
Voulez-vous tout de même lier ce contact esclave ?</source>
<target>O limite definido para este elemento mestre foi atingido (Limite: %1).
Deseja vincular este contato escravo mesmo assim?</target>
</phrase>
<phrase>
<source>Largeur</source>
<target>Largura</target>
</phrase>
<phrase>
<source>Le format DXF utilisé ici (AC1006) ne permet pas d'inclure d'image. Les images seront représentées uniquement par un rectangle de contour (position, taille, rotation et inclinaison conservées), sans le contenu de l'image.</source>
<target>O formato DXF usado aqui (AC1006) não permite incluir imagens. As imagens serão representadas apenas por um retângulo de contorno (posição, tamanho, rotação e inclinação preservados), sem o conteúdo da imagem.</target>
<definition>message box content</definition>
</phrase>
<phrase>
<source>Liste de câblage</source>
<target>Lista de cabeamento</target>
<definition>window title</definition>
</phrase>
<phrase>
<source>Liste de câblage (base de données)</source>
<target>Lista de cabeamento (banco de dados)</target>
<source> Contacts : NO : %1, NC : %2, inverseurs : %3, autres : %4
</source>
<target>触点:常开:%1,常闭:%2,转换:%3,其他:%4
</target>
</phrase>
<phrase>
<source> Contacts : NO : %1/%2, NC : %3/%4, inverseurs : %5/%6, autres : %7/%8
</source>
<target>触点:常开:%1/%2,常闭:%3/%4,转换:%5/%6,其他:%7/%8
</target>
</phrase>
<phrase>
<source> %</source>
<target>%</target>
</phrase>
<phrase>
<source> (Ctrl pendant le glissement = position libre, sans accrochage à la grille)</source>
<target>(拖动时按 Ctrl = 自由位置,不吸附到网格)</target>
</phrase>
<phrase>
<source> -- %1 : mode %2</source>
<target>-- %1:%2 模式</target>
</phrase>
<phrase>
<source> ; point rouge : glisser pour repositionner le centre de rotation</source>
<target>;红点:拖动以重新定位旋转中心</target>
</phrase>
<phrase>
<source> ; point turquoise : arc</source>
<target>;青色点:圆弧</target>
</phrase>
<phrase>
<source> ; un bord : inclinaison</source>
<target>;边:倾斜</target>
</phrase>
<phrase>
<source> °</source>
<target>°</target>
</phrase>
<phrase>
<source> — Cliquer : mode %1</source>
<target>— 单击:%1 模式</target>
</phrase>
<phrase>
<source>, Alt = créer des poignées</source>
<target>,Alt = 创建控制柄</target>
</phrase>
<phrase>
<source>, Alt = détacher en polyligne</source>
<target>,Alt = 分离为多段线</target>
</phrase>
<phrase>
<source>Ajoute une courbe de Bézier sur le folio actuel</source>
<target>在当前图纸上添加贝塞尔曲线</target>
</phrase>
<phrase>
<source>Ajouter un point à une courbe</source>
<target>向曲线添加点</target>
</phrase>
<phrase>
<source>Ajouter une courbe</source>
<target>添加曲线</target>
</phrase>
<phrase>
<source>Angle</source>
<target>角度</target>
</phrase>
<phrase>
<source>Anguleux</source>
<target>角点</target>
</phrase>
<phrase>
<source>Aperçu</source>
<target>预览</target>
</phrase>
<phrase>
<source>Arrondir les coins d'%1</source>
<target>将%1的角变圆</target>
</phrase>
<phrase>
<source>Borne 1</source>
<target>端子1</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Borne 2</source>
<target>端子2</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Catégorie</source>
<target>类别</target>
</phrase>
<phrase>
<source>Ce format ne prend pas en charge la transparence : l'image sera enregistrée telle qu'elle était avant l'application de la couleur transparente. Continuer ?</source>
<target>此格式不支持透明度:图像将按应用透明色之前的状态保存。是否继续?</target>
</phrase>
<phrase>
<source>Champ de texte</source>
<target>文本字段</target>
</phrase>
<phrase>
<source>Clic : positionner à la taille d'origine. Cliquer-glisser : positionner et redimensionner. Clic droit : pivoter de 90°. Ctrl+molette : ajuster la taille.</source>
<source>Clic gauche : positionner le coin opposé (Maj = carré, Ctrl = depuis le centre + position libre, Ctrl+Maj = carré centré) ; clic droit : annuler</source>
<source>Clic gauche : positionner le coin opposé (Maj = cercle, Ctrl = depuis le centre + position libre, Ctrl+Maj = cercle centré) ; clic droit : annuler</source>
<source>Clic gauche : positionner le point de départ (Ctrl = position libre)</source>
<target>左键单击:放置起点(Ctrl = 自由位置)</target>
</phrase>
<phrase>
<source>Clic gauche : positionner le point final (Ctrl = position libre) ; clic droit : annuler</source>
<target>左键单击:放置终点(Ctrl = 自由位置);右键单击:取消</target>
</phrase>
<phrase>
<source>Clic gauche : positionner le premier coin (Ctrl = point central, position libre)</source>
<target>左键单击:放置第一个角(Ctrl = 中心点,自由位置)</target>
</phrase>
<phrase>
<source>Clic gauche : positionner le premier point (Ctrl = position libre)</source>
<target>左键单击:放置第一个点(Ctrl = 自由位置)</target>
</phrase>
<phrase>
<source>Clic: point anguleux. Cliquer-glisser: point courbe. Clic sur le premier point: fermer. Échap/Entrée: terminer. Clic droit: annuler le dernier point.</source>
<source>Cliquez sur l'image pour ajouter une couleur. Ajustez la tolérance de chaque couleur avec son curseur, ou cliquez sur × pour la retirer.</source>
<source>Glisser : redimensionner (Maj = conserver les proportions, Ctrl = depuis le centre)</source>
<target>拖动:调整大小(Shift = 保持比例,Ctrl = 从中心)</target>
</phrase>
<phrase>
<source>Glisser : repositionner le centre de rotation (Ctrl = position libre)</source>
<target>拖动:重新定位旋转中心(Ctrl = 自由位置)</target>
</phrase>
<phrase>
<source>Glisser : rotation (Ctrl = position libre, Maj = 15°)</source>
<target>拖动:旋转(Ctrl = 自由位置,Shift = 15°)</target>
</phrase>
<phrase>
<source>Glisser le point violet : arrondir les coins</source>
<target>拖动紫色点:圆角</target>
</phrase>
<phrase>
<source>Glisser un coin : pivoter (Maj = par pas de 15°) ; glisser un bord : incliner (Maj = par pas de 15°) ; point rouge : déplacer le centre de rotation</source>
<source>Glisser une extrémité : la déplacer</source>
<target>拖动端点:移动它</target>
</phrase>
<phrase>
<source>Glisser une poignée ou la courbe : déformer (Alt = briser la tangente) ; Alt+glisser un point anguleux : créer des poignées ; clic droit : menu du nœud le plus proche</source>
<source>Images non incluses dans l'export DXF</source>
<target>图像未包含在 DXF 导出中</target>
<definition>message box title</definition>
</phrase>
<phrase>
<source>Impossible d'enregistrer l'image à cet emplacement.</source>
<target>无法将图像保存到该位置。</target>
</phrase>
<phrase>
<source>Impossible d'enregistrer la nomenclature dans %1.
%2</source>
<target>无法将物料清单保存到 %1。
%2</target>
</phrase>
<phrase>
<source>Impossible de charger l'image.</source>
<target>无法加载图像。</target>
</phrase>
<phrase>
<source>Inclinaison X</source>
<target>X 倾斜</target>
</phrase>
<phrase>
<source>Inclinaison Y</source>
<target>Y 倾斜</target>
</phrase>
<phrase>
<source>Incliner %1</source>
<target>倾斜 %1</target>
</phrase>
<phrase>
<source>Incliner une image</source>
<target>倾斜图像</target>
</phrase>
<phrase>
<source>La limite fixée pour cet élément maître est atteinte (Limite: %1).
Voulez-vous tout de même lier ce contact esclave ?</source>
<target>已达到此主元件的限制(限制:%1)。
是否仍要链接此从属触点?</target>
</phrase>
<phrase>
<source>Largeur</source>
<target>宽度</target>
</phrase>
<phrase>
<source>Le format DXF utilisé ici (AC1006) ne permet pas d'inclure d'image. Les images seront représentées uniquement par un rectangle de contour (position, taille, rotation et inclinaison conservées), sans le contenu de l'image.</source>
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.