Commit Graph

9195 Commits

Author SHA1 Message Date
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
Laurent Trinques 4d70fcf35c Merge pull request #877 from ispyisail/fix/f10-opens-menubar
Open the menu bar on F10, the key people expect
2026-09-15 09:46:24 +02:00
ispyisail 55c2c0df9d Paste under the cursor and let it be positioned before it lands
Ctrl+V pasted in place, which put the copy exactly on top of the original.
Nothing appeared to happen: the only clue was a doubled outline, and the
copy had to be dragged off the original to be seen at all. The cursor was
ignored entirely.

Ctrl+V now starts a placement. The items appear under the cursor and follow
it until a left click or Return drops them; Escape or a right click takes
them away again. That is the same interaction as placing a new element, so
paste behaves like every other way of putting something on a folio, and the
copy lands where the user is looking.

Implemented as a DiagramEventInterface beside the existing add-element and
add-macro tools. The pasted items are the real ones from the start rather
than a preview: Diagram::fromXml creates them exactly as before, this class
moves them, and PasteDiagramCommand is pushed only once they are dropped.
PasteDiagramCommand's first redo() deliberately does not add items to the
scene -- it assumes fromXml already did -- so pushing it on commit adopts
them rather than duplicating them. One copy of the paste logic, and a
cancelled paste leaves nothing on the undo stack.

Conductors are not moved directly; they are drawn from their terminals and
follow the elements they attach to. On cancel they are removed before the
elements, so none is left in the scene holding a pointer to a freed
terminal.

Verified by counting elements in the saved file rather than by eye:
56 to start, 56 after paste-then-Escape, 57 after paste-then-drop, and 56
again after undo. Save determinism run against this build: pass, no
regressions against baseline. Tests 5/5 on Qt 6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 17:07:15 +12:00
ispyisail b94b244919 Correct the comment: say what was measured, not what was assumed
Two claims in the previous comment were wrong.

"Qt implements F10 on Windows but not on X11" was inference I cannot test
here. What is measured is narrower and enough: QMenuBar given Key_F10
directly leaves it unaccepted, and sent to the window the key never reaches
the menu bar at all, because a key press goes to the focused child widget.

"&Édition takes É, which is not on a UK or US keyboard" was wrong outright.
It came from running an uninstalled binary, which cannot find its .qm files
and falls back to the French source strings. With translations loaded the
menus read File, Edit, Project, Display, Settings, Windows, Help, and Alt+E
opens Edit.

The comment now also says plainly that this is convenience rather than
access: Alt tap focuses the bar and Alt with a letter opens a menu, both
verified working, so the menus were already reachable without a mouse. F10
is the key people reach for out of habit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GtMZqGEiUMvDBqcFvVG2vb
2026-09-15 15:51:24 +12:00
ispyisail bd6bed8d61 Open the menu bar on F10, and add a test that can answer whether it works
F10 opens the menu bar in most applications and is the usual way to reach
the menus without a mouse. Qt provides this on Windows but not on X11, so on
Linux the key did nothing and the press fell through to whichever widget had
focus. It matters more here than it might elsewhere: "&Édition" takes É for
its own letter, which is not on a UK or US keyboard, so that menu has no
direct Alt route at all.

A window-context QShortcut rather than a key handler -- key presses go to
the focused child widget, so a keyPressEvent() on the window would never see
F10 while the canvas or a panel has focus.

tests/qttest/tst_menubarkeyboard.cpp covers three things: that Alt and a
letter opens a menu (the control), that plain F10 does nothing in Qt itself
(which is why the shortcut exists, and which will fail loudly if a future Qt
starts handling it), and that the shortcut mechanism opens the bar.

It uses QTest instead of driving a real X server for a specific reason.
xdotool on Xvfb delivers every function key with Alt held: a Qt key logger
shows Key_F10 arriving with modifiers == Qt::AltModifier. --clearmodifiers,
keydown/keyup pairs, --window targeting and flattening the keycode with
xmodmap all made no difference. Two rounds of GUI automation therefore gave
confident, wrong answers about F10 -- first that it was broken, then that
this very fix did not work. QTest posts the event straight to the widget, so
the key arrives as written.

What the test does not cover, since initCommonActions() calls
QETApp::instance() and constructing that pulls in the whole application: it
repeats the wiring rather than driving QETMainWindow. Confirming the real
window responds still needs someone to press F10 in a running QElectroTech.

