In the symbol editor, Space is the shortcut of the Rotate action, which
rotates the selected parts. The action is enabled whenever something is
selected, and a window shortcut is handled before the key reaches the
scene, so while a terminal or arc was being placed with something already
selected, Space rotated the selection and never reached ESEventAddTerminal /
ESEventAddArc. With nothing selected the action was disabled and the key
fell through, which is why it only happened with a selection.
The scene now emits behaviorChanged() when it enters or leaves AddPart,
and updateAction() keeps the selection actions (rotate, fine rotate, flip,
mirror, cut, delete, depth) disabled while a part is being added. They come
back when the tool ends. The selection itself is left alone.
ESEventInterface::init() also gives the view keyboard focus: a selection
made in the parts list left the focus there, so Space still never reached
the tool.
~ElementScene blocks its signals before deleting the event interface,
whose destructor resets the behavior while the editor is being destroyed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Windows crash report held the header and the log and nothing else: no
exception, no address, no stack (issue #1179, two reports that could not
be traced). It now also has:
- the exception, with its name ("access violation", "stack overflow"...)
- where it happened, as module+offset, and for an access violation the
address that was read or written
- a backtrace, one module+offset per frame, walked with the unwind
tables every x64 image carries (RtlLookupFunctionEntry and
RtlVirtualUnwind): no dbghelp, no symbols, no allocation. addr2line
on the same build turns each offset into a function and line.
The dump is written by a reporter thread started at install(), not on
the crashing thread, which may have no stack left or hold the loader
lock. The crashing thread waits for it for at most ten seconds.
Two kinds of crash wrote no report at all before and now do:
- abort() (std::terminate(), a failed assert): no SEH exception, so
SIGABRT is handled as well.
- qFatal(): Qt ends it with TerminateProcess() on Windows, so the
message handler now asks for the dump once the message is logged.
Elsewhere reportFatal() does nothing; SIGABRT follows there.
Tested with a cross-built RelWithDebInfo build under Wine 11, crashing
on purpose from inside the event loop: a null pointer, a call through a
null pointer, a stack overflow, abort() and qFatal(). Each report
resolved to the line that crashed. Linux reports are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A wire drawn to a folio report waited, keyed by its report, for the wire on
the linked report, in a QMap whose insert() replaces: a second wire on the
same report pushed the first out of the list, and a pair with several wires
on a side was joined by whichever came first. On the shipped examples,
re-saved as the export reads them, 130 of 3,189 wires were missing from the
list (industrial 86, m_000 41, affuteuse_250h 3).
All the report halves are now collected first, and two are joined only
when each report of a linked pair has exactly one wire. With several on a
side the diagram does not say which terminal is wired to which, so each
wire gets its own row ending at the report, as an unmatched half already
did. The result no longer depends on the order the folios are saved in.
The Page column printed each folio's number template ("%id/%total") and
the rows were sorted by it. It now prints the number the folio shows, and
the rows are sorted by the folio's position in the project.
tst_wiringlistexport checks a fixture with one report pair of each kind,
and the page column on every example.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only one conflict: the include block of
sources/ui/configpage/generalconfigurationpage.cpp, where upstream adds
editor/terminalnamecheck.h next to the two includes of the prefix
editor. All three are kept. The file list in
cmake/qet_compilation_vars.cmake merged itself this time.
Review findings on the prefix editor:
- OK without touching anything changed the file: every row was written
back, so an explicit <prefix/>, which reads as an empty field and
cancels the inheritance, was dropped and that folder started
inheriting its parent's prefix again. A row is now written only when
it was edited or no longer holds what the file has, and the two kinds
of empty field look different: hasPrefix() tells an explicit
<prefix/> from a missing one, which is what the field hint shows.
- load() copied a broken file aside before the user had chosen
anything, so every "Corriger le fichier" left a .bak behind while the
message said nothing had been modified. The copy is now made by
save(), right before the file is replaced: repairing or cancelling
leaves nothing behind, and a copy that cannot be made stops the write
rather than destroying the only copy.
- The button followed the combo box only for "Parcourir...": put back
on "Par defaut" it fell back to the previously saved path instead of
the default one. The directory of the entry being displayed is now
used, "Par defaut" being dataDir()/elements/.
The text properties showed and edited the top-left corner of a text,
whatever its alignment, and changing the alignment did not move the
text. A right-aligned label next to a symbol therefore needed a
different X for every text length to keep the same gap (#1155).
Position X/Y is now the anchor point: the point of the text chosen by
the alignment (right edge for "right", middle for "centre"...). Typing a
position puts that point there, and changing the alignment moves the
text so that the new anchor point is on the same X/Y. It is the same
point that already stays fixed when the text changes.
Applies to the element text properties on a folio and to the text
field properties in the symbol editor. The saved x/y is still the
top-left corner, so projects and symbols load and save unchanged and
look the same; only the numbers shown for texts that are not top-left
aligned change.
The geometry is in textanchor.h, shared by DiagramTextItem and
PartDynamicTextField, and tested in tst_textanchor.
Not changed: static texts of the symbol editor (PartText), whose
position is tied to the font baseline; texts inside a text group, which
have no position or alignment of their own; rotation still pivots on
the top-left corner (or the centre), not on the anchor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A template (.qetmak) was previewed and placed offset from the cursor by
the position its items had on the folio it was saved from: the preview
pixmap kept that offset, and addMacro() added it back before handing the
position to fromXml(). A template saved from the middle or the lower
right of a folio therefore landed far below and to the right of the
click, often off the folio, which reads as "the template does not drop"
(forum topic 3190). Anchor both the preview and the placement on the
template's own top-left corner instead.
fromXml() skips translating for a null position, so a click on the folio
origin is nudged by a fraction of a pixel that the grid snap removes.
Also:
- a template saved with "Create a template" only appeared in the
templates tab after reloading the collections or restarting; it is
now added to the tab of every open editor when it is saved;
- the placement status bar message ended in an untranslated
"(Makro-Anker)"; it now reads "x : y" like element placement.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the F2 conductor colour dialog and the toolbar's "Other colour..."
dialog, a double-click on a basic or custom colour swatch now chooses it
and closes the dialog, as OK does (forum topic 3190). Qt's dialog only
selects the swatch on a double-click.
ColorDialogDoubleClick filters the dialog's QWellArray swatch grids,
found by class name once the dialog is shown, because Qt builds them
only then when a native dialog was offered but not shown. Where the
platform's native colour dialog is used instead (macOS, some Linux
desktops) there are no swatches to watch and nothing changes. The
toolbar dialog is now an instance rather than QColorDialog::getColor(),
so the filter can be installed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts in cmake/qet_compilation_vars.cmake only, where upstream added
terminaluuids.cpp/.h next to the qetlabelsfile.cpp/.h this branch adds :
both are kept, qetlabelsfile first to stay alphabetical.
tr() returns the interface language, so an English or German session
would have written its own text under the "fr" name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Discussion #602. Right-click a selection holding an element with both a
manufacturer and a manufacturer reference: "Générer une vignette
d'armoire" adds a thumbnail element for each such device to a
"Cabinet thumbnails" folder of the project's embedded collection. The
thumbnail is a 120x30 frame whose dynamic text shows
%{manufacturer} / %{manufacturer_reference}; both values are also in
the definition's element information, so it shows them when placed.
A device already filed there is skipped, so running it again, or on
several copies of one device, adds nothing.
Revives #641, closed 2026-09-10 only to clear a review backlog. Changed
from it: the frame and text are placed around the hotspot (they were
drawn half outside the element), the frame is 120 wide instead of 60
(device names wrapped out of it), the file name goes through
QET::stringToFileName() (a '/' in a reference made a folder), the
version and font come from QetVersion and QETApp, and the action is
left out of the menu when no selected element qualifies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hovering a symbol's text shows its value and the hint "<Shift> to move"
("<Shift> pour déplacer" in French), which does not say what moves: a
plain drag moves the whole symbol, Shift+drag moves the text alone
(DynamicElementTextItem::mousePressEvent).
English: "Hold Shift and drag to move this text on its own".
French: "Maintenir Maj et faire glisser pour déplacer ce texte seul".
Translations only, as for #1124: the source string is unchanged, so the
other languages keep their current text.
Revives #499 (closed 2026-06-11 by its author to test locally first).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The prefixes of a collection's folders live in a qet_labels.xml that so
far could only be edited by hand, and a hand-edited file is easy to
break: one extra </category> and the file stops being well formed, which
the lookup answers with "no prefix at all". Labels then degrade
silently, without any error anywhere - the file is simply ignored. Add
a way to edit it from the settings.
- "Configurer les préfixes…" next to the user collection path opens
PrefixConfigurationDialog, which lists every folder of that
collection, subfolders included, one line edit each, with "Tout
déplier"/"Tout replier" and OK/Abbrechen. It writes on OK only :
cancelling leaves the collection exactly as it was, and an emptied
field drops the <prefix> again so the folder goes back to inheriting
its parent's.
- QetLabelsFile owns the reading, scanning, structure building and
writing of the file. prefixFromLabelFile() moves there unchanged from
assignvariables.cpp as prefixForPath(), so the lookup used by label
assignment and the one used by the dialog are one piece of code.
- Entries whose folder no longer exists on disk are offered as
Conserver or Supprimer and only applied on OK.
- An unparsable file is copied to qet_labels.xml.bak first, and the
dialog then reports the line and column of the syntax error, with
"Corriger le fichier" as the default choice - such a file may be one
forgotten tag away from being valid - and rebuilds the whole
structure only when "Reconstruire" is picked. A broken file that
cannot be backed up is refused rather than overwritten.
The scan reads non-hidden directories recursively in name order,
without following symlinks. A collection without any subfolder has
nothing to configure and says so instead of opening an empty dialog.
Tests: a standalone harness kept outside the tree (84 checks :
structure, inheritance, explicit empty prefix, orphan handling, broken
file, wrong root element, broken file whose backup cannot be written,
reload after save) and the dialog driven offscreen (29 checks,
including "reject creates no file"). ctest 27/28, the failing
tst_menubarkeyboard being the headless F10 test, unrelated to this.
The new strings are French source strings like the rest of the code;
the .ts files are left to the translation update.
Discussion #598, reviving PR #654. The crash-recovery backup written
every 20 minutes went to a single file. If the project was already in a
bad state when a backup ran, that bad state replaced the only recovery
copy.
QETProject now writes the backups in turn to three KAutoSaveFile slots
(BackupGenerations), so one bad write only replaces the oldest
snapshot. Scope is crash recovery only; the opt-in autosave is
unchanged.
After a crash, the recovery prompt groups the snapshots by project and
offers one row per project with a list to pick the snapshot to reopen,
newest selected by default. The snapshots not picked are deleted.
Ported onto current master: writeBackup() keeps the "skip if nothing
changed" check (bugtracker #273) and offerBackupFiles() keeps its place
after the stale-file filter and before the crash report (#901).
Tested with the backup interval shortened to 4 s (test build only):
after three changes, master holds one recovery file, overwritten each
time; this branch holds three, with 4, 5 and 6 folios. After killing
QET, the prompt lists the project; picking the oldest snapshot reopens
4 folios, the default reopens 6. Twice each. ctest 34/34 with and
without KDE Frameworks; the carried-over KAutoSaveFile test passes in
the nokde build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Revives #640, closed only to clear a review backlog. Two entries in
the Configuration menu: "Enregistrer la configuration sous..." saves
the settings to a .conf file, "Charger une configuration..." replaces
them with one. The file is always ini format, so a profile made on one
platform loads on the others.
Changes from #640:
- Window sizes, positions and dock layouts (".../geometry",
".../state") and the recent-files lists stay out of a profile and
are kept on load: they describe this computer, not a preference.
- A saved file carries a format marker; loading refuses files that
QElectroTech did not save.
- Loading closes QElectroTech and asks the user to start it again,
instead of starting a second copy: a second copy started while this
one is still running hands its arguments over and exits (main.cpp).
- The colour dialog's custom colours are reloaded from the profile
before quitting, since ~QETApp() saves them over the loaded ones.
- Both actions are registered with ShortcutManager, so they appear in
command search and can be given a shortcut.
- The copy rules live in the header-only utils/configprofile.h and
are unit tested (tst_configprofile).
Loading still replaces rather than merges: a setting the profile does
not have is removed, or switching profiles would keep settings from the
previous one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ElementsCollectionModel::loadCollections() runs setUpData() for every
item on worker threads (QtConcurrent::map), and setUpData() calls
setText(), setFlags(), setData() and setToolTip() on items that are
already in the model. Each of these changes the model and emits its
dataChanged() signal from a worker thread, which QAbstractItemModel
does not allow. Loading the shipped collection (8838 elements) emitted
dataChanged() 38958 times, all from worker threads.
The expensive part (reading every element file) stays on the worker
threads. Only the result is moved: ElementCollectionItem::setData()
keeps a value set from a worker thread on the item, data() returns it
to the same worker so setUpData() still reads back what it has set,
and the model applies the kept values on the GUI thread when the map
is finished, before emitting loadingFinished(). setUpData() called on
the GUI thread (macros collection, a single added or changed element)
is unchanged.
With this change the same load emits dataChanged() 38958 times, all on
the GUI thread.
Revives the still-needed part of #516, closed only to clear a review
backlog. Its other two changes are left out: the wait in
loadMacrosCollection() guarded a model shared with a running map,
which no longer happens (the macros always get a model of their own),
and qetinformation.h's static QString constants are a size clean-up,
not a bug.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
QETElementEditor, QETDiagramEditor and QETTemplateEditor already
persist their geometry via QSettings; none of the modal QDialog
subclasses did, so any dialog resized to see more of its content
(Search and Replace, Diagram properties, Export, ...) is back to its
default size the next time it's opened.
Adds QET::trackDialogGeometry(), one call at the end of each dialog
constructor (after any default resize()), following the same
restoreGeometry()/saveGeometry() pattern as the three editors above,
stored under [dialoggeometry] and keyed by class name by default. An
explicit key is used for PropertiesEditorDialog, a single class
templated over several unrelated wrapped editors, so they don't all
fight over one saved size.
Covers the 36 dialogs of the original change (#691) plus seven added
since: RenumberElementsDialog, MaterialEntryDialog, AiAssistantDialog,
DuplicateOffsetDialog, ImageTransparentColorDialog, PdfPagesDialog and
WiringListDialog.
Deliberately not touched: BackupDialog and ImageCropDialog, which
setFixedSize() themselves, and MaterialSelectionDialog, which already
restores its own size.
A size saved while the dialog sat partly off-screen is restored on
screen: restoreGeometry() moves it back inside the available screen
(checked: saved at 1502,972 on a 1600x1000 screen, reopened at 600,317).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Discussion #604. Revives #643, closed on 2026-09-10 only to clear the
review backlog, ported onto current master.
The element editor shows an infinite grid, with nothing to judge how
big a symbol will look on a real folio. Affichage > "Afficher le cadre
de fond" draws a dashed blue rectangle centred on the element's origin
(the hotspot), sized by default to the drawing area of a default folio
(BorderProperties: columns x column width by rows x row height,
1020 x 640). Affichage > "Taille du cadre de fond..." sets another
size. Both persist in QSettings (elementeditor/background_frame_*).
The frame is drawn in ElementView::drawBackground(), so it is not part
of the scene: it is never written to the .elmt file and never in the
SVG export (scene render).
Changed from #643: resolved against master's new-style connects and
the hotspot visibility switch; the toggle gets the unused
document-print-frame theme icon (light and dark), without which it
showed as a text button in the icon toolbar; the size action is
registered with ShortcutManager like the toggle.
(cherry picked from commit a245411f9f)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
masterpropertieswidget.cpp set an inline "background-color: #f0f0f0"
stylesheet on the disabled type combo box in the PLC master I/O
table. Qt already renders disabled widgets correctly without help;
the hardcoded light color just breaks under a dark palette, showing
a bright box in an otherwise dark dialog. Dropping the stylesheet
lets it render like every other disabled widget in the app.
The terminal strip window's tree dock connects to its project's
destroyed() signal with a lambda capturing `this`, but without `this`
as the connection's context, and never disconnects it. When the dock is
deleted before the project, closing the project runs the lambda on
freed memory.
The terminal strip window is a single instance, parented to the editor
window it was first opened from. With two editor windows:
1. open the terminal strip manager from window 1 (project A);
2. open it from window 2 (project B): the same window switches to B,
still a child of window 1;
3. close window 1: the terminal strip window and its dock are deleted,
the connection to B stays;
4. close window 2, or project B: heap-use-after-free in the lambda
(terminalstriptreedockwidget.cpp:66), reported by AddressSanitizer
3 times out of 3. Freed by ~TerminalStripEditorWindow from
~QETDiagramEditor.
Passing `this` as the context removes the connection with the dock, as
FreeTerminalEditor, FreeTerminalModel and TerminalStripEditor already
do for the same signal. With the fix: no error, 3 times out of 3.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A wire read "nan" or "inf" for its position, its segment lengths and
its label position without checking them: QString::toDouble() accepts
all three as numbers. PR #792 and #996 closed this gap for elements,
terminals and texts; wires were left out.
The worst case is a segment length: a single "nan" passes the check
that the segments add up to the distance between the two terminals,
because every comparison with NaN is false. The whole path then turns
to NaN: on 2612_ats_singlephase.qet one bad length out of six was saved
back as six "nan" vertical segments, and the next save dropped them and
routed the wire automatically, so each save changed the file.
Now:
- a non-finite segment length is skipped like any unreadable one, so
the path fails the coherence check and the wire is routed
automatically at load, as already happens for "inf";
- a non-finite wire position is read as 0;
- a non-finite label position or rotation is ignored, as if the label
had not been moved or rotated.
Found by the mutation sweep (python3 -m simulator sweep, mutators
inject_nan_coordinate / inject_inf_coordinate). The 24 example projects
resave byte-identical with and without this change.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Discussion #1157. IEC 61666 §4.1 requires each terminal to be
identified unambiguously within its object, so two terminals of one
element must not share a name.
On save, the element editor now:
- refuses to save when two terminals share a name, lists the names
("N ×3") and selects those terminals;
- warns, and still saves, when a terminal has no name. Folio reports,
conductor definitions and thumbnails are skipped.
Names are compared after trimming surrounding spaces, case sensitive.
Settings > General > Editor has a new checkbox, on by default, that
turns both checks off (elementeditor/check-terminal-names).
--check-elements applies the same rule: repeated names are a FAIL,
missing names a WARN. It ignores the setting, since it is an explicit
check. On the shipped collection this reports 82 FAILs, the elements
that repeat a terminal name today.
The rule lives in the header-only editor/terminalnamecheck.h, shared
by both, and is unit tested by tst_terminalnamecheck.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The lines of a wrapped or multi-line dynamic text were always centred,
whatever horizontal alignment the user picked, on the folio and in the
symbol editor alike. Follow the horizontal alignment instead.
The alignment still also sets the anchor point, as before, and saved
positions are unchanged. Existing multi-line texts set to the default
left alignment are now drawn left-aligned instead of centred.
Issue #1155
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A dragged shape put its pos() on the grid. pos() cannot be seen, and it
is off the drawn corner whenever the shape was drawn or resized with
Ctrl held, or rotated, so such a shape stayed off the grid however it
was dragged. Worse, Snap to grid (previous commit) moves pos() off the
grid to put the corner on it, so the next drag undid the snap.
QetShapeItem now overrides setPos(), which only the drag calls through
the virtual: dragged alone or with other shapes only, the top-left
corner of the drawn outline goes on the grid. Dragged together with
anything else it snaps by pos() as before, because the rest of the
selection follows this shape's movement and a corner correction would
take the symbols off the grid. Ctrl still drags freely: the snap goes
through Diagram::snapToGrid(), which reads it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>