Compare commits

...

181 Commits

Author SHA1 Message Date
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
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 d9db6e59e7 Let Escape step back out of the folio, so Tab cannot trap keyboard users
Tab cycles the folio's items, which means focusNextPrevChild() has to
refuse the usual focus traversal. On its own that leaves someone working
without a mouse able to reach the drawing area and never leave it -- the
exact person the Tab cycling was added for.

Escape now steps back out in two stages: it drops the selection first,
then hands focus to the next widget. The one-shot m_releasing_focus flag
is what lets that second Escape through the override.

Verified under Xvfb: with an item selected, Escape clears it (193k pixels
change); a second Escape changes nothing visually; a Tab after that moves
widget focus in the toolbar (306 pixels) instead of selecting on the
canvas, which is the behaviour of a view that no longer holds focus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 11:32:42 +12:00
ispyisail 22469813fe Register the two new selection actions with ShortcutManager
This branch was cut on 31 July, one day before ShortcutManager landed
in 5275fb44f, so the two actions it adds were written before the
convention existed and are the only members of the selection group not
registered: select_all, select_nothing and select_invert all are.

Without this they never appear in the shortcuts configuration page, so
a user cannot bind a key to either of them.

Registered with an empty default sequence. They are menu actions and
neither has an obvious default worth claiming; the point of registering
them is that a user can bind one if they want. ShortcutManager stores
an empty default without setting a shortcut, and the conflict checker
already skips empty sequences.