Verified by breaking it: bound to F11 instead, the test fails. Qt 5 and Qt 6
both build clean, 6/6 tests on each.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:44:59 +12:00
ispyisail 6a2b3973bc Put the drawing tools in a menu so they can be reached without a mouse
The nine "Ajouter" actions -- text field, image, PDF, line, rectangle,
ellipse, polyline, curve, terminal strip -- were only ever added to
m_add_item_tool_bar, and the automatic conductor break only to
diagram_tool_bar. None carried a shortcut. A toolbar button has no key,
so someone working without a mouse could not add anything at all to a
folio.

They now appear in an "Ajouter" submenu under Édition, and the conductor
break beside m_auto_conductor in Projet, the setting it pairs with. The
actions themselves are untouched: a QAction can sit in a menu and a
toolbar at once, which is what m_depth_action_group -- created a few lines
away, and added to both its toolbar and menu_edition -- has always done.
That contrast is why this reads as an oversight rather than a decision.

Verified by driving the menus with the keyboard alone under Xvfb: Alt+F
opens the File menu, Down then Right crosses to Édition, and Right again
opens the Ajouter submenu with all eight actions this build compiles
(add_pdf is behind QET_HAS_QTPDF and absent on Qt 5).

Found with tools/keyboard-audit in the qelectrotech-docker harness, which
reports these ten and now reports none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 11:45:51 +12:00
ispyisail 9363e9bc2e Add a regression test for the forwarded-file use-after-free
Covers the crash fixed in #868: a file forwarded from a second instance was
opened inside SingleApplication's socket handler, so the backup prompt's
nested event loop ran while that handler was still on the stack.

Run by hand; no build system or CI changes.

    tests/ipc-regression/run.sh --binary build/qelectrotech

Validated in both directions on Qt 6.10.2: ceda1e082 (before the fix) crashes
3 times in 3 with exit 139, 199444b6 (after) survives 3 times in 3.

Three requirements are not obvious and are documented in the script:

- Qt 6 only. An unfixed Qt 5 build survives every attempt, so the script
  refuses to run on a Qt 5 binary rather than report a pass that cannot fail.
- A Debug build. The same unfixed commit survives every attempt built
  -O3 -DNDEBUG; whether a use-after-free faults depends on what the allocator
  does with the freed block.
- Dismissing the backup prompt is the step that triggers it. Left open, the
  stack never unwinds and nothing fails, which is why the bug was twice
  reported as not reproducible.

The test runs in its own sandbox on its own X display, works on a copy of the
project so backup files do not land in examples/, and cleans up after itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 07:09:16 +12:00
Laurent Trinques 199444b6db Merge pull request #862 from ispyisail/fix/bugtracker-108-junction-dot-width
Fix bugtracker #108: the junction dot vanishes on a wide conductor
2026-09-14 13:25:13 +02:00
Laurent Trinques bdd52a0e2b Update ca translations, thanks Antoni 2026-09-14 13:06:16 +02:00
Laurent Trinques 86ce8fcd92 git submodule update --remote elements 2026-09-14 13:04:02 +02:00
Laurent Trinques 428687ee4b Update ca translations, thanks Antoni 2026-09-14 12:45:28 +02:00
Laurent Trinques 512d74c745 Merge pull request #868 from ispyisail/fix/ipc-open-deferred
Fix a use-after-free: forwarded files are opened inside the socket handler
2026-09-14 12:37:11 +02:00
ispyisail 561b9c4eb1 Fix indentation of the deferred-open comment block
The comment sat one tab deeper than the code around it. Flagged in
review on PR #868. Whitespace only; no change to behaviour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:14:36 +12:00
Laurent Trinques ceda1e082a Merge pull request #861 from ispyisail/fix/bugtracker-248-split-with-spaces
Partial fix for bugtracker #248: second-instance file arguments are lost
2026-09-14 11:31:26 +02:00
ispyisail ce890da342 Open forwarded files outside the socket handler, not inside it
QETApp::receiveMessage() called openFiles() directly. That slot runs inside
SingleApplication's socket handling: SingleApplicationPrivate::
slotDataAvailable() emits receivedMessage synchronously from the readyRead
lambda (singleapplication_p.cpp:452). openFiles() then loads a project --
seconds of work on a large one -- and openAndAddProject() puts up a modal
BackupDialog whose exec() runs a nested event loop while the socket handler
is still on the stack.

