Commit Graph

9257 Commits

Author SHA1 Message Date
ispyisail 8623dd4c6f Refuse to close an editor while a modal dialog is running (#904)
openAndAddProject() shows BackupDialog as a stack object parented to the
editor and exec()s it; every QET::QetMessageBox does the same. exec() runs a
nested event loop, and closing the editor during it turns WA_DeleteOnClose
into a deleteLater() that the nested loop processes: ~QWidget() deletes the
editor's children, the stack-allocated dialog among them, and the process
aborts. Reported on macOS, where File > Quit lives in the application menu
and stays usable while the backup question is up.

The close is now refused while any modal widget is active, and the dialog is
raised so the refused quit is not silent. It is done in QETMainWindow::event()
rather than in closeEvent(), because QETDiagramEditor::closeEvent() starts
closing projects before it decides whether to accept. That covers the diagram
and title-block editors; QETElementEditor is a plain QMainWindow, so its
closeEvent() calls the same helper before canClose(), which itself opens a
modal. QETApp::quitQET() needs nothing: closeEveryEditor() goes through each
editor's close(), and quitQET() already only quits when every close succeeded.

Rejected alternatives, both suggested on the issue:

- Giving the dialog no parent stops the abort but not the deletion. One
  caller of openAndAddProject() is the editor's own constructor, which goes
  on to open the next file and call slot_updateActions() on this -- a loud
  abort would become a silent use-after-free.
- Guarding only QETApp::closeEveryEditor(), which I first recommended on the
  issue, misses the reported route entirely: File > Quit is connected to
  QETDiagramEditor::close(), not to quitQET().

Verified on Linux, where there is nothing to click (the menu bar belongs to
the blocked window, and Qt ignores window-manager close requests for it), by
calling close() from gdb while the dialog's loop was running -- both
QETApp::quitQET() and QWidget::close() on the editor. Unfixed, both abort
with "free(): invalid size" in QObjectPrivate::deleteChildren() under
~QETDiagramEditor(), matching the report frame for frame; fixed, close()
returns false, the editor and the dialog stay up, and after answering the
dialog Ctrl+Q exits normally. The element-editor guard is the same helper
but was not exercised separately.

tests/modal-quit-regression/ turns that into a gate: it breaks on
QDialog::exec(), interrupts inside the nested loop, calls quitQET() and
checks the process survives. It matches no window titles (translated) and no
window ids, runs on the offscreen platform, and needs only gdb with Python.
Checked both ways: exit 1 with the backtrace above on a build without this
change, exit 0 with it.

ctest 8/8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 23:19:59 +12:00
Laurent Trinques adca07f26f Merge pull request #880 from arummler/remove-qt5-instruction
Remove Qt5
2026-09-17 06:35:57 +02:00
Andre Rummler 1ee828da5b Remove titleblocktemplate cahing remains. 2026-09-17 00:07:46 +02:00
Andre Rummler 837e1e7f09 Remove caching in titleblocktemplaterenderer which has not been used since Qt4.8. 2026-09-17 00:07:46 +02:00
Andre Rummler 25962118a2 Disable cacching which was enabled accidentially during Qt5 removal. 2026-09-17 00:07:46 +02:00
Andre Rummler 35275de4cc Remove blank lines. 2026-09-17 00:07:46 +02:00
Andre Rummler d641b964bd Remove very old Qt4 code path. 2026-09-17 00:07:46 +02:00
Andre Rummler e90dddbbc6 Add missing Qt6 code path and remove Qt5. 2026-09-17 00:07:46 +02:00
Andre Rummler ce8884394a Remove all code switches for Qt<6 with one exception: caching in titlebordertemplate. Function for caching not called since Qt4; might profit from complete removal.
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.
2026-09-17 00:07:46 +02:00
Andre Rummler 8606cc5328 Removing Qt5/KF5 from build system. 2026-09-17 00:07:46 +02:00
Andre Rummler 60d9299d45 New installation instructions for Qt6/CMake. Proposal: translations as INSTALL_XY.md 2026-09-17 00:07:46 +02:00
Laurent Trinques c71839a291 Merge pull request #869 from Kellermorph/fix-crossref-textfield
Clear stale xref data from PLC slave elementInformations on unlink and position change
2026-09-16 21:28:10 +02:00
Laurent Trinques d9a2539702 Merge pull request #890 from ispyisail/fix/734-conductor-self-retrace
Fix bugtracker #734: stop routing a bridge conductors don't need
2026-09-16 21:23:39 +02:00
Laurent Trinques 22c1ec9321 Merge pull request #891 from ispyisail/feature/162-js-scripting
Add JavaScript scripting: read, export, edit geometry, undo, navigate
2026-09-16 21:22:01 +02:00
ispyisail 4300a655bc Merge remote-tracking branch 'upstream/master' into feature/162-js-scripting
# Conflicts:
#	sources/qetdiagrameditor.cpp
#	sources/qetdiagrameditor.h
2026-09-17 06:50:35 +12:00
Laurent Trinques e7e2864641 Merge pull request #889 from ispyisail/fix/802-reload-element-drawings
Add "Reload element drawings" to refresh placed elements
2026-09-16 19:52:12 +02:00
ispyisail 2ed85f6fc7 Address review: qRound-consistent guards, regression test
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>
2026-09-17 05:08:14 +12:00
ispyisail e62d43d024 Revert .ts changes, translations will be updated separately
update_translations dropped 3 .ui files' <location> entries this run
(graphicstablepropertieseditor.ui, inditextpropertieswidget.ui,
projectdbmodelpropertieswidget.ui), marking ~38-44 existing translations
per language as vanished -- lrelease drops those, so merging would have
lost real, in-use translations in about twenty languages. Cause not
yet identified.

scorpio810 will run lupdate separately after merge, so the 37 new
scripting strings get picked up without this loss and without
conflicting with translation work in progress.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 04:45:29 +12:00
Laurent Trinques 6562498ca2 Snap update for qml QJSEngine 2026-09-16 15:27:49 +02:00
Laurent Trinques 38e504fb7e CI windows-build add mingw-w64-ucrt-x86_64-qt6-declarative for QJSEngine 2026-09-16 15:22:13 +02:00
Laurent Trinques 4aef4d17d2 git submodule update --remote elements 2026-09-16 14:58:55 +02:00
Laurent Trinques c89243a36f Merge pull request #894 from jp2images/fix-macos-fusion-dark-palette
Give Fusion a palette it can draw with on macOS
2026-09-16 14:21:13 +02:00
ispyisail 4a83521306 Fix scripting review findings: import, save, backups, timeout
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>
2026-09-16 22:28:06 +12:00
Kellermorph a7f406cd44 Merge remote-tracking branch 'origin/master' into fix-crossref-textfield 2026-09-16 11:49:55 +02:00
Kellermorph dad6206386 Address review feedback: parentGroup guard, ELMT_XREF constant, indentation
- 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.
2026-09-16 11:49:49 +02:00
Laurent Trinques 451f8c3296 Merge pull request #892 from jp2images/fix-terminal-destructor-snapshot
Delete a terminal's conductors from a snapshot of its conductor list
2026-09-16 11:49:40 +02:00
Laurent Trinques 43a1f6a1a9 Merge pull request #856 from Kellermorph/fix-copy-page
Fix text position shift when duplicating diagram pages
2026-09-16 11:44:18 +02:00
Jeff Patterson b208a4f131 Show the text color as a swatch and keep the terminal plan preview on white
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.
2026-09-16 04:39:31 -05:00
Jeff Patterson 1c8e7ed052 Give Fusion a palette it can draw with on macOS
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.
2026-09-16 04:39:31 -05:00
Laurent Trinques 9004db1d99 Reload element drawings: keep old drawing on failure, skip elements whose geometry changed (#802)
- 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.
2026-09-16 21:35:17 +12:00
Jeff Patterson edf483d88f Delete a terminal's conductors from a snapshot of its conductor list
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.
2026-09-16 04:27:27 -05:00
ispyisail 0920e82188 Add geometry editing, undo integration, and navigation to scripting
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>
2026-09-16 20:20:58 +12:00
ispyisail eba258f6cd Add JavaScript scripting: --run and "Run Script..." (bugtracker #162)
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>
2026-09-16 19:44:14 +12:00
ispyisail 2eed3acd3d Merge pull request #723 from IBSYSLevi/feature/cabinet-layout
Added width/height/depth properties to elements
2026-09-16 18:51:34 +12:00
ispyisail abd5e8978a Fix bugtracker #734: stop routing a bridge conductors don't need
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>
2026-09-16 17:17:03 +12:00
ispyisail 43d27a9563 Add "Reload element drawings" to refresh placed elements (#802)
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>
2026-09-16 16:24:44 +12:00
Laurent Trinques d3424db23d Merge pull request #883 from bhangart/persist-diagram-uuid
Persist the folio uuid, derived deterministically for legacy folios
2026-09-16 05:58:19 +02:00
Laurent Trinques 0d722b6033 Merge pull request #888 from ispyisail/fix/879-remember-conductor-color
Remember the F2 conductor color for the rest of the session
2026-09-16 05:52:29 +02:00
Laurent Trinques 96fac96b89 Merge pull request #884 from bhangart/persist-project-uuid
Persist the project uuid, derived from the file content for legacy files
2026-09-16 05:47:12 +02:00
ispyisail 1ec4f56a50 Remember the F2 conductor color for the rest of the session (#879)
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>
2026-09-16 15:40:41 +12:00
Laurent Trinques 839324f231 Merge pull request #887 from ispyisail/fix/885-summary-custom-sql-mode
Fix bugtracker #885: preserve custom summary-table SQL on accept
2026-09-16 05:37:43 +02:00
ispyisail a756bc0722 Fix bugtracker #885: preserve custom summary-table SQL on accept
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.
2026-09-16 13:09:48 +12:00
Beat Hangartner 2269a4641b Persist the project uuid, derived from the file content for legacy files
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
2026-09-15 23:34:24 +00:00
Beat Hangartner 7cd04f2a14 Persist the folio uuid, derived deterministically for legacy folios
Diagram::m_uuid was created in the constructor and never written, so a
folio got a new uuid on every load. Inside a running instance that is
enough (the project database keys on it), but nothing outside it could
tell which folio is which: in the file, folios were only identified by
their position.

Motivation

More and more .qet projects live in version control -- a Git repository
on GitHub or GitLab, reviewed through pull requests, sometimes edited
by several people -- or are synchronised through a cloud or key-value
store. A .qet file is plain XML, so in principle it can be diffed,
merged and split up, but only if the same folio can be recognised in
two versions of the file. Today it cannot:

- Inserting, deleting or reordering a folio shifts every following
  <diagram> element. A line-based diff, and GitHub's review view, then
  pair up unrelated folios and show far more change than was made.
- A three-way merge of two branches that both touched the project has
  no way to match "folio 3" on one side with "folio 3" on the other if
  either side reordered folios.
- Any tool that wants to say "folio X changed in this commit", keep
  per-folio history, lock a single folio, or store folios as separate
  objects has nothing stable to key on. The title and the folio number
  are user-editable and not unique.

Element uuids are already persisted and used for cross-folio links, so
the file format already relies on uuids for identity; the folio itself
was the missing piece. A stable folio uuid is the prerequisite for
later work towards better version control support: per-folio diffs and
locks (check-out / check-in), and possibly storing a project as a
directory with one file per folio.

Change

Write the uuid as an attribute of <diagram> when the whole content is
saved, and restore it first thing when the project is loaded, before any
item is created. Older versions ignore the attribute, so files stay
readable in both directions.

Folios without a uuid: why not a random one

The obvious migration -- keep the random uuid created by the
constructor and save it -- conflicts with #754 / #779: saving an
unmodified project must give the same bytes every time. Every example
project predates the attribute, so each load would invent different
uuids and write them out. Measured on the 24 example projects (resaved
3-4x 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 folio uuid is stable.

This is the same problem #779 solved for conductors by not writing an
invented uuid back at all. That is not an option here: legacy folios
would never get a persistent uuid, which is the whole point of the
change. Instead, a folio without a uuid gets a name-based (version 5)
uuid, derived only from data read from the file:

  QUuid::createUuidV5(<fixed QET folio namespace>,
                      "legacy" + project title
                               + position of the folio in the file
                               + folio title)

- The same input file always yields the same uuids, so resaving an
  unmodified legacy project stays reproducible.
- The uuid is derived once, at load time, and saved from then on. After
  that it is read, never recomputed: renaming, reordering or editing
  the folio later does not change it. Renaming in the same session as
  the migration does not change it either, since it was derived from
  the title as loaded.
- Two people opening the same legacy file on different branches get
  the same uuid for each folio, even if one of them reorders or renames
  folios before saving. With random uuids the two branches would
  disagree about every folio and a later merge could not match them.
- The folio content is deliberately not part of the name: QDom keeps
  attributes in a hash whose iteration order changes between runs, so
  hashing the content would need a canonical form for no real gain.

Folios are only guaranteed unique within their project. Two unrelated
legacy projects with the same title and the same first folio title get
the same uuid for that folio; anything keying folios globally has to
combine the folio uuid with a project identifier. (The project uuid is
not persisted yet; that is a separate change.)

Duplicated uuids

A hand-edited or merged file can contain the same uuid twice, e.g. a
folio copied by duplicating its XML block. Since the uuid is used as a
key, the second folio gets a derived uuid as well ("duplicate" + the
clashing uuid + the same inputs as above), so this case is
reproducible too. Should a derived uuid ever be taken already, which
takes a hand-crafted file, the name is salted with a counter until it
is free.

The namespace uuid is fixed in the code and must never change, or every
legacy folio would get a different uuid.

Tests (Qt 6.4, offscreen, qelectrotech --resave / --set-titleblock)

- 24 example projects, 3-4 resaves each from the same original: results
  above; all folio uuids identical across runs, no duplicates within
  any project.
- Resaving an already migrated file is byte-identical to the first
  output.
- Renaming a folio in a migrated file keeps its uuid.
- Swapping two <diagram> blocks in a migrated file: each uuid moves
  with its folio.
- Migrating and renaming in the same run (--set-titleblock title=...
  on a legacy file) gives the same uuids as a plain resave.
- A file with a duplicated uuid: the second folio gets a new uuid, the
  same one on every run.
- A migrated file opened with upstream master loads normally; the
  attribute is ignored and dropped on save.

Refs #754, #779

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDyt4txaott5JyPNGQaeVp
2026-09-15 22:09:10 +00:00
Laurent Trinques 265aa44320 Merge pull request #858 from Kellermorph/checkbox-plc-connection
Add Hide linked elements checkbox to PLC link widget
2026-09-15 18:44:24 +02:00
Laurent Trinques 395c6f6602 Merge pull request #876 from ispyisail/fix/keyboard-context-menu
Give the keyboard the folio's context menu, not a generic one
2026-09-15 16:15:04 +02:00
Laurent Trinques fd38f55724 Merge pull request #874 from ispyisail/feature/diagram-selection-shortcuts-v2
Add Tab selection cycling and select-all conductors/text fields
2026-09-15 10:20:28 +02:00
Laurent Trinques 85ed1b8a2a Merge pull request #878 from ispyisail/feature/paste-follows-cursor
Paste under the cursor, and let it be positioned before it lands
2026-09-15 10:16:06 +02:00
Laurent Trinques 607eb4b1ea Merge pull request #871 from ispyisail/test/ipc-open-forwarding-regression
Add a regression test for the forwarded-file use-after-free
2026-09-15 09:55:34 +02:00
Laurent Trinques 1e124450f2 Merge pull request #875 from ispyisail/fix/keyboard-reachable-drawing-tools
Put the drawing tools in a menu so they can be used without a mouse
2026-09-15 09:51:23 +02:00