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>
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>
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
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
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>
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>
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
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.
- 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.
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>
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.
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>
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>
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>
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>
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>
When duplicating a diagram page, copied slave elements retain stale
data from the source: labels, descriptions, link references, and PLC
master information (type, address, function, comment, cross-ref, timer
values) remain in the copies.
Fix by adding clearPendingLinks() to Element to prevent copies from
linking back to source elements via stale UUIDs, and by cleaning up
copied element data after fromXml():
- Slaves always lose their label, formula, comment, location, and
PLC master data, since their text comes from a master element which
is not available on the copy. The displayed text on slave elements
is cleared directly via setPlainText(), but UserText items (free
text typed by the user) are preserved.
- For PLC slaves, setElementInformations() is called without
m_block_alignment so that elementInfoChanged() can run
finishAlignment() to correctly adjust text positions for the
cleared content.
- Non-slave elements respect the existing erase-label-on-copy
preference, same as PasteDiagramCommand::redo().
- Conductor labels are also reset when erase-label-on-copy is
active, matching PasteDiagramCommand::redo() (issue #413).
- Text alignment is preserved by wrapping setElementInformations()
with m_block_alignment for non-slave elements, same as
Element::fromXml().
QET had no icon theme: the 446 entries of the icon table and the 116
iconsets in .ui files each named a resource path, so an icon could only
ever be one file, and a variant for another palette or a vector source
had nowhere to go (GitHub #466, #690, #870). This adds the theme layout
without changing a single pixel; a dark variant comes in a follow-up.
The theme "qet" follows the freedesktop layout Qt's icon loader
understands. misc/make_icon_themes.py generates ico/icon-themes.qrc,
which aliases the existing ico/<size>/<name>.png files into
themes/qet/<size>/<name>.png, and ico/themes/qet/index.theme. No file
moves. The four table entries that paired a 16 pixel file with a 22
pixel file of another name (ConductorSettings, DiagramAdd,
DiagramDelete, DialogInformation) get the 22 pixel file aliased under
the 16 pixel name.
QETApp::initIconTheme() registers the theme before initIcons() and makes
it current on every platform, so a desktop icon theme cannot replace
QET's icons. Icons are then looked up by name: QIcon::fromTheme() in
qeticons.cpp and in the few places that built a QIcon from a resource
path directly, and theme="..." on the iconsets in .ui files, with the
resource path kept as fallback. Flags, color swatches, application and
MIME icons stay on their paths.
One entry does not go through the theme. The elements panel draws the
project root with ProjectFileGP in the 50 pixel slot it reserves for
element previews, and the name "project" also carries the 128 pixel
file the configuration dialog uses. On a 2x display Qt's loader picks
that file for a 50 pixel request and fills the slot. ProjectFileGP
loads the 16 and 22 pixel files directly, as before.
tests/qttest/tst_qeticons: every name in the theme resolves, the four
aliases resolve at 22 pixels, a Fusion tool button shows its icon at
3:1 with disabled weaker than enabled, and the project root icon stays
at 22 pixels or less when asked for 50 at 2x while the configuration
dialog still gets its 128 pixel file. The rendering helpers shared
with tst_qetpalette moved to tests/qttest/inkcontrast.h.
Addresses scorpio810's review of PR #890 (bugtracker #734):
- The four cas "3"/"4" bridge-skip guards now compare qRound()ed
coordinates, matching how the bridge coordinate itself is computed --
an exact != would miss a pair already grid-equal after rounding but
off by a sub-pixel remainder, and still route a degenerate bridge for
it. Verified no behavior change on the shipped corpus: per-file
self-retrace counts are identical before/after (every coordinate
there already lands exactly on-grid).
- Added tests/qttest/tst_conductorselfretrace.cpp, fixture
qet_bug_repro_resaved.qet (the report's own canonical reproduction):
exports it via the built binary's --export-svg and asserts no
conductor path is self-retracing. Confirmed it actually catches the
regression, not just passes vacuously -- reverted conductor.cpp to
master and reran: fails, 1 self-retracing path found.
Not changed in code: the cas "4" descending-branch dead-code note (kept
for symmetry, as already agreed) and the schema_unifilaire_voltaique2.qet
trade-off (flagged for the reviewer's own visual check).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Add !parentGroup() guard to stale xref removal in updateXref()
to prevent deleting xref data that ElementTextItemGroup::updateXref()
just stored for a grouped text item.
- Replace QStringLiteral("xref") with QETInformation::ELMT_XREF
in linkelementcommand.cpp and masterelement.cpp.
- Fix indentation in plclinkwidget.cpp.
- Element::reloadPicture() now returns a ReloadPictureResult and never
clears the current drawing before a successful rebuild: a missing or
unreadable definition leaves the element as it was instead of blank.
- Elements whose size, hotspot or terminals (added, removed or moved)
differ from the new definition are not redrawn: the new drawing would
no longer match their bounding rect and live terminals.
- The action lists those elements and warns that they must be removed
and re-inserted, which deletes the conductors already connected to
them.
- Status tip states the action is not undoable.
Terminal::~Terminal() called qDeleteAll(m_conductors_list) on the live
member. Each Conductor destructor calls removeConductor() on both of its
terminals, and that removes the conductor from the same list qDeleteAll
is iterating. Mutating a QList while iterating it is undefined
behaviour; with two or more conductors on one terminal (terminal strips,
bridged terminals) it can skip a delete or delete one conductor twice,
which leaves another conductor's terminal1/terminal2 pointing at freed
memory.
The pattern dates from a00404bc9 (2021), which replaced a foreach loop
(iterating an implicit copy) with a direct qDeleteAll. It went unnoticed
until the deterministic sort keys added to Diagram::toXml() in #844
started reading pos() on both terminals of every conductor on every
save, including the periodic backup, which turned the stale pointer into
an EXC_BAD_ACCESS in QGraphicsItem::pos() while deleting an element.
Copy the list first and delete from the copy, restoring the pre-2021
behaviour. An isolated regression test (a hub terminal with 2 to 8
conductors, under AddressSanitizer) did not trigger the failure with the
old code, so none is included; the crash analysis and that attempt are
recorded in jp2images/qelectrotech-source-mirror#1.
generateConductorPath()'s cas "3"/"4" branches each insert a two-point
bridge along one axis, at a coordinate on the other axis computed as
the midpoint of depart/arrivee and then snapped to the routing grid by
a loop that walks it strictly downward until it divides evenly.
When depart and arrivee already agree on the axis the bridge would run
along, no bridge is needed at all -- the midpoint starts on the
correct, already-shared value. But the snap can still walk it off that
value, or, when it happens to already sit on the grid, the two bridge
points just duplicate depart and arrivee outright. Either way the
conductor renders with an unnecessary out-and-back excursion or a
small looping detour at a join that needed neither.
Fix: skip the bridge in exactly that degenerate case, per branch, on
the axis that branch actually bridges on. descendant and montant are
not mirror images of each other in which axis each cas guards on --
each site says so.
Verified against the report's own canonical reproduction
(qet_bug_repro_resaved.qet from the linked gist): 1 self-retracing
path -> 0. Full corpus of 24 shipped example projects (stored
segments, as shipped, not regenerated): 74 -> 65 self-retracing
conductor paths, zero regressions -- only two files changed, both
improved.
One disclosed trade-off, found while checking for regressions: in
schema_unifilaire_voltaique2.qet, 8 terminal pairs closer together
than twice the extension length (docked stubs already cross before
any bridge is considered) go from an existing small rectangular-loop
artifact to a straight out-and-back retrace covering the full gap --
same count of defective paths (8 -> 8), a different shape, still no
connectivity change either way. Not chased further; a real fix for
that narrower "crossed stubs" case is a separate piece of work.
The shipped affuteuse_250h.qet's own 12 self-retracing paths (verified
by stripping all 185 stored <segment> elements and forcing full
regeneration) are unchanged by this fix -- they are a structurally
different point-count signature (a 4-point back-and-forth reachable
from cas "1"/"2", not the cas "3"/"4" grid-snap bridge this fixes),
consistent with what the issue thread already flagged as a separate,
undiagnosed mechanism.
Refs #734 (own root-cause comment, 2026-08-13).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A placed element is drawn once from its definition at construction --
buildFromXml() only turns terminal/input/dynamic_text tags into live
child objects, every other primitive (line, rect, ellipse, polygon,
arc, text) is pre-rendered into a QPicture by ElementPictureFactory,
cached forever under the element's uuid with no invalidation path
anywhere in the codebase. Edit and save a symbol's drawing and every
already-placed instance keeps showing the old one until the project
is closed and reopened.
Fix, scoped to what is safe to do without ever risking a conductor or
a dynamic text's per-instance state:
- ElementPictureFactory::dropCache(location) forgets the cached
drawing for one location, so the next fetch rebuilds it from the
definition's current content.
- Element::reloadPicture() re-fetches and repaints one instance.
- Projet > "Recharger les dessins des éléments": walks every diagram,
drops each distinct location's cache once, then reloads every placed
instance.
Deliberately does not touch terminals or dynamic texts -- a definition
whose terminal positions moved still needs the existing remove-and-
reinsert workflow, since terminals are what conductors are attached to
and a wrong guess there would silently misconnect wires.
Verified: build clean, ctest 6/6. Triggered the new action on a real,
densely-wired project (76 elements) via exact keyboard-menu navigation
cross-checked against the menu's own addAction order -- ran to
completion, correct confirmation dialog, no crash, diagram unchanged
and uncorrupted afterward. Could not complete a live edit-and-watch-
it-update trace: opening the element editor on a selected item via
GUI automation was unreliable in this environment (same class of
friction as PR #888), and this sandbox has no file-based (common://)
element to mutate on disk as a shortcut -- every example project
embeds its elements. The mechanism itself is traced correct:
ElementsLocation::xml() for an embed:// location reads the project's
live in-memory collection DOM on every call, so a dropped cache
rebuilds from whatever was most recently saved.
Refs #802 (own analysis comment, 2026-08-31).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The F2 color editor recolors one conductor, but the next one drawn
falls straight back to defaultConductorProperties -- the choice made
via F2 is lost the moment you place another wire, and lost again on
restart. LastUsedStyle already solves the same problem for shapes
(pen/brush) and free text (font), session-scoped and deliberately not
QSettings-backed; this extends it with a conductor color, following
the identical has/get/set shape.
F2's handler records the color after pushing its undo command.
Conductor's constructor -- the one place a new conductor's properties
are set from defaultConductorProperties -- overrides just the color
field when a session color has been recorded, leaving every other
default (style, thickness, text) alone.
Verified: build clean, ctest 6/6. Could not get a reliable headless
GUI trace of "F2 one wire, draw a new one, see it inherit the color"
-- drag-and-drop element placement under Xvfb was unreliable in this
environment (one attempt did nothing, another drew an unintended long
conductor undo didn't fully clear). The code path is otherwise
identical to the already-shipped shape/text mechanism this mirrors.
Refs #461.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two additional bugs found after the initial xref cleanup:
1. Undo after unlink did not restore PLC variables (PLC_TYPE,
PLC_ADDRESS, etc.) because PlcLinkWidget::on_m_unlink_pb_clicked()
never stored the current group index in the LinkElementCommand.
makeLink() only populates PLC fields when group_idx >= 0, so
the slave was re-linked but appeared empty. Fixed by reading the
group index from the master via groupIndexForElement() and calling
setGroupIndex() before unlinking.
2. m_update_slave_Xref_connection was only cleared inside the
if(m_slave_Xref_item) block in the updateXref() cleanup path.
For AlignHCenter (Text field) position, no m_slave_Xref_item
is ever created — the connections are stored in
m_update_slave_Xref_connection but never cleaned up on unlink.
On re-link, the old stale entries prevented new connections from
being established. Fixed by clearing the list unconditionally
in the cleanup path.
Conductor::paint() drew every junction as a fixed 3.0-unit ellipse,
regardless of how wide the conductor carrying it is. The conductor width is
user-settable from 0.4 to 20.0, so at anything above about 3.0 the dot is
narrower than the line it sits on and disappears entirely -- exactly when a
junction most needs to be legible.
The dot now scales with m_properties.cond_size, floored at the historic 3.0
so nothing changes at or below the default width of 1.0. Only the wide
conductors the report is about are affected.
cond_size is used rather than the pen width because the pen is inflated by 4
while the mouse is over the conductor; the junction should not grow on
hover.
Measured with a temporary trace over examples/741.qet: at the default width
the diameter stays 3.00, and with condsize="5" it becomes 15.00. Visually,
a PNG export of that widened project shows two junctions that were invisible
under the line rendering as clear dots. ctest 4/4, Qt 5.15.18.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>