During that nested loop the secondary instance exits, the connection closes
and the QLocalSocket is deleted. When the dialog is dismissed and the stack
unwinds, QMetaObject::activate() carries on emitting on the freed sender and
the process dies.

A zero-timer returns to the event loop first, so the socket stack is fully
unwound before any project is opened.

Found by scorpio810 while testing PR #861, with a backtrace showing no QET
frame above the crash. His second suggestion, looking for a delete that
should be deleteLater(), turned out to be already satisfied at
singleapplication_p.cpp:331 -- which is why the deferred delete is not enough
on its own once a nested loop is in play.

Dismissing the dialog is the step that makes it fail: two earlier attempts to
reproduce it left the dialog open, the stack never unwound, and nothing
crashed. With the dialog dismissed it segfaults twice out of two; with this
change it survives twice out of two, opens the project as before, and ctest
stays green. Qt 6.10.2 on X11/xcb -- also checked under a headless Wayland
compositor and under Qt 5.15.18, so it is neither Wayland-specific nor a Qt6
regression.

The crash needs PR #861 to be reachable at all: without it splitWithSpaces()
returns an empty list, no project opens, and nothing enters this path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 21:27:57 +12:00
Laurent Trinques 2785e25569 Merge pull request #863 from ispyisail/fix/bugtracker-97-recent-files-menu
Fix bugtracker #97: "Recently opened" never updates during a session
2026-09-14 09:09:45 +02:00
Laurent Trinques c8e4396b2a Merge pull request #864 from ispyisail/fix/bugtracker-238-summary-order
Fix bugtracker #238: summary table ordered by its columns, not by folio
2026-09-14 09:04:25 +02:00
ispyisail 0147453494 Fix bugtracker #238: summary table ordered by its columns, not by folio
SummaryQueryWidget::queryStr() built its ORDER BY from the columns the user
chose to display, in the order they chose them:

    column   += key;
    order_by += key;

So a summary whose first column is Title came out sorted alphabetically by
title, and one starting with Author sorted by author. A table of contents
lists the folios of a project; its order is the project's order, not
whatever the first column happens to be.

It now orders by "pos", the folio position that project_summary_view already
exposes from diagram.pos. That column is an INTEGER, so the sort is numeric
and folio 10 does not land between folio 1 and folio 2. One row per folio
means pos fully determines the order, so no secondary key is needed.

Demonstrated against a stand-in view holding four folios:

    ORDER BY title, pos   Apple(2) Banana(3) Mango(10) Zebra(1)
    ORDER BY pos          Zebra(1) Apple(2) Banana(3) Mango(10)

The hand-written query path (m_edit_sql_query_cb) returns before this and is
untouched, so anyone wanting a different order still has one.

ctest 4/4, Qt 5.15.18.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 15:30:44 +12:00
ispyisail 181bbb7f21 Fix bugtracker #97: "Recently opened" never updates during a session
The File > Recently-opened submenu was filled once, at editor construction,
by copying the QActions that RecentFiles' menu happened to hold at that
moment:

    recentfile->addActions(QETApp::projectsRecentFiles()->menu()->actions());

RecentFiles::buildMenu() runs on every fileWasOpened(), clears its menu and
creates fresh QActions. The editor's copy therefore never gained an entry,
and the list only ever looked correct after a restart.

The submenu is now the RecentFiles menu itself. QMenu::addMenu() adds the
submenu's menuAction() rather than reparenting it, so several editor windows
can share the one live menu, which is what an application-wide recent-files
list should do anyway.

Measured with a temporary probe comparing the live menu against what the
File menu actually shows, after one file had been opened in the same
session:

    without the fix   live=1  shownInFileMenu=0
    with the fix      live=1  shownInFileMenu=1

ctest 4/4, GUI starts clean with the menu bar intact. Qt 5.15.18.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 15:25:51 +12:00
ispyisail 3665ec1bcd Fix bugtracker #108: the junction dot vanishes on a wide conductor
Conductor::paint() drew every junction as a fixed 3.0-unit ellipse,
regardless of how wide the conductor carrying it is. The conductor width is
user-settable from 0.4 to 20.0, so at anything above about 3.0 the dot is
narrower than the line it sits on and disappears entirely -- exactly when a
junction most needs to be legible.

