Commit Graph

4311 Commits

Author SHA1 Message Date
ispyisail 07d7aedc26 Fix bugtracker #240: crash when a recovery file cannot be opened
After a crash, QElectroTech offers to reopen its recovery files. If one
cannot be read, answering OK crashed the program instead of showing the
"could not open" warning: QETProject(KAutoSaveFile *) takes ownership of
the file and deletes it on failure, and openBackupFiles() then read the
file name from the deleted object to build the warning. Read the name
first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-25 21:45:07 +12:00
ispyisail 3a45625d69 Fix bugtracker #95: collections pane shows "Untitled project" for a titleless project
The root of a project's embedded collection showed "Projet sans titre"
whenever the project had no title, while the project panel shows the
file name. Fall back to the file name the same way, and only use
"Projet sans titre" for a project that has neither.

The name was also computed once, so changing the project title or saving
it under a new name left the pane stale until the collections were
reloaded. Update it on projectTitleChanged and projectFilePathChanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 17:24:17 +12:00
Laurent Trinques ca507dc0f3 Merge pull request #1025 from ispyisail/feature/spacemouse-hid
Add a 3D mouse backend that reads the device over USB (hidapi)
2026-09-25 04:12:35 +02:00
Laurent Trinques 1932150ea6 Merge pull request #1023 from ispyisail/feature/spacemouse-settings
Add 3D mouse speed, direction and twist-to-zoom settings
2026-09-25 03:53:15 +02:00
Laurent Trinques 23241fba69 Merge pull request #1022 from ispyisail/fix/spacemouse-element-editor
Fix 3D mouse doing nothing in the element editor
2026-09-25 03:52:23 +02:00
ispyisail aaeaff55cc Add a hidapi 3D mouse backend that needs no 3Dconnexion driver
The 3D mouse only worked on Linux, through spacenavd. This adds a second
backend that reads the device directly over USB through hidapi, with no
3Dconnexion driver or SDK: the route to Windows and macOS (discussion
#599), and usable on Linux without spacenavd.

SpaceMouseHid decodes the raw reports from the device's own report
descriptor -- where each axis and button sits, its range, absolute or
relative -- so no per-model table is needed, with the classic report
1/2/3 layout as a fallback when the descriptor cannot be read and the
0x1c button list newer devices send. Absolute axes are rescaled to
+-500 exactly as spacenavd does, so both backends give QET the same
values. HidBackend polls from the main thread (fast while moving, slow
when still), emits one sample per poll, and looks for a device every 3 s
so plugging one in or back in needs no restart.

QET_SPACEMOUSE_BACKEND (auto, spnav, hid) picks the backend; auto keeps
libspnav on Linux when it is found and uses hidapi otherwise. hidapi is
found through pkg-config as hidapi-hidraw (Linux) or hidapi (MSYS2,
Homebrew).

A sample arriving in the same millisecond as the previous one now counts
for no time instead of a full period, so a burst of queued samples no
longer moves the view further than the time it covers.

Tested without a device: tst_spacemousehid (descriptor parsing, broken
and hostile descriptors, every report form, recordings from real devices
once they are added to fixtures/spacemouse), and end to end on Linux
through a virtual USB device created with /dev/uhid: the same moves give
byte-identical screenshots through the hidapi and libspnav backends, an
absolute axis is rescaled as spacenavd does, buttons trigger their
bound action, and unplugging and replugging while QET runs (including
with a dialog open that a device button opened) reconnects cleanly.
Not tested on Windows, macOS or real hardware.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:55:30 +12:00
ispyisail 17ebbffbca Add 3D mouse speed, direction, dead zone and twist-to-zoom settings
The 3D mouse's pan and zoom speeds were fixed guesses, and each sample
was applied as it came, so the speed on screen depended on how often
the driver sends samples -- different for every platform and device.

Motion now goes through SpaceMouseMotion::map(), which scales each
sample by the time since the previous one, and applies the user's
settings from a new "Mouvement" section of Configuration > Souris 3D:
pan and zoom speed, a dead zone, inverting each axis, and zooming by
push/pull (as before) or by twisting the cap. The defaults keep the
previous behaviour. Zoom is now exponential in the deflection, so the
factor stays positive however hard the cap is pulled (1 + z/1000 went
negative past z = -1000) and an equal push and pull cancel out. Sub-
pixel pan is carried over between samples instead of being rounded
away. The backend now reports all six axes.

tst_spacemousemotion covers the mapping without a device and is built
whether or not QET_ENABLE_SPACEMOUSE is on. The new behaviour was also
checked end to end with tools/spnav-shim (qelectrotech-docker): twist
with a dead zone of 10 ignores push/pull and small drift, and a twist of
60 gives the same frame as a push of 50 with the defaults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 09:56:04 +12:00
ispyisail 04e2279a7f Make the 3D mouse pan and zoom the element editor too
The 3D mouse only acted on the diagram editor: SpaceMouseListener
ignored every other window, so the element editor did not move at
all (reported by scorpio810 in PR #635 with a SpacePilot Pro).

applyMotion() now also handles a QETElementEditor, driving its
ElementView through the same scrollbar pan and a new
ElementView::zoom(factor), which keeps the wheel zoom's clamping.
The element editor's scene rect only covers what is on screen, so it
is grown before each pan sample, as its middle-button pan does;
without that the scrollbars have no range and the pan does nothing.

Verified under Xvfb with an LD_PRELOAD stand-in for libspnav feeding
recorded motion samples: element editor zooms 1.63x for ten z=50
samples and pans; diagram editor screenshots are byte-identical to
master's for the same input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 09:32:10 +12:00
ispyisail a94f30b256 Fix bugtracker #217: suggest element information already used in the project
Each information field of the element properties now offers, as a
drop-down while typing, the values the other elements of the project
already carry for it: supplier, manufacturer, reference...

The values come from the project database's element_info table, not
from a walk over the folios. Spellings that differ only by case are
offered once, as most elements spell them, and the edited element's
own value is left out. The field key is checked against
elementInfoKeys() before it becomes part of the SQL text.

Browsing the list with live edit on applies each highlighted value,
as typing applies each keystroke; ChangeElementInformationCommand
merges them, so this still leaves one undo entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:28:41 +12:00
Laurent Trinques 48883b320e Merge branch 'master' into feature-3dmouse-spnav 2026-09-24 15:45:33 +02:00
Laurent Trinques ae41dd7686 Merge branch 'master' into master-feature-renum-elements-rebased 2026-09-24 15:17:17 +02:00
Laurent Trinques 7fbbb71aac Merge pull request #1018 from ispyisail/revive/785-info-flags-case-insensitive
Fix element flags written as True or 1 showing unticked
2026-09-24 13:08:28 +02:00
Laurent Trinques 232724bf82 Merge pull request #1017 from ispyisail/revive/752-rotate-texts-no-dialog-in-ctor
Move the rotate-texts dialog out of its undo command
2026-09-24 13:06:23 +02:00
Laurent Trinques 12f826e969 Merge pull request #1016 from ispyisail/revive/729-backup-prompt-drops-files
Fix projects opened during the start-up restore prompt being lost
2026-09-24 13:04:19 +02:00
Laurent Trinques 245aafb0d1 Merge pull request #1015 from ispyisail/revive/526-free-terminal-move
Fix the terminal strip's move button getting stuck disabled (#409)
2026-09-24 13:02:43 +02:00
Laurent Trinques 0d231ef548 Merge pull request #1014 from arummler/fix-explicit-file-open
Fix warning about reading XML without explicit file opening.
2026-09-24 13:00:47 +02:00
ispyisail 3513f692fd Read the remaining element flags through QET::infoFlagIsTrue()
5b0785fcc routed most reads of auto_num_locked, potential_isolating and
exclude_from_bom through QET::infoFlagIsTrue(), which accepts the same
spellings as the parts-list query (true/1/yes/on, trimmed, any case).
Four checkbox reads still compared against a literal lowercase "true":
the "exclude from parts list" box in the folio properties panel, and all
three flags in the element editor.

A value such as "True" or "1" from a hand-written .elmt/.qet was
therefore left out of the parts list, but shown unticked in both panels,
and pressing Apply there wrote back "false" and flipped the flag.

Revives the unconverted part of PR #785.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 22:34:42 +12:00
Laurent Trinques 5a27f3cf80 Merge pull request #1013 from ispyisail/fix/273-skip-open-backup
Fix bugtracker #273: skip crash-recovery backups of an unchanged project
2026-09-24 12:33:18 +02:00
Andre Rummler d3a9adbe44 Fix warning about reading XML without explicit file opening. This was implicit up to now and will go away in future Qt versions. 2026-09-24 12:32:47 +02:00
ispyisail 8ab4e24b61 Take the modal dialog out of RotateTextsCommand's constructor
RotateTextsCommand called QDialog::exec() from inside its constructor, so
the command could not be built without a human answering a dialog. That
made it untestable headlessly, undrivable from any script or test harness,
and it is why bugtracker #312 (PR #707) shipped with its save/reload
round-trip unverified -- the symptom could not be reproduced without a GUI.

The command now takes the angle as a parameter and does no asking. Two
statics carry the interactive half:

  hasSelectedTexts(diagram)  -- is there anything to rotate
  askRotation(rotation)      -- open the dialog, false if cancelled

The single call site in QETDiagramEditor asks first, then builds the
command, so the user-visible behaviour is unchanged: same dialog, same
title, same no-dialog-on-empty-selection. Keeping askRotation() in this
class also keeps the QObject tr() context, so existing translations of
"Orienter les textes sélectionnés" are not invalidated.

Also guards undo()/redo() against a null m_anim_group. When nothing is
selected the constructor calls setObsolete(true) without ever creating the
animation group, and QUndoStack::push() calls redo() before discarding an
obsolete command -- a latent null dereference on that path.

Verified headlessly, which was the point: driving the command through a
scratch --test-ops op on examples/741.qet (67 conductors), rotation
attributes written on save go 0 -> 67 with the #707 fix present and stay
at 0 with it reverted, while the reverted build instead writes userx on
all 67. That is bugtracker #312 reproduced and fixed under test for the
first time.
2026-09-24 22:30:20 +12:00
ispyisail 2f6ab8f63b Don't drop files handed to a starting instance while the backup prompts are up
QETApp's constructor ends with checkBackupFiles(), which opens modal
dialogs -- the "restore these files?" prompt, and the crash report offer.
Those dialogs run their own event loop, so the constructor does not return
until the user answers them.

main() still has work to do at that point. In particular this, a few lines
later:

    QObject::connect(&app, &SingleApplication::receivedMessage,
                     &qetapp, &QETApp::receiveMessage);

While the prompts are up that connection does not exist yet. A second
instance launched during the window -- double-clicking a project, or
xdg-open, while the first copy is still asking about restore files --
hands its file names over, SingleApplication accepts and delivers them,
and nothing is listening. The message is discarded and the second process
has already exited, so the file is simply lost with no error.

Deferring checkBackupFiles() to the event loop lets the constructor return
promptly. main() finishes wiring up, and the prompts appear immediately
afterwards exactly as before.

Verified with a stale restore file present, sending a project to the
running instance while the restore prompt is displayed: before, the file
was dropped and never appeared, even after answering the prompt; after, it
opens. The restore and backup prompts still appear and still work.

Note this is only observable together with the fix for bugtracker #248 --
before that, no file passed to a running instance was opened under any
circumstances.
2026-09-24 22:26:38 +12:00
ispyisail 2266497b0b Write the move button tooltips in French, the tr() source language
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 22:20:43 +12:00
Shane Ringrose dcb79b6553 fix(terminal-strip): free-terminal move button disabled and unresponsive
Three bugs stacked to produce the symptom in issue #409 (move button does
nothing):

1. selectionChanged() was declared in freeterminaleditor.h but had no body
   and was never connected to the selection model, so the move button had no
   awareness of whether a terminal was selected. The button could appear
   enabled with nothing selected, then silently return early in
   on_m_move_pb_clicked() at the real_t_vector.isEmpty() guard.

2. The dataChanged→setDisabledMove(true) connection was a one-way trap: any
   cell edit (including accidentally opening and closing a type/function
   combo, or toggling LED back to its current value) permanently disabled the
   move button until reload() was called. There was no tooltip explaining why
   the button was greyed out, so the user had no way to recover.

3. FreeTerminalModel::setData() for LED_CELL had no change guard, unlike
   LABEL_CELL which checks label != value. Clicking the LED combo while it
   was already at the same value still emitted dataChanged and triggered the
   disable.

Fix:
- Implement selectionChanged() to enable the move controls only when at
  least one row is selected AND there are no pending (yellow) edits.
- Connect it to both selectionModel::selectionChanged and model::dataChanged
  so the button state is always consistent with actual UI state.
- Route reload()'s re-enable through selectionChanged() instead of calling
  setDisabledMove(false) directly, so the selection state is respected
  immediately after a move.
- Add a tooltip to m_move_pb explaining the disabled state.
- Add the missing change guard to LED_CELL in setData().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-24 22:20:43 +12:00
ispyisail 745b4a936c Fix bugtracker #273: skip crash-recovery backups of an unchanged project
writeBackup() rebuilds the whole project's XML with toXml() on the GUI
thread before handing the file write to a worker thread. On a big project
that freezes the interface for seconds (bugtracker #273, and #329, where
the interval was raised from 2 to 20 minutes to make it rarer). It ran
right after every project was opened, and then every 20 minutes whether
or not anything had changed.

Only write a backup when something changed since the last one: the undo
stack moved, setModified(true) was called, or the embedded element or
title block collections changed. A project just opened from a file starts
clean, since the file is already what a crash would restore. New projects
and projects restored from a backup are backed up at once, as before.

Measured on examples/industrial.qet (50 folios), each backup blocks the
GUI thread for about 0.28 s on a fast machine; with a 2 s test interval,
the old code backed up on every tick, the new code only after a change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 21:57:42 +12:00
ispyisail 7ac3719c37 Fix bugtracker #340: map "MS Shell Dlg" aliases on Qt 6/Windows
"MS Shell Dlg 2" is a Windows font alias, not a font, and projects and
settings saved on Windows carry it. Qt 5's GDI backend let Windows resolve
it to Tahoma; Qt 6's DirectWrite backend does not know the alias and falls
back to Arial, so those texts render heavier on screen and in exported PDFs.

Register the substitutions Windows itself uses, before any application
object exists so the headless export and scripting paths get them too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 21:41:00 +12:00
Laurent Trinques ddfa234943 Merge pull request #1011 from Kellermorph/save-sheet-colour
Remember the sheet background colour between runs
2026-09-24 10:50:34 +02:00
Kellermorph 9af562fcbd Remember the sheet background colour between runs
Picking a sheet (folio) background colour in the diagram editor was lost
on every restart. Diagram::background_color is a static initialised to
white and PaletteGraphicsView's s_custom_bg a static bool, and neither
was ever written anywhere -- Diagram::toXml() carries no colour attribute
either -- so closing and reopening a project always came back on the
default and the choice had to be made again.

Store it in QSettings under diagrameditor/sheet_background_* as a pair of
values rather than one: the colour, and whether it was picked explicitly.
Both halves are needed. "#ffffff, follow the system" and "#ffffff, always
white" are the same colour and two behaviours -- the first is what the
views invert on a dark palette -- so keeping only the colour would
silently turn one into the other on the next start, which is the reported
problem one step removed.

The colour is written as HexRgb on purpose. The SVG export gives
Diagram::background_color an alpha of 0 to render a transparent
background and never puts it back, and that transient value must not be
persisted as a permanently transparent sheet.

Applied from main() after the headless export and scripting branch -- those
return before reaching it and must keep rendering on plain white, the rule
ProjectPrintWindow already enforces for printing -- and before QETApp is
constructed, since that constructor already loads the projects given on
the command line. The GUI export dialog is left alone: it renders through
drawBackground(), so what you see is what you export, as it already was
within a session.

Saved at the moment the colour is applied rather than at shutdown, so
neither the print window's temporary white nor the SVG export's alpha can
reach it. The button's constructor now mirrors the stored state instead of
always claiming "system colour", and the "recently used" list is stored
alongside it.

Covered by tst_sheetbackgroundsetting, which pins the custom flag and the
dropped alpha -- the two rules a single stored colour would lose.
2026-09-24 10:30:01 +02:00
ispyisail f3a31c291f Fix green fragments left by text resize handles when moving an element
The two width-resize handles added in #591 were free scene items, moved
with setPos() from inside DynamicElementTextItem::paint(). Moving an item
while the view is painting is outside what QGraphicsView's partial
repaint tracks: the handle was drawn only where the current repaint band
overlapped it, and its old position was not reliably cleared. Moving or
zooming a selected element left green slivers on the folio (#1002).
Since 29d16c333 the handles show on every text of a selected element,
so any ordinary element move triggered it.

Make the handles children of the text instead. Qt then moves and
repaints them together with the element, in local coordinates, and
their position only needs updating when the text's size changes, which
documentSizeChanged reports (text, font, width, undo of a resize).
This also takes paint() out of the handle logic entirely.

Reproduced on Linux (Xvfb), so the issue is not Windows-specific.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:46:26 +12:00
Laurent Trinques a06220e8dc Merge pull request #1008 from ispyisail/fix/1004-element-preview-cache
Fix #1004: element preview stays stale after editing until the collection reloads
2026-09-24 09:28:33 +02:00
Laurent Trinques 54b8fe5df4 Merge pull request #1007 from elevatormind/restore-sqlite-dependencies
Restore last sqlite dependencies
2026-09-24 04:02:28 +02:00
ispyisail 2f988e6e2c Refresh cached element preview after saving from the editor
Editing an element already in the user collection, saving, and closing
the editor left the old thumbnail in the elements panel until the
whole collection was reloaded. ElementsLocation::icon() serves the
preview from two caches keyed by path+uuid -- ElementPictureFactory's
in-memory picture cache and ElementsCollectionCache's on-disk SQLite
cache -- and neither was ever told the file changed.

QETElementEditor::toLocation() writes the new XML and returns;
ElementsCollectionWidget::locationWasSaved() then refreshes the panel
item, but it reads the icon through the same two stale caches, so the
refresh was a no-op. slot_reloadElementDrawings() already shows the
correct invalidation call for ElementPictureFactory; this wires the
same pattern, plus a matching refresh of ElementsCollectionCache's
row, into the save path itself.

Fixes the preview half of #1004. The paste-cursor-jump half of that
report is a live design disagreement between two recent commits from
a different contributor and is written up separately rather than
fixed here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 12:09:01 +12:00
Magnus Hellströmer 9786006ca4 fix(database): align sqlite3 forward declarations 2026-09-23 21:45:16 +02:00
ispyisail 6fad3390df Fix issue #1002: conductor-colour toolbar swatch skips the rest of the potential
ConductorColorToolButton::applyColor() only recoloured the selected
Conductor objects. A wire drawn across a junction is several separate
Conductor segments sharing one electrical potential, so selecting one
segment and picking a colour left the rest of the wire its old colour
-- visible only by falling back to F2/double-click, which already
expands to relatedPotentialConductors() for exactly this reason
(ConductorPropertiesDialog, and QetScriptApi::setConductorProperty).

Expand each selected conductor to its potential before building the
undo command, matching that existing pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 07:08:31 +12:00
Laurent Trinques f10275cb22 Merge pull request #997 from ispyisail/revive/659-preserve-links
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m15s
Preserve master/slave links when pasting or duplicating a folio
2026-09-23 18:40:34 +02:00
Laurent Trinques 3d0e7a27a3 Merge pull request #998 from ispyisail/revive/785-case-insensitive-flags
Make auto_num_locked/potential_isolating case-insensitive
2026-09-23 18:38:38 +02:00
Laurent Trinques f14278324c Merge pull request #995 from ispyisail/revive/713-collection-cancel-race
Cancel the collection-loading map before waiting on it in ~ElementsCollectionModel
2026-09-23 18:36:17 +02:00
Laurent Trinques d5318eb7b4 Merge pull request #996 from ispyisail/revive/682-nonfinite-coordinates
Reject non-finite values in QET::attributeIsAReal()
2026-09-23 18:34:57 +02:00
Laurent Trinques f10ef3ded7 Merge pull request #994 from ispyisail/revive/664-element-info-orphan
Fix element_info orphan row causing UNIQUE constraint errors on undo
2026-09-23 18:33:05 +02:00
Laurent Trinques 6f7ea63589 Merge pull request #1000 from ispyisail/fix/933-german-source-strings
Fix bugtracker #933: German source string and comments in templates code
2026-09-23 18:26:45 +02:00
Laurent Trinques 9a6029a2bb Merge pull request #1001 from Kellermorph/plc-table-size
Plc table size
2026-09-23 18:24:15 +02:00
Laurent Trinques d889a384aa Revert "fix: harden project database export" 2026-09-23 13:50:49 +02:00
Kellermorph 3457a88e81 Open the PLC master properties dialog at three times its width
Element::editProperty() gave PropertiesEditorDialog no size of its own,
so the dialog fell back to its sizeHint, which is sized for the compact
general-purpose editors it usually hosts. A PLC master instead shows a
six-column IO table that grows horizontally, and the dialog came out
too narrow to read those columns in.

For a master whose type is PLC, resize the dialog to three times its
natural width before exec(), keeping the natural height. The width is
clamped to the available screen so it cannot run off the display, and
every other element type opens exactly as before.
2026-09-23 10:28:00 +02:00
Kellermorph 4e361f93fe Let the PLC master IO table columns be resized, and remember the layout
The IO table of MasterPropertiesWidget -- the panel that opens when a
PLC master placed on a schematic is edited -- set every section to
QHeaderView::Stretch. Stretch spreads the sections evenly over the
widget and disables section dragging altogether, so the column
boundaries were permanently fixed: they could be neither widened nor
narrowed, and only the width of the whole panel had any effect.

Use QHeaderView::Interactive instead, with movable sections and a set
of default widths, so the columns can be dragged as they are everywhere
else in the application. The layout reached this way is written to
QSettings under masterpropertieswidget/plc-table-header-state on every
sectionResized and sectionMoved, and restored the next time the table
is built -- the same header-state trick the free and linked element
trees of this widget already use, except saved automatically rather
than only from the context menu.
2026-09-23 10:27:50 +02:00
ispyisail 6a8838e719 Fix bugtracker #933: German source string and comments in templates code
sources/ElementsCollection/fileelementcollectionitem.cpp had a German
tr() source string ("Makros") in a project whose source language is
French/English elsewhere. Renamed to "Macros" (identical in both
languages, so no translation catalog change is needed). Translated
three German-language comments in elementscollectionmodel.cpp and
diagramview.cpp to English.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 17:31:38 +12:00
ispyisail 5b0785fcce Make auto_num_locked/potential_isolating case-insensitive
Reviving the still-relevant part of #785, closed 2026-09-10 purely to
clear a review backlog, not on merit. Investigated fresh against
current master -- one of the original PR's three targets turned out
to already be fixed independently: element_nomenclature_view's SQL
predicate for exclude_from_bom already does
"COALESCE(LOWER(TRIM(ei.exclude_from_bom)), '') NOT IN ('true', '1',
'yes', 'on')" (projectDataBase::createElementNomenclatureView()).

auto_num_locked and potential_isolating had no equivalent: five call
sites across terminal.cpp, terminalnumberingdialog.cpp and
elementinfowidget.cpp compared the raw stored string against the
literal "true" with QString::operator==, silently treating "True",
"TRUE", a trailing space, or any value written by something other
than this app's own checkbox as off -- with no error and no visible
difference from the checkbox being genuinely unticked.

Added QET::infoFlagIsTrue(), matching the same accepted spellings
("true"/"1"/"yes"/"on", case-insensitive, trimmed) the SQL predicate
already uses, and switched all five call sites to it.

Verified the exact comparison logic in isolation, outside any QET
build: 15 cases including "True", "TRUE", padded whitespace, "1",
"yes", "on", and their false counterparts -- all correctly
discriminated. Qt 6.10.2, ctest 13/13.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 16:47:07 +12:00
ispyisail de9b3eae06 Preserve master/slave links when pasting or duplicating a folio
Reviving #659, closed 2026-09-10 purely to clear a review backlog
(#630), not on merit. Rebuilt fresh against current master rather than
merged from the old branch (elementspanelwidget.cpp had drifted enough
that a textual merge risked silently losing content, as it did earlier
in this same session for a different revival). Builds discussion #607.

Cutting/copying a linked group of elements -- a relay coil with its
contacts, a PLC master with its slave I/O elements -- dropped the
master/slave link entirely. Traced end to end: Element::toXml() writes
each partner's uuid into <link_uuid>, Element::fromXml() reads it back
into a deferred, unresolved buffer (tmp_uuids_link), and the only code
that ever resolves that buffer is initLink(QETProject *) -- called
only from Diagram::refreshContents(), itself only called from full
project load and macro-block insertion. Neither DiagramView::paste()
nor ElementsPanelWidget::duplicateDiagram() ever call it, so
tmp_uuids_link is populated correctly and never resolved: the link is
silently dropped. duplicateDiagram() already knew this and worked
around it by calling clearPendingLinks() -- correct to not link back
to a stale source, but it meant folio duplication never preserved a
link either.

Added Element::initLink(const QList<Element *> &candidates) --
resolves against a caller-supplied list instead of a project-wide
search. The scoping is the subtle part: right after the XML round-trip
and before uuids are renewed, a pasted/duplicated element's
tmp_uuids_link still holds its source's original partner uuid, which
at that exact moment still equals the not-yet-renewed uuid of that
partner's own copy, if it was carried along in the same batch.
Resolving only within the batch is what stops a linked pair pasted
together from matching an original element left elsewhere that
happens to still carry that same soon-to-be-replaced uuid. If only one
half of a linked group is in the batch, its entry finds no match and
is dropped -- the same "leave it unlinked" outcome as before.

Wired into PasteDiagramCommand::redo(), before the existing newUuid()
loop and gated by the same first_redo flag. Wired into
duplicateDiagram() the same way, replacing its clearPendingLinks()
call (initLink() clears tmp_uuids_link internally, matched or not).

Verified live -- the original PR's own test plan left both of these
unchecked, so this closes that gap rather than repeating it. Built a
project with a linked PLC master/slave pair (qet-mcp's link_elements),
then drove the real interaction under Xvfb:

  Ctrl+A, Ctrl+C, Ctrl+V:
    originals   95ad58fc <-> e728632c   (unchanged)
    pasted      513e6bf8 <-> 29aa60b4   (linked to each other)

  Right-click folio > "Copier et coller":
    originals   95ad58fc <-> e728632c   (unchanged)
    duplicated  0a33ccb4 <-> 3264fe66   (linked to each other)

Neither copy links back to an original or comes in unlinked. Qt 6.10.2,
ctest 13/13.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 16:37:45 +12:00
ispyisail 032f2249a3 Reject non-finite values in QET::attributeIsAReal()
Reviving the still-relevant third of #682 (closed 2026-09-18 purely to
clear a review backlog, not on merit). Investigated fresh against
current master rather than merged wholesale -- two of the original
PR's three findings turned out to already be resolved independently:

- Element::valideXml() and Terminal::valideXml() already reject a
  non-finite x/y (qIsFinite checks, with comments citing this exact
  class of bug) -- added by someone else since #682 was written.
  Verified live: a project with x="nan" on an element loads and
  exports cleanly on current master, 3.1s, no hang.
- The illegal-XML-control-byte segfault in QDomDocument::setContent()
  does not reproduce either. Tested both bytes from the original
  report (0x00, 0x0E) against a real Qt6 build: both are now refused
  cleanly (XmlParsingFailed, exit 0), no crash. Qt6's QDom parses
  differently to the Qt5 one #682 was written and tested against.

What's still genuinely open: QET::attributeIsAReal() itself --
QString::toDouble()'s output parameter reports success for "nan"/
"inf"/"-inf", and this shared helper (26+ call sites across the
codebase, per #682's own count) had no finiteness check independent of
element.cpp/terminal.cpp's own since-added ones. Confirmed several
call sites are not behind either of those two gates -- notably
elementpicturefactory.cpp's line/rect/ellipse/circle/arc parsing for a
symbol's own drawing (a corrupted .elmt, not just a corrupted project
file), which was and remains reachable through this helper alone.

Qt 6.10.2, ctest 13/13.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 16:25:39 +12:00
ispyisail 1890cee801 Cancel the collection-loading map before waiting on it in ~ElementsCollectionModel
Reviving #713, closed 2026-09-21 purely to clear a maintainer review
backlog (#630), not on merit; not superseded. The crash #713 was
originally named after (bugtracker #291) was already fixed separately
by 39ac5716c, merged 14 Aug -- confirmed still on master. What's left,
and what this revives, is the one-line follow-up #713 itself narrowed
to after that: m_future.cancel() before the wait.

Without it, ~ElementsCollectionModel()'s wait runs the whole queued
QtConcurrent::map() to completion, so cancelling the open-element
dialog blocks until every remaining item has been processed -- a
visible hang on the button pressed precisely to stop the work.
cancel() drops the not-yet-started items so the wait is short, while
still waiting for whatever item is already in flight (needed so it
can't dereference this object after it's gone).

Qt 6.10.2, ctest 13/13. The responsiveness gain itself is reasoned
from QFuture's documented cancel()/waitForFinished() semantics rather
than timed -- same as the original PR's own stated verification.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 16:20:38 +12:00
ispyisail 3eea3059bf Fix element_info orphan row causing UNIQUE constraint errors on undo
Reviving #664, closed 2026-09-18 purely to clear a maintainer review
backlog (#630), not on merit; not superseded. Rewritten fresh against
current master rather than merged from the old branch -- that branch
predates the Qt6-only switch and much of dataBase/projectdatabase.cpp's
later rewrite, and the two had diverged too far for a textual merge to
be trustworthy.

projectDataBase::removeElement() only ran DELETE FROM element WHERE
uuid=:uuid. It never touched element_info, even though every element
also has a row there (element_uuid is its PRIMARY KEY, with a FOREIGN
KEY back to element.uuid that isn't enforced by this connection -- no
ON DELETE CASCADE in effect). So deleting an element left its
element_info row orphaned.

Re-adding an element with that same uuid later -- undo of that same
deletion, or a redo replaying it -- goes through addElement(), which
INSERTs into both tables. The element insert succeeds (that row really
was removed). The element_info insert hits the orphaned row's primary
key and fails, silently: the error is logged and swallowed, so the
element re-enters the scene with no element_info row at all, and
nothing later re-syncs it.

removeDiagram() already cascades this cleanup when a whole folio is
removed (a later, unrelated addition) -- confirmed on current master --
but that path never runs for a single element removed on its own,
which is the case this fixes.

Verified on the built binary, not just read: placed an element, deleted
it (Ctrl+A, Delete), undid the deletion (Ctrl+Z). Reverting just this
fix and repeating the identical sequence reproduces the exact reported
error:

  Debug: projectDataBase::addElement insert element info error :
  QSqlError("1555", "Unable to fetch row",
  "UNIQUE constraint failed: element_info.element_uuid")

With the fix, the same sequence produces nothing. Qt 6.10.2, ctest
13/13.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 16:13:04 +12:00
ispyisail 28acbadaf0 Merge remote-tracking branch 'upstream/master' into pr983-rebase-check
# Conflicts:
#	sources/dataBase/projectdatabase.cpp
#	tests/qttest/CMakeLists.txt
2026-09-23 13:16:48 +12:00