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.
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.
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>