The dot now scales with m_properties.cond_size, floored at the historic 3.0
so nothing changes at or below the default width of 1.0. Only the wide
conductors the report is about are affected.

cond_size is used rather than the pen width because the pen is inflated by 4
while the mouse is over the conductor; the junction should not grow on
hover.

Measured with a temporary trace over examples/741.qet: at the default width
the diameter stays 3.00, and with condsize="5" it becomes 15.00. Visually,
a PNG export of that widened project shows two junctions that were invisible
under the line rendering as clear dots. ctest 4/4, Qt 5.15.18.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 15:15:39 +12:00
ispyisail fcd2a4e0e0 Fix bugtracker #248: opening a file while QET is running does nothing
QET::splitWithSpaces() split on QRegularExpression("[^\\]?(?:\\\\)* ").
That is not a valid pattern: "[^\\]" opens a character class whose "\\]" is
an escaped bracket, so the class is never closed. QRegularExpression
reported isValid() == false, QString::split() warned "invalid
QRegularExpression object", and the function returned an EMPTY list for
every input.

It is the receiving half of the SingleApplication handshake: a secondary
instance sends "launched-with-args: " + joinWithSpaces(args) (main.cpp) and
the running instance parses it in QETApp::receiveMessage() before calling
openFiles(). With the split always empty, the running instance received no
arguments at all -- so opening a project while QET was already running
silently did nothing.

The bug is reported against filenames containing spaces, which is how it
was noticed, but it is not limited to them: plain names failed identically.

A corrected regex is not available. The separator is a space preceded by an
even-length run of backslashes, and PCRE2 has no variable-length lookbehind,
so the run cannot be expressed in a lookbehind and anything that matches it
by consumption eats the character before the space -- which is what the
"[^\\]?" was for. Scanning the string explicitly is correct and easier to
read.

tests/qttest/tst_qetstrings.cpp asserts the round trip
splitWithSpaces(joinWithSpaces(x)) == x over plain names, embedded spaces,
embedded backslashes, a trailing backslash and a mixture, plus the specific
regression that a plain argument list does not come back empty.

Verified the test fails without the fix: 9 of 11 cases fail on the old
implementation and all 11 pass with it. Full suite 5/5, Qt 5.15.18.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:55:17 +12:00
Laurent Trinques 3cbb930751 Update pl translations, thanks Pawel 2026-09-13 17:04:41 +02:00
Laurent Trinques 51209d30b6 Merge pull request #855 from Kellermorph/copy-paste-fix
Clear PLC slave data on paste
2026-09-13 13:32:09 +02:00
Kellermorph 9c55d36d55 Clear PLC slave data on paste 2026-09-13 12:58:43 +02:00
Laurent Trinques 3ac557570c Merge pull request #853 from Kellermorph/fix-template-placing
Fix macro drag-and-drop placement and preview for Qt6
2026-09-13 12:27:51 +02:00
Kellermorph 0c2cf409f8 Fix macro drag-and-drop placement and preview for Qt6 2026-09-13 10:09:44 +02:00
Laurent Trinques 467a14df71 Merge pull request #849 from ispyisail/fix/bom-include-slave-terminal
Fix terminal and contact blocks missing from the parts list
2026-09-13 09:34:30 +02:00
Laurent Trinques 1b6768fcec Merge pull request #851 from ispyisail/feature/autonum-import-from-project
New feature: reuse automatic numbering rules from another project
2026-09-13 07:19:41 +02:00
ispyisail a6fd42ac5f Import automatic numbering rules from another project
Forum #3186 / issue #850: a user who has built up conductor and element
numbering rules in one project has no way to reuse them in the next one.
The only answer today is to open both .qet files in a text editor and copy
the XML across by hand.

Adds an "Import from another project..." button to the auto-numbering page
of the project properties dialog. It offers every numbering found in the
chosen file, per category, with names that already exist here unticked by
default and a "replace same-named numberings" option for when that is what
the user wants.

The source file is parsed as plain XML rather than opened as a QETProject.
Opening it would run the whole load path, including the modal dialog raised
for a file written by a different version of QElectroTech -- a dialog the
user has no reason to see, since nothing but the <newdiagrams> block is
being read.

