Fix multiple issues introduced by commit 55c2c0df9 (interactive paste):
Paste placement (diagrameventaddpaste):
- Items now load at their original XML coordinates instead of being
snapped to the cursor position, preventing jumps on initial placement
- Use delta-based movement: record the actual grid-snapped cursor
position on first mouseMoveEvent as baseline, then compute
grid-snapped deltas from there
- Bypass Diagram::snapToGrid() in moveTo() to avoid Ctrl modifier
causing pixel-snapping instead of grid-snapping
- Remove moveTo() from mouseReleaseEvent to prevent a final jump
on click
- Warp cursor to group bounding rect origin for visual feedback
Paste command (diagramcommands):
- Always clear PLC slave data (type, address, function, comment,
cross-ref, label, TC, T1-T4) on paste regardless of the
erase-label-on-copy user preference
- Block alignment (m_block_alignment / blockAlignmentUpdate) before
setElementInformations() for both slaves and non-slaves, preventing
finishAlignment() from shifting right/center-aligned text items
- Clear non-UserText items directly after setElementInformations()
for slaves as a safety net
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.
QETApp::checkBackupFiles() only reached checkCrashDump() when there was
nothing to recover:
if (stale_files.isEmpty()) {
checkCrashDump();
return;
}
A crash with a project open always leaves a stale KAutoSaveFile, so on the
next launch the recovery prompt won every time and the dump sat on disk
unoffered -- the report was unreachable in exactly the case it is most
wanted. Reported by ChuckNr11 as a side note in #898: "the report appeared
only once despite there being 10 or more crashes". It appears on the runs
that happen to have nothing to recover.
Discussion #644 step 5 asks that the two prompts never show at the same
time, which this keeps: the recovery prompt is answered first, then the
report. The recovery half moves into offerBackupFiles() so both paths fall
through to the same place.
Verified under Xvfb, from a real crash state (SIGABRT with a project open,
leaving both a stale file and a 5.4 KB crash_dump.log): the recovery prompt
appears, and dismissing it now brings up "Rapport de plantage" carrying the
version, git SHA, OS, Qt version and the log ring. Before this change the
report never appeared -- that half rests on the four lines above rather
than on a captured before/after, since re-creating the crash state for a
clean baseline run kept consuming it.
Not addressed: the dump is a single fixed path opened O_TRUNC, so
consecutive crashes still overwrite one another.
ctest 8/8.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
a) paste
- conductor does not move
- pressing escape does not abort
- pressing anything during the process crashes the program instead of aborting
b) similar issue with the escape not working fixed for graphics items, texxt fields (and probably others)
Refs discussion #886: a saved-report manager built on custom SQL and
nomenclature.json. Most of what was asked for already existed --
Projet > Exporter au format CSV already builds/saves/reuses named
SELECT queries via ElementQueryWidget and nomenclature.json, and
Projet > Ajouter une nomenclature already inserts one into a folio.
This fills the three real gaps.
- projectDataBase::isReadOnlySelect() rejects anything that isn't a
single SELECT/WITH statement. Checked in newQuery() itself, the one
choke point every query path already goes through -- including a
query loaded from a saved nomenclature/summary table's <query>
element on project open, not just the dialog's own custom-SQL box.
ElementQueryWidget shows the same check live as you type, and
BOMExportDialog surfaces it before running or exporting anything.
- BOMExportDialog gains a Preview button + table (QSqlQueryModel),
so a report can be checked on screen before committing to a CSV file.
- ElementQueryWidget gains Importer.../Exporter... buttons that
read/write nomenclature.json's saved reports as a JSON file, so a
report can be handed to a colleague or another install. Import asks
before overwriting a locally-saved report of the same name.
Verified: Qt 6.10.2, builds clean, ctest 8/8. Drove the real dialog
through Xvfb: typed "DROP TABLE element" into the custom-SQL box and
got the inline warning immediately, then confirmed Preview also
refuses it with a "Requête refusée" dialog rather than running it.
Preview against the real default query returned live column headers
and a row. Export opens a save dialog without crashing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Another exception: one non-converted code path in projectprintwindow.cpp to be followed-up.
One issue found in the Qt6 code path of diagramview.cpp which has been fixed.
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>
Addresses scorpio810's review of PR #891:
- addElement() now imports the element into the project's own embedded
collection first (QETProject::importElement()), same as the
drag-from-collection-panel path -- otherwise the saved .qet referenced
a definition outside the project, missing on another machine. Also
found and fixed while wiring this up: ElementsLocation::setPath()
forces any path to embed:// once a non-null project is passed, so the
unconditional ElementsLocation(locationPath, m_project) this method
used before silently broke every common://custom:// call. Refuses
on an import-collision case that would otherwise reach
QETProject::importElement()'s own modal ImportElementDialog, with
nobody there to answer it in a script.
- addElement()/setElementPosition()/moveElement()/deleteElement() refuse
on a read-only project, matching their GUI equivalents.
- save() goes through QETProject::write() when no path is given (read
-only handling, saveddate/savedtime, QSaveFile via writeXmlFile()) and
QET::writeXmlFile() directly for an explicit output path, instead of a
plain QFile that skipped all of that.
- Interactive "Run Script...": exports briefly disable project backups
around the temporary QETProject CLIExport::run() opens on the same
file, so it doesn't race the real open project's own KAutoSaveFile.
Restored right after -- the headless --run path is untouched, since it
depends on backups staying off for the whole run.
- A watchdog thread now calls QJSEngine::setInterrupted() after 30s,
so a runaway script (`while(true){}`) can't freeze the GUI or hang a
CI job forever. First version used sleep_for() and blocked every run,
fast ones included, for the full 30s on join() -- caught by testing a
one-line script, fixed with wait_for() on a condition variable so a
script that finishes early wakes the watchdog immediately.
- Interactive script errors now also show a QetMessageBox, not just
stderr (invisible on Windows).
- Ran update_translations (lupdate) to pick up the 37 strings this
feature had not yet added to the .ts files.
Not applied: wrapping the whole script run in one undo macro. It would
make the script's own qet.undo()/qet.redo() calls silent no-ops for the
run's duration -- QUndoStack ignores undo()/redo() while a macro is
open -- which would break that already-shipped, explicitly requested
capability to get one convenience Ctrl+Z instead.
Verified: Qt 6.10.2, builds clean, ctest 6/6. Ran each fix against a
real project: addElement() on a common:// path now succeeds and the
saved file embeds the definition (embed://import/...); read-only
project refuses addElement(); save("") and save(otherpath) both
produce a correctly embedded file; `while(true){}` under --run is
interrupted at 30s where it previously hung forever, and a normal
script now exits in ~0.3s instead of blocking for the full timeout
budget.
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.
The Selection properties "texts" tab painted the color value's own text in
that color, so the default black was unreadable on a dark palette. Show a
swatch in the cell instead and leave the text in the palette's color.
The terminal plan preview draws black ink with a white brush, like the
printed page it previews, on whatever background the view inherits from
the palette. Give the view a white background.
main.cpp has forced the Fusion style on macOS since 2019, but the palette
still came from Qt's macOS platform theme, which is built for the native
style. It hands Fusion a Window, Button and Base that are the same color,
a Dark lighter than Light, and in dark mode an Inactive ButtonText of
black. Fusion derives its frames, gradients and indicators from those
roles, so fields had no edges, the radio buttons in the text alignment
dialog vanished, and the "Handles" combo box in the diagram editor
toolbar drew its text black on a dark combo the moment the window lost
focus. Light mode had the same flatness, with Window, Button and Base
all white.
Add QET::Palette (sources/qetpalette.{h,cpp}) with a light and a dark
palette laid out the way Fusion expects, and install one from
QETApp::initStyle() on macOS when the running style is Fusion, choosing
by the platform palette's lightness and keeping the platform's accent
color when it reads at 4.5:1. On macOS the base palette is now applied
whether or not "use system colors" is checked, since the system palette
cannot be drawn by Fusion; that setting only decides whether style.css
is layered on top (#467). On Qt 6.5+ the app follows the OS light/dark
switch through QStyleHints::colorSchemeChanged.
Other platforms are untouched: Fusion is Qt's default style on Linux
desktops without a platform theme, and the palette there carries the
user's desktop colors. Making Fusion and this palette the default
everywhere is discussed in #870.
tests/qttest/tst_qetpalette checks every text role pair at WCAG 4.5:1
(3:1 disabled), that Inactive equals Active, and paints a Fusion combo
box, radio buttons, buttons and a line edit on the offscreen platform to
measure the ink against its background. Set QET_TEST_DUMP_DIR to keep
the rendered images.
- 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.
LineEditor::setPart() no-ops (skipping updateForm()) when the part
passed in is already m_part. That is harmless when the editor widget
is torn down between selections, but this branch keeps the same
editor instance installed across selection changes instead of
recreating it, so a line already shown alone can also be
parts.first() of a later multi-selection -- and the x1/y1/x2/y2
spinboxes then keep showing whatever was in them before, not this
selection's actual first line.
setParts() now always calls updateForm() after setPart() succeeds,
closing the gap regardless of the identity check inside setPart().
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extends the scripting surface from the previous commit with exactly
the three things explicitly scoped out there, per follow-up direction:
editing geometry, undo integration, and driving the GUI -- the last
one narrowed to select/zoom/message after discussion, since "invoke
any menu action by name" would let a script trigger a modal
QDialog::exec() with nobody there to dismiss it, the same hang class
investigated for bugtracker #882.
## New capabilities
- addElement/setElementPosition/moveElement/deleteElement, through the
same undo commands the GUI itself uses: AddGraphicsObjectCommand
(the same one drag-from-collection-panel placement uses),
QPropertyUndoCommand on the standard `pos` property, and
DeleteQGraphicsItemCommand (refuses a non-deletable terminal, same
as the Delete key).
- undo/redo/canUndo/canRedo against the project's real QUndoStack --
the same one QETDiagramEditor's Ctrl+Z is wired to via
undo_group.activeStack(), not a parallel mechanism.
- selectElement/deselectAll (scene state, no view required -- works
headless), zoomFit/zoomToContent/zoomReset (need the active
DiagramView, so false headless where there is nothing to zoom), and
showMessage (a modal QET::QetMessageBox::information -- safe headless
because non-interactive mode is already on for the whole process
before any script runs).
## Two real bugs caught by testing this, not assumed away
1. save() was still going through the same reopen-from-disk path as
every export method: it opened a *second*, unmodified copy of the
project from its file on disk and rewrote that. addElement() and
friends operate on the live in-memory project, so nothing they did
ever reached the saved file -- an element counted correctly in
memory and then silently vanished from the output. Fixed by having
save() write m_project->toXml() directly, the only method that
touches the live instance rather than a fresh copy of the file.
2. A script calling setElementPosition() then moveElement() on the
same element produced a saved position that didn't match either
call, and undo/redo didn't step through them independently. Traced
to QPropertyUndoCommand::mergeWith() (pre-existing, not new):
consecutive commands on the same object+property merge when their
text() also matches, and both calls build the identical "Déplacer
%1" text for a given element -- exactly the same collapsing
dragging an item repeatedly gets. Not a bug in the new code; a
wrong assumption in the first test. Re-verified against the
correct, merge-aware expectation: add -> merged move -> undo (back
to first position) -> undo (element removed) -> redo (element back)
-> redo (merged move reapplied) landed at the exact predicted final
position, read back from the saved XML.
## Verified
Qt6, build clean from a fresh reconfigure, ctest 6/6.
- Headless: addElement returns a real uuid and the count updates;
select/set-position/move all report correctly; the merge-aware
undo/redo/save round trip above, confirmed against the saved file's
actual XML, not just in-memory counters.
- Corpus: the existing read-model smoke script re-run against all 24
shipped example projects on the fixed binary, 0 failures.
- zoomFit correctly returns false headless (no view to act on),
confirming the "narrowed GUI-driving" scope holds in code, not just
in the doc comment.
- GUI: running the add-only script via "Exécuter un script..." marked
the project [modifié] in the title bar, the same change-tracking
path a manual edit goes through -- consistent with the undo command
actually being pushed onto the project's real stack rather than some
side channel invisible to the rest of the application.
Refs #162.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Following up on my own comments there: a deliberately small, mostly
read-only scripting surface, exposed to scripts as a single global
`qet` object (QetScriptApi) built on QJSEngine rather than an embedded
Python interpreter -- no new toolchain to package (QJSEngine ships in
every Qt SDK QET already targets, via the Qml module), no GIL, no
version pinning, automatic reflection of the QObject-derived core
classes' own methods with no hand-written binding layer.
## What a script can do
- Read the model: project title, file path, folio count/titles,
element/conductor counts per folio.
- Trigger the same operations the --export-* CLI flags already do
(pdf/png/svg/cables/wires/bom/wiring/nets/links/info), plus
set-titleblock and save -- thin wrappers around CLIExport::run(),
reusing its already-tested logic rather than duplicating it.
Deliberately NOT in this version: creating or editing diagram
geometry, undo integration, driving the GUI. All explicitly out of
scope per the discussion on #162.
## Two entry points, both built and tested
- `qelectrotech --run script.js project.qet` -- headless/CI.
- Projet > "Exécuter un script..." -- an interactive macro against the
currently open project. Export/save calls act on the project's file
on disk (see QetScriptApi's class comment for why), so unsaved GUI
edits aren't visible to the script; save first if that matters.
## Optional dependency, not a hard requirement
Qt::Qml is probed the same way QtPdf already is in this codebase:
QUIET, non-fatal, behind a QET_HAS_SCRIPTING compile definition. A
build without it compiles and links identically; the CLI flag and
menu action are simply absent (main.cpp) or compile to a clear
"not available" stderr message rather than silently disappearing
(qetscripting.cpp), matching the existing QtPdf pattern rather than
introducing a new one.
One real bug caught building this, not assumed away: my first pass
conditionally excluded the new source files from QET_SRC_FILES behind
`if(QET_HAS_SCRIPTING)` inside qet_compilation_vars.cmake -- but that
file is included before QET_HAS_SCRIPTING is set in the top-level
CMakeLists.txt, so the variable didn't exist yet at that point and the
files were silently never compiled, only caught by an undefined-symbol
link error. Fixed by following the QtPdf file's own precedent:
compile the files unconditionally, guard their Qt::Qml-dependent
content internally instead.
## Verified
Qt6, build clean, ctest 6/6.
- Headless: a script reading project/folio/element/conductor counts,
calling exportInfo() and exportPdf() against a real project --
correct JSON, a real single-page PDF confirmed with `file`.
Error paths: a thrown script exception reports file:line:message and
exit 1; missing script/project arguments exit 2 (matching
CLIExport's own usage-error convention); a missing project file is
reported and does not hang.
- Corpus: the same read-model script run against all 24 shipped
example projects, 0 failures.
- GUI: "Exécuter un script..." opens a real file dialog filtered to
*.js, running the picked script against the live open project
produced the exact expected JSON export file, and the application
was still fully responsive afterward.
Refs #162.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
setQuery() restored the loaded SQL text into the line edit but never
restored the "Edit SQL query" checkbox, so a custom query (a join, a
subquery, a view other than project_summary_view) displayed correctly
while the widget stayed in built-in mode. queryStr() only consults that
checkbox, so accepting the dialog without touching anything silently
replaced the custom query with a freshly generated one.
Detect it instead of trusting a flag that was never set: after parsing
columns from the loaded query, rebuild the query those columns would
produce and compare it against what was loaded. A mismatch means the
widget cannot reconstruct it, so it must be user-written -- check the
box and keep the literal text.
Verified against both cases this has to get right, not just the one
in the report: a genuinely custom query (join) now round-trips through
an untouched accept+save byte-for-byte, and a plain built-in query
written before the #238 pos-ordering fix (examples/industrial.qet, no
ORDER BY clause) is classified as custom rather than silently gaining
an ORDER BY it didn't have -- it round-trips unchanged rather than
being corrupted, though its column picker is now disabled until a user
rebuilds it by hand.
Based on the patch attached to #885.
QETProject::m_uuid was created in the constructor and never written, so
a project got a new uuid every time it was opened. Inside a running
instance it is only used to name the SQLite connection, but nothing
outside the instance could tell which project a file belongs to.
Motivation
A .qet file is increasingly handled by tools outside QElectroTech: Git
repositories on GitHub or GitLab, cloud storage, key-value stores,
per-project locks. All of them need a stable key for "this project":
- The file name and path are not stable: files get renamed, moved,
checked out in different places.
- The project title is user-editable and not unique.
- Folio uuids (persisted separately) are only unique within their
project; keying folios globally needs a project identifier as well,
e.g. projects/{projectUuid}/folios/{folioUuid}.
Change
Write the uuid as an attribute of <project> and restore it in
QETProject::openFile(), right after parsing and before the project is
built from the XML. Older versions ignore the attribute, so files stay
readable in both directions.
The project database is not affected: it takes its connection name
from the uuid created at construction (m_uuid is declared before
m_data_base), before the file is read. Two open projects carrying the
same persisted uuid therefore still get distinct connections.
Files without a uuid: why not a random one
Keeping the random uuid created by the constructor and saving it
conflicts with #754 / #779: saving an unmodified project must give the
same bytes every time. Every example project predates the attribute.
Measured on the 24 example projects (resaved 3x each from the same
original, QT_HASH_SEED=0 so that QDom's attribute order is stable,
isolated HOME per run):
upstream master 23/24 byte-identical
persist, random uuid 0/24
persist, derived uuid (this) 23/24
The remaining project, schema_indus.qet, differs only in element uuids,
the known residual #779 leaves for elements; its project uuid is
stable.
Instead, a project file without a uuid gets a name-based (version 5)
uuid derived from the raw content of the file:
QUuid::createUuidV5(<fixed QET project namespace>,
"qet-project-legacy\n" + file content without CR)
- The same file always yields the same uuid, so resaving an unmodified
legacy project stays reproducible.
- Different projects practically never share a uuid, because any
difference in content gives a different one. This is unlike folios,
where only data such as title and position could be used; the raw
file bytes are stable input for the whole project.
- Carriage returns are dropped before hashing. QFile's Text mode already
strips them on Windows but not elsewhere, and git's autocrlf can
change them on checkout; either way the uuid is the same on every
platform.
- The uuid is derived once, at load time, and saved from then on. After
that it is read, never recomputed: renaming the project, editing it
or changing it in the same session as the migration does not change
it.
- Two people opening the same legacy file on different branches get the
same project uuid.
The namespace uuid is fixed in the code and must never change, or every
legacy project would get a different uuid.
Known limitations, open for discussion
- Copies share the uuid. Two byte-identical legacy files get the same
uuid (examples/cablage-eclairages_sikli-v5.qet and
câblage-éclairages-sikli-v5.qet are such a pair), and so does a
migrated file copied in the file manager or saved with "Save as".
That is what identity means for a copy, and the same happens with Git,
but a tool that treats the uuid as globally unique has to cope with
it. Regenerating the uuid on "Save as" could be a follow-up, if that
is the preferred behaviour.
- A legacy file that differs from another only in formatting (e.g.
re-indented) gets a different uuid. The two sides of a merge only
agree if they started from the same bytes, which is the normal case.
Tests (Qt 6.4, offscreen, qelectrotech --resave / --set-titleblock /
--info)
- 24 example projects, 3 resaves each from the same original: results
above; the project uuid is identical across runs. All 24 uuids are
distinct, except the byte-identical pair mentioned above.
- Resaving an already migrated file is byte-identical to the first
output.
- The same legacy file with CRLF line endings gets the same uuid as
with LF.
- Changing the project title in a migrated file keeps its uuid.
- Migrating and modifying in the same run (--set-titleblock on a legacy
file) gives the same uuid as a plain resave.
- Re-indenting a legacy file gives a different uuid (expected).
- A migrated file opened with upstream master loads normally; the
attribute is ignored and dropped on save.
- --info on a migrated file still works.
Refs #754, #779
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDyt4txaott5JyPNGQaeVp