Master merged in first, because ShortcutManager does not exist at this
branch's original base.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 09:32:50 +12:00
ispyisail 88823ea35f Diagram: Tab/Shift+Tab item-selection cycling + select-all-conductors/text-fields (#574)
Implements the second pillar of #574: keyboard-driven selection on the
diagram canvas.

Tab / Shift+Tab select the next / previous item on the current
diagram, cycling through items() (z-order) and wrapping at either
end. If nothing is selected, Tab selects the first item and
Shift+Tab the last. Skipped while a text item has focus, for the
same reason arrow-key movement already guards on !focusItem().
Candidates use the same "what counts as a real selectable diagram
item" filter (QetGraphicsItem / DiagramTextItem / Conductor) already
established by Diagram::invertSelection(), so the cycling order
always matches what a user could reach by clicking.

Getting Tab to actually reach the scene needed two separate fixes,
each independently discovered by empirical testing rather than
assumption:

- QWidget (DiagramView) intercepts Tab/Backtab for widget focus-chain
  traversal before generating a key event at all. Overriding
  DiagramView::focusNextPrevChild() to return false disables that.
- QGraphicsScene (Diagram) has its own, separate item-focus-chain
  traversal, checked before keyPressEvent() is ever reached. The
  obvious fix -- overriding Diagram::focusNextPrevChild() the same
  way -- silently does nothing on Qt 5, because
  QGraphicsScene::focusNextPrevChild() only becomes virtual in Qt 6
  (guarded by the QT6_VIRTUAL macro); a compile error surfaced this
  immediately when attempted directly, rather than shipping a fix
  that worked on Qt 6 and silently no-opped on Qt 5. Intercepting
  QEvent::KeyPress in Diagram::event() instead is virtual on every Qt
  version and sidesteps the scene's internal traversal entirely.

Also adds Diagram::selectAllConductors() / selectAllTextFields(),
wired up as two new actions in the existing select_all /
select_nothing / select_invert action group in
qetdiagrameditor.cpp, so they appear in the Edit menu and go through
the same QAction -> data() -> selectGroupTriggered() dispatch as the
existing selection commands.

Verified end-to-end in a real running session (Xvfb + xdotool) with
a multi-transistor schematic: Tab/Shift+Tab correctly move a single
selection forward/backward through elements and text fields
(confirmed via the properties panel updating to each new item and
the visual selection box moving on canvas); Tab/Shift+Tab from no
selection correctly select the first/last item; "Select all
conductors" and "Select all text fields" each correctly select every
matching item and deselect everything else.

See discussion #574.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 09:32:50 +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
ispyisail 6e8d821853 Write the three auto-numbering collections in a stable order
Same defect as the <xref> ordering fixed in the previous commit, in the same
function and left behind by it: conductorAutoNum(), folioAutoNum() and
elementAutoNum() are QHash, whose key order is randomised per process, and
all three were iterated directly. A project holding more than one scheme in
any of the three categories therefore wrote those children in a different
order on every save, so opening and saving without an edit produced a file
that differed from the original, and differed again next time.

Three of the shipped examples are affected: Projet_vierge.qet has 8 conductor
schemes, industrial.qet has 4 element and 2 folio schemes, and
tableau_domestique.qet has 2 element schemes.

Sorting the key list is the same remedy already applied to the xrefs, and
changes nothing else: the same children are written, with the same contents.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 23:22:32 +12:00
ispyisail e64c0618c8 Break ties in the element sort key, so equal positions save in a stable order
Diagram::toXml() sorts elements by position alone. That is not a total
order: two elements can sit at the same x/y. lmdg.qet has a pair of
text elements both at 780,350, their sort keys are identical, and
std::stable_sort then falls back to the order QGraphicsScene handed us,
which varies between runs. The two swapped places on every save.

Appending the uuid gives a total order. This keeps the reasoning in the
existing comment intact rather than contradicting it: that comment warns
against sorting *by* uuid, because an element with no persisted uuid
attribute is given a fresh random one by fromXml() on every load. As a
tiebreaker the uuid is only consulted when two positions are equal, so
elements carrying a persisted uuid -- the colliding pair in lmdg.qet
included -- become deterministic, and a collision between two legacy
elements is no better ordered than before, but no worse.

Measured with tests/determinism, on top of the xref ordering fix:

  before both fixes      I1 0/23
  xref ordering only     I1 5/23
  with this as well      I1 6/23   (lmdg.qet newly reproducible)

No regressions against the baseline, I3 stays 23/23. Also checked
lmdg.qet directly three times rather than once, since the failure is
nondeterministic by nature and a single passing run proves nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 23:21:56 +12:00
ispyisail 3af551583c Write default XRef properties in a stable order
QETProject::toXml() iterated defaultXRefProperties().keys() straight into
the document. That is a QHash, and Qt randomises hash iteration order per
process, so every save wrote the <xref> children in a different sequence.

Saving an unchanged project therefore produced a different file each
time. The content was identical -- same size, same elements -- but the
order moved, so version control showed spurious changes on every save and
comparing two saved files showed differences that were not there.

Sorting the keys before writing makes a save reproducible. This is the
same class of problem, and the same fix, as the sort already applied to
Diagram::toXml()'s <elements> and <conductors> blocks.

Measured with tests/determinism (resave twice, compare):

  before: I1 idempotent save 0/23
  after:  I1 idempotent save 5/23

with ArduinoLCD, ShellyParts, convertisseur, schema_indus and
schema_unifilaire_voltaique2 newly reproducible, and no regressions
against the baseline.

Not the only remaining source of save instability -- the other 18
projects still fail I1 for other reasons. This fixes the hash-ordering
source only.

Note this is not a Qt6 regression. The Qt5 build happened to produce a
favourable hash order for four projects and Qt6 does not, but both were
writing an unspecified order; only the dice changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 23:21:55 +12:00
ispyisail b274170cff Merge pull request #828 from ispyisail/fix/slot-count-from-groups
Take the slot count from the contact groups when an element declares them
2026-09-12 23:19:12 +12:00
ispyisail bd7d60021c Merge branch 'master' into fix/slot-count-from-groups 2026-09-12 23:14:56 +12:00
ispyisail 12f443dd89 Merge pull request #792 from ispyisail/fix-nonfinite-coordinate-validation
Fix #781, #782: reject non-finite element/terminal coordinates on load
2026-09-12 23:08:08 +12:00
ispyisail 9afba7091c Merge branch 'master' into fix-nonfinite-coordinate-validation 2026-09-12 23:07:47 +12:00
ispyisail 3d5799773e Merge pull request #759 from ispyisail/fix-shortcut-conflict-scope
Scope shortcut conflict detection to the category (fixes #757)
2026-09-12 23:07:36 +12:00
ispyisail 9c3ab588a4 Merge branch 'master' into fix-shortcut-conflict-scope 2026-09-12 23:07:27 +12:00
ispyisail 3a26cc4b86 Merge pull request #733 from ispyisail/fix/bugtracker-243-copy-from-readonly
Fix bugtracker #243: allow copying out of a read-only element
2026-09-12 23:07:09 +12:00
ispyisail 88f5af08d7 Merge branch 'master' into fix/bugtracker-243-copy-from-readonly 2026-09-12 23:06:47 +12:00
ispyisail 29e4817254 Merge pull request #724 from ispyisail/fix/wire-name-export-double-count-forum3125
Fix wire-name export doubling every conductor's count
2026-09-12 23:06:35 +12:00
ispyisail c8b5901296 Merge branch 'master' into fix/wire-name-export-double-count-forum3125 2026-09-12 23:06:15 +12:00
ispyisail f8cb997f3b Merge pull request #719 from ispyisail/fix/titleblock-bare-variable-detection-bug245
Fix bugtracker #245: bare %name custom variables not detected in title blocks
2026-09-12 23:06:03 +12:00
ispyisail 1e8f5d8a72 Merge branch 'master' into fix/titleblock-bare-variable-detection-bug245 2026-09-12 23:05:41 +12:00
ispyisail ab8b85127f Merge pull request #788 from ispyisail/fix/projectconfigpage-virtual-cleanup
Make ProjectAutoNumConfigPage follow ProjectConfigPage's init() contract
2026-09-12 22:54:06 +12:00
ispyisail 52d5c7572c Merge branch 'master' into fix/projectconfigpage-virtual-cleanup 2026-09-12 22:49:42 +12:00
ispyisail 30661c2e2d Merge pull request #718 from ispyisail/fix/slave-xref-color-dark-theme-bug247
Fix bugtracker #247: XRef slave reference hidden with dark themes
2026-09-12 22:46:27 +12:00
ispyisail d74317c87a Merge branch 'master' into fix/slave-xref-color-dark-theme-bug247
# Conflicts:
#	sources/qetgraphicsitem/dynamicelementtextitem.cpp
2026-09-12 22:42:11 +12:00
ispyisail 256993e25b Merge pull request #721 from ispyisail/fix/element-editor-info-tab-slave-terminal-663
Enable Information tab in element editor for Slave and Terminal basetypes
2026-09-12 22:37:36 +12:00
ispyisail b18904415e Persist elementInformations for Slave elements too
Making the Informations tab visible for Slave elements is only half the
change: ElementScene::toXml() writes the <elementInformations> block for
Simple, Master, Terminal and Thumbnail, and Slave was not in that list. It
is the only place in the tree that writes that block, so the editor would
have shown an editable tab for a slave, accepted whatever the user typed
into it, and dropped it silently on save.

Visible in the shipped collection, which matches the condition exactly:
0 of 75 slave elements carry an <elementInformations> block, against 41 of
70 terminal elements.

Adding Slave is safe in both directions. ElementData::fromXml() reads
<elementInformations> unconditionally, with no check on the base type, so
existing slave elements are unaffected and newly written ones load back
correctly. It also makes populateTree()'s PLC-slave branch reachable for
the first time -- the five PLC info rows it adds are stored in
m_informations, so until now they could not have been saved either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 22:33:04 +12:00
ispyisail 18477c22b5 Merge branch 'master' into fix/element-editor-info-tab-slave-terminal-663 2026-09-12 22:32:39 +12:00
ispyisail 128d9e8dff Merge pull request #711 from ispyisail/fix/backup-restore-uaf-bug306
Fix bugtracker #306: crash when restoring backup files on startup
2026-09-12 22:22:44 +12:00
ispyisail 6eb5384048 Merge pull request #838 from arummler/avoid-lib-installation
Do not install PugiXML and Googletest static libraries.
2026-09-12 22:10:16 +12:00
Andre Rummler 4e9463ee8a Do not install PugiXML and Googletest static libraries. These are only needed during compile time. 2026-09-12 12:02:55 +02:00
Laurent Trinques baa3a16614 Merge pull request #843 from ispyisail/fix/bomexport-missing-qvariant-include
Include QVariant in bomexport.cpp
2026-09-12 11:46:58 +02:00
Laurent Trinques 32502bef2c Merge pull request #839 from ispyisail/fix/table-limitation-headless-hang
Fix command-line tools hanging on the table limitation dialog
2026-09-12 11:43:31 +02:00
Laurent Trinques c13971c0fe Merge pull request #842 from ispyisail/fix/element-picture-cache-key
Cache the drawing of element definitions that have no uuid
2026-09-12 11:41:36 +02:00
Laurent Trinques 4e8893929f Merge pull request #841 from ispyisail/fix/element-style-regex
Compile the element style pattern once instead of once per primitive
2026-09-12 11:40:21 +02:00
Laurent Trinques 59c51869b3 Merge pull request #840 from ispyisail/fix/database-rebuild-once-per-load
Rebuild the project database once per load, and not at all while closing
2026-09-12 11:28:19 +02:00
ispyisail f6f68871d1 Include QVariant in bomexport.cpp
exportBomCsv() calls query.value(i).toString(). QSqlQuery::value() returns
a QVariant, but the translation unit only ever sees the forward declaration
that arrives through qobject.h, so the call does not compile:

  sources/bomexport.cpp:79:50: error: invalid use of incomplete type
  'class QVariant'
     79 |    values.append(query.value(i).toString());

Reproduced on a clean checkout of master with Qt 5.15.18. Qt6 pulls the
full definition in by another path, so the Windows CI workflow does not
see it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N64mk33R9GdbU1PkYcc9SP
2026-09-12 18:48:13 +12:00
ispyisail 20a0a888c7 Fix command-line tools hanging on the table limitation dialog
When a placed nomenclature or summary table cannot display every row its
model holds, checkInsufficientRowsCount() informs the user with a modal
message box. It used QMessageBox directly rather than QET::QetMessageBox,
so it ignored the non-interactive mode that main.cpp sets for the
command-line verbs, and every headless verb (--info, --resave, --export-*)
blocked forever on a dialog nobody could answer.

This is the same defect fixed in e3d11a499 for the other modals reachable
from the command line; this call site was missed.

Found with a gdb backtrace on a hung --info: the process was parked in
QDialog::exec() under this function.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6MRq2Ach1ogvnGcbuqNLr
2026-09-12 18:32:14 +12:00
ispyisail d2e0532b12 Cache the drawing of element definitions that have no uuid
ElementPictureFactory caches the QPicture it builds for an element
definition, keyed by that definition's uuid. Definitions saved before uuids
were written do not have one, and every one of them presented the same null
uuid. getPictures() spotted that and took an uncached path, so the drawing
was rebuilt from the XML for every instance the project placed.

Counted on the shipped examples:

  examples/m_000.qet           831 builds for 97 definitions
  examples/affuteuse_250h.qet  256 builds for 106 definitions
  examples/industrial.qet      65 builds, 553 cache hits (has uuids)

13 of the 23 example projects carry definitions without a uuid, so this is
not a rare shape.

Derive a key from the location when the definition has no uuid of its own.
ElementsLocation::toString() qualifies an embedded path with the id of the
project owning it, and QETApp hands out project ids from an ever-increasing
counter and never reuses them, so the derived key cannot collide with an
element of another project.

Measured with callgrind, which counts instructions and so does not depend
on what else the machine is doing, opening examples/affuteuse_250h.qet:

  4,801,381,735 -> 4,285,411,908 instructions  (-10.7 %)
  ElementPictureFactory::build    995 M -> 478 M
  ElementPictureFactory::getPictures  1289 M -> 774 M

The halving of build() matches the counters independently: 106 definitions
against 256 instances is 41 %, and the cost falls to 48 %.

This also retires a latent aliasing bug rather than a measured one:
build() inserted into m_primitives_H under the same null uuid for every
definition lacking one, and getPrimitives() read back through that shared
key. Its only caller is the image export dialog, which the command line
does not reach, so no wrong output could be demonstrated here -- but the
entries could only ever have belonged to whichever element was built last.

--info stays byte identical on all 23 example projects, and the SVG export
of affuteuse_250h.qet -- a project whose definitions all lack uuids -- is
byte identical too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6MRq2Ach1ogvnGcbuqNLr
2026-09-12 18:32:14 +12:00
ispyisail a8df5e3df9 Compile the element style pattern once instead of once per primitive
setPainterStyle() built its QRegularExpression as a local, so the pattern
was compiled from scratch on every call -- and it is called for every
graphics primitive of every element instance a project places. A callgrind
profile of opening examples/affuteuse_250h.qet put 28 % of all instructions
inside libpcre2, and 12 % of the whole run inside this one function.

Making it static const compiles the pattern once for the life of the
process. Nothing else changes: same pattern, same matching, same named
captures.

Measured with callgrind, which counts instructions and so does not depend
on what else the machine is doing, opening examples/affuteuse_250h.qet:

  4,801,381,735 -> 4,297,629,948 instructions  (-10.5 %)
  setPainterStyle  582 M (12.13 %) -> 79 M (1.83 %)

--info stays byte identical on all 23 example projects, and so does every
SVG this produces for industrial.qet -- which is the output that would
change if the styles were parsed any differently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N64mk33R9GdbU1PkYcc9SP
2026-09-12 18:32:14 +12:00
ispyisail 73250bd746 Do not rebuild the project database while destroying the project
Destroying a project cost more than loading it: on a 1000 folio project
--info reported its work done in 160 s but the process ran for 657 s, and
the difference was ~QETProject().

Timing each destructor puts 94 % of that teardown in
~QetGraphicsTableItem(), with the cost per table doubling as the project
grows (48 ms at 100 folios, 122 ms at 250). The database's own per-element
deletes are 1 % of it and linear; element and conductor teardown is linear.

A table destructor repairs the chain it belonged to, which relinks the
neighbouring tables, which assigns a model -- and one branch of
setPreviousTable() builds a fresh ProjectDBModel, whose copy constructor
calls setQuery(), which rebuilds the whole database. Destroying a 250 folio
project did that 12 times, for a project that is being thrown away.

So block the rebuild for the lifetime of the destructor, next to the
blockSignals(true) already there for the same reason. Nothing can observe
the result: the database is destroyed moments later as a member of the
project. Teardown drops about fivefold at every size measured -- 1.30 s to
0.26 s at 100 folios, 7.75 s to 1.73 s at 250, 19.92 s to 4.02 s at 400 --
and the number of full rebuilds in a run stops growing with project size.

Teardown is still superlinear, now dominated by
QetGraphicsTableItem::setUpColumnAndRowMinimumSize() measuring every cell of
the nomenclature each time a chain is relinked. That is left alone here.

--info stays byte identical on all 23 example projects, as do --export-bom,
--export-wires, --export-cables, --export-nets and --export-wiring.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6MRq2Ach1ogvnGcbuqNLr
2026-09-12 18:32:14 +12:00
ispyisail 956836458a Rebuild the project database once per load instead of once per table model
ProjectDBModel::setQuery() calls projectDataBase::updateDB(), which drops
and repopulates every table in the database. The rebuild does not depend on
the query, so each table model that queries the database while a project is
being read triggers another complete repopulate of the same content.
Opening a 100 folio project ran updateDB() 26 times, 9.9 s of a 15.7 s load.

Two changes, because the first alone is not enough:

setUpdateBlocked() lets a bulk operation suppress the rebuild and do it
once when it is done. readProjectXml() already wrapped the load in
blockSignals(true) "to avoid hundreds of unnecessary emitted signal", but
that suppresses only the signal, not the work it announces; this extends
the same intent to the work. Both early returns in readProjectXml() sit
before the block, so no path leaves the database permanently blocked.

Further rebuilds are triggered after readProjectXml() returns, where the
load phase timers cannot see them -- with only the block in place
updateDB() still ran 5 times on examples/industrial.qet. So the database
now also tracks whether anything has changed since the last rebuild, and
skips repopulating when nothing has. dataBaseUpdated() is still emitted in
that case: callers and models rely on it to refresh, and what they read
back is the same either way. Every method of the class that writes rows
marks the flag; from outside, the database is reachable only through
newQuery(), and all five call sites read.

Repeating the rebuild was wasteful rather than wrong -- each
populate*Table() begins with a DELETE -- so this changes no output.
Verified byte identical --info on all 23 example projects, and identical
--export-bom, --export-wires, --export-cables, --export-nets and
--export-wiring on industrial.qet. Its load drops from 5.51 s to 5.31 s
(median of 6).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6MRq2Ach1ogvnGcbuqNLr
2026-09-12 18:32:14 +12:00
Laurent Trinques 2c88f78f69 Add new Shelly example Single-Phase And Three-Phase Designs 2026-09-12 01:13:30 +02:00
Laurent Trinques f85ee19fc0 Merge pull request #830 from enesgursoy6110/feature/smart-device-bom
Add smart device metadata to the existing BOM export
2026-09-11 23:25:04 +02:00
Laurent Trinques 2f415117f7 Merge pull request #837 from ispyisail/fix/prefix-nesting-and-multitree
Fix element prefix lookup: nesting-aware matching, multi-tree common collections (#671 items 2, 5)
2026-09-11 23:24:42 +02:00
ispyisail 7c0e867226 Rewrite element-prefix lookup for nesting and multi-tree common collections
The remaining two defects from bugtracker #671's original analysis,
which #686 knowingly didn't cover (see that PR's review thread and the
comment on the now-closed #672).

## #671 item 5: the XML matching ignored nesting

prefixFromLabelFile() was a flat token scan: it matched any <category
name="..."> whose name equalled the next path segment, with no check
that the match was actually a *child* of the previous match. It gave
correct results on the shipped 10_electric/qet_labels.xml only because
that file's document order happens to line up with its hierarchy --
any file with a same-named category at the wrong nesting depth would
silently return the wrong prefix.

Reproduced with a synthetic file where a top-level sibling category
happens to share a name with what should be an unmatched grandchild:
the old (already re-verified-fixed-for-whitespace) lookup returns a
prefix from a completely unrelated branch of the document; this
rewrite correctly reports "not found".

Fixed by replacing the QXmlStreamReader token walk with a QDomDocument
walk that only ever considers a matched node's direct <category>
children (firstChildElement()/nextSiblingElement(), scoped to that
node), which cannot cross into a same-named sibling subtree. This also
makes the whitespace-dependence fixed in #686 moot for the same
reason: DOM parsing doesn't distinguish pretty-printed from minified
input to begin with.

The inheritance rule ("if a directory has no prefix, use its parent's,
and so on") and the empty-<prefix/>-overrides-inheritance behaviour
#686 added both carry over unchanged: a category's own <prefix> child,
even an empty one, always overrides whatever a shallower ancestor
already provided; a category with no <prefix> child at all leaves the
inherited value untouched.

## #671 item 2: common-collection trees other than 10_electric

The lookup only ever consulted commonElementsDir()/10_electric --
literally: `if (current_location.fileName() == "10_electric")`. The
common collection ships four other top-level trees (20_logic,
30_hydraulic, 50_pneumatic, 60_energy); none of them could carry a
qet_labels.xml at all, because nothing ever looked for one.

Generalised to commonElementsDir()/<tree>/qet_labels.xml for whichever
top-level tree the element's path actually walks up to, tried first,
then custom, then company -- each of the latter two tried against both
a from-root layout (matching a custom/company file organised as a
mirror of the common collection, tree name included) and a
tree-relative one (matching a file scoped to just one tree), so
existing custom files keep working either way. This is the same
multi-candidate structure #686 already established for custom-then-
company; it now also covers which common-collection tree to check.

## Testing

Same constraint as #686: no working full build in this sandbox
(missing generated headers/deps), so the exact functions as committed
were extracted into a standalone Qt6 harness and run against the real
shipped 10_electric/qet_labels.xml (pretty-printed and minified),
a synthetic empty-prefix-override file, and the nesting-trap file
above -- 9/9, including the three cases #686 already fixed (direct
prefix, inherited prefix, not-found) staying correct, confirming this
rewrite doesn't regress that work.

Not exercised here (needs a real running QETApp / ElementsLocation,
which the standalone harness can't stand up): the elementPrefixForLocation()
candidate-list wiring itself -- collection_root computation, the
from-root/tree-relative dual lookup, and the common-then-custom-then-
company ordering. That code is mechanical and was reviewed carefully
by hand, but it has not been run.
2026-09-12 06:32:07 +12:00
ispyisail 0b7197118a Fix three follow-on defects in the prefix lookup this PR just refactored
Requested by @scorpio810 in review: an empty <prefix/> in the custom
collection should cancel a company-collection prefix, not fall through
to it. QXmlStreamReader::readElementText() returns a null QString for an
empty element, and the caller's isNull() check treats that the same as
"not found" -- distinguish the two so an explicit override actually
overrides. Verified in a standalone harness against a synthetic
override file, pretty-printed and minified.

Two more while in the same function, both from the original bugtracker
#671 analysis that this PR only partially addressed:

- QString path[10] with an unbounded index becomes a QStringList. The
  deepest category in the shipped collection already needs 9 of the 10
  slots; a custom collection can nest deeper, and overflow was writing
  QString objects past the end of a stack array (#671 item 3).
- The common-collection lookup still concatenated
  commonElementsDir() + "10_electric/qet_labels.xml" directly.
  commonElementsDir() returns the configured path verbatim with no
  guaranteed trailing separator, so relocating the collection to a path
  without one silently mangles this into one word and the file is never
  found -- the single most-reported cause of "prefixes don't work"
  (#671 item 1, forum #2178/#2651). QDir::filePath() joins correctly
  either way; applied to all three lookups (common, custom, company).

Also fixes a defect not in that original analysis: the token-matching
loop in prefixFromLabelFile() advanced twice per matched element --
once explicitly after a match, once more unconditionally at the bottom
of the loop -- which only produced the right result because a
pretty-printed file inserts a whitespace Characters token between
adjacent elements for the second advance to land on. A minified
qet_labels.xml has no such token, so the second advance skips clean
over the very element being searched for and the lookup silently finds
nothing -- reproduced against the real shipped 10_electric/qet_labels.xml
(returns "" instead of "K" for a plain coil, on every case tested, not
just the inheritance one). A single `continue` after a handled match
removes the double advance.

Testing: extracted the exact functions as committed into a standalone
Qt6 harness (outside the full QET build, which needs a dependency
fetch this sandbox doesn't have) and ran them against the real shipped
qet_labels.xml, pretty-printed and minified, covering a direct prefix,
inherited-from-ancestor prefix, not-found, and the explicit-empty-
override case -- 8/8, matching between formats, no regressions in the
pretty-printed results. The QDir::filePath() fix was verified
separately against both a trailing-slash and no-trailing-slash base
path. Not yet built inside the actual application (pugixml and other
generated headers aren't available standalone); the algorithm itself,
which is where all four defects lived, is what was under test.
2026-09-12 06:22:26 +12:00
enesgursoy6110 77f8e26ffc Address smart device BOM review feedback 2026-09-11 20:12:09 +03:00
enesgursoy6110 16dbc5c404 Add ungrouped device BOM CSV export 2026-09-11 20:11:28 +03:00
enesgursoy6110 73f7ea8f50 Add smart device metadata to element information 2026-09-11 20:11:19 +03:00
Laurent Trinques 99e9ce5aca QETApp: fall back to English when the QET translation is empty
A .qm compiled from a 0%-translated .ts (fi, no, rs, sk, sl, sr) loads
successfully but contains no messages, so setLanguage() treated the
language as loaded and never fell back to qet_en: users got the French
source strings instead of English. Treat an empty translator as not
loaded.

Also log the QET and Qt .qm files actually loaded in the startup
diagnostics (MachineInfo), to make translation reports easier to triage.
2026-09-11 16:28:11 +02:00
Laurent Trinques bc06f7d444 macOS: follow symlinks when looking for Qt translations
Homebrew's share/qt/translations is a symlink to the qttranslations keg;
find without -L did not descend into it and found no qtbase_*.qm.
2026-09-11 12:01:15 +02:00
Laurent Trinques 2620d34b59 macOS: look for Qt translations in the qttranslations Homebrew formula 2026-09-11 11:49:58 +02:00
Laurent Trinques 4b675dda1b CI/Windows: bundle Qt's own translations (qtbase) as lang/qt_XX.qm
windeployqt runs with --no-translations, so standard buttons (OK/Cancel)
and dialogs stayed in English. Copy each qtbase_XX.qm from the MSYS2 Qt
translations into files/lang/qt_XX.qm, where QETApp::setLanguage() looks,
with aliases for QET languages Qt only ships with a region (pt, zh).
2026-09-11 11:40:10 +02:00
Laurent Trinques e982778526 macOS: bundle Qt's own translations (qtbase) as lang/qt_XX.qm
Standard buttons (OK/Cancel) and dialogs are translated by qtbase_XX.qm,
which macdeployqt does not deploy. QETApp::setLanguage() falls back to
lang/qt_XX.qm, so copy each qtbase_XX.qm there, with aliases for QET
languages Qt only ships with a region (pt -> pt_PT, zh -> zh_CN).
2026-09-11 11:35:17 +02:00
Laurent Trinques 10e5cd00f4 macOS: rewrite absolute Homebrew refs and install ids left by macdeployqt
macdeployqt kept /opt/homebrew paths (e.g. libbrotlicommon's install id).
Rewrite them to @rpath/libX.dylib, copying the library into Frameworks
if needed, over 3 passes to handle chained dependencies.
2026-09-11 11:01:05 +02:00
Laurent Trinques 6bc53277ec macOS: bundle Homebrew @rpath deps macdeployqt cannot resolve
Recent Homebrew bottles (brotli, webp, sharpyuv) reference their deps as
@rpath/libX.dylib, which macdeployqt skips. Copy them from /opt/homebrew/lib
into Contents/Frameworks after macdeployqt, and abort if any @rpath or
/opt/homebrew reference remains unresolved. Drop the ineffective -libpath.
2026-09-11 10:50:48 +02:00
Laurent Trinques 7d4cbc30f1 cmake: add qet_ko.ts to TS_FILES
qet_ko.qm was tracked in git but qet_ko.ts was never listed in TS_FILES,
so Korean was no longer built after #751 removed the tracked .qm files.
2026-09-11 10:45:24 +02:00
Laurent Trinques 2fcf3540dd macOS: pass -libpath=/opt/homebrew/lib to macdeployqt, abort on unresolved rpath
Homebrew libs (brotli, webp, sharpyuv) now reference their deps via @rpath,
which macdeployqt could not resolve, leaving them out of the bundle.
2026-09-11 10:23:17 +02:00
Laurent Trinques a45a984b32 CI/Windows: use find instead of compgen -G to locate .qm files
$GITHUB_WORKSPACE is a Windows path under MSYS2 (D:\a\...); compgen -G
treats its backslashes as escapes, so the generated .qm were not found.
2026-09-11 10:00:35 +02:00
Laurent Trinques 1f647fa229 CI/Windows-build: copy generated .qm from build/lang after #751 2026-09-11 09:40:35 +02:00
Laurent Trinques c4d2f0c4fb MacQetDeploy_arm64_cmake.sh: take .qm from build/lang (#751), fail if translations are missing 2026-09-11 09:37:44 +02:00
Laurent Trinques ac47f52eb0 Merge pull request #751 from arummler/translation_file_cleanup
Compiled translation file proposal
2026-09-11 09:29:16 +02:00
Andre Rummler d2e75b4195 Removing the qm translation files as by default they are rebuild by CMake anyhow. Changing the output directory which pointed to the source directory. That avoids
polluting the source directory during out-of-source builds and it is standard that these files should be located inside the build directory during the build step.
2026-09-11 08:24:43 +02:00
Laurent Trinques bbc995c91e Merge pull request #835 from ispyisail/fix/wiring-list-numeric-order
Wiring list: numeric wire-number order, UTF-8 BOM and atomic write
2026-09-11 08:01:58 +02:00
ispyisail 2322e6fd12 Write the wiring list CSV atomically, with a UTF-8 byte order mark
Second of @scorpio810's review notes on #630:

  exportWiring() follows the existing CLI exporters (QTextStream, plain
  QFile). On Qt6 the output is UTF-8, so encoding is fine. Once #830 is
  in, it could optionally reuse BomExport::writeCsv() to get a BOM,
  which Excel needs to detect UTF-8 when opening the file directly, and
  an atomic write.

Done directly rather than waiting on #830, since neither half depends on
it and both are small.

The bytes were already UTF-8; what was missing is the mark that tells
Excel so. Opening a .csv without one, Excel falls back to the local
8-bit codepage and mangles any accented element label -- the common case
for this project's users.

QSaveFile replaces QFile so a failure part-way through leaves the
previous file intact instead of a truncated one. QSaveFile is already the
codebase's pattern for this (QET::writeToFile, qet.cpp:664).

Verified on perceuse.qet: output now starts ef bb bf, the header follows
intact, all 156 rows are preserved, and the file parses as utf-8-sig.
Pointing the exporter at a missing project leaves an existing target file
untouched, where before it would have been truncated.

Left the other CLI exporters alone. They share the same pattern, but
changing exportBom() would add a BOM to output that existing scripts
already consume, which is a behaviour change outside the scope of this
review note.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 12:52:19 +12:00
ispyisail b5722c3f2a Sort the wiring list by wire number as a number, not as text
Follows @scorpio810's review note on merging #630:

  ORDER BY diagram_position, wire_number sorts wire numbers as text,
  so "10" comes before "9".

Confirmed against the corpus: perceuse.qet put 111 before 12, and
affuteuse_250h.qet put 45 before 5. industrial.qet happened to look
correct only because its wire numbers are all the same width.

Wire numbers are free text and are not always numeric -- perceuse.qet
also carries an unresolved "%sequ_1" -- so the ordering has to cope with
both. Numeric values come first, ordered by value; anything else follows,
ordered as text. The trailing wire_number keeps ties stable.

Fixed in both places the query appears: the CLI exporter and the wiring
list dialog. They had the same ORDER BY, so fixing only one would have
made the dialog and --export-wiring disagree about the order of the same
data.

Verified on perceuse, affuteuse_250h, industrial and tremie_vibrante:
zero out-of-order numeric pairs afterwards, row counts unchanged, and
"%sequ_1" now sorts after the numbers rather than among them. Folio 3 of
perceuse.qet reads 0 1 2 3 4 4 5 5 6 6 7 7 12 12 where it previously
interleaved 111 before 12.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 12:43:07 +12:00
Laurent Trinques 2926ca7306 Doxyfile fix version name 2026-09-10 23:31:52 +02:00
Laurent Trinques c7893c8229 Merge pull request #630 from ispyisail/feature-wiring-list-export
Wiring list dialog + excluded-conductor count (discussion #503, slice 4)
2026-09-10 22:45:22 +02:00
Laurent Trinques 412bc7f71f Merge pull request #629 from ispyisail/feature-wiring-list-view
Add wiring_list_view: from-to wiring list over the conductor tables (discussion #503, slice 3)
2026-09-10 22:44:27 +02:00
Laurent Trinques c147e6562d snap: add libcups2-dev to build-packages
Qt6 PrintSupport records Cups::Cups as a third-party dependency
(qprint_p.h includes <cups/ppd.h>), so find_package(Qt6 PrintSupport)
runs FindCups at configure time and fails without the CUPS headers.
Build-time only, nothing is staged.
2026-09-10 21:49:02 +02:00
ispyisail bee25a4ca9 Merge pull request #661 from ispyisail/fix-cli-modal-dialog-hang
Fix command-line tools hanging forever on a modal message box
2026-09-11 07:10:15 +12:00
Laurent Trinques 6d09041dce Merge pull request #831 from ispyisail/feature/advisory-slave-limit
Make the slave limit advisory rather than a refusal
2026-09-10 14:50:56 +02:00
ispyisail 1a12d440b2 Merge pull request #834 from ispyisail/fix/qlabel-pixmap-deprecation
Use QLabel::pixmap()'s non-deprecated by-value form on Qt5
2026-09-10 16:51:55 +12:00
ispyisail 85f46f2b48 Use QLabel::pixmap()'s non-deprecated by-value form on Qt5
#824 read the pixmap through the pointer overload, which Qt 5.15
deprecates, so the fix it introduced compiled with two deprecation
warnings of its own. Qt 5.15 offers the by-value form behind
Qt::ReturnByValue, so both branches can take the same overload and the
difference reduces to the argument.

Equivalent: the pointer overload returns nullptr when no pixmap is set,
which the old expression turned into a null QPixmap; pixmap(
Qt::ReturnByValue) returns a null QPixmap directly. It also drops the
null check, so the Qt5 branch is now a single expression.

Verified both arms of the #if, since a preprocessor-branched change is
only half tested otherwise:

 - Qt 5.15.18: deprecation warnings for this file 2 -> 0, builds clean,
   binary runs
 - Qt 6.10.2: builds clean, 488/488, links
 - 22 example projects load and export with no crash or hang

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:50:49 +12:00
Laurent Trinques 9637420756 Update Links to Doygen documentation and qch file 2026-09-10 05:27:57 +02:00
Laurent Trinques 6c68c461b1 Set Doxyfile version to 0.200.1 2026-09-10 05:24:31 +02:00
Laurent Trinques c830101ba6 Update CMakeLists.txt set version to 0.200.1 2026-09-10 02:33:31 +02:00
Laurent Trinques a1c0907811 Set QT_VERSION_MAJOR to 6
https://github.com/qelectrotech/qelectrotech-source-mirror/pull/824#issuecomment-5610090520
2026-09-10 02:30:53 +02:00
ispyisail ffd829bb69 Make the slave limit advisory rather than a refusal
max_slaves records how many contacts a part is expected to carry. It was
enforced as a rule the drawing had to obey, which obstructs the way both
@scorpio810 and @IBSYSLevi described working in #819: draw the schematic
first, choose the physical hardware afterwards. A limit that refuses the
link forces the hardware decision up front, which is exactly what they
said gets in the way.

Two changes, both in the UI rather than in isFull(), which stays the
query it always was:

 - MasterPropertiesWidget::on_link_button_clicked() now says the limit
   is reached and asks whether to link anyway, defaulting to yes,
   instead of refusing outright.

 - LinkSingleElementWidget no longer removes a full master from the
   candidate list. That was the worse half: a master at its limit simply
   was not there, indistinguishable from one that does not exist, with
   nothing to say why. It now stays selectable and the user decides.

PLC masters are deliberately left alone. Their limit is the number of
declared IO slots, which is structural rather than advisory -- a link
past it would have no IO index to map to -- and PlcLinkWidget already
tells the user when it hides one, via m_hidden_masters_label.

Only coils that opt into a limit are affected: max_slaves defaults to
-1, and no project in examples/ sets it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 11:54:52 +12:00
Laurent Trinques 957bbe5edb macOS: drop obsolete lang1/ copy step
lang1/ was a leftover from the pre-Qt6 translation pipeline.
Qt6/CMake now produces all .qm files directly into lang/, which
is already copied above, making this step dead code.
2026-09-09 16:02:02 +02:00
Laurent Trinques ce0ba4a681 Merge pull request #829 from jp2images/fix-macos-bundle-identifier
Set a bundle identifier so macOS file dialogs work
2026-09-09 15:57:21 +02:00
Laurent Trinques 8295083f05 misc: align macOS bundle identifier with app's settings domain
CFBundleIdentifier was "org.qelectrotech", but Qt derives
"org.qelectrotech.QElectroTech" from setOrganizationDomain()
and setApplicationName() for the app's own preferences file
(~/Library/Preferences/org.qelectrotech.QElectroTech.plist).

Align the two so the shipped bundle and the CMake target (see
CMakeLists.txt MACOSX_BUNDLE_GUI_IDENTIFIER) use the same
identifier regardless of build path.

Note: this changes the bundle's LaunchServices identity, so
users may need to redo "Open With QElectroTech" file
associations once after updating.
2026-09-09 15:54:28 +02:00
Jeff Patterson a2441a6f81 Set a bundle identifier so macOS file dialogs work
CMakeLists.txt marks the macOS target as MACOSX_BUNDLE but never sets
MACOSX_BUNDLE_GUI_IDENTIFIER, so CMake's default Info.plist template
substitutes an empty string for CFBundleIdentifier.

An .app with an empty identifier is never registered by LaunchServices
(`lsappinfo info` reports bundleID="" and bundle path=[NULL]). AppKit
runs the open/save panel in an XPC service keyed on the client's bundle
identifier: the service is spawned on each request but presents no
window, so QFileDialog::getOpenFileName() and getSaveFileName() return
an empty string without a panel ever appearing. In QET this means
File > Open and File > Save as silently do nothing -- openProject()
receives an empty path and returns at its `if (filepath.isEmpty())`
guard. Every macOS CMake build has been affected since the target
became a bundle.

Fill in the identifier along with the other bundle metadata CMake's
template expects. org.qelectrotech.QElectroTech is the identifier Qt
already derives from setOrganizationDomain("qelectrotech.org") and
setApplicationName("QElectroTech") for the app's own preferences file,
so the bundle now agrees with what the app writes at runtime.

Verified on macOS 27 with Qt 6.11: before the change File > Open and
File > Save as present nothing; after it both panels open normally. No
code signing step is needed -- the linker's ad-hoc signature still
reports the executable name as its identifier, and the panels work
regardless once the plist is correct.
2026-09-09 05:16:50 -05:00
Laurent Trinques c1f9af8544 Merge pull request #822 from enesgursoy6110/fix/report-link-picker
Show folio identity first in report link picker
2026-09-09 07:49:42 +02:00
Laurent Trinques 033c2f93a8 Merge pull request #827 from ispyisail/feature/per-type-contact-budget
Show used against declared capacity where a master declares contact groups
2026-09-09 07:29:39 +02:00
Laurent Trinques 83623a0fa6 Merge pull request #826 from ispyisail/feature/general-tab-contact-count
Show the NO/NC/SW contact breakdown in a master's General tab
2026-09-09 07:29:07 +02:00
Laurent Trinques c85f80bbcf Merge pull request #825 from ispyisail/fix/contact-usage-counting-v2
Count master contacts in one place, and count contacts not elements
2026-09-09 07:28:15 +02:00
ispyisail c6995a0e7e Take the slot count from the contact groups when an element declares them
An element can declare contact groups and a max_slaves that disagree with
each other, and nothing reconciles them.

The element editor keeps the two in step: max_slaves sizes the contact
group table, one row per slot. Nothing does so on load, so a hand
written or generated file can carry five groups and max_slaves=2. That
loads without complaint, isFull() then caps linking at two, and
ContactGroupSelectionDialog still offers all five groups -- so the user
is shown groups that cannot be linked to, with nothing to explain why.

When groups are declared they are the slots: a slave occupies exactly
one, and the selection dialog offers exactly these. So take the limit
from the group count, which is also the number the user can see.
max_slaves stays as the fallback for the elements that declare no
groups, which today is every element in the standard collection.

No element in the collection declares contact groups, so this changes
nothing for existing projects.

Verified with two purpose-built fixtures, since no real element
exercises either path: a coil declaring five groups with max_slaves=2
now takes the limit from the groups, and a coil with max_slaves and no
groups still takes the fallback path unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 14:41:11 +12:00
ispyisail f18eda845b Merge branch 'feature/general-tab-contact-count' into feature/per-type-contact-budget
# Conflicts:
#	sources/ui/elementpropertieswidget.cpp
2026-09-09 11:47:26 +12:00
ispyisail 73e9473db4 Keep the slave total in slots, and label the breakdown as contacts
Two units were being stacked in the same block. The line above reports
max_slaves, which is a number of slots, so reporting the line below in
contacts made a coil with one 4 pole slave read "maximum 4 / used 4"
while three slots were still free.

The total goes back to counting linked elements, matching the unit of
the line above it and restoring the original behaviour of that line.

The per-type breakdown keeps the pole multiplier, because that is the
question it answers -- how many contacts an auxiliary block must
provide -- and is now prefixed "Contacts :" so the two units are not
mistaken for each other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 11:46:34 +12:00
ispyisail 7307128fc3 Merge branch 'fix/contact-usage-counting-v2' into feature/general-tab-contact-count 2026-09-09 11:45:49 +12:00
ispyisail 799ff5573f Revert the isFull() change: max_slaves counts slots, not contacts
The earlier commit changed MasterElement::isFull() to compare the
contacts in use against max_slaves. That was wrong, and this restores
the original comparison against the number of linked elements.

max_slaves is a number of slots, not of contacts:

 - it sizes the contact group table in the element editor, one row per
   slot (ElementPropertiesEditorWidget::populateSlaveGroupsTable)
 - a group must match the slave's own contact count before it can be
   chosen, so a 4 pole slave needs a group declaring 4 and occupies
   that single group (ContactGroupSelectionDialog)
 - each slave stores exactly one group index
   (Element::setGroupIndexForElement)

So a coil declaring 4 slots accepts 4 slaves, whatever their pole
count. Counting contacts made one 4 pole slave fill a 4 slot coil on
its own and refuse three further links that should have been allowed.

ContactUsage stays, and its per-type tally is still what the General
tab needs: how many contacts an auxiliary block must provide is a
different question from how many slots are occupied, and only the
former wants the pole multiplier. The header now says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 11:45:36 +12:00
ispyisail 6f2c66afef Show used against declared capacity where a master declares contact groups
Second half of #819: where a coil declares what contacts it provides, the
General tab now reports each type as used against declared rather than as
a bare count.

    NO : 3/4, NC : 1/2, inverseurs : 0/1, autres : 0/0

MasterElement::contactCapacity() sums contactCount over the element's
SlaveContactGroup list, per type, reusing the same ContactUsage tally the
used count is built on. The mapping from ElementData::SlaveState onto the
tally's own type is factored into one helper so the used count and the
declared capacity cannot classify a contact differently.

Falls back to the plain count from the previous commit when an element
declares no groups, which is every element in the standard collection
today -- nothing in the corpus declares slaveContactGroups, so this
changes no existing display.

A type used beyond what is declared reads as e.g. "1/0". That is
deliberate: it says this contact does not fit the part.

Display only. Whether a declared capacity should also feed
MasterElement::isFull() is the open question in #819 and is not touched
here.

Verified end to end against a purpose-built fixture, since no existing
element exercises this path: a coil declaring two NO groups of two, one
NC group of two and one changeover group of one parses and reports
NO=4 NC=2 SW=1 other=0 total=7, matching the declaration exactly.
tst_contactusage gains a case covering capacity summed across groups
(10 cases, all passing).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 10:49:16 +12:00
ispyisail c39c7414cf Show the NO/NC/SW contact breakdown in a master's General tab
Requested in #819: after drawing a schematic you need to know how many
NO, NC and changeover contacts a coil ended up using, so you can pick an
auxiliary block that satisfies it. Until now the General tab reported
only a single total, and counting the contacts by type meant counting
rows on the cross reference by hand.

Three changes to that block:

 - the used count now counts contacts rather than linked elements. The
   label already said "contacts" while the value was
   linkedElements().count(), so a slave standing for several contacts
   was under-reported. It reads MasterElement::contactUsage(), the
   same count isFull() uses.

 - a breakdown line is added below it, printed only when the master
   actually has contacts to break down.

 - a declared limit of -1 means "no limit set" rather than a real
   limit, so it is printed as such instead of showing "-1", which
   reads as a bad value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 10:36:25 +12:00
ispyisail c33f250910 Count master contacts in one place, and count contacts not elements
MasterElement::isFull() decided whether a coil had room left with

    connected_elements.size() >= max_slaves

which counts linked *elements*. A slave stands for as many contacts as
its "number" kind information declares, so a 4 pole contact consumed a
single contact from the coil's budget instead of four. 36 elements in
the standard collection declare a number between 2 and 4, so this is
reachable, not theoretical.

Add ContactUsage, a header-only tally holding the two rules that are
easy to get wrong:

 - a slave counts once per contact it declares, not once per element
 - a changeover is counted once, as sw, and never as one NO plus one
   NC. CrossRefItem::NOElements() and NCElements() both return
   changeovers, so a count built by adding those two lists together
   reports one changeover as two contacts.

The upcoming per-type displays (the used count in the element's General
tab, and the per-type budget on the cross reference) need exactly this
count, so it lives in one place rather than being written out three
times, and isFull() now reads it too.

The header carries no graphics dependency, so the counting rules are
unit tested on their own in tests/qttest/tst_contactusage.cpp,
following the same pattern as diagramsortkeys.h.

Verified: all 9 unit tests pass, and both rules were mutation checked
(counting elements instead of contacts fails 2 tests, counting a
changeover as both NO and NC fails 3). The 23 example projects still
load and export without crash or hang, and qet-lint reports no
regressions against its baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 10:08:00 +12:00
ispyisail c265f0206c Fix Qt5 build: QLabel::pixmap() returns a pointer, not a value
ClickableImageLabel::mousePressEvent() calls pixmap().isNull() and
pixmap().width(). That is the Qt6 signature; in Qt5 QLabel::pixmap()
returns const QPixmap * and the code does not compile:

  error: request for member 'isNull' in '...QLabel::pixmap()',
  which is of pointer type 'const QPixmap*'

CMakeLists.txt defaults QT_VERSION_MAJOR to 5 when it is not specified,
so a default configuration of master has not built since 6b577ee75.

Read the pixmap once into a local, guarded the way the rest of the
codebase handles this split, which also drops four repeated pixmap()
calls in the same expression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 09:52:06 +12:00
enesgursoy6110 cfa1e3ed27 Scope shortcut conflict detection by editor 2026-09-08 21:02:46 +03:00
enesgursoy6110 36dd1624d2 Show folio identity first in report link picker 2026-09-08 21:02:46 +03:00
Laurent Trinques a583b3c43c Merge pull request #688 from IBSYSLevi/fix/saving-coil-master
Fix: saving coil master
2026-09-08 08:35:00 +02:00
Laurent Trinques 1738c3ad6c Merge pull request #771 from Kellermorph/spinbox-crossref
Add configurable distance between label and slave XRef
2026-09-08 08:20:33 +02:00
Laurent Trinques 82026d8f7c Merge pull request #818 from qelectrotech/revert-804-feature/terminal-potential-grouping
Revert "Feature: Terminal potential grouping"
2026-09-07 15:47:08 +02:00
Kellermorph a494a8fd6c Add 'Text field' option for slave cross-reference positioning 2026-09-05 18:55:14 +02:00
ispyisail 95d7b74e9e Merge remote-tracking branch 'upstream/master' into pr721-merge
# Conflicts:
#	sources/editor/ui/elementpropertieseditorwidget.cpp
2026-09-04 22:05:13 +12:00
ispyisail 68e507a522 Reject non-finite element/terminal coordinates on load (fixes #781, #782)
QString::toDouble() reports a successful conversion for "nan"/"inf"/
"-inf" -- confirmed directly -- so the existing conv_ok checks in
Element::valideXml() and Terminal::valideXml() never caught a
non-finite x/y. A NaN-positioned element reaching the scene can hang
QGraphicsScene::addItem() forever: an existing conductor's itemChange()
runs a collision test (calculateTextItemPosition() ->
QGraphicsItem::collidesWithPath()) whose underlying QPathClipper spins
without terminating when fed a NaN-valued QPainterPath, since NaN
breaks the ordering comparisons the clipping algorithm's termination
depends on (#781). Short of that, a non-finite value that doesn't
happen to trigger a collision test simply gets written straight back
out on save with nothing to stop it (#782).

Element::valideXml() and Terminal::valideXml() now also check
qIsFinite() on the parsed x/y, rejecting the whole item the same way a
missing attribute already does. DynamicElementTextItem::fromXml() has
no such reject-the-item gate (void return, no caller check), so its x/y
are clamped to 0 instead -- the same fallback the attribute lookup
already uses when x/y is missing entirely.

Verified against both original findings' exact repro steps:
- #781: Habitat-Unifilaire.qet with x="nan" on one element -- hung
  (SIGTERM'd by a 25s timeout) on an unfixed build, resaves cleanly
  (exit 0) on this one.
- #782: grafcet.qet with y="nan" on a dynamic_elmt_text -- the value
  passed straight through to the resaved file on an unfixed build;
  clamped to 0 on this one. The specific field is now stable (y="0" on
  two consecutive resaves) where it read "nan" both times before.
  (grafcet.qet has an unrelated, already-known, unmerged fix
  (PR #779) for element/terminal-order non-determinism, so a whole-file
  diff across resaves still differs for reasons unconnected to this
  change -- checked the specific once-NaN field in isolation instead.)
- Also checked -inf on affuteuse_250h.qet: same rejection, same result.

Full qet-dbcheck.py sweep of the unmutated example corpus (23
projects), 0 regressions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:23:05 +12:00
ispyisail 6657b3de2b Make ProjectAutoNumConfigPage follow ProjectConfigPage's documented init() contract
ProjectConfigPage::init() is documented as "Typically, you should call this
function in your subclass constructor" -- it runs initWidgets(), initLayout(),
and (if a project is set) readValuesFromProject() and adjustReadOnly(), in
that order. ProjectMainConfigPage's constructor follows this. Until now,
ProjectAutoNumConfigPage's did not: it called initWidgets(), its own
buildConnections(), and readValuesFromProject() directly, skipping both
initLayout() and adjustReadOnly() entirely, and calling
readValuesFromProject() with no null-project guard.

In practice this was harmless today -- this subclass's initLayout() and
adjustReadOnly() overrides are both empty, and every construction site
happens to pass a real project -- but it is exactly the kind of latent
inconsistency Joshua's own refactor notes call out for this class ("remove
inconsistent virtual method usage... allow subclasses independent
implementation"). The day someone fills in adjustReadOnly() for this page
(e.g. to disable auto-numbering editing on a read-only project, which is
what the empty override's own doc comment says it is for), the constructor
path would silently never call it.

Fixed narrowly: the constructor now calls init() like its sibling does.
buildConnections() moves to the end of initWidgets(), the same relative
position it held in the constructor, so behaviour for the paths already
exercised is unchanged. This is the safe, no-redesign half of Joshua's
note; removing the init()/initWidgets()/initLayout()/readValuesFromProject()
scaffolding itself, so subclasses are free to sequence things however they
want, is a real redesign of the ConfigPage contract and needs his sign-off
on what should replace it -- not attempted here.

Verified with a GUI capture rather than by reading: opened Project
Properties on examples/industrial.qet, selected "Numérotation auto", and
confirmed the Management tab renders with its saved policy (Conductor/Element
"Both", "Apply to Entire Project") and the Conducteurs tab's combo box comes
up pre-populated with the project's saved context ("de la nouvelle
numérotation"), which on selection correctly fills the Type/Valeur/Formule
fields -- proving both readValuesFromProject() and the buildConnections()
signal wiring still work end-to-end through the new call sequence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 23:09:12 +12:00
Kellermorph 4c038a868b fix 2026-08-23 12:51:51 +02:00
ispyisail dab4c6c424 Scope shortcut conflict detection to the category
checkConflicts() compared key sequences across the whole registry, but
QElectroTech deliberately registers one key per editor window: Undo,
Redo, New, Open, Save and Ctrl+Shift+S each exist three times, once for
the diagram, element and titleblock editors. Those are not collisions --
they act on different windows.

The result was that 60 of the 95 shipped default bindings displayed as
conflicts, so the indicator carried no information and the page looked
broken on first open.

Conflicts are now keyed on (category, sequence). The category is the
registry's existing per-window grouping, so no new concept and no
ShortcutManager API change is needed.

Fixes #757
2026-08-23 21:50:42 +12:00
Kellermorph 6208c7e5df Add configurable distance between label and slave XRef 2026-08-22 12:03:35 +02:00
Levi Jetzer 31feb7b7b0 Show the element editor's Informations tab for Terminal and Thumbnail
setTabVisible(1, ...) only allowed Simple and Master, but the tree
(updateTree()) and the actual write path (ElementScene::toXml()) both
already support elementInformations for Terminal and Thumbnail too --
three independent "is this type allowed" checks that were never
reconciled, leaving already-working support unreachable through the
UI for those two types. Slave stays excluded here, consistent with
having no elementInformations support at either of the other two
points either (separate, larger gap, not addressed here).
2026-08-21 16:23:41 +02:00
Levi Jetzer 622791f994 Remove auto_num_locked/potential_isolating/exclude_from_bom checkboxes'
"false" writes, delete key instead

Unchecking a checkbox previously wrote the key with value "false"
rather than omitting it -- behaviorally identical to every consumer
(all three do a case-sensitive == "true" comparison), but left dead
entries cluttering the .elmt file, inconsistent with how other
elementInformation fields (manufacturer, designation) are only present
when actually set. Now removes the key entirely when unchecked.
2026-08-21 15:59:06 +02:00
ispyisail a48124a27a Stop reporting wires as excluded that the list is now showing
excludedConductorCount() counted conductors whose terminals had no uuid,
which was the right rule when that was the reason they were dropped. It no
longer is: Terminal::stableUuid() derives an identity from the terminal's
geometry, so those conductors are in the table.

Left unchanged, the dialog would have told the user "671 conductors excluded"
on industrial.qet while listing all 671 of them -- a worse failure than the
one the count exists to prevent, because it undermines a list that is now
correct.

The count and the dialog's explanation both now describe the case that
actually remains: an endpoint attached to no element at all, which has no
identity to key on under any scheme.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 21:10:28 +12:00
ispyisail e19d60ae55 Report the real wire count, and show current data
The summary line exists so that an empty wiring list is distinguishable
from one where every conductor was excluded, and it was reporting the wrong
number to do it. QSqlQueryModel fetches lazily, so rowCount() straight after
setQuery() returns the rows fetched so far -- 256 -- not the size of the
query. Measured with Qt's own QSQLITE driver: a 1000-row view reports 256
until the model is drained, then 1000. The test project quoted in slice 2
has 280 conductors, so this was already displaying 256 on our own data,
plausibly enough that nobody looked twice.

Drain the model before reading the count.

Also refresh the database before building the model. The dialog queries the
database rather than the diagrams, so anything not yet written through was
invisible here; with conductor text now updated on change that gap is
smaller, but a project loaded before this dialog was ever opened still
relies on the repopulate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 21:10:28 +12:00
ispyisail 030e6ebf00 Insert an element the same way whether it is added live or on load
The element and element_info tables had two independent insert paths --
addElement() for an element added to a live diagram, and
populateElementTable()/populateElementInfoTable() for a full rebuild --
which bound the same row differently. The incremental path wrote
kindInformations()["type"] into element.sub_type; the bulk path wrote
elementData().masterTypeToString(). So the table held different values
depending on whether the project had been reloaded since the element was
placed, and element_nomenclature_view exposes that column as
element_sub_type, which ElementQueryWidget filters on for the Coil,
Protection, Commutator and PLC nomenclature options.

That divergence is the same shape as the type-filter one fixed in the
previous commit, and it is the reason this stack kept finding bugs that
were invisible while editing and only appeared after a reload. Rather
than correct a second instance of it, both paths now go through
bindElementValues() and bindElementInfoValues(), following the
bindDiagramInfoValues() helper this class already had. Live and reloaded
now agree by construction instead of by coincidence.

The bulk path's values are the ones kept, because they are what every
already saved project contains: nothing a reload produces changes, and
the previous commit's 19-project BOM regression stays valid. It is the
live path that moves, onto the values a reload would have given it
anyway.

Measured, placing one element into a new project and then saving and
reopening it:

  live      element table: slave/ x1
  reloaded  element table: slave/ x1

and for the same element, what the two paths would have stored:

  bulk (now shared): ""      incremental (before this commit): "simple"

Re-ran the BOM regression over the same 19 projects after this change:
content identical to the pre-change baseline on all 19, and identical
line-for-line on 18, the exception being the three byte-identical
photovoltaique rows already described in the previous commit.

Note for anyone reading masterTypeToString(): the const no-argument
overload returns an empty string for anything that is not a Master, so
the "coil" fallback in the static overload is only reached for real
master elements. Non-master elements get an empty sub_type, not a
spurious "coil".
2026-08-21 21:10:27 +12:00
ispyisail b855d760a8 Populate every element type; move the nomenclature filter into its view
Closes the gap left open by the previous commit, at the root rather than
around it.

populateElementTable()/populateElementInfoTable() only inserted elements
matching Simple|Terminal|Master|Thumbnail. That quietly made the element
table mean "the elements a nomenclature cares about" rather than "the
elements of the project": slave elements (relay contacts) and report
elements -- ordinary conductor endpoints -- had no row at all after a
project load, so the wiring list could not name either end of a wire
that terminated on one.

Both tables are now populated with every ElementData::Type, and the type
restriction moves into element_nomenclature_view, which is where a
"what belongs in a bill of materials" decision belongs. The mask in the
view is character-for-character the one the population used to apply, so
a relay contact is still not a BOM line item.

This is safe to do in one place because every consumer of the project
database goes through a view: element_nomenclature_view (the on-diagram
nomenclature table via ElementQueryWidget, the BOM dialog, and the
--export-bom CLI) or project_summary_view (which does not reference
element at all). Nothing queries the element or element_info tables
directly -- checked across the whole tree.

Regression evidence. --export-bom runs updateDB() and then queries
element_nomenclature_view, so it is an exact harness for what the GUI
BOM shows. Captured for 19 projects (all 17 usable examples/ plus two
slave-element fixtures) before and after:

  - BOM content byte-identical on all 19, compared as a multiset.
  - 18 of 19 are also identical line-for-line in order.
  - photovoltaique differs only in the position of three byte-identical
    rows among themselves. Its query is ORDER BY label and those rows
    share an empty label, so their relative order was never defined;
    they are indistinguishable in the output. The on-diagram
    nomenclature orders by every displayed column, so a tie there means
    the rows are identical on screen too.

Effect on the wiring list, same project and same reload path: element_info
rows 0 -> 2, and the two component columns go from blank to K2 -> K1.

Cost: the database phase of loading examples/industrial.qet (150 folios,
1794 terminals) moves from 0.210 s to 0.233 s.
2026-08-21 21:10:27 +12:00
ispyisail 5b8d05fc1e Add a wiring list dialog and an excluded-conductor count
Slice 4 of discussion #503, on top of slice 3 (#629): the smallest
surface that makes wiring_list_view visible, plus the diagnostic the
view needs to be honest about what it is missing.

Projet > "Liste de câblage (base de données)" opens a read-only table of
wiring_list_view, headed by a line stating how many conductors are
listed and, when non-zero, how many were excluded and why.

Deliberately not another exporter. QET already ships a wiring-list CSV
export (Projet > Exporter le plan de câblage, and --export-cables) which
walks the project XML; measured on the same projects it produces a row
per conductor and resolves labels correctly when the project has them.
Adding a second, competing CSV would be worse, not better -- the
database path's value is what it unlocks (terminal plans, BOM joins),
not replacing that export.

projectDataBase::excludedConductorCount() counts, from the live scene,
the conductors deliberately absent from the conductor table because a
terminal has no uuid. Counted from the scene precisely because the
database is where those conductors are not. Verified: 671 on
examples/industrial.qet (which has 1794 terminals and no terminal uuids
at all, so its list is empty and now says so), 0 on a project whose
elements do carry terminal uuids.

KNOWN GAP, not fixed here and the reason this is opened for discussion
rather than merge: after a save/reload the component columns are blank
for slave elements. populateElementTable()/populateElementInfoTable()
only insert Simple|Terminal|Master|Thumbnail, so slave elements -- relay
contacts, i.e. a large share of real wire endpoints -- have no row in
element_info for the view to read a label from. Measured on a two-slave-
contact project after reload: element rows 0, element_info rows 0,
terminal rows 2, conductor rows 1; the wire is listed (slice 3's LEFT
JOIN keeps it) but both component names are empty, where the existing
CSV export shows K1 -> K2 for the same file.

Closing that gap means widening a filter shared with the nomenclature
and summary views, which would change what those existing, shipped
features contain. That is a maintainer decision, not one to take
unilaterally inside an additive slice.
2026-08-21 21:10:27 +12:00
ispyisail b034b1a634 Add --export-wiring: the wiring list, headless
The wiring_list_view added by this slice was only reachable through the GUI,
which meant the one thing worth proving about it -- that it still describes
the project -- could not be checked without a person clicking. This is the
same shape as the existing --export-bom, which reads
element_nomenclature_view, and it makes the view verifiable in CI.

It also makes this slice useful on its own: a from-to wiring list is a thing
people want as a CSV, and it no longer waits on the dialog in the next slice.

There is deliberately an overlap with --export-cables, which produces the same
logical list from the document XML rather than the database. Keeping both is
the point: running them and diffing them is a direct check that the cache and
the document still agree, which nothing else in the codebase can do.

Measured on the example corpus, the two also differ in what they can actually
fill in. Rows carrying any endpoint data:

                        --export-cables    --export-wiring
  industrial.qet             0 / 671          541 / 671
  m_000.qet                  0 / 457          362 / 457
  affuteuse_250h.qet         0 / 263          197 / 263
  tremie_vibrante.qet        0 /  77           61 /  77
  tableau_domestique.qet    58 / 130          104 / 130

Both return a row per conductor; the XML-derived one leaves the component and
terminal columns empty on the older projects, and emits an unresolved "%id"
in its folio column. That is not an argument for removing it -- it carries
columns the view does not, and it is the independent second opinion -- but it
does mean the database path is the one with the data on the projects people
actually have.

The terminal-name columns come back empty on most projects. That is absent
source data, not a loss in transit: tableau_domestique.qet has no terminal
name on 457 of 457 terminals, and industrial.qet stores the "_" placeholder
on 1421 of 1790.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 21:06:41 +12:00
ispyisail 44ed01ff5d Don't let the wiring list lose a wire to the diagram join
The comment above this view promised that it "returns exactly as many rows
as the conductor table holds", and argued carefully for the two joins that
could have broken that -- no inner join to element, and element_info LEFT
joined. Then it ended with an inner join to diagram that it never mentioned,
which can drop rows just as easily.

Feeding the real schema a conductor whose diagram_uuid has no diagram row
returned 2 view rows for 3 conductors. With the join made LEFT it returns 3,
with a null folio instead of a missing wire.

In practice this should never fire: QETProject::diagramAdded is connected to
addDiagram(), so the folio exists before anything can be drawn on it. But an
inner join turns that into an assumption the view enforces silently, and of
all the things this view can get wrong, dropping a wire from a wiring list
is the one that matters most. The comment now says which joins are inner and
why those two are safe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 20:44:16 +12:00
ispyisail 6061c63809 Add wiring_list_view: from-to wiring list over the conductor tables
Slice 3 of discussion #503, on top of slice 2 (#628). One row per
conductor, each endpoint resolved to its element label and terminal
name -- the `F1:4 -> M200:U1` shape from the original prototype.

The view deviates from the SQL sketched in the discussion in two ways,
both because the sketched version silently loses wires:

- **No join to the `element` table.** A terminal row already carries its
  `element_uuid`, so joining `element` back just to read the same uuid
  adds nothing. Worse, it filters: `populateElementTable()` only inserts
  elements matching `Simple|Terminal|Master|Thumbnail`, so `Slave`
  elements (relay contacts and the like -- extremely common at the end
  of a wire) and report elements are simply absent from that table after
  a project load, and an inner join through it drops their conductors.
- **`element_info` is LEFT joined** for the same reason. A wire whose
  endpoint element has no info row still belongs in a wiring list; it
  comes back with an empty label rather than vanishing. Losing a wire
  from a wiring list is a worse failure than showing one with a blank
  end.

Note this only bites after a save/reload. The incremental `addElement()`
path does not apply the type filter, so a slave element placed live is
present in `element`/`element_info` and an inner join looks fine -- it
is the bulk repopulate on project load that drops it. Testing only the
live-editing path would have missed this entirely.

Measured, comparing this view against an inner-join-through-element
variant built from the same tables in the same session:

| project | conductors | wiring_list_view | inner-join variant |
|---|---|---|---|
| Polonez MR'89 wiring diagram | 280 | 280 | 280 |
| two slave contacts, after save+reload | 1 | **1** | **0** |

Polonez happens to have no slave elements at conductor ends, so both
agree there and the problem is invisible. The second case is the
minimal reproduction: place two "Simple contact" elements
(`link_type="slave"`) so autoconnect wires them, save, reload -- the
sketched view returns zero rows for a project that plainly has a wire
in it.

Acceptance criterion held throughout: `wiring_list_view` row count
equals `conductor` row count, i.e. the view itself drops nothing.
Conductors already excluded upstream (legacy terminals without uuids,
see #628) stay excluded; that remains the only thing missing from the
list, and is what slice 4 should surface a count for.
2026-08-21 20:44:16 +12:00
Levi Jetzer 884ec4bbb9 Add editable "auto_num_locked"/"potential_isolating"/"exclude_from_bom"
checkboxes to the element editor

These elementInformation keys were previously only editable on an
already-placed instance (via ElementInfoWidget on the diagram side).
Since elementInformation values are seeded from the .elmt file's own
<elementInformations> block at placement time, a symbol author had no
proper way to set these as the *default* for every future placement --
only a workaround via the generic, unvalidated key/value tree.

Adds dedicated checkboxes to ElementPropertiesEditorWidget, mirroring
ElementInfoWidget's own labels/behavior: auto_num_locked and
potential_isolating inside the existing terminal-only group
(m_terminal_gb, shown only for ElementData::Terminal), exclude_from_bom
always visible regardless of type. Written after the generic tree loop
so they take precedence over any stale raw entry for the same key.
No new storage or file format change -- purely a missing editor UI for
an already-existing mechanism.
2026-08-19 17:58:16 +02:00
ispyisail 9285d12333 Route the last load-path message box through the wrapper
The "Système de contacts modifié" warning in QETProject::addElement()'s
Erase branch called QMessageBox::warning directly, bypassing
QET::QetMessageBox and so the non-interactive guard. It is reachable
during a load, which is exactly the path this PR exists to unblock, so it
could still hang a headless run.

Behaviour is unchanged interactively, and unattended it now answers with
the Yes the call site already passes as its default -- the same "continue"
the previous code took.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 10:44:53 +12:00
Levi Jetzer fb0649ceee Fixed incorrect OR and AND checks 2026-08-14 22:23:25 +02:00
ispyisail 6f9f5f84c2 Fix bugtracker #243: allow copying out of a read-only element
Opening an element you cannot write -- anything from the QET collection,
for instance -- disabled Copy along with everything else, so there was no
way to reuse a primitive from it. The workaround was to save the whole
element into your own collection first, just to take one shape out of it.

Two actions stood in the way, and neither of them modifies anything:

- Select All and Invert Selection were in the list disabled outright when
  read only, so nothing could be selected in the first place;
- Copy was enabled only when "!m_read_only && selectedItems().count()", so
  even a mouse selection left it greyed out.

Both now work on a read-only element. Copy is safe there:
ElementScene::copy() serialises the current selection to the clipboard and
touches neither the element nor the file.

Cut, Paste, Paste-in-area, Delete, Rotate, Flip, Mirror, the depth actions
and the add-primitive tools stay disabled exactly as before, so the element
is still protected -- this only stops the editor from refusing to read out
what it is already displaying.

Verified with a chmod 444 element: Select All then Copy now work, the
clipboard receives the expected <definition> with all three primitives,
Cut/Paste/Delete remain greyed out, Save stays disabled, and the file is
untouched (same permissions, same checksum).
2026-08-13 11:28:51 +12:00
ispyisail c26cf34397 Fix wire-name export doubling every conductor's count
Forum report (qelectrotech.org/forum, topic 3125): "Export the names
of list of wires" doubled every conductor's name in the output - a
single conductor named "16AWG" was exported as two "16AWG" lines.

Root cause: ConductorNumExport::fillHash() incremented the name's
tally once per terminal instead of once per conductor - a separate
if-block for terminal1 and another for terminal2, each bumping the
same m_hash entry. Since an ordinary conductor has two terminals and
neither is a folio-report terminal in the common case, both blocks
fired and every real conductor was counted twice. wiresNum() then
faithfully repeats each name m_hash.value(key) times, so the doubled
count became doubled output lines.

Fixed by incrementing once per conductor, only skipping it entirely
when *both* ends are folio-report terminals (neither represents a
real connection) rather than checking each terminal independently.

Verified with a minimal two-conductor project via the --export-wires
CLI verb: pre-fix build produced 4 lines for 2 named conductors
(exact doubling), post-fix build produces the correct 2.
2026-08-12 10:13:05 +12:00
ispyisail 7afc1eb56b Enable Information tab in element editor for Slave and Terminal basetypes
GitHub issue #663: the "Informations" tab in the element editor's
properties dialog was only made visible for Simple and Master basetypes
(setTabVisible gate in on_m_base_type_cb_currentIndexChanged), hiding it
entirely for Slave and Terminal Block elements.

This wasn't a data-model limitation: ElementData::m_informations is read
and written identically for every basetype (elementdata.cpp), and
updateTree() already special-cased Terminal as enabled and injected
PLC-specific info rows for PLC Slave elements - that logic was simply
unreachable because the tab itself was hidden for both types. Also
flipped updateTree()'s Slave case from setDisabled to setEnabled so the
tree is actually editable once visible, matching Terminal's existing
behavior.

This lets users attach manufacturer/part-number/reference metadata
directly to Terminal Block and Slave (e.g. multi-part contactor)
elements, as requested in the issue - useful when a Slave's part number
differs from its Master's (e.g. a contactor's auxiliary contact block
vs. its coil).

Not verified: interactive element-editor GUI testing wasn't performed
in this sandbox; verified via clean incremental build only.
2026-08-11 18:00:43 +12:00
ispyisail 0c2027fd77 Fix bugtracker #245: bare %name custom variables not detected in title blocks
TitleBlockTemplate::listOfVariables() -- which scans a title block
template's cells to auto-populate the "Custom" tab in Project
Properties (a feature recently added by another contributor, see
TitleBlockPropertiesWidget::addTemplateVariables()) -- only matched
the braced "%{name}" placeholder form. The bare "%name" form (also a
legitimate, fully-supported substitution syntax -- see
TitleBlockTemplate::interpreteVariables(), which already replaces both
forms) was never matched at all, not merely mishandled on edge cases:
a cell containing "%name2" alone, "%name2 " with a trailing space, or
"%name2 %name3" with two bare variables all produced zero detected
variables, exactly matching the report (manually adding the variable
in Project Properties works fine and renders correctly, since
rendering goes through interpreteVariables()'s simple string
replacement against already-known keys, not this regex).

Fix: extend the regex to also match a bare "%name" as the longest run
of identifier characters immediately after '%', via a second
alternative/capture group. This naturally stops at whitespace, so
"%name2 " and "%name2 %name3" are both now correctly detected -- no
change to the existing braced-form handling, and the existing
globalMatch() loop already correctly finds multiple matches per cell.

Verified: clean rebuild, only the intended object file recompiled and
linked successfully. Wrote a standalone test of the regex/extraction
logic covering exactly the reported repro cases -- "%name2", "%name2 "
(trailing space), "%name2 %name3" (two bare variables), "%{name2}"
(braced form, unaffected), a braced+bare mix, plain text with no
variables, and two built-in-style names -- all extracted correctly
with no regressions to the previously-working braced form.

Not verified: the actual Project Properties "Custom" tab UI
auto-populating live, since exercising the full Xvfb GUI flow (title
block template editor > add a bare-form custom variable to a cell >
save > open Project Properties > select that template > confirm the
Custom tab lists it) was out of scope for the time available given the
extraction logic itself was already precisely verified in isolation.
2026-08-11 13:02:03 +12:00
ispyisail f9cbf2365d Fix bugtracker #247: XRef slave reference hidden with dark themes
DynamicElementTextItem's slave cross-reference sub-item
(m_slave_Xref_item, the small "_(1-D3)_"-style text next to a slave
element pointing back to its master) hardcoded Qt::black in three
places: on creation, on hover-leave, and when restoring color after
text editing. The PARENT text item's color is a real, user-configurable
property (color()/setColor(), persisted in the diagram XML, exposed in
the text editor's color picker) -- but the slave-Xref sub-item never
used it, so a user applying a dark theme/stylesheet had no way to make
this specific text visible even by explicitly setting a text color,
unlike every other text item in the diagram.

Fix: use color() (the parent DynamicElementTextItem's own configured
color, inherited from DiagramTextItem) instead of the Qt::black
constant in all three places. This doesn't change the default
appearance (color() defaults to black, same as before) but makes the
slave-Xref text finally respect whatever color the user sets on the
parent text field, giving dark-theme users the same escape hatch
already available for all other diagram text.

Verified: clean rebuild, only the intended object file recompiled and
linked successfully.

Not verified: a live visual confirmation of the slave-Xref text
picking up a non-default color, since reproducing this requires
constructing a master/slave-linked element pair with composite text
containing %{label} in an actual multi-folio project, which was out of
scope for the time available. Confidence rests on this being a direct,
mechanical substitution of an existing, already-used accessor
(color()) for a hardcoded constant, applied identically to the exact
three call sites that previously hardcoded Qt::black for this item,
with no other logic changed.
2026-08-11 12:51:54 +12:00
ispyisail 52992b31eb Fix bugtracker #306: crash when restoring backup files on startup
QETDiagramEditor::openBackupFiles() deleted the just-constructed
QETProject when it failed to reach ProjectState::Ok, but had no
continue/else after the delete - so addProject(project) ran
unconditionally on the now-dangling pointer, and addProject()
immediately dereferences it (new ProjectView(project), etc.).

This matches the report exactly: clicking Cancel on the restore-files
dialog (which just deletes the stale markers directly, never calling
openBackupFiles()) works fine, while clicking OK crashes whenever any
listed backup fails to open cleanly. Because the crash happens mid-
loop, cleanup for that file (and any later ones in the same batch)
never completes, which also explains the reporter's second complaint
that the restore list kept growing across sessions.

Fix: add the missing `continue` so a failed project is skipped
instead of being passed use-after-free to addProject().

Verified: clean rebuild, only the intended object file recompiled
and linked successfully. I attempted a live repro by crafting a
malformed stale-file marker to force ProjectState != Ok and clicking
OK under Xvfb, but this local build links against real KDE Frameworks
(BUILD_WITH_KF5=ON, confirmed via CMakeCache.txt) rather than the
in-tree nokde/kautosavefile.cpp reimplementation I initially targeted,
which uses a different marker directory/naming scheme
(~/.local/share/stalefiles/<app>/ via real KF5::KAutoSaveFile) that
I wasn't able to reverse-engineer well enough in the time available
to produce a matching malformed marker. Confidence in the fix instead
rests on the code being an unambiguous, textbook use-after-free (this
exact object is deleted on the line immediately above the missing
continue) with a single-line, side-effect-free fix.
2026-08-11 10:08:51 +12:00
Levi Jetzer d7c75ea5a5 Fixed saving coil master -> correction +
Added space for a better style ;)
2026-08-08 09:07:22 +02:00
Levi Jetzer b9153269a4 Fixed saving coil master -> correction
Added check to the type of master so that types which are not using "max slave elements" are saved with the value -1 (infinity).
2026-08-08 09:05:27 +02:00
Levi Jetzer eab9603d8a Fixed saving coil master
Removed "isVisible" check on max_slave_checkbox before saving because isVisible is maybe not true, when the ok button is pressed
Then the value -1 is written and so on not saved to the elements xml
2026-08-07 21:52:40 +02:00
Levi Jetzer e7cbb8f50e Reading prefixes from company collection
Added a reading for company collection prefixes which are overwritten by user collection prefixes
2026-08-07 17:20:45 +02:00
ispyisail e3d11a4992 Fix command-line tools hanging forever on a modal message box
`qelectrotech --resave examples/schema_indus.qet out.qet` never returns.
It is not slow -- ten minutes of wall clock consumed 0.16s of CPU, so it
is blocked, not working. The GUI opens the same project without
complaint, so the file is fine and the fault is in the headless path.

A backtrace of the stuck process:

    main
      CLIExport::run
        QETProject::QETProject(QString const&, QObject*)
          QETProject::openFile(QFile*)
            QETProject::readProjectXml(QDomDocument&)
              QET::QetMessageBox::warning(...)
                QDialog::exec()          <- waits forever

That project records version="0.3", so loading it raises the "partially
compatible with your version" warning. Interactively somebody presses
Open; with no display nobody can, and exec() spins its event loop
indefinitely. Any modal box reachable while loading does this -- the
version warning is just the one an example file happens to trigger.

Fixed at the wrapper all 52 call sites already go through rather than at
the one warning, so the whole class is closed: QetMessageBox gains a
non-interactive mode which writes the message to stderr and returns an
answer instead of constructing a dialog. main.cpp turns it on in the
CLI branch, beside the existing setBackupEnabled(false).

The answer is the caller's defaultButton when it gave one, otherwise the
first "carry on" button offered (Ok, Open, Yes, Save...), otherwise the
first button set. Both warnings in readProjectXml offer Open|Cancel and
abort on Cancel, so they resolve to Open and the project loads, which is
what a batch invocation wants. The text still reaches the user on
stderr, where previously it was lost inside an invisible dialog.

GUI behaviour is unchanged: the flag defaults to false and is set in
exactly one place, the command-line branch of main().

Verified: schema_indus.qet goes from hanging to resaving in 0.3s; all 23
example projects now complete a double-resave with element, conductor,
terminal and uuid sets intact; unit tests pass.
2026-08-05 05:26:57 +12:00
166 changed files with 59054 additions and 32321 deletions
+65 -4
View File
@@ -217,10 +217,71 @@ jobs:
cp -r "$GITHUB_WORKSPACE/examples" "$FILES/examples" || true
cp -r "$GITHUB_WORKSPACE/fonts" "$FILES/fonts" || true
cp -r "$GITHUB_WORKSPACE/lang" "$FILES/lang" || true
find "$GITHUB_WORKSPACE/build" -name "*.qm" -exec cp {} "$FILES/lang/" \; 2>/dev/null || true
echo "=== .qm files in files/lang/ ==="
ls "$FILES/lang/"*.qm 2>/dev/null | wc -l || echo "0 .qm files"
# --- Translations ---
# Since PR #751 the .qm files are no longer tracked in git: lrelease
# generates them in build/lang/ (OUTPUT_LOCATION "lang", relative to
# the build dir). Expected set = the .ts listed in TS_FILES
# (cmake/qet_compilation_vars.cmake): a missing .qm fails the job
# instead of silently shipping an untranslated build; a .ts present
# in lang/ but not listed in TS_FILES only raises a warning.
# find/grep only, no shell glob: $GITHUB_WORKSPACE is a Windows path
# (D:\a\...) and its backslashes break glob patterns.
QM_SRC="$GITHUB_WORKSPACE/build/lang"
mkdir -p "$FILES/lang"
TS_LISTED=$(grep -o 'lang/qet_[A-Za-z_]*\.ts' "$GITHUB_WORKSPACE/cmake/qet_compilation_vars.cmake" \
| sed -e 's#^lang/##' -e 's#\.ts$##' | LC_ALL=C sort -u || true)
[ -n "$TS_LISTED" ] || { echo "ERROR: cannot read TS_FILES from cmake/qet_compilation_vars.cmake"; exit 1; }
TS_PRESENT=$(find "$GITHUB_WORKSPACE/lang" -maxdepth 1 -name 'qet_*.ts' -exec basename {} .ts \; | LC_ALL=C sort)
UNLISTED=$(LC_ALL=C comm -13 <(echo "$TS_LISTED") <(echo "$TS_PRESENT"))
if [ -n "$UNLISTED" ]; then
echo "::warning::.ts files not in TS_FILES, no .qm built:" $UNLISTED
fi
find "$QM_SRC" -maxdepth 1 -name 'qet_*.qm' -exec cp {} "$FILES/lang/" \; 2>/dev/null || true
QM_PRESENT=$(find "$FILES/lang" -maxdepth 1 -name 'qet_*.qm' -exec basename {} .qm \; | LC_ALL=C sort)
MISSING=$(LC_ALL=C comm -23 <(echo "$TS_LISTED") <(echo "$QM_PRESENT"))
QM_COUNT=$(printf '%s\n' "$QM_PRESENT" | grep -c . || true)
TS_COUNT=$(printf '%s\n' "$TS_LISTED" | grep -c .)
echo "=== $QM_COUNT .qm files copied to files/lang/ (expected: $TS_COUNT) ==="
if [ -n "$MISSING" ]; then
echo "ERROR: missing translations:" $MISSING
find "$GITHUB_WORKSPACE/build" -name '*.qm' || true
exit 1
fi
# Qt's own translations (OK/Cancel buttons, standard dialogs...):
# they come from qtbase_XX.qm, not from QET's .ts, and windeployqt
# runs with --no-translations. QETApp::setLanguage() falls back to
# lang/qt_XX.qm, so copy each qtbase_XX.qm there under that name
# (qtbase_XX.qm is standalone, unlike Qt's qt_XX.qm meta catalogs).
QT_TR_DIR=/ucrt64/share/qt6/translations
if [ ! -d "$QT_TR_DIR" ]; then
QT_TR_DIR=$(cygpath -u "$(/ucrt64/bin/qtpaths6 --query QT_INSTALL_TRANSLATIONS 2>/dev/null)" 2>/dev/null || true)
fi
find "$QT_TR_DIR" -maxdepth 1 -name 'qtbase_*.qm' 2>/dev/null | while read -r f; do
l=$(basename "$f" .qm)
cp "$f" "$FILES/lang/qt_${l#qtbase_}.qm"
done
# QET languages Qt only ships with a region (pt -> pt_PT, zh -> zh_CN):
# QTranslator only shortens codes (fr_FR -> fr), it never extends them.
for q in $TS_LISTED; do
l=${q#qet_}
if [ ! -e "$FILES/lang/qt_$l.qm" ]; then
if [ -e "$QT_TR_DIR/qtbase_${l}_${l^^}.qm" ]; then
alt="$QT_TR_DIR/qtbase_${l}_${l^^}.qm"
else
alt=$(find "$QT_TR_DIR" -maxdepth 1 -name "qtbase_${l}_*.qm" 2>/dev/null | LC_ALL=C sort | head -1 || true)
fi
if [ -n "$alt" ]; then
cp "$alt" "$FILES/lang/qt_$l.qm"
fi
fi
done
QT_QM_COUNT=$(find "$FILES/lang" -maxdepth 1 -name 'qt_*.qm' | wc -l)
echo "=== $QT_QM_COUNT Qt translation files (qt_*.qm) copied from $QT_TR_DIR ==="
if [ "$QT_QM_COUNT" -eq 0 ]; then
echo "ERROR: no qtbase_*.qm found in '$QT_TR_DIR' (mingw-w64-ucrt-x86_64-qt6-translations installed?)"
exit 1
fi
for f in LICENSE ChangeLog CREDIT README ELEMENTS.LICENSE; do
cp "$GITHUB_WORKSPACE/$f" "$FILES/$f" 2>/dev/null || true
+2 -1
View File
@@ -6,4 +6,5 @@
doc/*
!doc/QElectroTech.qch
QElectroTech.tag
!doc/doc-utils
!doc/doc-utils
lang/*.qm
+20 -4
View File
@@ -19,7 +19,7 @@ include(cmake/hoto_update_cmake_message.cmake)
cmake_minimum_required(VERSION 3.5...4.2)
project(qelectrotech
VERSION 0.100.1
VERSION 0.200.1
DESCRIPTION "QET is a CAD/CAE editor focusing on schematics drawing features."
HOMEPAGE_URL "https://qelectrotech.org/"
LANGUAGES C CXX)
@@ -36,7 +36,7 @@ set(QET_DIR ${PROJECT_SOURCE_DIR})
# includes below, so every subdirectory and every FetchContent dependency
# sees a consistent, already-defined value.
if(NOT DEFINED QT_VERSION_MAJOR)
set(QT_VERSION_MAJOR 5)
set(QT_VERSION_MAJOR 6)
endif()
# Some third-party CMake projects we pull in via FetchContent (e.g.
@@ -134,6 +134,7 @@ include(cmake/fetch_kdeaddons.cmake)
option(PACKAGE_TESTS "Build the tests" ON)
if(PACKAGE_TESTS)
message("Add sub directory tests")
enable_testing()
add_subdirectory(tests)
endif()
@@ -163,7 +164,22 @@ else()
endif()
if(APPLE)
set_target_properties(${PROJECT_NAME} PROPERTIES MACOSX_BUNDLE TRUE)
# CFBundleIdentifier must not be empty. CMake's default Info.plist
# template fills it from MACOSX_BUNDLE_GUI_IDENTIFIER; with that unset
# the bundle ships an empty identifier, LaunchServices never registers
# the .app, and AppKit's open/save panel service (which is keyed on the
# client's bundle id) silently presents nothing -- every
# QFileDialog::getOpenFileName()/getSaveFileName() call returns an empty
# string without a panel ever appearing, so File > Open and File > Save
# as do nothing at all.
set_target_properties(${PROJECT_NAME} PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_GUI_IDENTIFIER "org.qelectrotech.QElectroTech"
MACOSX_BUNDLE_BUNDLE_NAME "QElectroTech"
MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}"
MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}"
MACOSX_BUNDLE_COPYRIGHT "Copyright 2006-2026 The QElectroTech Team"
)
endif()
# The default build only compiles the tracked .ts files to .qm (lrelease).
@@ -174,7 +190,7 @@ endif()
# fail with "Premature end of document".
set_source_files_properties(
${TS_FILES}
PROPERTIES OUTPUT_LOCATION "${QET_DIR}/lang"
PROPERTIES OUTPUT_LOCATION "lang"
)
if(QT_VERSION_MAJOR EQUAL 6)
if(Qt6_VERSION VERSION_LESS "6.2")
+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = QElectroTech
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = " 0.100.0-dev"
PROJECT_NUMBER = " 0.200.1"
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
+17 -13
View File
@@ -1,3 +1,20 @@
[ca]
La col·lecció d'elements que s'inclou amb QElectrotech es proporciona tal com és
i sense cap garantia que sigui adequada per al vostre ús o que funcioni correctament.
L'ús, la modificació i la integració d'aquests elements en esquemes elèctrics
estan permesos sense restriccions, independentment de la llicència final que regeixi
els esquemes.
No es permet utilitzar aquest programari ni cap fitxer associat
com a dades de mostra per crear models d'aprenentatge automàtic.
Si redistribuïu la totalitat o una part de la col·lecció QElectroTech,
amb o sense modificacions, fora d'un esquema elèctric, heu de complir
les condicions de la llicència CC-BY:
Aquesta obra està subjecta a la llicència Reconeixement 3.0,
disponible en línia a http://creativecommons.org/licenses/by/3.0/ o bé
sol·licitant-la per correu a Creative Commons, 171 Second Street, Suite 300, San Francisco,
Califòrnia 94105, EUA.
[en]
The elements collection provided along with QElectroTech is provided as is and
without any warranty of fitness for your purpose or working.
@@ -96,19 +113,6 @@ Para ver una copia de esta licencia, visite
http://creativecommons.org/licenses/by/3.0/ o envie una carta a Creative
Commons, 171 Second Street, Suite 300, San Francisco, California 94105, USA.
[ca]
La col·lecció de símbols QElectrotech és distribuïda tal qual i sense cap
garantia d'idoneïtat d'ús ni de funcionament.
Es permet incondicionalment, amb independència de la llicència final, emprar,
editar, i incloure aquests símbols en esquemes elèctrics.
Si vostè redistribueix una part de la col·lecció de QElectrotech o tota ella,
amb condicions o sense, separadament d'un esquema elèctric, haurà de respectar
les condicions de la llicència CC-BY:
Aquesta obra es troba sota una llicència Reconeixement 3.0 de Creative Commons.
Per veure una còpia d'aquesta llicència visiti
http://creativecommons.org/licenses/by/3.0/ o enviï una carta a Creative
Commons, 171 Second Street, Suite 300, San Francisco, California 94105,
[cs]
Sbírka prvků poskytovaná společně s QElectroTechem je poskytována tak, jak je,
bez záruky nebo vhodnosti pro váš účal nebo práci.
+2 -2
View File
@@ -48,8 +48,8 @@ Here are the technical choices made for the software development:
* Coding language: [C++](https://en.wikipedia.org/wiki/C%2B%2B)
* GUI translations: [Qt Linguist](http://doc.qt.io/qt-5/qtlinguist-index.html)
* Version control: [GIT](https://github.com/qelectrotech/qelectrotech-source-mirror.git)
* Doxygen documentation :[Doxygen](https://qelectrotech.github.io/qelectrotech-source-mirror/)
* QtCreator qch doxygen :[QElectroTech.qch](https://github.com/qelectrotech/qelectrotech-source-mirror/blob/master/doc/QElectroTech.qch)
* Doxygen documentation :[Doxygen](https://download.qelectrotech.org/qet/doxygen/html/)
* QtCreator qch doxygen :[QElectroTech.qch](https://download.qelectrotech.org/qet/doxygen/QElectroTech.qch)
* File format for projects, elements and titleblocks: [XML](http://www.w3schools.com/xml/xml_whatis.asp)
* Main development platform: [GNU/Linux](http://getgnulinux.org/en/linux/)
* Targeted platforms: Windows, GNU/Linux, Mac OS X, BSDs
+5
View File
@@ -85,6 +85,11 @@ parts:
- qt6-tools-dev
- qt6-base-private-dev
- pkgconf
# Qt6 PrintSupport records Cups::Cups as a third-party dependency
# (qprint_p.h includes <cups/ppd.h>), so find_package(Qt6 PrintSupport)
# runs FindCups at configure time and fails without the CUPS headers.
# Build-time only: nothing from it is staged into the snap.
- libcups2-dev
override-build: |
displayed_version=$(cat sources/qetversion.cpp | grep "return QVersionNumber{"| head -n 1| awk -F "{" '{ print $2 }' | awk -F "}" '{ print $1 }' | sed -e 's/,/./g' -e 's/ //g')
snap_version="${displayed_version}-g$(git rev-parse --short=8 HEAD)"
+1 -1
View File
@@ -26,7 +26,7 @@ if(BUILD_PUGIXML)
pugixml
GIT_REPOSITORY https://github.com/zeux/pugixml.git
GIT_TAG v1.15)
set(PUGIXML_INSTALL OFF CACHE INTERNAL "")
FetchContent_MakeAvailable(pugixml)
else()
find_package(pugixml REQUIRED)
+9
View File
@@ -174,7 +174,11 @@ set(QET_SRC_FILES
${QET_DIR}/sources/conductornumexport.cpp
${QET_DIR}/sources/wiringlistexport.h
${QET_DIR}/sources/wiringlistexport.cpp
${QET_DIR}/sources/ui/wiringlistdialog.h
${QET_DIR}/sources/ui/wiringlistdialog.cpp
${QET_DIR}/sources/conductornumexport.h
${QET_DIR}/sources/bomexport.cpp
${QET_DIR}/sources/bomexport.h
${QET_DIR}/sources/conductorprofile.cpp
${QET_DIR}/sources/conductorprofile.h
${QET_DIR}/sources/conductorproperties.cpp
@@ -326,6 +330,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/diagramevent/diagrameventinterface.h
${QET_DIR}/sources/diagramevent/diagrameventaddmacro.cpp
${QET_DIR}/sources/diagramevent/diagrameventaddmacro.h
${QET_DIR}/sources/diagramevent/diagrameventaddpaste.cpp
${QET_DIR}/sources/diagramevent/diagrameventaddpaste.h
${QET_DIR}/sources/dvevent/dveventinterface.cpp
${QET_DIR}/sources/dvevent/dveventinterface.h
@@ -473,6 +479,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/project/projectusagetracker.cpp
${QET_DIR}/sources/project/projectusagetracker.h
${QET_DIR}/sources/properties/deviceinformation.cpp
${QET_DIR}/sources/properties/deviceinformation.h
${QET_DIR}/sources/properties/elementdata.cpp
${QET_DIR}/sources/properties/elementdata.h
${QET_DIR}/sources/properties/propertiesinterface.cpp
@@ -856,6 +864,7 @@ set(TS_FILES
${QET_DIR}/lang/qet_hu.ts
${QET_DIR}/lang/qet_it.ts
${QET_DIR}/lang/qet_ja.ts
${QET_DIR}/lang/qet_ko.ts
${QET_DIR}/lang/qet_mn.ts
${QET_DIR}/lang/qet_nb.ts
${QET_DIR}/lang/qet_nl.ts
+93
View File
@@ -0,0 +1,93 @@
# Smart Device Information and BOM CSV
## Architecture
- A `.qet` project is XML. Placed component properties are held in
`ElementData::m_informations`, a `DiagramContext`, and are serialized by the
existing `Element::fromXml()` and `Element::toXml()` path.
- `DeviceInformation` is a typed view over that existing context. It does not
introduce a second data store. Updates made by the properties editor continue
to use `ChangeElementInformationCommand`, including undo/redo and dirty state.
- The project database copies element contexts into `element_info` and exposes
reporting fields through `element_nomenclature_view`.
- GUI and CLI BOM exports both use `BomExport::defaultQuery()` against that
view. The existing `BOMExportDialog` remains the only GUI export entry point;
users can still customize columns, filters, and grouped/ungrouped output.
## Device Properties
| Device field | XML/context key | Default BOM |
| --- | --- | --- |
| Device tag | `label` | Yes |
| Manufacturer | `manufacturer` | Yes |
| Part number | `manufacturer_reference` | Yes |
| Model | `model` | Yes |
| Description | `description` | Yes |
| Category | `category` | Yes |
| Quantity | `quantity` | Yes |
| Voltage rating | `voltage_rating` | Yes |
| Current rating | `current_rating` | Yes |
| Page | `folio` from the diagram | Yes |
| Notes | `notes` | Yes |
The five new context keys are `model`, `category`, `voltage_rating`,
`current_rating`, and `notes`. They are available in the existing information
editor for Simple and Master elements, in symbol definitions, in the SQL
nomenclature view, and in the export column picker.
No XML version bump or migration is required. Missing keys read as empty
strings, empty properties are omitted by the existing writer, and unknown
context keys remain preserved. Quantity and ratings stay free text so existing
unit conventions and decimal quantities are not changed implicitly.
## BOM Policy
The default BOM query exports one row per Simple or Master element. Slave
contacts, terminal-strip elements, reports, thumbnails, and conductor
definitions are excluded. A Master device is counted once and PLC I/O entries
are not expanded. The existing `exclude_from_bom` property is honored by the
SQL view, including common true representations (`true`, `1`, `yes`, `on`).
The default columns are:
`Device tag, Designation, Manufacturer, Part number, Model, Description,
Category, Quantity, Voltage rating, Current rating, Page, Notes`.
The existing **Format as bill of materials** checkbox provides grouped output;
it is off by default so the initial export remains ungrouped. Users may change
the query and column order through the existing dialog. The command-line
`--export-bom` operation uses the same default query and column order.
CSV output is UTF-8 with a BOM, semicolon-delimited, and quotes every field.
Embedded quotes are doubled and embedded newlines are retained. `QSaveFile`
provides atomic replacement; query, open, write, and commit failures are
reported without deleting an existing destination first.
## Verification
The smart-device test is registered explicitly in
`tests/qttest/CMakeLists.txt` and uses only Qt Test and Qt SQL. It is therefore
part of the normal cross-platform CMake/CTest flow:
```sh
cmake -S . -B build -DPACKAGE_TESTS=ON
cmake --build build
ctest --test-dir build --output-on-failure
```
`tst_smart_device` checks the metadata column contract, Simple/Master filtering,
stable page ordering, UTF-8 with Latin and non-Latin samples, CSV escaping, and
atomic write success/failure. Existing project XML save/load tests cover the
generic `DiagramContext` persistence path used by the new keys.
Manual checks:
1. Open an existing project and edit a Simple or Master element's information.
2. Set all device fields, apply, undo, redo, save, close, and reopen.
3. Open Project > Export as CSV and verify the device columns are preselected.
4. Export with grouping both disabled and enabled.
5. Verify leading-zero part numbers, non-Latin text, quotes, and newlines in a
UTF-8 CSV reader.
Cable management, terminal-strip planning, automatic wire numbering, PLC I/O
expansion, panel layout, and manufacturer catalog integration are out of scope.
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.
+1120 -836
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+574 -543
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1095 -818
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1092 -817
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1093 -818
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1092 -817
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1099 -818
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1116 -839
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+4891 -1856
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1389 -1112
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1109 -830
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1090 -817
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1093 -818
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1089 -816
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1088 -813
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1092 -815
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1116 -839
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+4544 -1620
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1093 -818
View File
File diff suppressed because it is too large Load Diff
+1127 -850
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+4904 -1869
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+574 -511
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1107 -830
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1093 -818
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1115 -838
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1129 -850
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1095 -818
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1117 -838
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1119 -838
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1117 -838
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1092 -817
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1090 -817
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1095 -818
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1090 -817
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
+1 -1
View File
@@ -42,7 +42,7 @@
<key>CFBundleIconFile</key>
<string>qelectrotech</string>
<key>CFBundleIdentifier</key>
<string>org.qelectrotech</string>
<string>org.qelectrotech.QElectroTech</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
-337
View File
@@ -1,337 +0,0 @@
#!/bin/sh
# Copyright 2019 The QElectroTech Team
# This file is part of QElectroTech.
# QElectroTech is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
# QElectroTech is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
# Need homebrew and coreutils installed see <http://brew.sh>.
#Force MacOSX12.3.sdk
#see: https://www.downtowndougbrown.com/2023/08/how-to-create-a-qt-5-arm-intel-universal-binary-for-mac/
export DEVELOPER_DIR=/Applications/Xcode_14.01.app/Contents/Developer
# configuration
APPNAME='qelectrotech'
BUNDLE=$APPNAME.app
APPBIN="$BUNDLE/Contents/MacOS/$APPNAME"
# Emplacement du script
current_dir=$(dirname "$0")
# On se remet au depart
cd "${current_dir}/../"
# Emplacement courant
current_dir=$(PWD)
### get system configuration ########################################
echo
echo "______________________________________________________________"
echo "This script prepares a Qt application bundle for deployment."
echo "Please see the \"Deploying an Application on Qt/Mac\""
echo "page in the Qt documentation for more information."
echo
echo "This script :"
echo "\t - up date the svn depot"
echo "\t - built the application bundle,"
echo "\t - copy over required Qt frameworks,"
echo "\t - copy additional files: translations, titleblocks and elements,"
echo "\t - create image disk."
echo
echo "Enjoy ;-)"
echo
# as long as we can find qmake, we don't need QTDIR
FWPATH=`qmake -query QT_INSTALL_LIBS`
if [ ! -d $FWPATH/QtGui.framework ] ; then
echo "ERROR: cannot find the Qt frameworks. Make sure Qt is installed"
echo "and qmake is in your environment path."
exit
fi
### GIT ####################################################
echo
echo "______________________________________________________________"
echo "Run GIT:"
# recupere le numero de la dernière revision
# Si il y a ':' garde la 2e partie
# Remplace les lettres par 'rien'
#revAv=$(svnversion | cut -d : -f 2 | tr -d '[:alpha:]')
# Fait une mise à jour
git submodule init
git submodule update
git pull --recurse-submodules
git pull
#git checkout foliolist_position
# recupere le numero de la nouvelle revision
GITCOMMIT=$(git rev-parse --short HEAD)
A=$(git rev-list HEAD --count)
HEAD=$(($A+473))
VERSION=$(cat sources/qetversion.cpp | grep "return QVersionNumber{"| head -n 1| awk -F "{" '{ print $2 }' | awk -F "}" '{ print $1 }' | sed -e 's/,/./g' -e 's/ //g')
#VERSION=$(cat sources/qetversion.cpp | grep "return QVersionNumber{ 0, "| head -n 1| cut -c25-35| sed -e 's/,/./g' -e 's/ //g') #Find major, minor, and micro version numbers in sources/qetversion.cp
# Dmg de la dernière revision déjà créé
if [ -e "build-aux/mac-osx/${APPNAME} $VERSION r$HEAD.dmg" ] ; then
echo "There are not new updates, make disk image can"
echo "take a lot of time (5 min). Can you continu?"
echo "[y/n]"
read userinput
if [ "$userinput" == "n" ] ; then
echo
echo "Process is stopped."
echo
exit
fi
fi
### make install ####################################################
echo
echo "______________________________________________________________"
echo "Run make install:"
# pour effacer lancienne compilation
if [ -d $BUNDLE ] ; then
echo "Removing hold bundle..."
rm -rf $BUNDLE
fi
if [ -e Makefile ] ; then
echo "Removing hold Makefile..."
make clean
fi
# genere le Makefile
echo "Generating new makefile..."
qmake -spec macx-clang
# compilation
if [ -e Makefile.Release ] ; then
START_TIME=$SECONDS
# arret du script si erreur de compilation
testSuccessBuild () {
if [ $? -ne 0 ]; then
cleanVerionTag
ELAPSED_TIME=$(($SECONDS - $START_TIME))
echo
echo "make failed - $(($ELAPSED_TIME/60)) min $(($ELAPSED_TIME%60)) sec"
exit 1
fi
}
# utilise tout les coeurs pour une compilation plus rapide
coeur=$(sysctl hw.ncpu | awk '{print $2}')
if [ $? -ne 0 ]; then
make -f Makefile.Release
testSuccessBuild
else
make -j$(($coeur + 1)) -f Makefile.Release
testSuccessBuild
fi
cleanVerionTag
ELAPSED_TIME=$(($SECONDS - $START_TIME))
echo
echo "The time of compilation is $(($ELAPSED_TIME/60)) min $(($ELAPSED_TIME%60)) sec"
else
echo "ERROR: Makefile not found. This script requires the macx-clang makespec"
exit
fi
cp -R ${current_dir}/misc/Info.plist qelectrotech.app/Contents/
cp -R ${current_dir}/ico/mac_icon/*.icns qelectrotech.app/Contents/Resources/
# On rajoute le numero de version pour "cmd + i"
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION r$HEAD" "qelectrotech.app/Contents/Info.plist" # Version number
### copy over frameworks ############################################
echo
echo "______________________________________________________________"
echo "Copy Qt libraries and private frameworks:"
echo "Processing Mac deployment tool..."
if [ ! -d $BUNDLE ] ; then
echo "ERROR: cannot find application bundle \"$BUNDLE\" in current directory"
exit
fi
#~/Qt/5.5/clang_64/bin/macdeployqt $BUNDLE
macdeployqt $BUNDLE
### add file missing #######################################
echo
echo "______________________________________________________________"
echo "Copy file missing:"
# Dossier à ajouter
QET_ELMT_DIR="${current_dir}/elements/"
QET_TBT_DIR="${current_dir}/titleblocks/"
QET_LANG_DIR="${current_dir}/lang/"
QET_FONTS_DIR="${current_dir}/fonts/"
QET_LICENSES_DIR="${current_dir}/licenses/"
# Add new folder for Qt dialog translation see
## see <https://download.tuxfamily.org/qet/Qt_lang/>.
LANG_DIR="${current_dir}/lang1/"
if [ -d "${QET_ELMT_DIR}" ]; then
echo "Copying add elements in the bundle..."
#mkdir $BUNDLE/Contents/Resources/elements
cp -R ${QET_ELMT_DIR} $BUNDLE/Contents/Resources/elements
fi
if [ -d "${QET_TBT_DIR}" ]; then
echo "Copying titleblocks in the bundle..."
#mkdir $BUNDLE/Contents/Resources/titleblocks
cp -R ${QET_TBT_DIR} $BUNDLE/Contents/Resources/titleblocks
fi
if [ -d "${QET_LANG_DIR}" ]; then
echo "Copying translations in the bundle... "
mkdir $BUNDLE/Contents/Resources/lang
cp ${current_dir}/lang/*.qm $BUNDLE/Contents/Resources/lang
fi
if [ -d "${LANG_DIR}" ]; then
echo "Copying translations in the bundle... "
cp ${current_dir}/lang1/*.qm $BUNDLE/Contents/Resources/lang
fi
if [ -d "${QET_FONTS_DIR}" ]; then
echo "Copying fonts in the bundle... "
mkdir $BUNDLE/Contents/Resources/fonts
cp ${current_dir}/fonts/*.ttf $BUNDLE/Contents/Resources/fonts
fi
if [ -d "${QET_LICENSES_DIR}" ]; then
echo "Copying licenses in the bundle..."
mkdir $BUNDLE/Contents/Resources/licenses
cp -R -L ${QET_LICENSES_DIR} $BUNDLE/Contents/Resources/licenses
fi
### create disk image ###############################################
echo
echo "______________________________________________________________"
echo "Create disk image:"
# Direction
EX_Dir="examples"
DOC_Dir="docs/Documentation.webloc"
imagedir="/tmp/$APPNAME.$$"
# Remove any previous disk folder or DMG
echo 'Preparing (removing hold files)... '
if [ -e "/Volumes/${APPNAME}" ]; then
hdiutil detach -quiet "/Volumes/${APPNAME}"
fi
if [ -e "${APPNAME} $VERSION r$HEAD.dmg" ] ; then
rm -f "${APPNAME} $VERSION r$HEAD.dmg"
fi
if [ -e "build-aux/mac-osx/${APPNAME} $VERSION r$HEAD.dmg" ] ; then
rm -f "build-aux/mac-osx/${APPNAME} $VERSION r$HEAD.dmg"
fi
if [ -e $imagedir ] ; then
rm -rf $imagedir
fi
# Create disk folder
echo 'Copying application bundle and examples... '
mkdir $imagedir
cp -Rf $BUNDLE $imagedir
cp -Rf $EX_Dir $imagedir
if [ -e $DOC_Dir ] ; then
cp $DOC_Dir $imagedir
else
echo "Documentation in ${DOC_Dir} not find."
fi
ln -s /Applications $imagedir/Applications
# Elimine les accents, symbole ...
strip "$imagedir/$APPBIN"
# Creating a disk image from a folder
echo 'Creating disk image... '
hdiutil create -quiet -ov -srcfolder $imagedir -format UDBZ -volname "${APPNAME}" "${APPNAME} $VERSION r$HEAD.dmg"
hdiutil internet-enable -yes -quiet "${APPNAME} $VERSION r$HEAD.dmg"
# Clean up disk folder
echo 'Cleaning up... '
cp -Rf "${APPNAME} $VERSION r$HEAD.dmg" "build-aux/mac-osx/${APPNAME}-$VERSION-r$HEAD-intel_X86_64.dmg"
rm -f "${APPNAME} $VERSION r$HEAD.dmg"
rm -rf $imagedir
rm -rf $BUNDLE
### The end, process is done ##########################################
echo
echo "______________________________________________________________"
echo "The process of creating deployable application bundle is done."
echo The disque image is in the folder \'build-aux/mac-osx\'.
# Affiche les mise à jour depuis l'ancienne revision
#if [ ! $(($HEAD - $revAv)) -eq 0 ] ; then
# echo
# echo "There are new updates. This numero of revision is $HEAD."
# svn log -l $(($HEAD - $revAv))
#else
# echo
# echo "There are not new updates. This numero of revision is $HEAD."
# fi
# echo
# La version en local n'est pas conforme à la dernière version svn
# svnversion | grep -q '[MS:]' ; if [ $? -eq 0 ] ; then
# echo Please note that the latest \local version is $(svnversion).
# echo This is not the same version as the deposit.
# echo You can use \'svn diff\' to see the differences.
# echo And use \'svn revert \<fichier\>\' to delete the difference.
# echo To go back, you can use svn update -r 360
# echo to go to revision number 360.
# echo
#fi
#rsync to TF DMG builds
echo -e "\033[1;31mWould you like to upload MacOS packages "${APPNAME}"-"$VERSION"_"r$HEAD-intel.dmg", n/Y?.\033[m"
read a
if [[ $a == "Y" || $a == "y" ]]; then
cp -Rf "build-aux/mac-osx/${APPNAME}-$VERSION-r$HEAD-intel_X86_64.dmg" /Users/laurent/MAC_OS_X/
rsync -e ssh -av --delete-after --no-owner --no-g --chmod=g+w --progress --exclude='.DS_Store' /Users/laurent/MAC_OS_X/ server:download.qelectrotech.org/qet/builds/MAC_OS_X/intel_X86_64/
if [ $? != 0 ]; then
{
echo "RSYNC ERROR: problem syncing ${APPNAME}-$VERSION-r$HEAD-intel_X86_64.dmg"
rsync -e ssh -av --delete-after --no-owner --no-g --chmod=g+w --progress --exclude='.DS_Store' /Users/laurent/MAC_OS_X/ server:download.qelectrotech.org/qet/builds/MAC_OS_X/intel_X86_64/
} fi
else
echo -e "\033[1;33mExit.\033[m"
fi
-509
View File
@@ -1,509 +0,0 @@
#!/bin/sh
# Copyright 2023 The QElectroTech Team
# This file is part of QElectroTech.
# QElectroTech is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
# QElectroTech is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
# Need homebrew and coreutils installed see <http://brew.sh>.
#Force MacOSX12.3.sdk
#see: https://www.downtowndougbrown.com/2023/08/how-to-create-a-qt-5-arm-intel-universal-binary-for-mac/
export DEVELOPER_DIR=/Applications/Xcode_14.01.app/Contents/Developer
# configuration
APPNAME='qelectrotech'
BUNDLE=$APPNAME.app
APPBIN="$BUNDLE/Contents/MacOS/$APPNAME"
IDENTITY="Developer ID Application: Laurent TRINQUES (Y73WZ6WZ5X)"
# Temp paths
RW_DMG="/tmp/qet_rw.dmg"
MOUNT_POINT="/tmp/qet_dmg_mount"
STAGING="/tmp/qet_dmg_staging"
# Script location
current_dir=$(dirname "$0")
# Go back to repo root
cd "${current_dir}/../"
# Current directory
current_dir=$(PWD)
### get system configuration ########################################
echo
echo "______________________________________________________________"
echo "This script prepares a Qt application bundle for deployment."
echo "Please see the \"Deploying an Application on Qt/Mac\""
echo "page in the Qt documentation for more information."
echo
echo "This script :"
echo "\t - update the git depot"
echo "\t - build the application bundle,"
echo "\t - copy over required Qt frameworks,"
echo "\t - copy additional files: translations, titleblocks and elements,"
echo "\t - notarize the .app, then create a signed DMG."
echo
echo "Enjoy ;-)"
echo
# as long as we can find qmake, we don't need QTDIR
FWPATH=`qmake -query QT_INSTALL_LIBS`
if [ ! -d $FWPATH/QtGui.framework ] ; then
echo "ERROR: cannot find the Qt frameworks. Make sure Qt is installed"
echo "and qmake is in your environment path."
exit
fi
### GIT ####################################################
echo
echo "______________________________________________________________"
echo "Run GIT:"
git submodule init
git submodule update
git pull --recurse-submodules
git pull
# Get revision number and version
GITCOMMIT=$(git rev-parse --short HEAD)
A=$(git rev-list HEAD --count)
HEAD=$(($A+473))
VERSION=$(cat sources/qetversion.cpp | grep "return QVersionNumber{"| head -n 1| awk -F "{" '{ print $2 }' | awk -F "}" '{ print $1 }' | sed -e 's/,/./g' -e 's/ //g')
DMG_NAME="${APPNAME}-$VERSION-r$HEAD-arm64.dmg"
DMG_PATH="build-aux/mac-osx/$DMG_NAME"
# Check if already built
if [ -e "$DMG_PATH" ] ; then
echo "There are not new updates, make disk image can"
echo "take a lot of time (5 min). Can you continu?"
echo "[y/n]"
read userinput
if [ "$userinput" == "n" ] ; then
echo
echo "Process is stopped."
echo
exit
fi
fi
### make install ####################################################
echo
echo "______________________________________________________________"
echo "Run make install:"
# Remove old bundle
if [ -d $BUNDLE ] ; then
echo "Removing old bundle..."
rm -rf $BUNDLE
fi
if [ -e Makefile ] ; then
echo "Removing old Makefile..."
rm .qmake.stash
make clean
fi
# Generate Makefile
echo "Generating new makefile..."
qmake -spec macx-clang
# Compile
if [ -e Makefile.Release ] ; then
START_TIME=$SECONDS
testSuccessBuild () {
if [ $? -ne 0 ]; then
cleanVerionTag
ELAPSED_TIME=$(($SECONDS - $START_TIME))
echo
echo "make failed - $(($ELAPSED_TIME/60)) min $(($ELAPSED_TIME%60)) sec"
exit 1
fi
}
coeur=$(sysctl hw.ncpu | awk '{print $2}')
if [ $? -ne 0 ]; then
make -f Makefile.Release
testSuccessBuild
else
make -j$(($coeur + 1)) -f Makefile.Release
testSuccessBuild
fi
ELAPSED_TIME=$(($SECONDS - $START_TIME))
echo
echo "The time of compilation is $(($ELAPSED_TIME/60)) min $(($ELAPSED_TIME%60)) sec"
else
echo "ERROR: Makefile not found. This script requires the macx-clang makespec"
exit
fi
cp -R ${current_dir}/misc/Info.plist qelectrotech.app/Contents/
cp -R ${current_dir}/ico/mac_icon/*.icns qelectrotech.app/Contents/Resources/
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION r$HEAD" "qelectrotech.app/Contents/Info.plist"
### copy over frameworks ############################################
echo
echo "______________________________________________________________"
echo "Copy Qt libraries and private frameworks:"
echo "Processing Mac deployment tool..."
if [ ! -d $BUNDLE ] ; then
echo "ERROR: cannot find application bundle \"$BUNDLE\" in current directory"
exit
fi
macdeployqt $BUNDLE
### add missing files ###############################################
echo
echo "______________________________________________________________"
echo "Copy missing files:"
QET_ELMT_DIR="${current_dir}/elements/"
QET_TBT_DIR="${current_dir}/titleblocks/"
QET_LANG_DIR="${current_dir}/lang/"
QET_EXAMPLES_DIR="${current_dir}/examples/"
QET_FONTS_DIR="${current_dir}/fonts/"
QET_LICENSES_DIR="${current_dir}/licenses/"
LANG_DIR="${current_dir}/lang1/"
if [ -d "${QET_ELMT_DIR}" ]; then
echo "Copying elements in the bundle..."
cp -R ${QET_ELMT_DIR} $BUNDLE/Contents/Resources/elements
fi
if [ -d "${QET_TBT_DIR}" ]; then
echo "Copying titleblocks in the bundle..."
cp -R ${QET_TBT_DIR} $BUNDLE/Contents/Resources/titleblocks
fi
if [ -d "${QET_LANG_DIR}" ]; then
echo "Copying translations in the bundle..."
mkdir $BUNDLE/Contents/Resources/lang
cp ${current_dir}/lang/*.qm $BUNDLE/Contents/Resources/lang
fi
if [ -d "${LANG_DIR}" ]; then
echo "Copying extra translations in the bundle..."
cp ${current_dir}/lang1/*.qm $BUNDLE/Contents/Resources/lang
fi
if [ -d "${QET_EXAMPLES_DIR}" ]; then
echo "Copying examples in the bundle..."
mkdir $BUNDLE/Contents/Resources/examples
cp ${current_dir}/examples/*.qet $BUNDLE/Contents/Resources/examples
fi
if [ -d "${QET_FONTS_DIR}" ]; then
echo "Copying fonts in the bundle..."
mkdir $BUNDLE/Contents/Resources/fonts
cp ${current_dir}/fonts/*.ttf $BUNDLE/Contents/Resources/fonts
fi
if [ -d "${QET_LICENSES_DIR}" ]; then
echo "Copying licenses in the bundle..."
mkdir $BUNDLE/Contents/Resources/licenses
cp -R -L ${QET_LICENSES_DIR} $BUNDLE/Contents/Resources/licenses
fi
### Sign the bundle #################################################
# Sign in correct order: all dylibs first (including flat libs copied
# by macdeployqt from Homebrew), then frameworks, plugins, bundle last.
echo
echo "______________________________________________________________"
echo "Code signing (dylibs -> frameworks -> plugins -> bundle):"
# 1. Sign all flat .dylib files in Frameworks/
echo "-- Signing dylibs in Frameworks/..."
find "$BUNDLE/Contents/Frameworks" -name "*.dylib" | while read lib; do
echo " $(basename $lib)"
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$lib"
done
# 2. Sign .framework bundles
echo "-- Signing .framework bundles..."
find "$BUNDLE/Contents/Frameworks" -maxdepth 1 -name "*.framework" | while read fw; do
echo " $(basename $fw)"
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$fw"
done
# 3. Sign plugins
echo "-- Signing plugins..."
find "$BUNDLE/Contents/PlugIns" \( -name "*.dylib" -o -name "*.so" \) | while read lib; do
echo " $(basename $lib)"
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$lib"
done
# 4. Sign any dylibs in MacOS/
echo "-- Signing dylibs in MacOS/..."
find "$BUNDLE/Contents/MacOS" -name "*.dylib" | while read lib; do
echo " $(basename $lib)"
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$lib"
done
# 5. Sign the main executable explicitly
echo "-- Signing main executable..."
codesign --force --sign "$IDENTITY" --timestamp --options=runtime \
"$BUNDLE/Contents/MacOS/$APPNAME"
# 6. Sign the bundle itself last
echo "-- Signing bundle..."
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$BUNDLE"
# 7. Verify
echo
echo "Verifying bundle signature..."
codesign --verify --deep --strict --verbose=2 "$BUNDLE"
if [ $? -ne 0 ]; then
echo "ERROR: bundle signature verification failed, aborting."
exit 1
fi
spctl -a -vv "$BUNDLE"
echo "Bundle signature OK."
### Notarize the .app (via temporary ZIP) ###########################
echo
echo "______________________________________________________________"
echo "Create temporary ZIP for notarization:"
NOTARIZE_ZIP="/tmp/${APPNAME}-$VERSION-r$HEAD-arm64-notarize.zip"
/usr/bin/ditto -c -k --keepParent "$BUNDLE" "$NOTARIZE_ZIP"
echo -e "\033[1;31mWould you like to notarize the .app \"${APPNAME}-${VERSION}-r${HEAD}\", n/Y?\033[m"
read a
if [[ $a == "Y" || $a == "y" ]]; then
echo
echo "______________________________________________________________"
echo "Notarizing .app:"
xcrun notarytool submit "$NOTARIZE_ZIP" --keychain-profile "org.qelectrotech" --wait
if [ $? -ne 0 ]; then
echo "ERROR: notarization failed. Check the log with:"
echo " xcrun notarytool log <submission-id> --keychain-profile org.qelectrotech"
rm -f "$NOTARIZE_ZIP"
exit 1
fi
else
echo -e "\033[1;33mExit.\033[m"
fi
echo "Cleaning up temporary notarization ZIP..."
rm -f "$NOTARIZE_ZIP"
### Staple the .app #################################################
echo -e "\033[1;31mWould you like to staple the .app \"${APPNAME}-${VERSION}-r${HEAD}\", n/Y?\033[m"
read a
if [[ $a == "Y" || $a == "y" ]]; then
xcrun stapler staple -v "$BUNDLE"
if [ $? -ne 0 ]; then
echo "ERROR: stapling .app failed."
exit 1
fi
echo "Verifying staple on .app..."
xcrun stapler validate -v "$BUNDLE"
spctl -a -vv "$BUNDLE"
echo ".app stapled OK."
else
echo -e "\033[1;33mExit.\033[m"
fi
### Create staging folder with Applications symlink #################
# The staging folder contains the .app and a symlink to /Applications
# so the user can drag-and-drop to install directly from the DMG.
echo
echo "______________________________________________________________"
echo "Preparing DMG staging folder:"
rm -rf "$STAGING"
mkdir -p "$STAGING"
cp -R "$BUNDLE" "$STAGING/"
ln -s /Applications "$STAGING/Applications"
echo "Staging folder ready: $STAGING"
### Create writable DMG (UDRW) ######################################
# We use a writable DMG first so we can re-sign the .app inside
# after hdiutil copies it (hdiutil can invalidate Sealed Resources
# during the copy, so we must re-sign inside the mounted volume).
echo
echo "______________________________________________________________"
echo "Create writable DMG (UDRW) and re-sign .app inside:"
rm -f "$RW_DMG"
hdiutil create \
-volname "QElectroTech $VERSION" \
-srcfolder "$STAGING" \
-ov \
-format UDRW \
-fs HFS+ \
"$RW_DMG"
if [ $? -ne 0 ]; then
echo "ERROR: hdiutil failed to create writable DMG."
rm -rf "$STAGING"
exit 1
fi
# Mount the writable DMG
rm -rf "$MOUNT_POINT"
mkdir -p "$MOUNT_POINT"
hdiutil attach "$RW_DMG" -mountpoint "$MOUNT_POINT" -nobrowse -noverify
if [ $? -ne 0 ]; then
echo "ERROR: failed to mount writable DMG."
rm -f "$RW_DMG"
rm -rf "$STAGING"
exit 1
fi
# Re-sign all binaries inside the mounted DMG
echo "-- Re-signing dylibs inside DMG..."
find "$MOUNT_POINT/$BUNDLE/Contents/Frameworks" -name "*.dylib" | while read lib; do
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$lib"
done
find "$MOUNT_POINT/$BUNDLE/Contents/Frameworks" -maxdepth 1 -name "*.framework" | while read fw; do
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$fw"
done
find "$MOUNT_POINT/$BUNDLE/Contents/PlugIns" \( -name "*.dylib" -o -name "*.so" \) | while read lib; do
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$lib"
done
echo "-- Re-signing main executable inside DMG..."
codesign --force --sign "$IDENTITY" --timestamp --options=runtime \
"$MOUNT_POINT/$BUNDLE/Contents/MacOS/$APPNAME"
echo "-- Re-signing bundle inside DMG..."
codesign --force --sign "$IDENTITY" --timestamp --options=runtime \
"$MOUNT_POINT/$BUNDLE"
# Verify signature inside the mounted DMG
echo "Verifying bundle signature inside DMG..."
codesign --verify --deep --strict --verbose=2 "$MOUNT_POINT/$BUNDLE"
if [ $? -ne 0 ]; then
echo "ERROR: bundle signature invalid inside DMG, aborting."
hdiutil detach "$MOUNT_POINT"
rm -f "$RW_DMG"
rm -rf "$STAGING" "$MOUNT_POINT"
exit 1
fi
echo "Bundle signature inside DMG OK."
# Detach the writable DMG
hdiutil detach "$MOUNT_POINT"
### Convert UDRW to final compressed UDZO ###########################
echo
echo "______________________________________________________________"
echo "Convert to final compressed DMG (UDZO):"
mkdir -p "build-aux/mac-osx"
rm -f "$DMG_PATH"
hdiutil convert "$RW_DMG" \
-format UDZO \
-o "$DMG_PATH"
if [ $? -ne 0 ]; then
echo "ERROR: hdiutil convert failed."
rm -f "$RW_DMG"
rm -rf "$STAGING" "$MOUNT_POINT"
exit 1
fi
rm -f "$RW_DMG"
rm -rf "$STAGING" "$MOUNT_POINT"
### Sign the final DMG ##############################################
echo "Signing final DMG..."
codesign --sign "$IDENTITY" --timestamp "$DMG_PATH"
### Notarize and staple the final DMG ###############################
echo -e "\033[1;31mWould you like to notarize the DMG \"${DMG_NAME}\", n/Y?\033[m"
read a
if [[ $a == "Y" || $a == "y" ]]; then
echo
echo "______________________________________________________________"
echo "Notarizing DMG:"
xcrun notarytool submit "$DMG_PATH" --keychain-profile "org.qelectrotech" --wait
if [ $? -ne 0 ]; then
echo "ERROR: DMG notarization failed. Check the log with:"
echo " xcrun notarytool log <submission-id> --keychain-profile org.qelectrotech"
exit 1
fi
echo "Stapling DMG..."
xcrun stapler staple "$DMG_PATH"
if [ $? -ne 0 ]; then
echo "ERROR: stapling DMG failed."
exit 1
fi
echo "DMG notarized and stapled OK."
echo "Verifying final DMG..."
spctl -a -vv "$DMG_PATH"
else
echo -e "\033[1;33mExit.\033[m"
fi
### Clean up bundle #################################################
echo "Cleaning up bundle..."
rm -rf "$BUNDLE"
### The end #########################################################
echo
echo "______________________________________________________________"
echo "The process is done."
echo "DMG is in the folder 'build-aux/mac-osx'."
### Upload via rsync ################################################
echo -e "\033[1;31mWould you like to upload MacOS package \"${DMG_NAME}\", n/Y?\033[m"
read a
if [[ $a == "Y" || $a == "y" ]]; then
cp -Rf "$DMG_PATH" /Users/laurent/MAC_OS_X/
rsync -e ssh -av --delete-after --no-owner --no-g --chmod=g+w \
--progress --exclude='.DS_Store' \
/Users/laurent/MAC_OS_X/ \
server:download.qelectrotech.org/qet/builds/MAC_OS_X/arm64/
if [ $? != 0 ]; then
echo "RSYNC ERROR: problem syncing ${DMG_NAME}, retrying..."
rsync -e ssh -av --delete-after --no-owner --no-g --chmod=g+w \
--progress --exclude='.DS_Store' \
/Users/laurent/MAC_OS_X/ \
server:download.qelectrotech.org/qet/builds/MAC_OS_X/arm64/
fi
else
echo -e "\033[1;33mExit.\033[m"
fi
+172 -7
View File
@@ -126,7 +126,8 @@ ELAPSED_TIME=$(($SECONDS - $START_TIME))
echo
echo "The time of compilation is $(($ELAPSED_TIME/60)) min $(($ELAPSED_TIME%60)) sec"
# TODO: confirmer le chemin exact de sortie du .app selon CMakeLists.txt
# Le .app sort a la racine de $BUILD_DIR : add_executable(... MACOSX_BUNDLE)
# sans RUNTIME_OUTPUT_DIRECTORY dans CMakeLists.txt.
echo "Copying built bundle into place..."
cp -R "$BUILD_DIR/qelectrotech.app" "./$BUNDLE"
@@ -147,6 +148,98 @@ fi
macdeployqt $BUNDLE
### fix Homebrew dependencies macdeployqt could not handle ##########
# Recent Homebrew bottles (brotli, webp, sharpyuv...) reference their own
# dependencies as @rpath/libX.dylib. macdeployqt only resolves @rpath in
# Contents/lib and in Qt's lib dir (-libpath does not help): it prints
# "Cannot resolve rpath" and leaves some references untouched, either
# absolute /opt/homebrew paths or @rpath libs missing from the bundle.
# Fix both here: every /opt/homebrew reference is rewritten to
# @rpath/libX.dylib (install ids too), and every @rpath/libX.dylib is
# copied into Contents/Frameworks, where the executable's LC_RPATH
# (@executable_path/../Frameworks) lets dyld find it. Everything is signed
# below with the rest of Frameworks/.
echo
echo "______________________________________________________________"
echo "Fix Homebrew dependencies left by macdeployqt:"
FW="$BUNDLE/Contents/Frameworks"
chmod -R u+w "$BUNDLE/Contents/MacOS" "$FW" "$BUNDLE/Contents/PlugIns" 2>/dev/null
# Every Mach-O candidate of the bundle (otool silently ignores the others)
list_macho() {
find "$BUNDLE/Contents/MacOS" "$FW" "$BUNDLE/Contents/PlugIns" -type f \
\( -name '*.dylib' -o -perm -u+x \) 2>/dev/null
}
# Dependencies of one binary, without its own install id (dylibs only)
list_deps() {
_id=$(otool -D "$1" 2>/dev/null | sed -n 2p)
otool -L "$1" 2>/dev/null | awk 'NR>1 { print $1 }' | while read _dep; do
[ "$_dep" = "$_id" ] || echo "$_dep"
done
}
# The @rpath/libX.dylib names referenced anywhere in the bundle
list_rpath_libs() {
list_macho | while read bin; do
list_deps "$bin" | sed -n 's#^@rpath/\([^/]*\.dylib\)$#\1#p'
done | LC_ALL=C sort -u
}
if ! otool -l "$BUNDLE/Contents/MacOS/$APPNAME" | grep -q "@executable_path/../Frameworks" ; then
install_name_tool -add_rpath "@executable_path/../Frameworks" "$BUNDLE/Contents/MacOS/$APPNAME"
echo " Added LC_RPATH @executable_path/../Frameworks to $APPNAME"
fi
# 3 passes, since each copied library can bring its own dependencies:
# a. rewrite absolute /opt/homebrew references (install ids included),
# copying the referenced library into Frameworks/ if needed
# b. copy the @rpath/libX.dylib still missing from Frameworks/
for PASS in 1 2 3; do
list_macho | while read bin; do
_id=$(otool -D "$bin" 2>/dev/null | sed -n 2p)
case "$_id" in
/opt/homebrew/*)
install_name_tool -id "@rpath/$(basename "$_id")" "$bin" 2>/dev/null
echo " Fixed id (pass $PASS): $(basename "$bin")"
;;
esac
list_deps "$bin" | grep '^/opt/homebrew/' | while read ref; do
name=$(basename "$ref")
if [ ! -e "$FW/$name" ]; then
cp -L "$ref" "$FW/$name" && chmod u+w "$FW/$name"
echo " Copied (pass $PASS): $name"
fi
install_name_tool -change "$ref" "@rpath/$name" "$bin" 2>/dev/null
echo " Fixed ref (pass $PASS): $(basename "$bin") -> @rpath/$name"
done
done
list_rpath_libs | while read lib; do
if [ ! -e "$FW/$lib" ] && [ -e "/opt/homebrew/lib/$lib" ]; then
cp -L "/opt/homebrew/lib/$lib" "$FW/$lib" && chmod u+w "$FW/$lib"
echo " Copied (pass $PASS): $lib"
fi
done
done
# 3. Checks
UNRESOLVED=$(list_rpath_libs | while read lib; do [ -e "$FW/$lib" ] || echo "$lib"; done)
if [ -n "$UNRESOLVED" ]; then
echo "ERROR: @rpath libraries still missing from Frameworks/:" $UNRESOLVED
exit 1
fi
HOMEBREW_REFS=$(list_macho | while read bin; do
otool -L "$bin" 2>/dev/null | awk 'NR>1 { print $1 }' | grep '^/opt/homebrew/' \
| sed "s#^# $(basename "$bin") -> #"
done)
if [ -n "$HOMEBREW_REFS" ]; then
echo "ERROR: bundle still references Homebrew paths:"
echo "$HOMEBREW_REFS"
exit 1
fi
echo "All dependencies resolved inside the bundle."
### install Info.plist and app icon #################################
# NOTE: this must run AFTER macdeployqt, not before. macdeployqt
# rewrites/regenerates parts of Contents/Resources, and files copied
@@ -175,7 +268,6 @@ QET_LANG_DIR="${current_dir}/lang/"
QET_EXAMPLES_DIR="${current_dir}/examples/"
QET_FONTS_DIR="${current_dir}/fonts/"
QET_LICENSES_DIR="${current_dir}/licenses/"
LANG_DIR="${current_dir}/lang1/"
if [ -d "${QET_ELMT_DIR}" ]; then
cp -R ${QET_ELMT_DIR} $BUNDLE/Contents/Resources/elements
@@ -183,12 +275,85 @@ fi
if [ -d "${QET_TBT_DIR}" ]; then
cp -R ${QET_TBT_DIR} $BUNDLE/Contents/Resources/titleblocks
fi
if [ -d "${QET_LANG_DIR}" ]; then
mkdir $BUNDLE/Contents/Resources/lang
cp ${current_dir}/lang/*.qm $BUNDLE/Contents/Resources/lang
# Traductions : depuis la PR #751, les .qm ne sont plus versionnes ; lrelease
# les genere dans $BUILD_DIR/lang/. Jeu attendu = les .ts listes dans TS_FILES
# (cmake/qet_compilation_vars.cmake) : un .qm manquant arrete le script, un
# .ts present dans lang/ mais absent de TS_FILES donne seulement un WARNING.
# Fichiers temporaires plutot que <(...) : /bin/sh de macOS (bash 3.2 en mode
# POSIX) n'a pas la substitution de processus.
QM_SRC="${current_dir}/${BUILD_DIR}/lang"
QM_TMP=$(mktemp -d /tmp/qet_qm.XXXXXX)
grep -o 'lang/qet_[A-Za-z_]*\.ts' "${current_dir}/cmake/qet_compilation_vars.cmake" \
| sed -e 's#^lang/##' -e 's#\.ts$##' | LC_ALL=C sort -u > "$QM_TMP/listed"
if [ ! -s "$QM_TMP/listed" ]; then
echo "ERROR: cannot read TS_FILES from cmake/qet_compilation_vars.cmake"
rm -rf "$QM_TMP"
exit 1
fi
if [ -d "${LANG_DIR}" ]; then
cp ${current_dir}/lang1/*.qm $BUNDLE/Contents/Resources/lang
find "${QET_LANG_DIR}" -maxdepth 1 -name 'qet_*.ts' -exec basename {} .ts \; | LC_ALL=C sort > "$QM_TMP/present"
UNLISTED=$(LC_ALL=C comm -13 "$QM_TMP/listed" "$QM_TMP/present")
if [ -n "$UNLISTED" ]; then
echo "WARNING: .ts files not in TS_FILES, no .qm built:" $UNLISTED
fi
mkdir -p $BUNDLE/Contents/Resources/lang
find "${QM_SRC}" -maxdepth 1 -name 'qet_*.qm' -exec cp {} $BUNDLE/Contents/Resources/lang/ \; 2>/dev/null
find $BUNDLE/Contents/Resources/lang -maxdepth 1 -name 'qet_*.qm' -exec basename {} .qm \; | LC_ALL=C sort > "$QM_TMP/built"
MISSING=$(LC_ALL=C comm -23 "$QM_TMP/listed" "$QM_TMP/built")
echo "$(wc -l < "$QM_TMP/built" | tr -d ' ') .qm files copied to Contents/Resources/lang (expected: $(wc -l < "$QM_TMP/listed" | tr -d ' '))"
# Traductions de Qt lui-meme (boutons OK/Annuler, dialogues standard...) :
# elles viennent de qtbase_XX.qm, pas des .ts de QET, et macdeployqt ne les
# deploie pas. QETApp::setLanguage() charge "qt_XX" depuis le chemin de
# traductions de Qt (absent du bundle), puis depuis le dossier lang/ de QET :
# on y depose donc chaque qtbase_XX.qm sous le nom qt_XX.qm. qtbase_XX.qm est
# autonome, contrairement aux qt_XX.qm de Qt qui dependent de tous les modules.
# Premier dossier contenant des qtbase_*.qm : celui annonce par qtpaths, puis
# la formule Homebrew separee qttranslations, puis les autres emplacements
# Homebrew possibles. Homebrew fait de ces dossiers des liens symboliques
# (-> Cellar/qttranslations/...) : find doit donc les suivre (-L), sinon il
# ne voit que le lien lui-meme et ne trouve aucun fichier dedans.
QT_TR_DIR=""
for d in "$("$QT_PREFIX/bin/qtpaths" --query QT_INSTALL_TRANSLATIONS 2>/dev/null)" \
"$(brew --prefix qttranslations 2>/dev/null)/share/qt/translations" \
"$QT_PREFIX/share/qt/translations" \
/opt/homebrew/share/qt/translations \
/opt/homebrew/opt/*/share/qt/translations ; do
if ls "$d"/qtbase_*.qm >/dev/null 2>&1 ; then
QT_TR_DIR="$d"
break
fi
done
LANG_DST="$BUNDLE/Contents/Resources/lang"
find -L "$QT_TR_DIR" -maxdepth 1 -name 'qtbase_*.qm' 2>/dev/null | while read f; do
l=$(basename "$f" .qm | sed 's/^qtbase_//')
cp "$f" "$LANG_DST/qt_$l.qm"
done
# Langues QET sans equivalent Qt exact (pt -> pt_PT, zh -> zh_CN...) :
# QTranslator ne sait que raccourcir le code (fr_FR -> fr), pas l'allonger.
# On prefere la variante "principale" (pt_PT), sinon la premiere trouvee.
sed 's/^qet_//' "$QM_TMP/listed" | while read l; do
if [ ! -e "$LANG_DST/qt_$l.qm" ]; then
main="$QT_TR_DIR/qtbase_${l}_$(echo "$l" | tr 'a-z' 'A-Z').qm"
if [ -e "$main" ]; then
alt="$main"
else
alt=$(find -L "$QT_TR_DIR" -maxdepth 1 -name "qtbase_${l}_*.qm" 2>/dev/null | LC_ALL=C sort | head -1)
fi
[ -n "$alt" ] && cp "$alt" "$LANG_DST/qt_$l.qm"
fi
done
QT_QM_COUNT=$(find "$LANG_DST" -maxdepth 1 -name 'qt_*.qm' | wc -l | tr -d ' ')
echo "${QT_QM_COUNT} Qt translation files (qt_*.qm) copied from ${QT_TR_DIR:-<not found>}"
if [ "${QT_QM_COUNT}" -eq 0 ]; then
echo "ERROR: no qtbase_*.qm found (Qt translations not installed?)."
echo " Check with: find /opt/homebrew -name 'qtbase_fr.qm'"
rm -rf "$QM_TMP"
exit 1
fi
rm -rf "$QM_TMP"
if [ -n "$MISSING" ]; then
echo "ERROR: missing translations:" $MISSING
exit 1
fi
if [ -d "${QET_EXAMPLES_DIR}" ]; then
mkdir $BUNDLE/Contents/Resources/examples
-381
View File
@@ -1,381 +0,0 @@
######################################################################
# Fichier projet de QElectroTech #
######################################################################
# Chemins utilises pour la compilation et l'installation de QET
unix {
# Chemins UNIX
COMPIL_PREFIX = '/usr/local/'
INSTALL_PREFIX = '/usr/local/'
QET_BINARY_PATH = 'bin/'
QET_COMMON_COLLECTION_PATH = 'share/qelectrotech/elements/'
QET_COMMON_TBT_PATH = 'share/qelectrotech/titleblocks/'
QET_LANG_PATH = 'share/qelectrotech/lang/'
QET_EXAMPLES_PATH = 'share/qelectrotech/examples/'
QET_LICENSE_PATH = 'doc/qelectrotech/'
QET_MIME_PACKAGE_PATH = '../share/mime/packages/'
QET_DESKTOP_PATH = 'share/applications/'
QET_ICONS_PATH = 'share/icons/hicolor/'
QET_MAN_PATH = 'man/'
QET_APPDATA_PATH = 'share/appdata'
}
win32 {
# Chemins Windows
COMPIL_PREFIX = './'
INSTALL_PREFIX = './'
QET_BINARY_PATH = './'
QET_COMMON_COLLECTION_PATH = 'elements/'
QET_COMMON_TBT_PATH = 'titleblocks/'
QET_LANG_PATH = 'lang/'
QET_LICENSE_PATH = './'
# Liste des ressources Windows
#RC_FILE = qelectrotech.rc
}
macx {
# Chemins MacOS X
COMPIL_PREFIX = './'
INSTALL_PREFIX = '/usr/local/'
QET_BINARY_PATH = 'bin/'
QET_COMMON_COLLECTION_PATH = '../Resources/elements/'
QET_COMMON_TBT_PATH = '../Resources/titleblocks/'
QET_LANG_PATH = '../Resources/lang/'
QET_EXAMPLES_PATH = 'share/qelectrotech/examples/'
QET_LICENSE_PATH = 'doc/qelectrotech/'
QET_DESKTOP_PATH = 'share/applications/'
QET_ICONS_PATH = 'share/icons/hicolor/'
QET_MAN_PATH = 'man/'
ICON = 'ico/mac_icon/qelectrotech.icns'
}
# Commenter la ligne ci-dessous pour desactiver l'option --common-elements-dir
DEFINES += QET_ALLOW_OVERRIDE_CED_OPTION
# Comment the line below to disable the --common-tbt-dir option
DEFINES += QET_ALLOW_OVERRIDE_CTBTD_OPTION
# Commenter la ligne ci-dessous pour desactiver l'option --config-dir
DEFINES += QET_ALLOW_OVERRIDE_CD_OPTION
# Commenter la ligne ci-dessous pour desactiver l'option --data-dir
DEFINES += QET_ALLOW_OVERRIDE_DD_OPTION
#comment the line below to disable the project database export
DEFINES += QET_EXPORT_PROJECT_DB
# warn on *any* usage of deprecated APIs
#DEFINES += QT_DEPRECATED_WARNINGS
######################################################################
include(sources/PropertiesEditor/PropertiesEditor.pri)
include(sources/QetGraphicsItemModeler/QetGraphicsItemModeler.pri)
include(sources/QPropertyUndoCommand/QPropertyUndoCommand.pri)
include(SingleApplication/singleapplication.pri)
include(sources/QWidgetAnimation/QWidgetAnimation.pri)
DEFINES += QAPPLICATION_CLASS=QApplication
DEFINES += QT_MESSAGELOGCONTEXT
DEFINES += GIT_COMMIT_SHA="\\\"$(shell git -C \""$$_PRO_FILE_PWD_"\" rev-parse --verify HEAD 2>/dev/null || true)\\\""
# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
# You can make your code warn on compile time for the TODO's
# In order to do so, uncomment the following line.
#DEFINES += TODO_LIST
TEMPLATE = app
#ToDo comment close the gap with Cmake
#this is my next problem now
INCLUDEPATH += sources/ui
#comment close the gap with Cmake (De-Backer)
#DEPENDPATH += .
#INCLUDEPATH += sources \
# sources/titleblock \
# sources/ui \
# sources/qetgraphicsitem \
# sources/qetgraphicsitem/ViewItem \
# sources/qetgraphicsitem/ViewItem/ui \
# sources/richtext \
# sources/factory \
# sources/properties \
# sources/dvevent \
# sources/editor \
# sources/editor/esevent \
# sources/editor/graphicspart \
# sources/editor/ui \
# sources/editor/UndoCommand \
# sources/undocommand \
# sources/diagramevent \
# sources/ElementsCollection \
# sources/ElementsCollection/ui \
# sources/autoNum \
# sources/autoNum/ui \
# sources/ui/configpage \
# sources/SearchAndReplace \
# sources/SearchAndReplace/ui \
# sources/NameList \
# sources/NameList/ui \
# sources/utils \
# pugixml \
# sources/dataBase \
# sources/dataBase/ui \
# sources/factory/ui \
# sources/print
# Fichiers sources
HEADERS += $$files(sources/*.h) \
$$files(sources/project/*.h) \
$$files(sources/ui/*.h) \
$$files(sources/editor/*.h) \
$$files(sources/titleblock/*.h) \
$$files(sources/richtext/*.h) \
$$files(sources/qetgraphicsitem/*.h) \
$$files(sources/qetgraphicsitem/ViewItem/*.h) \
$$files(sources/qetgraphicsitem/ViewItem/ui/*.h) \
$$files(sources/factory/*.h) \
$$files(sources/properties/*.h) \
$$files(sources/editor/ui/*.h) \
$$files(sources/editor/esevent/*.h) \
$$files(sources/editor/graphicspart/*.h) \
$$files(sources/editor/UndoCommand/*h) \
$$files(sources/dvevent/*.h) \
$$files(sources/undocommand/*.h) \
$$files(sources/diagramevent/*.h) \
$$files(sources/ElementsCollection/*.h) \
$$files(sources/ElementsCollection/ui/*.h) \
$$files(sources/autoNum/*.h) \
$$files(sources/autoNum/ui/*.h) \
$$files(sources/ui/configpage/*.h) \
$$files(sources/SearchAndReplace/*.h) \
$$files(sources/SearchAndReplace/ui/*.h) \
$$files(sources/NameList/*.h) \
$$files(sources/NameList/ui/*.h) \
$$files(sources/utils/*.h) \
$$files(pugixml/src/*.hpp) \
$$files(sources/dataBase/*.h) \
$$files(sources/dataBase/ui/*.h) \
$$files(sources/factory/ui/*.h) \
$$files(sources/print/*.h) \
$$files(sources/TerminalStrip/*.h) \
$$files(sources/TerminalStrip/ui/*.h) \
$$files(sources/TerminalStrip/ui/ConfigPage/*h) \
$$files(sources/TerminalStrip/UndoCommand/*.h) \
$$files(sources/TerminalStrip/GraphicsItem/*.h) \
$$files(sources/TerminalStrip/GraphicsItem/properties/*.h) \
$$files(sources/xml/*.h) \
$$files(sources/dxf/*.h) \
$$files(sources/qet_elementscaler/*.h) \
$$files(sources/svg/*.h) \
$$files(sources/import/edz/*.h) \
$$files(sources/import/edz/lzma/*.h) \
$$files(sources/logging/*.h) \
$$files(sources/logging/ui/*.h)
SOURCES += $$files(sources/*.cpp) \
$$files(sources/editor/*.cpp) \
$$files(sources/project/*.cpp) \
$$files(sources/titleblock/*.cpp) \
$$files(sources/richtext/*.cpp) \
$$files(sources/ui/*.cpp) \
$$files(sources/qetgraphicsitem/*.cpp) \
$$files(sources/qetgraphicsitem/ViewItem/*.cpp) \
$$files(sources/qetgraphicsitem/ViewItem/ui/*.cpp) \
$$files(sources/factory/*.cpp) \
$$files(sources/properties/*.cpp) \
$$files(sources/editor/ui/*.cpp) \
$$files(sources/editor/esevent/*.cpp) \
$$files(sources/editor/graphicspart/*.cpp) \
$$files(sources/editor/UndoCommand/*cpp) \
$$files(sources/dvevent/*.cpp) \
$$files(sources/undocommand/*.cpp) \
$$files(sources/diagramevent/*.cpp) \
$$files(sources/ElementsCollection/*.cpp) \
$$files(sources/ElementsCollection/ui/*.cpp) \
$$files(sources/autoNum/*.cpp) \
$$files(sources/autoNum/ui/*.cpp) \
$$files(sources/ui/configpage/*.cpp) \
$$files(sources/SearchAndReplace/*.cpp) \
$$files(sources/SearchAndReplace/ui/*.cpp) \
$$files(sources/NameList/*.cpp) \
$$files(sources/NameList/ui/*.cpp) \
$$files(sources/utils/*.cpp) \
$$files(pugixml/src/*.cpp) \
$$files(sources/dataBase/*.cpp) \
$$files(sources/dataBase/ui/*.cpp) \
$$files(sources/factory/ui/*.cpp) \
$$files(sources/print/*.cpp) \
$$files(sources/TerminalStrip/*.cpp) \
$$files(sources/TerminalStrip/ui/*.cpp) \
$$files(sources/TerminalStrip/ui/ConfigPage/*cpp) \
$$files(sources/TerminalStrip/UndoCommand/*.cpp) \
$$files(sources/TerminalStrip/GraphicsItem/*.cpp) \
$$files(sources/TerminalStrip/GraphicsItem/properties/*.cpp) \
$$files(sources/xml/*.cpp) \
$$files(sources/dxf/*.cpp) \
$$files(sources/qet_elementscaler/*.cpp) \
$$files(sources/svg/*.cpp) \
$$files(sources/import/edz/*.cpp) \
$$files(sources/import/edz/lzma/*.c) \
$$files(sources/logging/*.cpp) \
$$files(sources/logging/ui/*.cpp)
# Needed for use promote QTreeWidget in terminalstripeditor.ui
INCLUDEPATH += sources/TerminalStrip/ui
# Needed for the EPLAN .edz importer (PR #513) and its bundled lzma/7z sources
INCLUDEPATH += sources/import/edz
INCLUDEPATH += sources/import/edz/lzma
# Liste des fichiers qui seront incorpores au binaire en tant que ressources Qt
RESOURCES += qelectrotech.qrc
# Liste des ressources Windows
#RC_FILE = ico/windows_icon/qelectrotech.rc
# Fichiers de traduction qui seront installes
TRANSLATIONS += lang/*.ts
# Modules Qt utilises par l'application
QT += xml svg network sql widgets printsupport concurrent gui-private
# KDE Frameworks 5 (KWidgetsAddons, KCoreAddons) are optional.
#
# Pass CONFIG+=no_kf5 to qmake to build against the Qt-only replacements in
# sources/ui/nokde instead. This mirrors the CMake option BUILD_WITH_KF5=OFF
# and is the easiest route on Windows/MinGW, where KF5 is awkward to obtain.
#
# qmake CONFIG+=no_kf5 qelectrotech.pro
#
# sources/ui/nokde is only added to the include path in that configuration, so
# a normal KF5 build is unaffected.
no_kf5 {
DEFINES += BUILD_WITHOUT_KF5
INCLUDEPATH += sources/ui/nokde
HEADERS += $$files(sources/ui/nokde/*.h)
SOURCES += $$files(sources/ui/nokde/*.cpp)
} else {
QT += KWidgetsAddons KCoreAddons
}
# Private Qt GUI headers (needed for QPdfEngine::drawHyperlink)
# gui-private should add this automatically, but some distros need it explicit
INCLUDEPATH += $$[QT_INSTALL_HEADERS]/QtGui/$$[QT_VERSION]/QtGui
# UI DESIGNER FILES AND GENERATION SOURCES FILES
FORMS += $$files(sources/richtext/*.ui) \
$$files(sources/ui/*.ui) \
$$files(sources/editor/ui/*.ui) \
$$files(sources/ElementsCollection/ui/*.ui) \
$$files(sources/autoNum/ui/*.ui) \
$$files(sources/ui/configpage/*.ui) \
$$files(sources/SearchAndReplace/ui/*.ui) \
$$files(sources/NameList/ui/*.ui) \
$$files(sources/qetgraphicsitem/ViewItem/ui/*.ui) \
$$files(sources/dataBase/ui/*.ui) \
$$files(sources/factory/ui/*.ui) \
$$files(sources/print/*.ui) \
$$files(sources/TerminalStrip/ui/*.ui) \
$$files(sources/TerminalStrip/ui/ConfigPage/*.ui)
UI_SOURCES_DIR = sources/ui/
UI_HEADERS_DIR = sources/ui/
# Configuration de la compilation
CONFIG += c++17 debug_and_release warn_on link_pkgconfig
# Nom du binaire genere par la compilation
TARGET = qelectrotech
# Ajustement des bibliotheques utilisees lors de l'edition des liens
unix:QMAKE_LIBS_THREAD -= -lpthread
unix|win32: PKGCONFIG += sqlite3
# Enable C++17
QMAKE_CXXFLAGS += -std=c++17
# Description de l'installation
target.path = $$join(INSTALL_PREFIX,,,$${QET_BINARY_PATH})
elements.path = $$join(INSTALL_PREFIX,,,$${QET_COMMON_COLLECTION_PATH})
elements.files = elements/*
tbt.path = $$join(INSTALL_PREFIX,,,$${QET_COMMON_TBT_PATH})
tbt.files = titleblocks/*
lang.path = $$join(INSTALL_PREFIX,,,$${QET_LANG_PATH})
lang.files = $$replace(TRANSLATIONS, '.ts', '.qm')
examples.path = $$join(INSTALL_PREFIX,,,$${QET_EXAMPLES_PATH})
examples.files = examples/*
copyright.path = $$join(INSTALL_PREFIX,,,$${QET_LICENSE_PATH})
copyright.files = LICENSE ELEMENTS.LICENSE CREDIT README ChangeLog
mime_package.path = $$join(INSTALL_PREFIX,,,$${QET_MIME_PACKAGE_PATH})
mime_package.files = misc/qelectrotech.xml
desktop.path = $$join(INSTALL_PREFIX,,,$${QET_DESKTOP_PATH})
desktop.files = misc/org.qelectrotech.qelectrotech.desktop
appdata.path = $$join(INSTALL_PREFIX,,,$${QET_APPDATA_PATH})
appdata.files = misc/qelectrotech.appdata.xml
icons.path = $$join(INSTALL_PREFIX,,,$${QET_ICONS_PATH})
icons.files = ico/breeze-icons/16x16 \
ico/breeze-icons/22x22 \
ico/breeze-icons/32x32 \
ico/breeze-icons/48x48 \
ico/breeze-icons/64x64 \
ico/breeze-icons/128x128 \
ico/breeze-icons/256x256
man.path = $$join(INSTALL_PREFIX,,,$${QET_MAN_PATH})
man.files = man/files/*
man.extra = sh man/compress_man_pages.sh
# L'installation comprend la copie du binaire, des elements, des fichiers de langue et du fichier LICENSE
INSTALLS += target elements tbt lang copyright
# Sous Unix, on installe egalement l'icone, un fichier .desktop, des fichiers mime et les pages de manuel
unix {
INSTALLS += desktop mime_package icons man examples appdata
}
# Options de compilation communes a Unix et MacOS X
unix {
# Chemin des fichiers de traduction ; par defaut : lang/ dans le repertoire d'execution
DEFINES += QET_LANG_PATH=$$join(COMPIL_PREFIX,,,$${QET_LANG_PATH})
# Chemin de la collection commune ; par defaut : elements/ dans le repertoire d'execution
DEFINES += QET_COMMON_COLLECTION_PATH=$$join(COMPIL_PREFIX,,,$${QET_COMMON_COLLECTION_PATH})
DEFINES += QET_COMMON_TBT_PATH=$$join(COMPIL_PREFIX,,,$${QET_COMMON_TBT_PATH})
}
# Options de compilation specifiques a MacOS X
macx {
# les chemins definis precedemment sont relatifs au dossier contenant le binaire executable
DEFINES += QET_LANG_PATH_RELATIVE_TO_BINARY_PATH
DEFINES += QET_COMMON_COLLECTION_PATH_RELATIVE_TO_BINARY_PATH
equals(QMAKE_TARGET.arch, x86) {
message( "It's x86" )
LIBS += /opt/digikam.org.x86_64/lib/libsqlite3.0.dylib
}
equals(QMAKE_TARGET.arch, arm64) {
message( "It's arm64" )
LIBS += /opt/digikam.org.arm64/lib/libsqlite3.0.dylib
}
}
# Compilers-specific options
unix {
QMAKE_COPY_DIR = 'cp -f -r --preserve=timestamps'
}
+139 -98
View File
@@ -24,6 +24,8 @@
#include "../qetgraphicsitem/element.h"
#include "../qetxml.h"
#include "../qetproject.h"
#include <QDir>
#include <QDomDocument>
#include <QStringList>
#include <QVariant>
#include <utility>
@@ -703,6 +705,74 @@ namespace autonum
return formula;
}
/**
@brief prefixFromLabelFile
Look up a prefix for @a path (path[dirLevel] outermost, path[1] the
deepest directory; path[0], the element's own file name, is never
matched) in the qet_labels.xml at @a filepath.
Descends through nested \<category name="..."\> elements matching
path[dirLevel], path[dirLevel-1], ..., path[1] in turn, considering
only *direct* children at each step -- unlike a flat token scan,
this cannot be fooled by a same-named category living elsewhere in
the document at the wrong nesting depth (bugtracker #671 item 5).
At each matched level, that category's own \<prefix\> child -- even
an empty one -- overrides whatever a shallower ancestor already
provided, so an explicit empty \<prefix/\> cancels inheritance
rather than silently falling back to it (the behaviour requested in
PR #686 review). A category with no \<prefix\> child at all leaves
the inherited value untouched, which is how a directory with no
prefix of its own comes to inherit its parent's, as the file's own
header comment documents.
@return the prefix that applies, or a null QString if the file
cannot be read, is not well-formed, or does not describe this
path at all (as opposed to describing it with no prefix
anywhere along it, which is a non-null empty string).
*/
static QString prefixFromLabelFile(const QString &filepath, const QStringList &path, int dirLevel)
{
QFile file(filepath);
if (!file.open(QFile::ReadOnly | QFile::Text))
return QString();
QDomDocument document;
if (!document.setContent(&file))
return QString();
QDomElement node = document.documentElement();
if (node.isNull())
return QString();
QString prefix;
for (int i = dirLevel ; i >= 1 ; --i) {
QDomElement child = node.firstChildElement(QStringLiteral("category"));
while (!child.isNull()
&& child.attribute(QStringLiteral("name")) != path[i]) {
child = child.nextSiblingElement(QStringLiteral("category"));
}
if (child.isNull())
return QString();
node = child;
const QDomElement own = node.firstChildElement(QStringLiteral("prefix"));
if (!own.isNull()) {
//readElementText()'s null-vs-empty distinction that PR
//#686 needed for the old QXmlStreamReader-based lookup
//has a QDomElement equivalent: text() on an empty
//element can itself come back null depending on how the
//XML was written, so the same explicit fallback applies
//-- an empty QString here means "found, deliberately
//blank", not "not found".
prefix = own.text();
if (prefix.isNull())
prefix = QString("");
}
}
return prefix;
}
/**
@brief elementPrefixForLocation
@param location
@@ -716,114 +786,85 @@ namespace autonum
if (!location.isProject())
return QString();
QXmlStreamReader rxml;
QString path[10];
int i = -1;
//Directory names from the element up to (not including) the
//collection root, outermost last -- path[dirLevel] is the
//top-level category, path[1] the element's immediate parent
//directory, path[0] the element's own file name (never matched
//against a category: the search stops descending once it has
//matched path[1], the deepest real directory). An unbounded
//QStringList rather than a fixed-size array, because a custom
//collection can nest deeper than the shipped one -- see
//bugtracker #671 item 3.
QStringList path;
ElementsLocation current_location = location;
int dirLevel = -1;
//Add location name to path array
while((current_location.parent() != current_location) && (current_location.parent().fileName() != "import"))
while ((current_location.parent() != current_location)
&& (current_location.parent().fileName() != "import"))
{
i++;
path[i]=current_location.fileName();
path << current_location.fileName();
current_location = current_location.parent();
dirLevel++;
}
//User Element without folder treatment
if (i == -1)
{
i = 0;
path[i]=current_location.fileName();
//User element without folder treatment
if (path.isEmpty()) {
path << current_location.fileName();
current_location = current_location.parent();
dirLevel = 0;
}
const int dirLevel = path.size() - 1;
//Name of the top-level tree the element's path was found
//under, e.g. "10_electric" -- or, for a custom/company
//collection not organised that way, whatever its top-level
//folder happens to be called.
const QString collection_root = current_location.fileName();
//Every top-level common-collection tree (10_electric,
//20_logic, 30_hydraulic, ...) may carry its own
//qet_labels.xml, with categories relative to that tree, the
//same way 10_electric/qet_labels.xml already does -- not just
//10_electric, which is all the hardcoded check this replaces
//used to allow (bugtracker #671 item 2). commonElementsDir()
//-- unlike customElementsDir(), which normalises this itself
//-- returns whatever path the user configured verbatim, with
//no guaranteed trailing separator; concatenating a suffix onto
//it directly used to silently mangle the path (and so the
//prefix lookup) for any install relocated to a directory
//without a trailing slash (#671 item 1). QDir::filePath()
//joins correctly either way.
{
const QString common_file = QDir(QETApp::commonElementsDir())
.filePath(collection_root + QStringLiteral("/qet_labels.xml"));
const QString prefix = prefixFromLabelFile(common_file, path, dirLevel);
if (!prefix.isNull()) {
return prefix;
}
}
// Create Custom labels if qet_labels.xml exits in customElementsDir
if (current_location.fileName() != "10_electric"){
QString custom_labels = "qet_labels.xml";
QString customfilepath = QETApp::customElementsDir().append(custom_labels);
QFile file(customfilepath);
file.isReadable();
if (!file.open(QFile::ReadOnly | QFile::Text))
return QString();
rxml.setDevice(&file);
rxml.readNext();
/* Which collection an element actually came from is not
* recoverable post-import (addElement() strips the protocol),
* so custom and company labels files are tried against two
* possible layouts: with the collection's top-level tree name
* folded into the path (a custom/company file organised as a
* mirror of the common collection, tree name included) and
* without it (a file scoped to just this one tree, matching
* how the common collection's own files are written). Custom
* is tried before company, so a user override wins over a
* shared one.
*/
QStringList path_from_root = path;
path_from_root << collection_root;
while(!rxml.atEnd())
{
if (rxml.attributes().value("name").toString() == path[i])
{
rxml.readNext();
i=i-1;
//reached element directory
if (i==0)
{
for (int j=i; j<= dirLevel; j = j +1)
{
//if there is a prefix available apply prefix
if(rxml.name().toString()=="prefix")
{
return rxml.readElementText();
}
//if there isn't a prefix available, find parent prefix in parent folder
else
{
while (rxml.readNextStartElement() && rxml.name().toString()!="prefix")
{
rxml.skipCurrentElement();
rxml.readNext();
}
}
}
const QStringList candidate_dirs = {
QETApp::customElementsDir(),
QETApp::companyElementsDir()
};
for (const QString &dir : candidate_dirs) {
const QString candidate =
QDir(dir).filePath(QStringLiteral("qet_labels.xml"));
for (const QStringList &segments : {path_from_root, path}) {
const QString prefix = prefixFromLabelFile(
candidate, segments, segments.size() - 1);
if (!prefix.isNull()) {
return prefix;
}
}
rxml.readNext();
}
}
else
{
QString qet_labels = "10_electric/qet_labels.xml";
QString filepath = QETApp::commonElementsDir().append(qet_labels);
QFile file(filepath);
file.isReadable();
if (!file.open(QFile::ReadOnly | QFile::Text))
return QString();
rxml.setDevice(&file);
rxml.readNext();
while(!rxml.atEnd())
{
if (rxml.attributes().value("name").toString() == path[i])
{
rxml.readNext();
i=i-1;
//reached element directory
if (i==0)
{
for (int j=i; j<= dirLevel; j = j +1)
{
//if there is a prefix available apply prefix
if(rxml.name().toString()=="prefix")
{
return rxml.readElementText();
}
//if there isn't a prefix available, find parent prefix in parent folder
else
{
while (rxml.readNextStartElement() && rxml.name().toString()!="prefix")
{
rxml.skipCurrentElement();
rxml.readNext();
}
}
}
}
}
rxml.readNext();
}
}
return QString();
}
@@ -54,6 +54,10 @@ FolioAutonumberingW::~FolioAutonumberingW()
*/
void FolioAutonumberingW::setContext(QList <QString> autonums)
{
// Replace the list rather than append to it: this is called again
// whenever the project's numberings change (import, for instance),
// and appending gave a second copy of every name.
ui->m_autonums_cb->clear();
foreach (QString str, autonums) { ui->m_autonums_cb->addItem(str);}
}
+133
View File
@@ -0,0 +1,133 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "bomexport.h"
#include <QSaveFile>
#include <QSqlQuery>
#include <QSqlRecord>
#include <QVariant>
namespace {
QString csvField(QString value)
{
value.replace(QLatin1Char('"'), QStringLiteral("\"\""));
return QLatin1Char('"') + value + QLatin1Char('"');
}
QByteArray csvRecord(const QStringList &values)
{
QStringList escaped;
for (const auto &value : values) {
escaped.append(csvField(value));
}
return (escaped.join(QLatin1Char(';')) + QLatin1Char('\n')).toUtf8();
}
}
QStringList BomExport::defaultColumns()
{
return {
QStringLiteral("label"),
QStringLiteral("designation"),
QStringLiteral("manufacturer"),
QStringLiteral("manufacturer_reference"),
QStringLiteral("model"),
QStringLiteral("description"),
QStringLiteral("category"),
QStringLiteral("quantity"),
QStringLiteral("voltage_rating"),
QStringLiteral("current_rating"),
QStringLiteral("folio"),
QStringLiteral("notes")
};
}
QString BomExport::defaultQuery()
{
//Slaves and terminals are included because both are 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 shares
//a line with its master: the query is ungrouped, one row per element,
//so each appears as the distinct item it is.
//
//Anything that should not be ordered is kept out by setting
//exclude_from_bom on the element, which the view already honours --
//a relay's own auxiliary contact, say.
//
//Thumbnails are deliberately left out for now even though ten of
//them in the shipped examples carry manufacturer and reference data
//(the assembly-plan mounting-plate symbols), because that has not
//been asked for and is a separate question. The folio report arrows
//and the conductor definition stay out because they are not hardware.
//
//See discussion #847.
return QStringLiteral("SELECT %1 FROM element_nomenclature_view "
"WHERE element_type IN "
"('simple', 'master', 'slave', 'terminal') "
"ORDER BY diagram_position, position, label")
.arg(defaultColumns().join(QStringLiteral(", ")));
}
QByteArray BomExport::toCsv(QSqlQuery &query, const QStringList &headers,
bool include_headers, int *row_count)
{
QByteArray csv("\xEF\xBB\xBF");
if (include_headers) {
csv += csvRecord(headers);
}
int rows = 0;
while (query.next())
{
QStringList values;
for (int i = 0; i < query.record().count(); ++i) {
values.append(query.value(i).toString());
}
csv += csvRecord(values);
++rows;
}
if (row_count) {
*row_count = rows;
}
return csv;
}
bool BomExport::writeCsv(const QString &file_path, const QByteArray &csv,
QString *error)
{
if (error) {
error->clear();
}
QSaveFile file(file_path);
if (!file.open(QIODevice::WriteOnly) || file.write(csv) != csv.size())
{
if (error) {
*error = file.errorString();
}
file.cancelWriting();
return false;
}
if (!file.commit())
{
if (error) {
*error = file.errorString();
}
return false;
}
return true;
}
+37
View File
@@ -0,0 +1,37 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef BOMEXPORT_H
#define BOMEXPORT_H
#include <QByteArray>
#include <QString>
#include <QStringList>
class QSqlQuery;
namespace BomExport
{
QStringList defaultColumns();
QString defaultQuery();
QByteArray toCsv(QSqlQuery &query, const QStringList &headers,
bool include_headers = true, int *row_count = nullptr);
bool writeCsv(const QString &file_path, const QByteArray &csv,
QString *error = nullptr);
}
#endif // BOMEXPORT_H
+89 -41
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "cli_export.h"
#include "bomexport.h"
#include "bordertitleblock.h"
#include "conductornumexport.h"
@@ -39,6 +40,7 @@
#include <QDomDocument>
#include <QDate>
#include <QFile>
#include <QSaveFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
@@ -70,6 +72,7 @@ const QHash<QString, QString> &exportFlags()
{"--export-cables", "cables"},
{"--export-wires", "wires"},
{"--export-bom", "bom"},
{"--export-wiring", "wiring"},
{"--export-nets", "nets"},
{"--export-links", "links"},
{"--info", "info"},
@@ -302,61 +305,40 @@ int exportCsv(QETProject &project, const QString &format, const QString &output)
return 0;
}
/// Quote a field for CSV output (RFC-4180 style, ';' delimiter).
QString csvField(const QString &value)
{
if (value.contains(';') || value.contains('"')
|| value.contains('\n') || value.contains('\r')) {
QString v = value;
v.replace('"', "\"\"");
return '"' % v % '"';
}
return value;
}
/// Bill of materials: one row per element, key component-data fields.
/// Pulls from QET's own project database (the same source as the GUI BOM
/// export), so the output matches what the editor produces.
/// Bill of materials from the same project database and default query as the
/// GUI nomenclature export.
int exportBom(QETProject &project, const QString &output)
{
// The project database is built lazily; force a (re)build before querying.
project.dataBase()->updateDB();
static const QStringList columns {
"label", "designation", "manufacturer", "manufacturer_reference",
"quantity", "location", "function", "title", "folio"
};
QSqlQuery query = project.dataBase()->newQuery(
"SELECT " % columns.join(", ") %
" FROM element_nomenclature_view ORDER BY label");
QSqlQuery query = project.dataBase()->newQuery(BomExport::defaultQuery());
if (!query.exec()) {
err << "BOM query failed: " << query.lastError().text() << "\n";
return 1;
}
QString csv = columns.join(";") % "\n";
int rows = 0;
while (query.next()) {
QStringList values;
for (int i = 0; i < columns.size(); ++i)
values << csvField(query.value(i).toString());
csv += values.join(";") % "\n";
++rows;
}
QFile file(output);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
err << "Cannot open '" << output << "' for writing.\n";
const auto csv = BomExport::toCsv(
query, BomExport::defaultColumns(), true, &rows);
QString error;
if (!BomExport::writeCsv(output, csv, &error)) {
err << "Cannot write '" << output << "': " << error << "\n";
return 1;
}
QTextStream fout(&file);
fout << csv;
file.close();
out << "Exported " << rows << " component(s) -> " << output << "\n";
return 0;
}
QString csvField(const QString &value)
{
if (value.contains(QLatin1Char(';')) || value.contains(QLatin1Char('"'))
|| value.contains(QLatin1Char('\n')) || value.contains(QLatin1Char('\r')))
{
QString escaped = value;
escaped.replace(QLatin1Char('"'), QStringLiteral("\"\""));
return QLatin1Char('"') % escaped % QLatin1Char('"');
}
return value;
}
/// Count terminals on @p element that no conductor connects to.
int freeTerminals(Element *element)
{
@@ -543,6 +525,70 @@ QHash<Element *, int> folioIndex(QETProject &project)
return folio;
}
/// From-to wiring list: one row per conductor, each endpoint resolved to its
/// element label and terminal name.
///
/// Reads wiring_list_view out of the project database. --export-cables produces
/// the same logical list from the document XML instead, and the two are meant
/// to agree: running both and diffing them is a direct check that the database
/// still describes the project, which is otherwise only observable through the
/// GUI.
int exportWiring(QETProject &project, const QString &output)
{
// The project database is built lazily; force a (re)build before querying.
project.dataBase()->updateDB();
static const QStringList columns {
"wire_number", "from_element_label", "from_terminal",
"to_element_label", "to_terminal", "diagram_position", "conductor_uuid"
};
QSqlQuery query = project.dataBase()->newQuery(
"SELECT " % columns.join(", ") %
" FROM wiring_list_view"
//Wire numbers are text, so a plain sort puts "10" before "9".
//Numeric ones first, ordered by value; anything non-numeric after,
//ordered as text. The trailing wire_number keeps ties stable.
" ORDER BY diagram_position,"
" CASE WHEN wire_number GLOB '[0-9]*' THEN 0 ELSE 1 END,"
" CAST(wire_number AS INTEGER),"
" wire_number");
if (!query.exec()) {
err << "Wiring list query failed: " << query.lastError().text() << "\n";
return 1;
}
QString csv = columns.join(";") % "\n";
int rows = 0;
while (query.next()) {
QStringList values;
for (int i = 0; i < columns.size(); ++i)
values << csvField(query.value(i).toString());
csv += values.join(";") % "\n";
++rows;
}
//Written through QSaveFile so a failure part-way leaves the previous
//file intact rather than a truncated one, and with a UTF-8 byte order
//mark: without it Excel opens a .csv as the local 8-bit codepage and
//mangles any accented element label. Qt writes UTF-8 by default, so
//the bytes were already right -- the mark is what tells Excel so.
QSaveFile file(output);
if (!file.open(QIODevice::WriteOnly)) {
err << "Cannot open '" << output << "' for writing.\n";
return 1;
}
static const char utf8_bom[] = "\xEF\xBB\xBF";
file.write(utf8_bom, 3);
file.write(csv.toUtf8());
if (!file.commit()) {
err << "Cannot write '" << output << "': " << file.errorString() << "\n";
return 1;
}
out << "Exported " << rows << " conductor(s) -> " << output << "\n";
return 0;
}
/// Electrical nets: groups of terminals joined into one potential.
/// Walks QET's own potential graph, so each net is a connected component
/// of terminals across all folios. The ground truth for connectivity.
@@ -847,6 +893,8 @@ int run(const QStringList &args)
return exportCsv(project, format, output);
if (format == "bom")
return exportBom(project, output);
if (format == "wiring")
return exportWiring(project, output);
if (format == "nets")
return exportNets(project, output);
if (format == "links")
+6
View File
@@ -48,6 +48,7 @@ namespace CLIExport {
qelectrotech --export-cables <project.qet> <output.csv>
qelectrotech --export-wires <project.qet> <output.csv>
qelectrotech --export-bom <project.qet> <output.csv>
qelectrotech --export-wiring <project.qet> <output.csv>
qelectrotech --export-nets <project.qet> <output.json>
qelectrotech --export-links <project.qet> <output.csv>
qelectrotech --info <project.qet> [output.json]
@@ -64,6 +65,11 @@ namespace CLIExport {
cables: wiring list (one row per conductor) as CSV.
wires: list of distinct wire numbers as CSV.
bom: bill of materials (one row per element) as CSV.
wiring: from-to wiring list (one row per conductor) as CSV, read from
the project database. Same logical list as `cables`, which
reads the document XML instead; the two are meant to agree,
so diffing them checks that the database still describes the
project.
nets: electrical nets (connected-terminal groups) as JSON.
links: element cross-references (coil/contact) as CSV, with
unresolved links flagged.
+9 -9
View File
@@ -124,15 +124,15 @@ void ConductorNumExport::fillHash()
continue;
}
//We must define if the connected terminal is a folio report, if it is the case
//we don't add the num to the hash because the terminal doesn't represent a real terminal.
if(!(c->terminal1->parentElement()->linkType() & Element::AllReport))
{
int value = m_hash.value(num, 0);
++value;
m_hash.insert(num, value);
}
if(!(c->terminal2->parentElement()->linkType() & Element::AllReport))
//We must define if the connected terminals are folio reports, if
//both are the case we don't add the num to the hash because
//neither terminal represents a real terminal. Otherwise the
//conductor counts once, regardless of how many of its two ends
//are real -- counting per terminal instead of per conductor
//doubled every ordinary two-real-terminal conductor's tally.
bool terminal1_is_report = c->terminal1->parentElement()->linkType() & Element::AllReport;
bool terminal2_is_report = c->terminal2->parentElement()->linkType() & Element::AllReport;
if (!(terminal1_is_report && terminal2_is_report))
{
int value = m_hash.value(num, 0);
++value;
+91
View File
@@ -0,0 +1,91 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef CONTACTUSAGE_H
#define CONTACTUSAGE_H
#include <algorithm>
/**
@brief The ContactUsage struct
How many slave contacts a master element currently uses, broken down
by contact type.
Header-only and free of any graphics dependency so that the counting
rules can be unit tested on their own. MasterElement::contactUsage()
is the thin wrapper that feeds it the linked elements.
This counts contacts, which is what tells you how many contacts an
auxiliary block must provide. It is deliberately not the count that
MasterElement::isFull() uses: a master's max_slaves is a number of
slots, and a slave fills exactly one slot however many contacts it
carries.
Two rules are easy to get wrong, and both live here so that every
caller gets them right:
- a slave stands for as many contacts as its "number" kind
information says, so a 4 pole contact counts as 4, not as 1
- a changeover contact is counted once, as sw. CrossRefItem's
NOElements() and NCElements() both return it, so adding those two
lists together would count it twice.
*/
struct ContactUsage
{
/**
Contact types a slave can declare. Mirrors
ElementData::SlaveState, which is not used directly so that this
header stays free of the element data dependencies and can be
unit tested on its own. MasterElement::contactUsage() maps
between the two.
*/
enum Type
{
NO, ///< Normally open
NC, ///< Normally closed
SW, ///< Changeover
Other ///< Neither of the above
};
int no = 0; ///< Normally open
int nc = 0; ///< Normally closed
int sw = 0; ///< Changeover
int other = 0; ///< Neither of the above
int total() const { return no + nc + sw + other; }
/**
Add one slave element to the tally.
@param type the contact type the slave declares
@param contacts how many contacts it stands for. Values below 1
are treated as 1: an element which declares no
contact count is still one contact.
*/
void addSlave(Type type, int contacts)
{
const int n = std::max(1, contacts);
switch (type)
{
case NO: no += n; break;
case NC: nc += n; break;
case SW: sw += n; break;
case Other: other += n; break;
}
}
};
#endif // CONTACTUSAGE_H
+237 -32
View File
@@ -53,6 +53,7 @@ projectDataBase::projectDataBase(QETProject *project, QObject *parent) :
});
connect(m_project, &QETProject::projectDiagramsOrderChanged, [this]()
{
m_content_changed = true;
for (auto diagram : m_project->diagrams())
{
m_diagram_order_changed.bindValue(":pos", m_project->folioIndex(diagram)+1);
@@ -85,14 +86,42 @@ projectDataBase::~projectDataBase()
*/
void projectDataBase::updateDB()
{
//A bulk operation is in progress and updates the database itself once
//it is done : rebuilding now would only be thrown away by that final
//rebuild. @see setUpdateBlocked().
if (m_update_blocked) {
return;
}
//Nothing in the project has changed since the last rebuild, so
//repopulating would insert exactly the rows that are already there.
//The signal is still emitted : callers and models rely on it to
//refresh, and what they read back is unchanged either way.
if (!m_content_changed)
{
emit dataBaseUpdated();
return;
}
populateDiagramTable();
populateDiagramInfoTable();
populateElementTable();
populateElementInfoTable();
populateConductorTable();
m_content_changed = false;
emit dataBaseUpdated();
}
/**
@brief projectDataBase::setUpdateBlocked
@param blocked : whether updateDB() should skip the full rebuild
*/
void projectDataBase::setUpdateBlocked(bool blocked)
{
m_update_blocked = blocked;
}
/**
@brief projectDataBase::project
@return the project of this database
@@ -111,35 +140,65 @@ QSqlQuery projectDataBase::newQuery(const QString &query) {
return QSqlQuery(query, m_data_base);
}
/**
@brief projectDataBase::excludedConductorCount
@return how many conductors of the project are absent from the conductor
table because an endpoint has no parent element to key on.
Counted from the live scene rather than from the database, precisely
because the database is where these conductors are *not*.
This used to count conductors whose terminals had no uuid, which was most
of them on most projects. Terminal::stableUuid() now derives an identity
from the terminal's geometry when the definition provides no uuid, so that
is no longer a reason to exclude anything, and this counts only the case
that remains genuinely unkeyable.
This is what lets a caller tell the user "N wires are missing and here
is why", instead of silently presenting a short list as if it were
complete.
*/
int projectDataBase::excludedConductorCount() const
{
if (!m_project) {
return 0;
}
int count = 0;
for (auto *diagram : m_project->diagrams())
{
const auto conductor_list = diagram->conductors();
for (auto *conductor : conductor_list)
{
//Must match addConductor()'s guard exactly, or this reports
//wires as missing that the list is in fact showing.
if (!conductor->terminal1->parentElement()
|| !conductor->terminal2->parentElement()) {
++count;
}
}
}
return count;
}
/**
@brief projectDataBase::addElement
@param element
*/
void projectDataBase::addElement(Element *element)
{
m_content_changed = true;
if (!element || !element->diagram()) {
qDebug() << "projectDataBase::addElement: null element or diagram";
return;
}
m_insert_elements_query.bindValue(":uuid", element->uuid().toString());
m_insert_elements_query.bindValue(":diagram_uuid", element->diagram()->uuid().toString());
m_insert_elements_query.bindValue(":pos", element->diagram()->convertPosition(element->scenePos()).toString());
m_insert_elements_query.bindValue(":type", element->elementData().typeToString());
m_insert_elements_query.bindValue(":sub_type", element->kindInformations()["type"].toString());
bindElementValues(m_insert_elements_query, element, element->diagram());
if (!m_insert_elements_query.exec()) {
qDebug() << "projectDataBase::addElement insert element error : " << m_insert_elements_query.lastError();
}
m_insert_element_info_query.bindValue(":uuid", element->uuid().toString());
auto hash = elementInfoToString(element);
for (auto key : hash.keys())
{
QString value = hash.value(key);
QString bind = key.prepend(":");
m_insert_element_info_query.bindValue(bind, value);
}
bindElementInfoValues(m_insert_element_info_query, element);
if (!m_insert_element_info_query.exec()) {
qDebug() << "projectDataBase::addElement insert element info error : " << m_insert_element_info_query.lastError();
} else {
@@ -153,6 +212,7 @@ void projectDataBase::addElement(Element *element)
*/
void projectDataBase::removeElement(Element *element)
{
m_content_changed = true;
m_remove_element_query.bindValue(":uuid", element->uuid().toString());
if(!m_remove_element_query.exec()) {
qDebug() << "projectDataBase::removeElement remove error : " << m_remove_element_query.lastError();
@@ -167,6 +227,7 @@ void projectDataBase::removeElement(Element *element)
*/
void projectDataBase::elementInfoChanged(Element *element)
{
m_content_changed = true;
auto hash = elementInfoToString(element);
for (auto str : QETInformation::elementInfoKeys()) {
m_update_element_query.bindValue(":" + str, hash.value(str));
@@ -181,6 +242,7 @@ void projectDataBase::elementInfoChanged(Element *element)
void projectDataBase::elementInfoChanged(QList<Element *> elements)
{
m_content_changed = true;
this->blockSignals(true);
//Block signal for not emit dataBaseUpdated at
//each call of the method elementInfoChanged(Element *element)
@@ -197,6 +259,7 @@ void projectDataBase::elementInfoChanged(QList<Element *> elements)
void projectDataBase::addDiagram(Diagram *diagram)
{
m_content_changed = true;
m_insert_diagram_query.bindValue(":uuid", diagram->uuid().toString());
m_insert_diagram_query.bindValue(":pos", m_project->folioIndex(diagram)+1);
if(!m_insert_diagram_query.exec()) {
@@ -225,6 +288,7 @@ void projectDataBase::addDiagram(Diagram *diagram)
void projectDataBase::removeDiagram(Diagram *diagram)
{
m_content_changed = true;
const QString uuid_str = diagram->uuid().toString();
//Order matters: element_info and terminal are scoped through a
@@ -280,6 +344,7 @@ void projectDataBase::removeDiagram(Diagram *diagram)
void projectDataBase::diagramInfoChanged(Diagram *diagram)
{
m_content_changed = true;
bindDiagramInfoValues(m_update_diagram_info_query, diagram);
if (!m_update_diagram_info_query.exec()) {
@@ -291,6 +356,7 @@ void projectDataBase::diagramInfoChanged(Diagram *diagram)
void projectDataBase::diagramOrderChanged()
{
m_content_changed = true;
}
/**
@@ -299,6 +365,7 @@ void projectDataBase::diagramOrderChanged()
*/
void projectDataBase::addConductor(Conductor *conductor)
{
m_content_changed = true;
if (!conductor || !conductor->diagram()) {
qDebug() << "projectDataBase::addConductor: null conductor or diagram";
return;
@@ -332,6 +399,7 @@ void projectDataBase::addConductor(Conductor *conductor)
*/
void projectDataBase::removeConductor(Conductor *conductor)
{
m_content_changed = true;
m_remove_conductor_query.bindValue(":uuid", conductor->uuid().toString());
if (!m_remove_conductor_query.exec()) {
qDebug() << "projectDataBase::removeConductor delete error : " << m_remove_conductor_query.lastError();
@@ -352,6 +420,7 @@ void projectDataBase::removeConductor(Conductor *conductor)
*/
void projectDataBase::updateConductor(Conductor *conductor)
{
m_content_changed = true;
if (!conductor) {
return;
}
@@ -394,6 +463,7 @@ void projectDataBase::watchConductor(Conductor *conductor)
*/
void projectDataBase::conductorPropertiesChanged()
{
m_content_changed = true;
if (auto *conductor = qobject_cast<Conductor *>(sender())) {
updateConductor(conductor);
}
@@ -550,6 +620,7 @@ bool projectDataBase::createDataBase()
createElementNomenclatureView();
createSummaryView();
createWiringListView();
prepareQuery();
updateDB();
return true;
@@ -570,6 +641,11 @@ void projectDataBase::createElementNomenclatureView()
"ei.designation AS designation,"
"ei.manufacturer AS manufacturer,"
"ei.manufacturer_reference AS manufacturer_reference,"
"ei.model AS model,"
"ei.category AS category,"
"ei.voltage_rating AS voltage_rating,"
"ei.current_rating AS current_rating,"
"ei.notes AS notes,"
"ei.machine_manufacturer_reference AS machine_manufacturer_reference,"
"ei.supplier AS supplier,"
"ei.quantity AS quantity,"
@@ -628,7 +704,21 @@ void projectDataBase::createElementNomenclatureView()
"di.folio AS folio,"
"e.pos AS position "
" FROM element_info ei, diagram_info di, element e, diagram d"
" WHERE ei.element_uuid = e.uuid AND e.diagram_uuid = d.uuid AND di.diagram_uuid = d.uuid AND (ei.exclude_from_bom IS NOT 'true')");
" WHERE ei.element_uuid = e.uuid AND e.diagram_uuid = d.uuid AND di.diagram_uuid = d.uuid"
" AND COALESCE(LOWER(TRIM(ei.exclude_from_bom)), '') NOT IN ('true', '1', 'yes', 'on')"
//The element table holds every element of the project; which
//kinds belong in a nomenclature is this view's business, not
//the table's. Kept identical to the mask populateElementTable()
//used to apply, so what this view returns does not change --
//a slave element (a relay contact) is still not a line item.
//Slave is here because an auxiliary contact block is
//separately orderable hardware with its own part
//number, even though it shares its master's BMK.
//Anything that should not be ordered -- a relay's
//own auxiliary contact, say -- is kept out by
//exclude_from_bom above, not by its base type.
//See discussion #847.
" AND e.type IN ('simple', 'terminal', 'master', 'slave', 'thumbnail')");
QSqlQuery query(m_data_base);
if (!query.exec(create_view)) {
@@ -667,6 +757,64 @@ void projectDataBase::createSummaryView()
}
}
/**
@brief projectDataBase::createWiringListView
A from-to wiring list: one row per conductor, each endpoint resolved to
its element label and terminal name.
Two deliberate differences from an ordinary inner-join view like
element_nomenclature_view:
- No join to the element table. A terminal row already carries its
element_uuid, so joining element back just to read the same uuid adds
nothing -- and would actively drop rows, because populateElementTable()
only inserts elements matching Simple|Terminal|Master|Thumbnail. Slave
elements (relay contacts and the like, extremely common at the end of a
wire) and report elements are absent from that table after a project
load, so an inner join through it silently loses their conductors.
- element_info is LEFT joined for the same reason. A wire whose endpoint
element carries no info row still belongs in a wiring list; it comes
back with an empty label rather than vanishing. Losing a wire from a
wiring list is a worse failure than showing one with a blank end.
- diagram is LEFT joined for the same reason. It should
always match, since QETProject::diagramAdded is wired to addDiagram()
and a conductor cannot exist before its folio -- but an inner join here
would make that an assumption the view silently enforces, and a wire
missing from a wiring list is the one failure this view must not have.
The result is that this view returns exactly as many rows as the
conductor table holds -- what is already excluded upstream (conductors
on legacy terminals without uuids) stays excluded, and nothing new is
dropped here. Only the terminal joins are inner, and both are guaranteed
by insertTerminal() running for each endpoint before the conductor row
is written.
*/
void projectDataBase::createWiringListView()
{
QString create_view ("CREATE VIEW wiring_list_view AS SELECT "
"c.uuid AS conductor_uuid,"
"c.text AS wire_number,"
"t1.element_uuid AS from_element_uuid,"
"ei1.label AS from_element_label,"
"t1.name AS from_terminal,"
"t2.element_uuid AS to_element_uuid,"
"ei2.label AS to_element_label,"
"t2.name AS to_terminal,"
"d.pos AS diagram_position"
" FROM conductor c"
" JOIN terminal t1 ON c.terminal1_uuid = t1.uuid AND c.terminal1_element_uuid = t1.element_uuid"
" JOIN terminal t2 ON c.terminal2_uuid = t2.uuid AND c.terminal2_element_uuid = t2.element_uuid"
" LEFT JOIN element_info ei1 ON t1.element_uuid = ei1.element_uuid"
" LEFT JOIN element_info ei2 ON t2.element_uuid = ei2.element_uuid"
" LEFT JOIN diagram d ON c.diagram_uuid = d.uuid");
QSqlQuery query(m_data_base);
if (!query.exec(create_view)) {
qDebug() << query.lastError();
}
}
void projectDataBase::populateDiagramTable()
{
QSqlQuery query_(m_data_base);
@@ -682,6 +830,30 @@ void projectDataBase::populateDiagramTable()
}
}
/**
@brief allElementTypes
Every ElementData::Type, i.e. no filtering at all.
The element table used to be populated with only
Simple|Terminal|Master|Thumbnail, which quietly made it "the elements a
nomenclature cares about" rather than "the elements of the project".
Anything else reading the table -- the wiring list, and terminal plans
later -- then could not see slave elements (relay contacts) or report
elements, which are ordinary conductor endpoints. The filter now lives in
element_nomenclature_view, where it belongs; see createElementNomenclatureView().
*/
static ElementData::Types allElementTypes()
{
return ElementData::Simple
| ElementData::NextReport
| ElementData::PreviousReport
| ElementData::Master
| ElementData::Slave
| ElementData::Terminal
| ElementData::Thumbnail
| ElementData::ConductorDefinition;
}
/**
@brief projectDataBase::populateElementTable
Populate the element table
@@ -694,16 +866,11 @@ void projectDataBase::populateElementTable()
for (auto diagram : m_project->diagrams())
{
const ElementProvider ep(diagram);
const auto elmt_vector = ep.find(ElementData::Simple | ElementData::Terminal | ElementData::Master | ElementData::Thumbnail);
const auto elmt_vector = ep.find(allElementTypes());
//Insert all values into the database
for (const auto &elmt : elmt_vector)
{
const auto elmt_data = elmt->elementData();
m_insert_elements_query.bindValue(":uuid", elmt->uuid().toString());
m_insert_elements_query.bindValue(":diagram_uuid", diagram->uuid().toString());
m_insert_elements_query.bindValue(":pos", diagram->convertPosition(elmt->scenePos()).toString());
m_insert_elements_query.bindValue(":type", elmt_data.typeToString());
m_insert_elements_query.bindValue(":sub_type", elmt_data.masterTypeToString());
bindElementValues(m_insert_elements_query, elmt, diagram);
if (!m_insert_elements_query.exec()) {
qDebug() << "projectDataBase::populateElementTable insert error : " << m_insert_elements_query.lastError();
}
@@ -723,20 +890,12 @@ void projectDataBase::populateElementInfoTable()
for (const auto &diagram : m_project->diagrams())
{
const ElementProvider ep(diagram);
const auto elmt_vector = ep.find(ElementData::Simple | ElementData::Terminal | ElementData::Master | ElementData::Thumbnail);
const auto elmt_vector = ep.find(allElementTypes());
//Insert all values into the database
for (const auto &elmt : elmt_vector)
{
m_insert_element_info_query.bindValue(QStringLiteral(":uuid"), elmt->uuid().toString());
const auto hash = elementInfoToString(elmt);
for (const auto &key : hash.keys())
{
QString value = hash.value(key);
QString bind = QStringLiteral(":") + key;
m_insert_element_info_query.bindValue(bind, value);
}
bindElementInfoValues(m_insert_element_info_query, elmt);
if (!m_insert_element_info_query.exec()) {
qDebug() << "projectDataBase::populateElementInfoTable insert error : " << m_insert_element_info_query.lastError();
}
@@ -943,6 +1102,52 @@ QHash<QString, QString> projectDataBase::elementInfoToString(Element *elmt)
return hash;
}
/**
@brief projectDataBase::bindElementValues
Bind one element's row for the element table.
Shared by addElement() (a single element added to a live diagram) and
populateElementTable() (a full rebuild), because those two used to bind
the same row differently: the incremental path wrote
kindInformations()["type"] into sub_type while the bulk path wrote
elementData().masterTypeToString(). The element table therefore held
different values depending on whether the project had been reloaded
since the element was placed. One binder means live and reloaded agree
by construction rather than by coincidence.
The bulk path's values are the ones kept: they are what every already
saved project contains, so nothing a reload produces changes.
@param query : prepared insert query to bind into
@param element : element to bind
@param diagram : diagram holding @element
*/
void projectDataBase::bindElementValues(QSqlQuery &query, Element *element, Diagram *diagram)
{
const auto element_data = element->elementData();
query.bindValue(QStringLiteral(":uuid"), element->uuid().toString());
query.bindValue(QStringLiteral(":diagram_uuid"), diagram->uuid().toString());
query.bindValue(QStringLiteral(":pos"), diagram->convertPosition(element->scenePos()).toString());
query.bindValue(QStringLiteral(":type"), element_data.typeToString());
query.bindValue(QStringLiteral(":sub_type"), element_data.masterTypeToString());
}
/**
@brief projectDataBase::bindElementInfoValues
Bind one element's row for the element info table.
Shared by addElement() and populateElementInfoTable() for the same
reason as bindElementValues().
@param query : prepared insert query to bind into
@param element : element to bind
*/
void projectDataBase::bindElementInfoValues(QSqlQuery &query, Element *element)
{
query.bindValue(QStringLiteral(":uuid"), element->uuid().toString());
const auto hash = elementInfoToString(element);
for (const auto &key : hash.keys()) {
query.bindValue(QStringLiteral(":") + key, hash.value(key));
}
}
void projectDataBase::bindDiagramInfoValues(QSqlQuery &query, Diagram *diagram)
{
query.bindValue(":uuid", diagram->uuid());
+22
View File
@@ -47,8 +47,21 @@ class projectDataBase : public QObject
virtual ~projectDataBase() override;
void updateDB();
/**
Suppress the full rebuild performed by updateDB().
While blocked, updateDB() returns immediately instead of
repopulating every table. Meant for bulk operations -- notably
loading a project, where each table model re-queries the
database as it is built and would otherwise trigger one
complete rebuild of it. The caller unblocks and calls
updateDB() once when done; @see QETProject::readProjectXml().
*/
void setUpdateBlocked(bool blocked);
QETProject *project() const;
QSqlQuery newQuery(const QString &query = QString());
QSqlDatabase database() const {return m_data_base;}
int excludedConductorCount() const;
void addElement (Element *element);
void removeElement (Element *element);
@@ -77,6 +90,7 @@ class projectDataBase : public QObject
bool createDataBase();
void createElementNomenclatureView();
void createSummaryView();
void createWiringListView();
void populateDiagramTable();
void populateElementTable();
void populateElementInfoTable();
@@ -89,9 +103,17 @@ class projectDataBase : public QObject
static QHash<QString, QString> elementInfoToString(
Element *elmt);
void bindDiagramInfoValues(QSqlQuery &query, Diagram *diagram);
static void bindElementValues(QSqlQuery &query, Element *element, Diagram *diagram);
static void bindElementInfoValues(QSqlQuery &query, Element *element);
private:
QPointer<QETProject> m_project;
bool m_update_blocked = false;
//Starts true : the database is empty until the first rebuild.
//Set by every method of this class that writes rows, cleared by
//updateDB(). Callers reach the database from outside only through
//newQuery(), and every such call site reads.
bool m_content_changed = true;
QSqlDatabase m_data_base;
QSqlQuery m_insert_elements_query,
m_insert_element_info_query,
+21 -9
View File
@@ -160,6 +160,8 @@ void ElementQueryWidget::setQuery(const QString &query)
{
auto str_type = rxm.captured(1);
where.remove(str_type);
const bool all_master_types = str_type.contains(
ElementData::typeToString(ElementData::Master));
int c=0;
ui->m_simple_cb->setChecked (str_type.contains(ElementData::typeToString(ElementData::Simple)) ? true : false);
@@ -170,23 +172,23 @@ void ElementQueryWidget::setQuery(const QString &query)
if (ui->m_terminal_cb->isChecked()) {
++c;
}
ui->m_coil_cb->setChecked (str_type.contains(ElementData::masterTypeToString(ElementData::Coil)) ? true : false);
ui->m_coil_cb->setChecked (all_master_types || str_type.contains(ElementData::masterTypeToString(ElementData::Coil)));
if (ui->m_coil_cb->isChecked()) {
++c;
}
ui->m_button_cb->setChecked (str_type.contains(ElementData::masterTypeToString(ElementData::Commutator)) ? true : false);
ui->m_button_cb->setChecked (all_master_types || str_type.contains(ElementData::masterTypeToString(ElementData::Commutator)));
if (ui->m_button_cb->isChecked()) {
++c;
}
ui->m_protection_cb->setChecked(str_type.contains(ElementData::masterTypeToString(ElementData::Protection)) ? true : false);
if (ui->m_protection_cb) {
ui->m_protection_cb->setChecked(all_master_types || str_type.contains(ElementData::masterTypeToString(ElementData::Protection)));
if (ui->m_protection_cb->isChecked()) {
++c;
}
ui->m_thumbnail_cb->setChecked (str_type.contains(ElementData::typeToString(ElementData::Thumbnail)) ? true : false);
if (ui->m_thumbnail_cb->isChecked()) {
++c;
}
ui->m_plc_cb->setChecked (str_type.contains(ElementData::masterTypeToString(ElementData::PLC)) ? true : false);
ui->m_plc_cb->setChecked (all_master_types || str_type.contains(ElementData::masterTypeToString(ElementData::PLC)));
if (ui->m_plc_cb->isChecked()) {
++c;
}
@@ -352,22 +354,32 @@ QString ElementQueryWidget::queryStr() const
where += QStringLiteral(" element_type = '") += ElementData::typeToString(ElementData::Simple) += "'";
b = true;
}
if (ui->m_button_cb->isChecked()) {
const bool all_master_types = ui->m_button_cb->isChecked()
&& ui->m_coil_cb->isChecked()
&& ui->m_protection_cb->isChecked()
&& ui->m_plc_cb->isChecked();
if (all_master_types) {
if (b) where += " OR";
where += QStringLiteral(" element_type = '")
+= ElementData::typeToString(ElementData::Master) += "'";
b = true;
}
if (!all_master_types && ui->m_button_cb->isChecked()) {
if (b) where +=" OR";
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::Commutator) += "'";
b = true;
}
if (ui->m_coil_cb->isChecked()) {
if (!all_master_types && ui->m_coil_cb->isChecked()) {
if (b) where +=" OR";
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::Coil) += "'";
b = true;
}
if (ui->m_protection_cb->isChecked()) {
if (!all_master_types && ui->m_protection_cb->isChecked()) {
if (b) where +=" OR";
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::Protection) += "'";
b = true;
}
if (ui->m_plc_cb->isChecked()) {
if (!all_master_types && ui->m_plc_cb->isChecked()) {
if (b) where +=" OR";
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::PLC) += "'";
b = true;
+17 -3
View File
@@ -63,7 +63,6 @@ QString SummaryQueryWidget::queryStr() const
QStringList keys = selectedKeys();
QString select ="SELECT ";
QString order_by = " ORDER BY ";
QString column;
bool first = true;
@@ -72,14 +71,29 @@ QString SummaryQueryWidget::queryStr() const
first = false;
} else {
column += ", ";
order_by +=", ";
}
column += key;
order_by += key;
}
QString from = " FROM project_summary_view";
//Always ordered by the folio's own position, never by the columns
//the user happened to choose to display. This is a table of
//contents: it lists the folios of the project and its order is the
//project's order. Ordering by the displayed columns instead meant
//that choosing, say, Title as the first column silently sorted the
//summary alphabetically (bugtracker #238).
//
//project_summary_view exposes the position as "pos", from
//diagram.pos, which is an INTEGER -- so this sorts numerically and
//folio 10 does not land between folio 1 and folio 2. One row per
//folio means pos fully determines the order and no secondary key is
//needed.
//
//A user who wants a different order can still write the query by
//hand; that path returns above, untouched.
QString order_by = " ORDER BY pos";
QString q(select + column + from + order_by);
return q;
}
+121 -1
View File
@@ -56,7 +56,20 @@ namespace {
/// non-deterministic across process runs for any legacy file.
QString elementSortKey(Element *elmt)
{
return positionKey(elmt->pos());
//Position alone is not a total order: two elements can sit at the
//same x/y (lmdg.qet has a pair of text elements both at 780,350).
//With equal keys std::stable_sort falls back to the order the
//scene handed us, which varies per run, so those two swapped
//places on every save. The uuid breaks the tie.
//
//For an element with a persisted uuid attribute this is fully
//deterministic. For a legacy element without one, fromXml()
//invents a fresh uuid per load, so a collision between two such
//elements is no better ordered than before -- but no worse
//either, and the tiebreaker is only consulted when the positions
//are equal.
return positionKey(elmt->pos())
+ QLatin1Char(':') + elmt->uuid().toString();
}
/// Sort key for a terminal: its parent element's position, then the
@@ -437,6 +450,34 @@ void Diagram::wheelEvent(QGraphicsSceneWheelEvent *event)
QGraphicsScene::wheelEvent(event);
}
/**
@brief Diagram::event
QGraphicsScene has its own Tab/Shift+Tab item-focus-chain traversal
(mirroring QWidget's), checked before keyPressEvent() is ever reached:
by default it would silently consume Tab/Backtab to move focus among
the scene's own focusable items. Intercepting the key press here,
ahead of that, is the only way to reliably override it: unlike
QWidget::focusNextPrevChild(), QGraphicsScene::focusNextPrevChild() is
only virtual starting in Qt 6 (guarded by the QT6_VIRTUAL macro), so a
Diagram:: override of it would silently do nothing on a Qt 5 build.
@param event
*/
bool Diagram::event(QEvent *event)
{
if (event->type() == QEvent::KeyPress) {
auto *key_event = static_cast<QKeyEvent *>(event);
if ((key_event->key() == Qt::Key_Tab || key_event->key() == Qt::Key_Backtab)
&& !isReadOnly() && !focusItem()) {
bool forward = key_event->key() == Qt::Key_Tab
&& !(key_event->modifiers() & Qt::ShiftModifier);
selectNextItem(forward);
event->accept();
return true;
}
}
return QGraphicsScene::event(event);
}
/**
@brief Diagram::keyPressEvent
This event is managed by diagram event interface if any.
@@ -1889,6 +1930,85 @@ void Diagram::invertSelection()
emit selectionChanged();
}
/**
@brief Diagram::selectAllConductors
Select every conductor on this diagram, deselecting anything else.
*/
void Diagram::selectAllConductors()
{
if (items().isEmpty()) return;
blockSignals(true);
for (auto item : items()) {
item -> setSelected(dynamic_cast<Conductor *>(item) != nullptr);
}
blockSignals(false);
emit selectionChanged();
}
/**
@brief Diagram::selectAllTextFields
Select every text field on this diagram (independent/static text,
conductor labels, and dynamic element texts), deselecting anything else.
*/
void Diagram::selectAllTextFields()
{
if (items().isEmpty()) return;
blockSignals(true);
for (auto item : items()) {
item -> setSelected(dynamic_cast<DiagramTextItem *>(item) != nullptr);
}
blockSignals(false);
emit selectionChanged();
}
/**
@brief Diagram::selectNextItem
Select the next (or, if @a forward is false, the previous) selectable
item on this diagram, cycling through items() (z-order) and wrapping
around at either end. If nothing is currently selected, selects the
first (or last) item. Uses the same "what counts as a real selectable
diagram item" filter as invertSelection(), so the candidate list and
its order always match what the user could reach by clicking.
@param forward true to select the next item, false for the previous one
*/
void Diagram::selectNextItem(bool forward)
{
QList<QGraphicsItem *> candidates;
for (auto item : items()) {
if (dynamic_cast<QetGraphicsItem *>(item) ||
dynamic_cast<DiagramTextItem *>(item) ||
dynamic_cast<Conductor *>(item)) {
candidates << item;
}
}
if (candidates.isEmpty())
return;
int current_index = -1;
for (int i = 0; i < candidates.size(); ++i) {
if (candidates.at(i) -> isSelected()) {
current_index = i;
break;
}
}
int next_index;
if (current_index == -1) {
next_index = forward ? 0 : candidates.size() - 1;
} else {
next_index = forward
? (current_index + 1) % candidates.size()
: (current_index - 1 + candidates.size()) % candidates.size();
}
clearSelection();
QGraphicsItem *next_item = candidates.at(next_index);
next_item -> setSelected(true);
next_item -> ensureVisible();
}
/**
@brief Diagram::insertFolioSeqHash
This class inserts a stringlist containing all
+6 -1
View File
@@ -150,8 +150,11 @@ class Diagram : public QGraphicsScene
void wheelEvent (QGraphicsSceneWheelEvent *event) override;
void keyPressEvent (QKeyEvent *event) override;
void keyReleaseEvent (QKeyEvent *) override;
bool event(QEvent *event) override;
private:
void selectNextItem(bool forward);
public:
void correctTextPos(Element* elmt);
void restoreText(Element* elmt);
@@ -287,6 +290,8 @@ class Diagram : public QGraphicsScene
void selectAll();
void deselectAll();
void invertSelection();
void selectAllConductors();
void selectAllTextFields();
signals:
void showDiagram (Diagram *);
+22
View File
@@ -21,6 +21,7 @@
#include "qetgraphicsitem/conductortextitem.h"
#include "qetgraphicsitem/element.h"
#include "qetgraphicsitem/elementtextitemgroup.h"
#include "qetinformation.h"
#include "qgimanager.h"
/**
@@ -97,6 +98,27 @@ void PasteDiagramCommand::redo()
dc.addValue("label", "");
dc.addValue("comment", "");
dc.addValue("location", "");
// PLC slaves store master data (type, address, comment,
// cross-ref, etc.) in their own elementInformations.
// Remove them the same way MasterElement::unlinkElement()
// does, so pasted PLC slaves start clean like regular
// slaves.
if (e->linkType() == Element::Slave) {
dc.remove(QETInformation::ELMT_PLC_TYPE);
dc.remove(QETInformation::ELMT_PLC_ADDRESS);
dc.remove(QETInformation::ELMT_PLC_FUNCTION);
dc.remove(QETInformation::ELMT_PLC_COMMENT);
dc.remove(QETInformation::ELMT_PLC_CROSSREF);
dc.remove(QETInformation::ELMT_LABEL);
dc.remove(QETInformation::ELMT_PLC_TC);
dc.remove(QETInformation::ELMT_PLC_T1);
dc.remove(QETInformation::ELMT_PLC_T2);
dc.remove(QETInformation::ELMT_PLC_T3);
dc.remove(QETInformation::ELMT_PLC_T4);
dc.remove(QStringLiteral("xref"));
}
e->setElementInformations(dc);
//Reset the text of conductors, the same way the label/comment/

Some files were not shown because too many files have changed in this diff Show More