Two supporting changes:

  - readValuesFromProject() clears the three combo boxes before filling
    them. It only ran once before; it now runs again after an import, and
    without the clear every name appeared twice.

  - FolioAutonumberingW::setContext() likewise replaces its list instead
    of appending to it. It has a single caller, the line above.

This deliberately does not attempt the project-template feature also raised
on the forum thread. That needs decisions about where templates live and
what else they carry, and is better settled in a discussion first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 09:56:25 +12:00
Laurent Trinques ce17b5c984 Update EN translations files 2026-09-12 17:51:44 +02:00
Laurent Trinques b46a4cfc2a Update FR EN translations files 2026-09-12 17:47:50 +02:00
Laurent Trinques 00d8822583 Remove again .pro now use /usr/lib/qt6/bin/lupdate -no-obsolete sources/ -ts lang/qet_*.ts
to generate new translation update on *TS files
2026-09-12 17:36:45 +02:00
Laurent Trinques 8edda68e96 Restore .pro files for lupdate translation 2026-09-12 17:20:49 +02:00
Laurent Trinques ad9a47602d Delete ols .pro and unused macOS script based on Qt5 2026-09-12 17:12:26 +02:00
ispyisail b034d3c5b3 Include slave and terminal elements in the bill of materials
A slave and a terminal are both routinely separately orderable hardware. A
circuit breaker can carry ten or twenty auxiliary blocks, each with its own
order code, and a terminal block is a purchased part in its own right.
Neither was reaching the bill of materials.

Decided in discussion #847: @IBSYSLevi -- "I would not expect that a defined
piece of hardware is excluded from BOM when not specifically defined as so" --
with use cases from @jozi332 covering Siemens breakers with ten to twenty
auxiliary blocks and PLC cards carrying per-channel data.

Two filters had to change, which is easy to miss: BomExport::defaultQuery()
and, upstream of it, the WHERE clause of element_nomenclature_view itself.
Changing only the query does nothing for slaves, because the view had already
removed them. Terminals were already in the view, so they appeared as soon as
the query allowed them -- which made a half-finished change look like it had
worked.

Measured on examples/industrial.qet, which holds 96 terminals and 41 slaves:
258 rows before, 354 with terminals, 395 with both. A slave given a
manufacturer and part number now appears in the export; previously it could
not, at any setting.

Nothing that should stay out of a bill of materials is newly included. The
folio report arrows and the conductor definition are still excluded because
they are not hardware, and anything else -- a relay's own auxiliary contact,
which is not orderable separately -- is kept out with exclude_from_bom, which
the view already honours and which #721 and #765 made settable on the symbol
itself.

tst_smart_device is updated rather than weakened. @enesgursoy6110 wrote it in
#830 to prove the filter works, inserting rows designated "Must not be
exported"; the slave and terminal rows now carry real designations and are
asserted present, and a folio report arrow takes over as the negative case,
so the test still proves filtering happens -- at the boundary we now want.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 02:00:50 +12:00
ispyisail dd08b3daa5 Merge pull request #848 from arummler/fix_debug_message
Guard debug output in titleblocktemplate
2026-09-13 00:58:35 +12:00
ispyisail 2bdad43371 Merge pull request #846 from ispyisail/fix/titleblock-template-save-order
Write embedded title block templates in a stable order
2026-09-13 00:52:39 +12:00
Andre Rummler c05033d44c Guard debug output. 2026-09-12 14:50:22 +02:00
ispyisail 15c893de89 Merge pull request #765 from IBSYSLevi/feature/Add-existing-editor-properties-to-element
Editable auto_num_locked/potential_isolating/exclude_from_bom in the element editor
2026-09-13 00:36:02 +12:00
ispyisail 3e7dd15392 Merge pull request #767 from IBSYSLevi/fix/element-editor-informations-tab-visibility
Fix Informations tab visibility to match existing Terminal/Thumbnail support
2026-09-13 00:35:02 +12:00
ispyisail 061f07ef9e Merge branch 'master' into fix-informations-tab-visibility
# Conflicts:
#	sources/editor/ui/elementpropertieseditorwidget.cpp
2026-09-13 00:30:27 +12:00
ispyisail cb0d8c8f63 Write embedded title block templates in a stable order
Fifth site of the hash-ordering defect fixed in #844.
TitleBlockTemplatesProjectCollection::templates() returns
titleblock_templates_xml_.keys(), a QHash, and QETProject::toXml() iterated
it directly. A project embedding more than one template therefore wrote the
<titleblocktemplate> children in a different order on every save.

examples/affuteuse_250h.qet embeds three -- A4_1, DIN_A4 and DIN_A4_copy --
and two saves of it produced "DIN_A4 A4_1 DIN_A4_copy" and
"DIN_A4_copy A4_1 DIN_A4". It was the last of the two projects #844 could not
make reproducible.

Worth recording because the first reading of that diff was wrong: seeing
name="DIN_A4" on one side and name="DIN_A4_copy" on the other looked like the
save path renaming a template, which would have been far more serious -- a
diagram referring to it by name would have been left dangling. The file
simply contains both, and they had swapped places.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 00:06:31 +12:00
ispyisail f9d063bd38 Merge pull request #821 from enesgursoy6110/fix/shortcut-conflict-scope
Scope shortcut conflicts to overlapping editors
2026-09-13 00:00:54 +12:00
ispyisail fab5571ea2 Merge pull request #845 from ispyisail/revert-759-shortcut-scope
Revert #759, so #821 can land instead
2026-09-12 23:56:44 +12:00
ispyisail a0acf878a2 Revert "Merge pull request #759 from ispyisail/fix-shortcut-conflict-scope"
This reverts merge commit 3d5799773, restoring shortcutsconfigpage.cpp to
its state before it.

#759 and #821 fix the same issue (#757). #821 was opened on 8 September and
is the better fix; #759 was merged on 12 September without checking whether a
PR for it already existed, and its merge is what left #821 conflicting with
master. Reverting is the way to let the right change land.

#759 keys conflict detection on the row's category, which is a tr() string.
#821 keys on the shortcut ID prefix, which is stable and untranslated, and
encodes the overlaps the category cannot express: main-window actions are
live while any editor is open, and the depth.* actions are installed into
both the diagram and the element editor.

Checked against the registry rather than by reading -- 94 registered actions
plus the four depth.* ones registered through QObject::tr. On the shipped
defaults the two behave identically: all 24 shared sequences are legitimate
cross-editor duplicates and neither flags them. They diverge on shortcuts a
user assigns, where #759 misses four classes of real conflict that #821
catches: a diagram or element editor action given the main window's F1, and
a diagram or element action given a depth.* sequence.

The reason #759 looked adequate is that the scope prefix currently maps
one-to-one onto the translated category for all seven scopes, so same-scope
detection comes out the same either way. It fails only where scopes overlap,
which is the case #821 exists to handle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 23:51:41 +12:00
ispyisail 21588a9c10 Merge pull request #844 from ispyisail/fix/save-reproducible-ordering
Make saving a project reproducible: sort four hash-ordered XML writes
2026-09-12 23:45:10 +12:00
ispyisail fcbea2dafd Write model header roles in a stable order
Fourth and last instance of the ordering defect, and the inner half of the
one fixed in the previous commit. ProjectDBModel::toXml() builds each
section's role list from m_header_data.value(key).keys(), and m_header_data
is a QHash<int, QHash<int, QVariant>> -- so both levels are randomised per
process. Sorting the sections left the roles inside each section still
arriving shuffled, which showed up as <data> children with the same
section="0" swapping places between two saves.

With this, save idempotence across the shipped examples goes from 6 of 23 to
22 of 24.

The two that remain fail for unrelated reasons, not for ordering:
schema_indus.qet stores no uuid attribute on its elements at all, so
fromXml() invents a fresh one on every load; and affuteuse_250h.qet loses a
title block logo's storage attribute and renames a title block template on
save. Both are separate defects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 23:31:26 +12:00
ispyisail 1272b9db06 Write model header data sections in a stable order
Third instance of the ordering defect the two previous commits fixed, and
the one that was still making five of the shipped examples save
irreproducibly after those: QETXML::modelHeaderDataToXml() iterates
data_hash.keys() directly, and data_hash is a QHash<int, QList<int>> whose
key order is randomised per process. The <data> children of <header_data>
therefore came out in a different order on every save, which is what a
diff of two saves of industrial.qet showed -- the same EditRole, FontRole
and TextAlignmentRole entries, shuffled.

Sorting the section list fixes it. The roles within a section are a QList
and were already written in a stable order, so they are left alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 23:27:46 +12:00