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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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
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
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
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
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
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
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.
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.
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.
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).
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).
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.
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.
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.
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>
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>
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.
#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>
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>
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.
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.
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.
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>
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>
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>
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>
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>
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>
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>
Follow-up to the positionKey() fix merged directly in #779
(b2f4ef5d2): per review request, add a small regression test so this
class of bug (fixed-precision "%.4f" formatting compares out of
numeric order once the integer part's digit count differs) can't
silently reappear.
positionKey()/coordinateKey() move out of diagram.cpp's anonymous
namespace into a small header-only diagramsortkeys.h so the test can
link against the exact same code Diagram::toXml() uses, instead of
duplicating the algorithm. Behavior is unchanged.
tst_diagramsortkeys covers: single- vs double-digit, double- vs
triple-digit, negative-vs-negative, negative-vs-positive, and
negative-vs-zero coordinate pairs, plus sub-precision deltas.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
update instead of the image a placeholder box is drawn and a warning is given during export.
Current dxf version is extremely old and does not support any image embedding. Future possible
update is the introduction of a newer dxf version export option which would allow to embed a link to an external
image file.
Plain fixed-precision formatting ("%.4f") produces strings that don't
compare in numeric order once the integer part has a different digit
count -- e.g. "15.0000" sorts before "5.0000" as text, even though
15 > 5. That silently broke the determinism goal of this branch for
any diagram with coordinates spanning more than one digit width.
Shift into a non-negative range and zero-pad to a fixed width instead,
so the formatted string sorts the same way the number does, including
negative values.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each cascade delete (element_info/terminal/conductor/element) only
logged its error and fell through to the next query regardless, so a
mid-cascade failure (e.g. a locked DB) still let the diagram row get
deleted while its child rows survived -- the same inconsistency this
branch set out to fix, just via a different failure path. Wrap the
cascade in a transaction and roll back + bail on the first failed
exec(), matching the existing transaction pattern in updateDB().
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mirror (M) and Flip (F) in the element editor reflected the selected
parts across the element origin: every part's mirror() and flip()
negated the scene x or y coordinate. A part drawn to the right of the
origin landed the same distance to the left, so the selection jumped
to the other side of the canvas instead of turning around.
MirrorElementsCommand and FlipElementsCommand now compute the united
scene bounding rectangle of the selected items and reflect across the
vertical or horizontal line through its center. The center is snapped
to the nearest half of the diagram grid, so points that were on the
grid stay on the grid after the reflection. Terminals in particular
keep their grid alignment.
Each part's mirror() and flip() takes the axis coordinate as a
parameter with a default of 0, so the previous behavior remains
available to any other caller. The command stores the axis when it is
created and undo reapplies the same reflection, which is its own
inverse, so the existing undo path is unchanged.
Parts covered: PartArc, PartDynamicTextField, PartEllipse, PartLine,
PartPolygon, PartRectangle, PartTerminal, PartText.
Fixes#812
Shapes and images can now be resized, rotated, and skewed directly
on the canvas, not just moved. Both share one small transform
struct (rotation, then skew, then scale, anchored on a movable
pivot) and one handle widget, so a corner drag, an edge skew, or
grabbing the rotate handle behaves the same way and runs through
the same matrix math everywhere, instead of every item type
reinventing its own.
Shapes also gained a proper pen tool (bezier paths, corner/smooth/
symmetric nodes), arc support, and mirroring. Images gained
non-destructive cropping and colour-keyed transparency, both
remember their own settings, so reopening the dialog picks up
where you left off instead of starting over.
Properties dialogs for both were extended to match (position,
size, angle, skew), with undo/redo wired through for every handle
drag.
Old XML files can read easily as the transformation is only added
if needed and the old syntax is still used and understood if it is
not needed.
1. Drop dead prepareGeometryChange()/update() calls in
setPotential(), split unrelated terminaleditor.h re-indent into its
own change
2. Removed: setPotential() had prepareGeometryChange()/update() left over from
the label-display mechanism that was dropped before this PR
windows-build.yml:
- Remove the Qt5 build job (build-windows). Qt6 is now the sole
Windows track built in CI.
- publish-nightly-assets: only delete/replace .exe and .zip assets
tagged "qt6" on the nightly release. Assets without "qt6" in the
name (the last Qt5 build ever published) are left untouched and
stay downloadable indefinitely as a frozen legacy build.
- Release notes: drop the "Try Qt6 — soon the only track" notice,
add a line explaining the Qt5 (frozen) vs Qt6 (maintained) split.
windows-msi.yml:
- Matrix reduced to the single "qt6" entry (flavor string kept as
"qt6", not renamed, since it seeds the MSI ProductCode and a
rename would break upgrade detection for existing installs).
- Sign the Qt6 MSI via SignPath (previously Qt5-only). Guard is now
just the upstream-repo fork check; no per-flavor exclusion left.
- deploy-pages: also detect legacy (non-"qt6") release assets and
pass them to generate-page.py as LEGACY_INSTALLER_URL /
LEGACY_PORTABLE_URL / LEGACY_MSI_URL.
generate-page.py:
- Drop the old Qt5/Qt6 dual-track rendering; INSTALLER_URL /
PORTABLE_URL / MSI_URL now point at the Qt6 build directly.
- Add an optional "Windows — x86_64 — Qt5 (legacy, unmaintained)"
card, rendered only when LEGACY_* URLs are set, with a frozen/
no-longer-updated notice.
Before merging: manually trigger the current (pre-merge) "Windows
Build" + "Windows MSI" workflows once to publish an up-to-date,
signed Qt5 snapshot — that run becomes the frozen legacy reference,
since the Qt5 job won't exist to re-run afterwards.
No changes to QElectroTech.wxs (Qt5/Qt6-agnostic, only
QtPlatformArgs varies and is already handled at the CI level).
New QLineEdit (m_potential_le) between Type and Nom: an optional,
symbol-author-chosen grouping identifier shared by terminals that
belong to the same physical terminal within a multi-terminal block.
Stored as TerminalData::m_potential (new field, persisted as the
"potential" XML attribute, empty by default).
Used by relatedPotentialTerminal() in terminal.cpp: when
potential_isolating is enabled on a Terminal-type element, terminals
sharing a non-empty, matching potential value now stay electrically
linked to each other instead of every terminal in the block being
isolated from every other one.
Tooltip on the field for explanation
Elements already inherited QetGraphicsItem::isMovable()/setMovable() --
the same mechanism images and drawn shapes use for their "lock position"
checkbox -- but nothing exposed it in the element properties panel, and
Element::toXml()/fromXml() never persisted it.
- ElementPropertiesWidget::generalWidget(): add a "Verrouiller la
position" checkbox mirroring ShapeGraphicsItemPropertiesWidget's
m_lock_pos_cb, toggling the element's inherited setMovable().
- Element::toXml()/fromXml(): persist is_movable, same attribute name
and default-true behavior as DiagramImageItem/QetShapeItem.
Verified via headless --resave round-trip: is_movable="0" survives
load -> save unchanged, existing elements without the attribute default
to movable.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdWpDp3TrPKbHnv7YUcNJj
DiagramView::zoom() had the same unbounded scale() as the element editor:
a held scroll-wheel zoom could overflow the view transform. Clamp the
resulting scale to [m_min_zoom, m_max_zoom] before applying it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdWpDp3TrPKbHnv7YUcNJj
ElementView applied scale() on every wheel notch with no bound on the
resulting view transform. Held down, the scroll-wheel zoom drives the
transform scale (m11) to floating-point overflow; the transform becomes
non-invertible, mapToScene() returns NaN and the next background paint
aborts the editor ("program closes completely" as reported on Windows).
Route zoomIn/zoomOut/zoomInSlowly/zoomOutSlowly through a new
scaleClamped() helper that only applies the scale while the result stays
within [m_min_zoom, m_max_zoom] (0.1 .. 200). Behaviour within that range
is unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdWpDp3TrPKbHnv7YUcNJj
CFBundleIconFile in misc/Info.plist was set to qelectrotech.icns, including the file extension.
Per Apple convention this key should hold the icon name without the extension — macOS appends .icns itself.
On recent macOS versions this mismatch causes the Finder to fall back to the generic placeholder icon instead of the actual app icon.
Fix: CFBundleIconFile → qelectrotech (no extension), matching the existing CFBundleTypeIconFile entries (elmt, titleblock, qet) which were already correct.
**1. `autonumberingmanagementw.ui`: legacy Qt5 font weight**
`uic` emitted `font.setWeight(75)` / `setWeight(50)` from old `<weight>` XML properties. Qt6's `QFont::setWeight()` takes a `QFont::Weight` enum instead of a raw int, so this fails to compile. GCC apparently tolerates it as a warning (`-fpermissive`), but it's a hard error on Apple Clang. `<bold>` was already set on every affected widget, so the `<weight>` tags were redundant — dropped.
**2. `CMakeLists.txt`: no macOS app bundle**
The executable target had no macOS-specific handling (`if(WIN32)/else()` only), so CMake produced a flat Mach-O binary instead of a `.app` bundle — nothing for `macdeployqt`/codesign/DMG steps to package. Added `MACOSX_BUNDLE` via `set_target_properties(APPLE)`. This in turn made `install(TARGETS ...)` fail configure (`no BUNDLE DESTINATION for MACOSX_BUNDLE executable`), so added `BUNDLE DESTINATION .`. Both changes are no-ops on Linux/Windows.
Built successfully end-to-end with `MacQetDeploy_arm64_cmake.sh` on macOS 14, arm64, Qt 6.11.1 (Homebrew) + KF6.
Without this, CMake produced a plain Mach-O binary on macOS instead
of a .app bundle, so MacQetDeploy_arm64_cmake.sh's cp/macdeployqt/
codesign steps had nothing to package. No effect on Linux/Windows.
Qt6's QFont::setWeight() now takes a QFont::Weight enum instead of
a raw int, so uic-generated code from the old <weight>75/50</weight>
XML properties fails to compile (worked on GCC via -fpermissive,
but hard error on Apple Clang for the macOS build).
<bold> is already set on all affected widgets, so <weight> was
redundant and can be dropped without any visual change.
Follow-up to the review of #790: four pre-existing messages whose source
is the DEGREE SIGN (U+00B0) were translated with the MASCULINE ORDINAL
INDICATOR (U+00BA) — GeneralConfigurationPage, IndiTextPropertiesWidget,
ReplaceConductorDialog and TextEditor. They render as an ordinal in the
rotation spin box suffixes.
Also fixes punctuation in the two SelectAutonumW help texts: a stray
space in "N ° página" / "n ° da página" and two unbalanced quotes.
Sources, comments, message count and ordering are untouched (2850
messages, 0 unfinished); .qm regenerated with lrelease.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9qWZfNpKZqrAzUE2QB3TJ
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>
Finish the 364 messages still marked unfinished in lang/qet_pt_BR.ts,
bringing pt_BR from 87.2% to 100% of the 2850 messages. Of those, 228
were empty and are translated here; the remaining 136 carried Linguist
suggestions that were reviewed, 14 of them corrected. Several contexts
were previously untranslated in full: ContactGroupSelectionDialog,
PlcLinkWidget, TerminalNumberingDialog, ShortcutsConfigPage,
BackupDialog, DiagnosticsReportDialog, GuidesPropertiesWidget,
PdfPagesDialog and EdzArchive.
Terminology follows what the file already established: "borne" ->
"terminal", "bornier" -> "régua de terminais", "folio" -> "página",
"cartouche" -> "bloco de legenda", "schéma" -> "esquema",
"maître/esclave" -> "mestre/escravo", "pivoter" -> "girar". PLC terms
use the Brazilian abbreviation CLP, and NO/NC contacts use NA/NF.
Notable fixes among the reviewed suggestions: "Annuler" read "Desfazer"
(undo) where it is a dialog button next to OK, and the degree symbol
used U+00BA MASCULINE ORDINAL INDICATOR instead of U+00B0 DEGREE SIGN.
Only <translation> elements are touched; sources, locations and
comments are unchanged. lang/qet_pt_BR.qm is regenerated with lrelease.
QETProject::removeDiagram() detaches a diagram from m_diagrams_list and
schedules it via deleteLater(), but that deferred delete only runs on
a future event-loop iteration. If ~QETProject() runs first (e.g. a
CLI/headless caller with no event loop, or a project closed
immediately after removeDiagram()), the diagram is still a QObject
child of the project and gets destroyed later by QObject's own
automatic child cleanup -- which runs after m_data_base has already
been torn down as a plain C++ member. Diagram::~Diagram() calls back
into dataBase()->removeElement() for each of its elements, so that
ordering is a use-after-free (SIGSEGV in QSqlResult::exec()).
Delete any such still-parented diagrams synchronously in ~QETProject()
while m_data_base is still alive, before the base QObject destructor
runs. Any deleteLater() event that does eventually fire afterward is a
safe no-op on an already-deleted QObject.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
removeDiagram() only ever deleted the diagram's own row. No foreign key in
this schema is declared ON DELETE CASCADE (and SQLite foreign-key
enforcement is never turned on for this connection anyway), so removing a
diagram left every element, element_info, terminal and conductor row that
belonged to it behind in the database -- silently, since nothing reads them
until the next full updateDB() rebuild papers over it.
Traced why this had never crashed anything: Diagram::~Diagram() explicitly
walks and deletes its top-level items through removeItem() (which does call
dataBase()->removeElement() correctly), but deliberately skips conductors --
because a conductor's destructor touches both of its terminals
(terminal1->removeConductor(this)), and those terminals may belong to an
element already destroyed earlier in the same sweep. Conductors are instead
destroyed as a side effect of Terminal::~Terminal()'s qDeleteAll() on its own
conductor list, which is a plain C++ delete that never goes through
Diagram::removeItem() and therefore never calls dataBase()->removeConductor()
at all. So the object graph is torn down safely, but the database is never
told about the conductors or their terminals.
Fixed by adding the missing bulk deletes to projectDataBase::removeDiagram()
itself, run while the diagram (and its live scene) still exist -- verified
that QETProject::detachDiagram() emits diagramRemoved() (which this class's
constructor connects to this slot) synchronously, before the Diagram object
is scheduled for destruction via deleteLater(), so nothing here races the
C++ teardown described above. Order matters: element_info and terminal have
no diagram_uuid column of their own, so both are scoped through a subquery
on element and must run before element itself is deleted.
Verified against examples/industrial.qet (50 diagrams) by calling
projectDataBase::removeDiagram() directly and comparing table counts before
and after, with no intervening updateDB() call to mask a gap:
element=354->335 element_info=354->335 terminal=1087->1033 conductor=671->626 diagram=50->49
Every delta matches a direct SQL count for that diagram's own rows exactly
(19 elements, 54 terminals), and both "orphan rows still referencing the
removed diagram" checks read 0 afterward -- so the cascade is complete and,
just as importantly, scoped: nothing belonging to the other 49 diagrams
moved.
Separate finding, not fixed here: QETProject::removeDiagram(Diagram*) (the
synchronous, non-undoable variant, not the usual GUI
ProjectView::removeDiagram() path) segfaults if the enclosing QETProject is
destroyed before an event loop iteration lets its pending deleteLater() run
-- reproduces identically on unmodified master, so it predates and is
unrelated to this change. Worth its own report; a headless caller is the
only realistic way to hit it, which is how this surfaced.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ElementQueryWidget::queryStr() reads FROM element_nomenclature_view, and that
view already excludes flagged elements in its own WHERE clause (see
createElementNomenclatureView() in projectdatabase.cpp). This widget then
added a second condition on top: "exclude_from_bom IS NULL OR
exclude_from_bom != '1'" -- but nothing anywhere ever writes the literal
string "1" to this key (the only writer stores "true"/"false"), so the
clause was true for every row that could possibly reach this point and did
nothing.
Confirmed dead three separate ways while reviewing qelectrotech#765: reading
the value only ever comes back "true" or "false" (never "1"), an
exclude_from_bom="1" element still appeared in --export-bom output on a test
fixture, and the surrounding filter_ construction shows this AND'd clause
cannot change the query's result set regardless of what filter_ already
holds. Confirmed it a fourth way once already, by initially misreading this
same clause as evidence the feature was broken -- it was reading the WHERE
without the FROM three lines above, which is exactly the trap being removed
here for the next reader.
ElementQueryWidget backs the BOM export dialog and the diagram table
properties widget; neither has a headless CLI equivalent, so this could not
be verified end-to-end through --export-bom the way the case-insensitivity
fix could. Verified instead: the file compiles clean, and a
load/resave/--export-bom smoke test on examples/tremie_vibrante.qet shows no
change in app behaviour (98 components, matching the pre-change baseline --
expected, since --export-bom does not go through this widget at all).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Saving an unmodified project produced a different byte stream on
every run: QGraphicsScene::items() returns items in stacking order,
and ties between same-Z items follow the scene's internal index --
not any content-derived order -- so it isn't reproducible across
process runs. The legacy terminal-id table inherits the same
instability, since ids are assigned sequentially in element order.
Sort list_elements and list_conductors into a deterministic order
before serializing, using a key built from data that's actually
stable across loads (position), not Element::uuid()/Conductor::uuid():
for an item with no persisted uuid attribute, fromXml() invents a
fresh random one on every load, so sorting by uuid would still be
non-deterministic across process runs for any legacy file -- which
this corpus has plenty of.
Also fixes a second, related source of byte-level non-determinism
found while verifying the above: Conductor::toXml() unconditionally
wrote m_uuid back out, including the synthetic value fromXml() just
invented for a conductor with no uuid attribute in the file. Every
conductor in every example project checked has no persisted uuid at
all, so this alone meant no project with conductors could ever
resave identically, regardless of ordering. Conductor gets a
m_persist_uuid flag, false only when the uuid it's holding was
synthesized rather than loaded, so toXml() stops writing a value
that was never meant to be permanent.
Deliberately NOT applying the same uuid-persistence fix to Element:
element uuids are cross-referenced by other elements' <links_uuids>
blocks for master/slave/report linking (element.cpp, tmp_uuids_link,
matched by elmt->uuid() == stored uuid on load). Making an element's
own uuid non-persistent would silently break that match for any
linked element without one already -- a real regression, not a
theoretical one. Left as a smaller, separate residual: 1-6 elements
per project across the corpus (a few tenths of a percent) still get
a fresh uuid on each load, same class of bug, needs the link-aware
version of this fix instead of this one.
Verified against 8 example projects (the ones with conductors, plus
the two zero-conductor control cases from FINDINGS.md F002), 5
resaves each in isolated HOME/XDG environments:
- Element and conductor ORDER: 0 churning sections across the whole
corpus (previously the majority of diagrams in industrial.qet,
m_000.qet and tremie_vibrante.qet churned on every run).
- Conductor uuid VALUES: 0 churn (previously every conductor in
every project, since none have a persisted uuid).
- 6 of 8 projects are now byte-for-byte identical (md5) across all 5
runs. The remaining 2 (industrial.qet, m_000.qet) differ only in
the handful of element uuids covered by the known Element residual
above -- confirmed by checking those uuids specifically, not
inferred.
- Element/conductor counts before and after resave match exactly on
every project (no data loss from the sort).
Fixes#754.
redo() has a direct-write path guarded by m_first_time: on the very
first call it writes the property immediately, and only animates on
calls after that (per setAnimated()'s documented contract). undo()
had no equivalent -- it always animated, so undo() both returned
before the property was restored (stale state visible to anything
sharing the call stack) and, with no running event loop, never
restored it at all.
The obvious fix -- reuse m_first_time in undo()'s guard too -- turns
out not to work, and I verified this with a standalone build before
picking an approach: QUndoStack::push() always calls redo() once
before any undo() can run, and redo()'s direct-write branch sets
m_first_time = true as it completes. So by the time undo() is ever
called, m_first_time has already flipped, and reusing it would make
undo() take the animate branch on every call, unconditionally --
syntactically symmetric with redo(), but behaviourally unchanged for
the exact scenario reported.
Instead, undo() gets its own m_undo_first_time flag, seeded from the
same first_time argument setAnimated() already takes, and set true by
undo()'s own direct-write branch the same way m_first_time is set by
redo()'s. That gives undo() a real, reachable direct-write path on its
own first call, independent of how many times redo() has already run.
Verified against a standalone build of just this class (as the issue's
own repro does): first redo and first undo are both now synchronous
with no event loop running; with an event loop present, both settle to
the correct value once "broken in"; behaviour for every other caller
of QPropertyUndoCommand -- everywhere that calls plain enableAnimation()
or the bare setAnimated() (first_time defaulting true) -- is provably
unchanged, since m_undo_first_time starts true either way and the
animate branch never modifies it.
Fixes#755.
- Remove dead #if QT_VERSION conditionals (both branches were identical)
- Add settings.remove() guard on restoreState() failure consistently
across all three editors (now safe since all run after show())
The Windows Qt5 build is going to be removed from CI/CD soon (Qt6/KF6
is becoming the sole supported track). Replace the 'experimental'
warning wording on the nightly download page and in the nightly
release body with a call to action inviting users to switch to Qt6
now and report issues, ahead of the Qt5 removal.
QPdfDocument::pagePointSize() (used for PDF page import) requires
Qt >= 6.4, and the QtPdf module itself is missing entirely on some
Qt6 distributions (e.g. the Flatpak org.kde.Platform runtime), since
it ships from the qtwebengine source tree rather than Qt6 core.
CMake: probe Pdf with find_package(... QUIET) instead of REQUIRED,
mirroring the existing GuiPrivate pattern. Define QET_HAS_QTPDF
only when the module is found AND Qt >= 6.4.
Replace the ad-hoc QT_VERSION_CHECK(6, 0, 0) / (6, 4, 0) guards in
qetdiagrameditor.cpp, diagrameventaddpdf.{h,cpp} and
pdfpagesdialog.{h,cpp} with #ifdef QET_HAS_QTPDF, so version and
module-availability checks live in one place.
Fixes the Flatpak build (missing Qt6Pdf) and the Windows/Debian CI
failures (QPdfDocument::pagePointSize undeclared on Qt < 6.4). The
PDF import toolbar action is now silently unavailable wherever
QtPdf isn't usable, instead of breaking the whole build.
Compiled into the binary but never instantiated -- searching the tree
for any reference outside its own three files finds nothing, and this
still holds on current master. Contains a latent bug that would be
user-visible if the widget were ever reachable
(on_tableDiagram_customContextMenuRequested compares QMenu::exec()'s
return value against one action but falls through to "select all" on
both the other action and on a plain dismiss, since exec() returns
nullptr on Escape/click-away and that's not equal to either QAction*),
which supports genuine disuse rather than temporary disconnection.
Removed the three files and their three explicit entries in
cmake/qet_compilation_vars.cmake (qelectrotech.pro globs sources/ui/*
so needs no change). Builds clean; no other file references
diagramselection.
Fixes#756.
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
m_jump_to_element was the only action in the tree that set its
QKeySequence directly instead of going through
ShortcutManager::registerAction() -- of 98 actions carrying a runtime
shortcut, 95 matched a registerAction() call, 2 were Qt built-ins, and
this was the sole exception (verified by dumping every QAction from a
running instance and cross-checking against a static scan of the
source; the only other setShortcut() call in the tree clears a
shortcut rather than setting one).
Bypassing the registry meant the binding didn't appear on the
Shortcuts preferences page (so it couldn't be discovered or rebound),
and checkConflicts() couldn't see it either, so assigning Ctrl+G to
another action there would silently collide at runtime instead of
being flagged.
Fixes#758.
Apply the same split readSettings()/readSettingsState() pattern from
QETDiagramEditor to the other two main windows:
- QETElementEditor: split in constructor, call readSettingsState() after show()
- QETTitleBlockTemplateEditor: split readSettings(), callers call
readSettingsState() after show() (newTemplate + 2x openTitleBlockTemplate)
- Remove destructive settings.remove() guards that would delete saved
state on every Qt6 launch when restoreState() fails before show()
Co-authored-by: ispyisail
- Add DPI selection (150/300/600) to the page selection dialog
- Add live page preview in the selection dialog
- Conditionally compile PDF import only for Qt6 (#if QT_VERSION)
- Add custom pdf-import icon (PDF document with + symbol)
- Register new icon in qelectrotech.qrc
- Qt5 builds: PDF import action is hidden, everything else works as before
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).
"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.
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>
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>
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".
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.
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.
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>
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>
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.
The conductor table keyed on Terminal::uuid(), which comes from the catalog
.elmt definition and is empty for every element authored before that field
existed. A conductor was dropped unless *both* its terminals had one, so the
tables this slice adds were empty on almost every project in existence:
examples corpus conductor rows in the database
industrial.qet 0 of 671
affuteuse_250h.qet 0 of 263
tremie_vibrante.qet 0 of 77
741.qet 0 of 67
Across the 23 example projects, 16 of the 20 that contain conductors have
zero terminal uuids -- 2366 of 3002 conductors -- and overall coverage is
7.3%. Meanwhile --export-cables, already on master, lists all 671 conductors
of industrial.qet from the document. A feature that only works on newly
authored elements is not one users can rely on.
Terminal::stableUuid() returns the terminal's own uuid when it has one and
otherwise derives one from its local position and orientation inside its
element. That is not an invented scheme: it is what the project format
already does. TerminalData::fromXml() says so where it parses the field --
"if the attribute not exists, means, the element is created with an older
version of qet. So use the legacy approach to identify terminals" -- and the
legacy approach is the terminal's position. m_pos is read from the definition
and is not touched by moving the element on a folio, so the identity survives
loads, saves and folio moves. Derived values are UUID v5 in a fixed namespace,
so they are reproducible without being stored, and cannot collide with the v4
uuids the element editor generates.
Every project in the corpus now has exactly as many conductor rows as the
document has conductors -- 20 of 20 measured, 0 mismatches. (schema_indus.qet
is excluded: it blocks on a modal dialog at zero CPU under any CLI flag, the
pre-existing hang PR #661 addresses.)
Two things this deliberately does not key on:
- The terminal name. It is not stable: QET rewrites a terminal named "_" as
unnamed, which would have silently changed the identity of 1421 of
industrial.qet's 1790 terminals on their first resave. Measured across the
corpus, dropping it costs nothing -- geometry alone yields exactly the same
three collisions -- and it means renaming a terminal no longer changes what
it is.
- Uniqueness in the face of a definition that declares two terminals at the
same point and orientation. Three cases exist in the whole corpus. They
merge to a single terminal row, which is harmless: two terminals identical
in position and orientation are indistinguishable in every observable
respect, and every conductor on either still resolves to the right element
and terminal name. Both affected projects (industrial, perceuse) return
their full conductor count.
The only conductor still skipped is one whose terminal has no parent element,
which has no identity to key on at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three fixes to the tables added by this slice.
A conductor's text was written once at insert and never again. Renaming a
wire left the database holding the old number, so the wiring list showed a
stale value until the next full repopulate -- elements have
elementInfoChanged() for exactly this and conductors had nothing.
Conductor::setProperties() has around a dozen call sites (auto-numbering,
the properties dialog, element moves, the delete command's re-links), so
rather than adding a call to each and missing the ones added later, listen
to the propertiesChange() signal it already emits. Qt::UniqueConnection
means a repeated insert or a full repopulate cannot double-subscribe, and
the connection is established on both insert paths because conductors read
from a file never pass through addConductor().
addConductor() and populateConductorTable() each carried their own copy of
the same seven bindValue() lines. They had not drifted yet, but that is the
same duplication the element paths had before bindElementValues(), where
they had drifted -- one binding kindInformations()["type"] and the other
masterTypeToString(). One bindConductorValues() for both.
Finally, index the conductor columns that get looked up per element rather
than per conductor. element_nomenclature_view counts the wires touching each
element with a correlated subquery, so without an index every element row
full-scans the conductor table and the cost grows as elements x conductors.
Measured on a standalone SQLite harness at 2000 elements x 5000 conductors:
2134 ms unindexed, 10 ms indexed. diagram_uuid is indexed too, since the
wiring list view joins on it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Slice 2 of discussion #503 (from-to wiring list built on projectDataBase),
building on the conductor uuid from slice 1 (#625). Pure plumbing: two
new additive tables plus their populate/add/remove hooks. No view, no UI,
no visible behavior change yet -- the wiring-list view is slice 3.
Follows the existing shape of the class throughout: same table/column
naming, same prepared-statement idiom in prepareQuery(), same
bind/exec/qDebug-lastError error handling, same DELETE-then-loop
populate pattern.
- `terminal (uuid, element_uuid, name)` and
`conductor (uuid, diagram_uuid, terminal1_uuid, terminal1_element_uuid,
terminal2_uuid, terminal2_element_uuid, text)` created alongside the
existing tables in createDataBase().
- populateConductorTable() added as a fifth populate* call in updateDB().
Terminal population is folded into it, since a terminal only matters
here in the context of a conductor referencing it.
- addConductor()/removeConductor() hooked into the already-existing
Conductor::Type branch of Diagram::addItem()/removeItem(), mirroring
the Element::Type branch directly above.
Two things the original schema sketch in the discussion got wrong, found
by testing rather than inspection:
1. Terminal::uuid() is NOT unique per placed terminal. It is the
terminal-position id baked into the catalog .elmt definition ("the
top terminal"), so every placed instance of the same catalog element
shares it. A terminal instance is only uniquely identified by
(uuid, element_uuid) together, so that pair is the terminal table's
primary key and the conductor table carries both halves for each
endpoint. With uuid alone as PK, the second placed instance of any
element silently lost its terminals to the INSERT OR IGNORE.
2. Conductors whose terminals predate terminal uuids are omitted rather
than given a fabricated identity, as agreed in the discussion. This
turns out to matter far more than expected in practice -- see below.
Testing (all live, in the running app):
- Incremental add: fresh project, two vertically aligned contacts placed
so autoconnect creates a conductor -> 2 terminals, 1 conductor.
- Incremental remove: deleting that conductor -> conductor count 1 -> 0.
- Undo: ctrl+Z after the delete -> back to 1, no duplicate-primary-key
error (the same Conductor object keeps its uuid).
- Bulk populate: examples/weneedpolonez-Polonez_MR89_wiring_diagram.qet
(366 conductors) -> 478 terminals, 280 conductors; the 86 conductors
touching legacy terminals correctly omitted.
- Join correctness: conductor -> terminal (composite key) -> element_info
resolves real from-to rows with real element labels.
- Legacy-only project: examples/industrial.qet has 1794 terminals and
*zero* terminal uuids, so all 671 of its conductors are omitted. Loads
and renders fine, no crash, no spurious rows -- but worth stating
plainly that a from-to wiring list for that project would be empty
today. This is a property of the element catalog definitions, not of
the project file, and is the strongest argument for surfacing an
"N conductors excluded" count to the user when the view lands.
- No SQL errors logged in any of the above.
Known limitation, consistent with existing behavior: removeDiagram()
does not cascade-delete the conductor rows of that diagram, exactly as
it already does not cascade to element/element_info. A full updateDB()
rebuild clears them, and the future wiring-list view INNER JOINs from
conductor, so orphan terminal rows never surface.
ElementsPanelWidget::duplicateDiagram() round-trips the folio through XML
and then gives the copied *elements* fresh uuids, because element.uuid is
the primary key of the project database and a duplicate silently fails to
insert. Conductors now have the same problem and needed the same loop:
conductor.uuid is likewise a primary key, its insert is a plain INSERT
rather than INSERT OR IGNORE, and a failure only reaches qDebug(). Without
this, every wire on a duplicated folio is missing from the wiring list and
from the per-element wire count, with nothing shown to the user.
Verified against the real schema: inserting the same conductor uuid for a
second folio fails with "UNIQUE constraint failed: conductor.uuid", leaving
one row where two were expected.
Also harden the uuid read in Conductor::fromXml(). The default argument of
QDomElement::attribute() is evaluated whether or not the attribute exists,
so a uuid was minted for every conductor on every load and thrown away; and
the default only applies when the attribute is *absent*, so a present but
empty or malformed uuid="" parsed to a null QUuid rather than a fresh one --
and null uuids collide with each other exactly as duplicates do.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
During an ESEvent, the mouse position was used without format with
`snapToGrid` to display the coordinates. However, since the `helpCross`
is positioned using `snapToGrid` during these events, the displayed
coordinates did not match the `helpCross` position.
The command for sending the coordinates has been moved to the
`ESEventInterface` to function 'updateHelpCross' and now transmits the
position of the intersection point of the helpCross lines.
Replace the flat QTableWidget with a QTreeWidget that groups actions under
one collapsible top-level node per category. Fix the search box so it also
matches the current key sequence (exactly), accepts multi-keyword queries
(AND, any word order) and is accent-insensitive, auto-expands matching
groups and shows an "N actions" count. Add a quick filter (all / bound /
unbound / conflicts) that combines with the text query. Conflict detection,
per-row reset, reset-all and persistence are preserved.
Co-Authored-By: Claude <noreply@anthropic.com>
switch runtime/sdk to org.kde.Platform/org.kde.Sdk 6.10
migrate qelectrotech module from qmake to cmake buildsystem
add config-opts: QT_VERSION_MAJOR=6, BUILD_WITH_KF=ON, BUILD_KF=OFF,
PACKAGE_TESTS=OFF, BUILD_PUGIXML=OFF, QET_EXPORT_PROJECT_DB=ON
drop fix-the-installation-paths.patch (qmake-only, obsolete under cmake)
re-attach fix-appdata.patch, previously unreferenced in sources
document open verification points for Qt6 private headers and the
SQLite driver, which have no Flatpak build-depends equivalent
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>
QET_MIME_PACKAGE_PATH was "../share/mime/packages/", a path relative
to CMAKE_INSTALL_PREFIX. This only worked by accident with the old
default prefix (/usr/local -> ../share resolves to /usr/share/mime,
the conventional system location regardless of app prefix).
With -DCMAKE_INSTALL_PREFIX=/usr (as used by Debian/Ubuntu packaging),
the same "../share" escapes /usr entirely, landing at /share/mime
instead of /usr/share/mime, which breaks dh_install (file not found
under usr/) and would silently install the mime package definition
outside any path desktop environments actually scan.
Drop the "../" so the mime package path stays under the install
prefix, matching standard practice (/usr/share/mime/packages or
/usr/local/share/mime/packages).
FetchContent_Declare unconditionally tries to clone SingleApplication
from GitHub, which breaks offline builds (e.g. Debian/Ubuntu pbuilder
with FETCHCONTENT_FULLY_DISCONNECTED=ON, Launchpad PPA builds).
If the SingleApplication submodule is already checked out in the
source tree, point FETCHCONTENT_SOURCE_DIR_SINGLEAPPLICATION at it so
FetchContent skips the network step entirely and reuses the local
copy. Falls back to the existing git clone behavior otherwise, so
this is a no-op for setups that don't vendor the submodule.
Follow-up to #740, which fixed the slave-side "(n-Xn)" cross-reference
label. The master-side item - the small table/cross drawn next to a
report or master element, listing where each of its slaves is used -
was still missing from DXF export. Measured against examples/
industrial.qet with the PDF export as an oracle (renders the whole
scene, so it shows what should be there):
before after PDF
slave xrefs "(n-Xn)" 41 41 41 (already fixed, #740)
folio/position strings 358 403 403
DXF now matches the PDF exactly.
## Why this needed a different approach than #740
The slave label is a plain QGraphicsTextItem - one string, trivial to
walk and re-emit as a single DXF TEXT entity, which is what #740 did.
The master-side item (CrossRefItem) is not: it paints itself with
~600 lines of hand-written QPainter calls across three modes
(drawAsCross/drawAsContacts/drawAsPlcTable), including a header
table, contact symbols, and rules. Hand-porting that logic to emit
DXF primitives directly would mean maintaining two divergent
implementations of the same drawing that have to be kept in sync by
hand forever.
## Approach: a QPaintEngine that intercepts CrossRefItem's own paint()
DxfPaintEngine/DxfPaintDevice (sources/dxfpaintdevice.{h,cpp}) is a
QPaintEngine/QPaintDevice pair - the same mechanism QPrinter and
QSvgGenerator use to redirect QPainter output elsewhere. Constructing
a QPainter on a DxfPaintDevice and calling item->paint() on it produces
DXF entities instead of pixels, using the exact same drawing code that
already renders correctly on screen. CrossRefItem::paint() is
unmodified.
Scope is deliberately narrow - only the QPainter calls CrossRefItem's
paint() is observed to make: drawLines -> LINE, drawRects/drawPath's
fill case -> outline-only LWPOLYLINE (no HATCH support in v1 - DXF's
fill primitive is a separate, more involved entity type; documented as
a known limitation rather than attempted here), drawEllipse -> CIRCLE
or a flattened polygon for rotated ellipses, drawPath's arc case (from
drawArc/drawPie) -> chord-flattened LINE segments, drawPolygon ->
LWPOLYLINE, drawTextItem -> TEXT. drawPixmap is intentionally
unimplemented (qWarning + skip) since CrossRefItem never calls it -
this is not a general-purpose DXF paint engine, and isn't meant to be
in this PR.
CrossRefItem::paint() is protected, per the normal QGraphicsItem
contract - added a small paintForExport() wrapper rather than making
paint() itself public, or reaching around access control.
## Explicitly out of scope
QetShapeItem::toDXF() and QetGraphicsTableItem::toDXF() (both already
implemented and working) are untouched. Rewriting working exporters
onto this engine to prove an architectural point would be a large,
unrelated diff with no user-visible benefit - if that consolidation is
wanted later, it's a separate proposal once this engine has shipped
and proven out on the one item that currently has no DXF export at
all.
## Testing
Built clean on Qt5/Linux. Verified via the GUI export dialog
(Fichier > Exporter > DXF) against examples/industrial.qet, 50 folios:
export completes without error or crash, all 50 .dxf files are
structurally well-formed (balanced SECTION/ENDSEC, single EOF each),
and grepping the folio-position pattern gives the before/after/PDF
numbers above. Spot-checked several real label strings (e.g. "18-B18",
"20-A2") present as TEXT entity values in the output, not just an
artifact of the count matching.
- build-windows-qt6: install kwidgetsaddons/kcoreaddons/extra-cmake-modules,
switch -DBUILD_WITH_KF=OFF to ON, add -DBUILD_KF=OFF to use precompiled
MSYS2 packages instead of building KF6 from source via FetchContent
- build-windows: swap unsuffixed kwidgetsaddons/kcoreaddons (actually KF6
packages after MSYS2's renaming) for the -qt5 suffixed ones, add
-DBUILD_KF=OFF so the installed packages are actually consumed instead
of being ignored by the default FetchContent-from-source build
Follow-up to #158 / PR #501. While investigating that position bug,
found a second, separate one in the same area: PartText::setFont()
never updated real_font_size_, so it stayed frozen at whatever size the
item was constructed with.
That field isn't cosmetic - it's live data two other operations depend
on:
- startUserTransformation()/handleUserTransformation() use it as the
base size when scaling the font as the user drags a resize handle.
With it stale, dragging a handle after changing the size via the
toolbar (or loading a file with a non-default size) scales from the
wrong starting point - the resulting size has nothing to do with
what's visibly on screen.
- flip() reads it directly to compute the repositioning offset, so a
stale value also mis-positions the item on flip.
Fix: update real_font_size_ inside setFont(), the same place PR #501
already re-runs adjustItemPosition() for the same reason (font changed,
keep everything that depends on it in sync). fromXml() already routes
both its "size" and "font" attribute branches through setFont(), so
loaded elements pick this up for free.
Verified with a temporary instrumented build: typed a size into the
element editor's font-size field three times (9 -> 4 -> 48). Each
setFont() call's "before" value exactly matched the previous call's
"after" value, confirming real_font_size_ now tracks every change
instead of freezing at its construction-time value (9). Instrumentation
removed before committing.
https://github.com/qelectrotech/qelectrotech-source-mirror/issues/413
## Bug
Copy-pasting an element pair joined by a conductor with no label
results in the pasted conductor having a literal "_" label, even
though the source conductor's label was empty. Repeating copy+paste on
the result keeps stacking the same "_" back on, since the pasted
conductor now legitimately has that text.
## Root cause
PasteDiagramCommand::redo() (sources/diagramcommands.cpp), when the
"erase label on copy" option is enabled (the default), resets each
pasted element's formula/label/comment/location to "" - a real erase.
Right next to it, the equivalent reset for conductors doesn't erase:
cp.text = c->diagram() ? c->diagram()->defaultConductorProperties.text : "_";
It unconditionally overwrites the conductor's text with the *project's
configured default text for newly drawn conductors* - a setting that
happens to default to a literal "_" character (visible in the project/
new-folio "Conductors" tab), and is otherwise unrelated to whether this
particular copy's label should be kept or cleared. The `: "_"` fallback
for the "no diagram" case doesn't help either, since these conductors
are already added to the scene before this code runs.
## Fix
Reset conductor text to "" too, matching every other field reset in
the same block. "Erase on copy" should erase, not "replace with
whatever the project's unrelated new-conductor default happens to be."
## Verification
Built clean. I was not able to get a reliable live GUI reproduction
under Xvfb + xdotool for this one - drawing conductors between
terminals via simulated drag kept mis-firing as element placement
instead in this environment, the same class of automation friction
noted on PR #743. Confidence rests on tracing the exact code path
(confirmed defaultConductorProperties.text is a project-level setting
for freshly-drawn conductors, unrelated to paste; confirmed the
sibling element-info reset four lines above uses "" specifically) plus
the fact this is a one-line change to match an already-correct pattern
right next to it, not new logic.
https://qelectrotech.org/bugtracker/view.php?id=335
## Bug
Element library icons (collection tree thumbnails, drag icon, preview
panels) render with a fully transparent background. Element definitions
almost always hardcode a black stroke color, on the assumption of the
white diagram sheet they are normally drawn on. Against a dark widget/
tree-view background (e.g. KDE Plasma dark theme), that black stroke
disappears entirely - reported as icons being "black and almost
invisible". scorpio810_mantis linked this to the same recurring family
as #231, #247, #267.
## Fix
ElementPictureFactory::pixmap() is the single shared point where every
consumer of these icons gets its QPixmap (collection tree via
ElementsCollectionCache -> Element::pixmap(), master/slave properties
tree, element properties preview, drag icon). Change its background
fill from fully transparent to opaque white - exactly what the element
already visually assumes in every context this pixmap is used, so it
is correct regardless of the surrounding widget's palette.
## Testing
Built both variants and compared under Xvfb using a simple, decisive
visual test: select the tree row (giving it a highlighted/colored
background) and compare what shows immediately around the icon's
glyph.
- Before: the icon's background matches the row's selection color -
confirms it is transparent, so on a dark unselected row the black
strokes would have the same problem.
- After: a solid white square is visible behind the glyph regardless
of the row's background color.
Note for the on-disk pixmap cache used by ElementsCollectionCache
(~/.local/share/QElectroTech/QElectroTech/elements_cache.sqlite):
existing cached PNGs predate this fix and will keep their transparent
background until regenerated. That cache already keys strictly on
path+uuid with no invalidation on QET version, so this is an existing
characteristic of that cache, not something introduced here.
Bugtracker #291: clicking Cancel on the open/save-element dialog before
the user collection finishes loading crashes the whole application with
an unhandled pointer exception.
ElementsCollectionModel::loadCollections() loads collections in the
background via QtConcurrent::map(m_items_list_to_setUp, setUpData) -
worker threads call setUpData() on each ElementCollectionItem
(a QStandardItem), which does setFlags()/setData() on it.
ElementDialog::execConfiguredDialog() deletes the dialog immediately
after exec() returns:
element_dialog->exec();
...
delete element_dialog;
That destroys the tree view and its ElementsCollectionModel, which as
a QStandardItemModel frees all its items in its destructor. Nothing
waited for the QtConcurrent::map() to finish first, so on Cancel before
loading completes, background threads were still calling setUpData()
on items the main thread had just freed - a use-after-free race.
Add an ElementsCollectionModel destructor that waits for the future
before QStandardItemModel's destructor runs. QFuture::waitForFinished()
on a default-constructed (never-started) future returns immediately, so
this is a no-op whenever loading already completed - the crash path is
the only one affected.
Fix print window clipping diagram when titleblock on right edge is hidden.
Fixes a frequently made mistake: confusing width and height when rotating something... 😉
Cross-references were missing from DXF exports, as reported on the
forum: https://qelectrotech.org/forum/viewtopic.php?id=2481
generateDxf() walks the scene and collects items by cast. A slave
element's cross-reference label ("(6-G15)", pointing back to its master)
is a plain QGraphicsTextItem hung off a DynamicElementTextItem as a
child, so it matches neither the IndependentTextItem nor the
DynamicElementTextItem branch and was dropped on the floor. Nothing was
wrong with the label itself; it was simply never collected.
Collect it through the existing DynamicElementTextItem::slaveXrefItem()
accessor and draw it with the same placement, rotation and multi-line
handling as the other text items, using defaultTextColor() since a bare
QGraphicsTextItem has no DiagramTextItem::color().
Measured on examples/industrial.qet, comparing against the PDF export
(which renders the whole scene and so shows everything):
before after PDF
slave xrefs "(n-Xn)" 0 41 41
folio/position strings 317 358 403
The slave cross-references now match the PDF exactly.
Still missing, and not addressed here: the master-side cross-reference
table drawn by CrossRefItem, which accounts for the remaining 45
strings. CrossRefItem is a QGraphicsObject that renders itself with
custom QPainter code in three different modes (drawAsCross,
drawAsContacts, drawAsPlcTable) including contact symbols and rules, so
giving it a DXF representation is a larger piece of work than this.
"exclude_from_bom" is listed in QETInformation::elementInfoKeys() so the
project database can build the element_info table column for it, but it
is not a free-text property: ElementInfoWidget already gives it its own
"Exclure de la nomenclature" check box.
Because buildInterface() creates one ElementInfoPartWidget per key in
that list, the key also got a second, generic edit row. And since
translatedInfoKey() has no case for it and falls through to
"return QString()", that row carries no label at all - an anonymous edit
line at the bottom of the panel. currentInfo() then writes
exclude_from_bom unconditionally, so as soon as the user edits anything
the nameless row fills with "true"/"false".
Drop the key from the list buildInterface() iterates. The check box
remains the only way to set it, currentInfo() still writes it exactly as
before, elementInfoKeys() is untouched so the database schema and
elementquerywidget are unaffected, and predefinedKeys() already excluded
it from the custom-property rows.
Reported by plc-user on #642.
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).
Terminal::paint() draws the red terminal stroke, the blue docking dot
and the terminal name whenever the diagram's drawTerminals() /
drawTerminalNames() flags are set, and both default to true. The GUI
export dialog clears them through Diagram::applyProperties(), but the
headless CLI export (--export-pdf/--export-png/--export-svg) never
did, so every terminal shipped as coloured editor UI in otherwise
finished drawings.
Toggle both flags off around the render in renderDiagram(), exactly
like the existing grid/guides handling, and restore them afterwards.
QVector<int>(Qt::DisplayRole) creates a vector of size 0 (since Qt::DisplayRole == 0), not a vector containing DisplayRole. Fixed
to {Qt::DisplayRole}.
The sam applies to QVector<int>(role) which gets changed to {role}.
<private/qpdf_p.h> (QPdfEngine::drawHyperlink) needs Qt's private GUI
module, previously flagged only by a #warning at compile time.
Qt >= 6.7 ships GuiPrivate as a proper find_package component, but some
distro packages (e.g. Ubuntu's qt6-base-private-dev, Qt 6.8.3) do not
install Qt6GuiPrivateConfig.cmake and only provide the implicit
Qt6::GuiPrivate target created alongside Qt6::Gui. Requesting the
component unconditionally would therefore break distro-Qt builds.
Instead: try the component quietly, then hard-verify the Qt6::GuiPrivate
target exists after the main find_package, failing at configure time
with an actionable message if the private headers are missing. The
compile-time #warning in pdf_links.cpp and projectprintwindow.cpp is
now redundant and removed.
Verified: cmake configure + compile of both translation units on
Ubuntu 25.04 / Qt 6.8.3 (system KF6), cmake configure on Qt 5.15.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
a) using a system provided KF6
b) downloading and compiling KF6
c) using the vendored-in re-creation of the functionality
The behaviour for both Qt5 and Qt6 is steered with the same two variables which were renamed to become version agnostic:
a) BUILD_WITH_KF=ON BUILD_KF=OFF
b) BUILD_WITH_KF=ON BUILD_KF=ON
c) BUILD_WITH_KF=OFF
The version is automatically derived from the chosen Qt major version.
Selecting more than one dynamic text field in the element editor silently
replaced every selected field's colour with the colour of the first one.
Nothing was clicked -- merely extending the selection destroyed the others'
colours, and the change went onto the undo stack as if the user had asked
for it.
Cause: updateForm() loads the current part's colour into the colour button
with m_color_kpb->setColor(). KColorButton::changed is emitted for a
programmatic setColor() just as it is for user interaction, and it is
connected to m_color_kpb_changed(), which applies the new colour to *every*
part in m_parts. So simply displaying the first part's colour wrote that
colour to all the others.
Every other widget in updateForm() is immune because it is wired to a
user-only signal -- on_m_x_sb_editingFinished(), on_m_frame_cb_clicked() --
which setValue() and setChecked() do not emit. The colour button is the one
control whose signal cannot distinguish the two, so block it while loading.
This also explains why the reporter saw it only when rubber-band selecting
bottom-to-top: the write happens only when the first part's colour differs
from what the button already shows, which depends on selection order.
Verified in the element editor with two dynamic texts, one red and one blue:
select the red one, then ctrl-click the blue one. Before: the blue text
turned red. After: both keep their colours. Changing the colour deliberately
with the button still applies to all selected texts, as intended.
Forum report (qelectrotech.org/forum, topic 3005, reporter oc67): a
project named e.g. "Mon_projet.avec_un_point.qet" exported to PDF as
"Mon_projet.pdf" - everything after the first "." in the filename was
silently dropped.
Root cause: ProjectPrintWindow::docName() used QFileInfo::baseName(),
which returns the filename up to the FIRST "." rather than stripping
only the final suffix. docName() feeds both the PDF print job's
setOutputFileName() and the QFileDialog::getSaveFileName default in
exportToPDF(), so the truncation showed up as the actual exported
file's name, not just a dialog suggestion.
sources/exportdialog.cpp's SVG/PNG/DXF export path already uses the
correct QFileInfo::completeBaseName() (strips only the last suffix) -
this fix brings the PDF/print path in line with that existing,
correct pattern rather than introducing a new approach.
Verified the exact before/after behavior with a standalone QFileInfo
test: baseName() on "Mon_projet.avec_un_point.qet" returns
"Mon_projet" (the bug); completeBaseName() returns
"Mon_projet.avec_un_point" (correct). Also did a clean incremental
build with no new warnings/errors.
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.
Fix bugtracker #281: new-part wizard's element editor opens behind main window
Cannot reproduce on Debian/GNU Linux, but implementation is reasonable and clean!
Fix bugtracker #251: title block template with slash in name fails silently
There are some characters that are not allowed in filenames.
Absolutely correct to mark a filename containing (one of) them as invalid!
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.
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.
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.
Saving a new user title block template (right-click "Cartouches
utilisateur" > "Nouveau modèle" > "Enregistrer sous") with a name
containing a slash (or other filesystem-reserved character) silently
did nothing, with no error shown. The entered name is turned directly
into a filename (TitleBlockTemplatesFilesCollection::toFileName()), so
e.g. "foo/bar" becomes a path "foo/bar.titleblock" -- since "foo/"
essentially never exists as a directory, the underlying file write
fails, but that failure was never surfaced:
- TitleBlockTemplateLocation::isValid() only checked for an empty
name, so an invalid name still counted as "valid" and got passed
through to save.
- QETTitleBlockTemplateEditor::saveAs(const TitleBlockTemplateLocation&)
discarded the bool result of setTemplateXmlDescription() and
unconditionally returned true, marking the undo stack clean as if
the save had actually succeeded.
Fix:
- isValid() now also rejects names containing \ / : * ? " < > |,
matching the character set that's actually unsafe once the name
becomes a filename.
- saveAs() (the no-arg entry point that asks the user for a location)
now shows a clear error dialog when the entered name is rejected,
distinguishing "user cancelled" (location.name() empty) from
"name was invalid" (non-empty but rejected by isValid()).
- saveAs(location) now checks setTemplateXmlDescription()'s return
value and shows an error dialog instead of reporting false success
on any future/other write failure, not just this one.
Verified: clean rebuild, only the intended files recompiled and
linked successfully. Live-tested under Xvfb: creating a new template
and using "Enregistrer sous" with the name "foo/bar" now shows
"Le nom « foo/bar » n'est pas valide : il ne doit pas contenir les
caractères suivants : \ / : * ? " < > |" instead of silently doing
nothing; reopening the save-as dialog afterward showed the name field
correctly empty (nothing was partially written). Saving again with a
valid name ("mytemplate_valid") completed with no error dialog, and
the resulting mytemplate_valid.titleblock file was confirmed present
on disk in the user's title-block collection directory.
ProjectView::askUserForFilePath() only appended the .qet extension when
FLATPAK_ID/SNAP_NAME were NOT set, on the assumption that the
xdg-desktop-portal file dialog used by sandboxed Snap/Flatpak builds
always appends the selected filter's extension itself (avoiding a
double ".qet.qet"). In practice, on the reporter's Snap/Ubuntu 22.04
setup the portal dialog does not append it, so the environment-based
skip left Save As producing a file with no extension at all.
Portal behavior isn't something QET controls or can reliably detect via
environment variables -- it depends on the desktop's actual portal
implementation/version. Rather than guessing per-environment, normalize
unconditionally: strip any existing .qet suffix (case-insensitive) and
re-append exactly one. This produces the correct single extension
whether or not the dialog already added it, on every environment.
Verified: clean rebuild, only the intended object file recompiled and
linked successfully. Wrote a standalone test of the normalization logic
covering no-extension, already-has-extension, uppercase-extension, and
a literal dot in the base filename -- all four produced exactly one
correct ".qet" suffix with no double-extension and no missing extension.
Not verified: the actual Snap-sandboxed portal dialog behavior itself,
since building/running the Snap package and testing its file-save
dialog under a portal is outside what's practical to set up in this
sandbox. Confidence rests on the fix removing the environment-guessing
entirely in favor of unconditional, dialog-implementation-agnostic
normalization, which is correct regardless of what the underlying
dialog does.
Double-clicking a placed element (or right-click > "Éditer l'élément")
opens its properties via Element::editProperty(), which constructs a
PropertiesEditorDialog with a real parent (QApplication::activeWindow()).
On macOS, a QDialog that has both a parent and Qt::WindowModal set falls
back to Cocoa's automatic sheet presentation. Reports (and this
codebase's own existing workarounds) indicate this can render stuck
centered on screen, non-draggable, with symmetric resize -- rather than
a proper attached, movable window -- unlike Linux/X11 where the same
dialog behaves as a normal draggable QDialog.
Two other dialogs in this codebase already explicitly opt into the
correct macOS sheet presentation for this exact reason:
ElementDialog::setUpWidget() and DiagramPropertiesDialog's setup both do
setWindowModality(Qt::WindowModal);
#ifdef Q_OS_MACOS
setWindowFlags(Qt::Sheet);
#endif
PropertiesEditorDialog -- used for editing Element, QetShapeItem, and
DiagramImageItem properties, all reached via double-click on a diagram
item -- had neither this opt-in nor an opt-out, so it likely fell into
the same automatic-sheet behavior but without the explicit flag,
matching the reported "stuck centered, can't drag" symptom.
Fix: apply the same setWindowModality()/Q_OS_MACOS Qt::Sheet pattern
already used by the other two dialogs, in PropertiesEditorDialog's
constructor -- fixing all three call sites (element, shape, and image
property editing) at once, since they share this one dialog class.
Verified: clean rebuild, all three call sites (element.cpp,
qetshapeitem.cpp, diagramimageitem.cpp) recompiled and linked
successfully with no errors or warnings.
Not verified: the actual reported symptom is macOS/Cocoa-specific
window presentation behavior, which cannot be reproduced or confirmed
fixed in this Linux/Xvfb sandbox -- no macOS environment is available
here. Confidence rests on the exact same fix pattern already being
established and presumably working for two other dialogs in this
codebase for the identical class of problem.
NewElementWizard::createNewElement() creates and show()s a QETElementEditor
for the freshly-created part, but never calls raise()/activateWindow().
On the reporter's macOS setup, the wizard (a modal sheet/child of the main
window) closing right before the new editor is shown apparently leaves the
main window as the active/key window, so the new editor window is created
but stays behind it -- and, being neither key nor frontmost, it also never
surfaces in the Dock's window list or the app's own Windows menu. This
matched the report exactly: the reporter saw the wizard finish with
seemingly no result, when in fact a new part genuinely was created and its
editor genuinely was opened, just hidden from view.
Fix: explicitly raise() and activateWindow() the new editor after show(),
so it becomes the frontmost/key window regardless of what state the wizard
leaves the main window in.
Verified: clean rebuild, only the intended object file recompiled and
linked successfully. Ran the full wizard flow live under Xvfb on Linux
(right-click user collection > "Nouvel élément" > through all 3 steps >
Finish) and confirmed the element editor opens correctly with a blank new
part, with no regression in the flow.
Not verified: the actual reported symptom is macOS-specific window-manager
behavior (key/frontmost window handling, Dock window-list registration),
which cannot be reproduced or confirmed fixed in this Linux/Xvfb sandbox --
no macOS or Wine-with-Cocoa environment is available here. raise()/
activateWindow() are the standard cross-platform Qt calls for this exact
problem and match the pattern already used elsewhere in the codebase
(QETApp::openElementLocations()'s already-open-editor branch), so
confidence rests on that precedent rather than a macOS-side confirmation.
XRefProperties::fromSettings() read the "xrefpos" QSettings key with no
default value. On a fresh install/project, the key doesn't exist yet, so
settings.value(...).toString() returns an empty string. QMetaEnum::keyToValue("")
returns -1 (invalid), which was then cast directly into m_xref_pos as
Qt::AlignmentFlag(-1) -- garbage, despite the class's own default
constructor documenting the intended default as Qt::AlignBottom.
This explains the reported symptom: dynamically generated cross-reference
text for master/slave-linked elements (e.g. magneto-thermal breaker,
thermal relay NC) rendered at an undefined position and overlapped the
element's own label, making the reference unreadable. The reporter's
manual workaround -- explicitly setting alignment to "Bottom" in Project
Properties > New Folio/Cross Referencing -- side-steps the bug precisely
by writing a valid "AlignBottom" value into QSettings, which fromSettings()
then reads back correctly on subsequent loads.
Fix: supply "AlignBottom" as the fallback default for the QSettings read,
matching the constructor's documented default and the reporter's
functioning workaround.
Verified: clean rebuild, only the intended object file recompiled and
linked successfully. Confirmed via a small standalone QMetaEnum test that
keyToValue("") returns -1/invalid while keyToValue("AlignBottom") returns
64 (== Qt::AlignBottom), reproducing the exact mechanism before the fix and
confirming the corrected default resolves to the intended value.
Not verified: a live before/after visual comparison of the rendered
cross-reference text position on an actual magneto-thermal/thermal-relay
diagram (would require constructing a multi-folio project with linked
master/slave elements and comparing label geometry, which was out of
scope for the time available). Confidence rests on the QMetaEnum
mechanism being unambiguous and the fix being a one-line default-value
correction with no other code path affected.
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.
Fix crash changing dynamic text color and confirming with Enter (bugtracker #323)
Works like charm:
- color is updated immediately
- no additional errors or warnings
- no crash anymore!
RotateTextsCommand::undo()/redo() called cti->forceMovedByUser(...)
instead of cti->forceRotateByUser(...) for ConductorTextItem entries
- a copy-paste mix-up between the two parallel user-override flags
that track independently whether a conductor's text was manually
moved vs manually rotated.
Because rotate_by_user_ was never actually set to true, the rotation
attribute-writing gate in Conductor::toXml() (which checks
wasRotatedByUser()) never fired, so a manual rotation applied via
"Orienter les textes" (Edit > Orienter les textes / Ctrl+Space) was
silently dropped on save: the rotation displayed correctly until the
project was closed and reopened, at which point it reverted to
default orientation.
Fix swaps both calls to forceRotateByUser(...), matching what the
constructor reads via wasRotatedByUser() when building m_cond_texts.
Verified: clean rebuild (506/506, no new warnings). Live-verified
under Xvfb that RotateTextsCommand's rotation correctly animates and
applies to ConductorTextItem text (confirmed via the "Orienter les
textes" dialog). A full save/close/reopen round-trip on a from-scratch
two-element wire was attempted but not completed due to unreliable
terminal-to-terminal wire drawing via synthetic mouse events in the
window-manager-less Xvfb sandbox; confidence in the fix instead rests
on tracing the exact save-gate code path (Conductor::toXml() gates
solely on wasRotatedByUser(), which the constructor/undo/redo all
already correctly reference elsewhere for the parallel
moved-by-user flag).
Bugtracker #309: selecting a result from the Search/Replace hit list
highlights the matching element, but on a diagram too large to fit
the current view, the view itself never scrolls -- the highlighted
element can be entirely off-screen with no indication of where it
went. The reporter pinpointed the exact spot,
searchandreplacewidget.cpp:1022, and suggested repositioning the
view's scrollbars.
SearchAndReplaceWidget::on_m_tree_widget_currentItemChanged() already
calls setHighlighted()/setSelected() on the matched element, text, or
conductor when a hit is selected; it just never brings it into view.
Added a call to QGraphicsItem::ensureVisible() alongside each of the
three existing highlight/select calls, so the view scrolls the
minimum needed for the match to be visible.
Followed the same approach as JumpToElementDialog's
activateCurrentItem() (added this session for #676) rather than
computing scrollbar positions by hand as suggested: ensureVisible()
scrolls every view showing the diagram automatically, works correctly
if a folio is open in more than one window, and needed no lookup of
which QGraphicsView the widget is attached to -- this widget doesn't
currently hold one. It was the only existing precedent for this exact
"scroll to reveal a matched item" problem anywhere in the codebase.
Verified live: built and ran the app under Xvfb, zoomed into one
corner of an example diagram until it needed scrollbars, searched for
text appearing in two different elements ("Offset null", both
op-amp offset-null pins), and confirmed selecting each result
scrolled the view to a different part of the diagram, centering the
matched element's highlight circle in the visible area each time. No
new build warnings.
plc-user on PR #693: the crash is fixed, but the color/font field and
the on-diagram text no longer update until you leave the properties
list and click in the diagram -- previously it updated as soon as you
clicked OK.
That's a side effect of the crash fix itself. The old, crashing code
returned a *live* QColorDialog as the item view's editor; clicking its
OK button called accept()/hide() on it, and hiding the active editor
happens to trip the base delegate's own focus-lost commit path -- so
the value applied immediately, racily, as a side effect of the same
mechanism that crashed on Enter. The fix (commit 4bd9b6b21) replaced
that with running the dialog synchronously inside createEditor() and
returning an inert placeholder with the result stashed in a property.
Correct for the crash, but it also removed that accidental commit
trigger: the placeholder never had focus to lose, so nothing tells
the view to read the value back until some unrelated interaction
(clicking away) incidentally triggers it.
Fix: explicitly emit commitData()/closeEditor() for the resolved
editor, deferred via QTimer::singleShot(0, ...) since the view only
registers createEditor()'s return value as "the active editor" after
createEditor() itself returns -- emitting synchronously, before
returning, would target a widget the view doesn't know about yet.
Applied to both font and color, since both share the exact same
"resolve synchronously in createEditor(), return an inert
placeholder" shape and thus the exact same gap; font just hadn't been
reported.
Verified with the same standalone harness from the crash fix (real
QTreeView + DynamicTextItemDelegate + QAbstractItemView::edit()),
this time deliberately *not* sending the synthetic Enter keypress the
crash-fix verification needed: clicks the dialog's real OK button,
lets the event loop run, and confirms the picked color lands in the
model on its own. Also reconfirmed the crash fix itself still holds
(clean exit, no synthetic-Enter needed either way now) and did a full
Release build (504/504) with no new warnings.
Weekly cron replaced with a monthly run (1st of each month, 02:00 UTC)
to reduce unnecessary CI load.
retention-days raised from 14 to 40 across all six artifact uploads
(Qt5 + Qt6 tracks) to cover the new monthly interval with a safety
margin -- 14 days was shorter than the gap between two cron runs,
so the latest build's artifacts could expire before the next one
replaced them.
When multiple elements are pasted or moved in one batch, each call to
autoBreakConductors() now receives the shared state from the previous
call. This prevents two elements in the same batch from independently
claiming the same conductor, which would result in a double-delete on
redo().
Requested by ispyisail in PR review.
openTitleBlockTemplate needs a lambda: its matching overload has a default bool argument, so its pointer-to-member type
requires two parameters regardless of the default, while the signal provides only one -- no cast alone can both resolve the overload and
connect to a single-argument signal.
setAutoNum(QString)/setAutoNum(int,int) is a sender-side signal overload which needed a qOverload<QString> to match setFolioAutonum's
single-argument slot.
etc.), these three are ambiguous on the *slot* side:
activateProject(QETProject*)/activateProject(ProjectView*),
closeProject(ProjectView*)/closeProject(QETProject*), and
showError(const QETResult&)/showError(const QString&) each have two declarations on QETDiagramEditor. &QETDiagramEditor::activateProject
etc. alone won't compile with two candidates present; qOverload<T>() picks the one matching the actual signal's argument type, same as
the old SIGNAL()/SLOT() macro text did implicitly.
Default arguments aren't part of a function's pointer-to-member type, so &Class::slot has a type requiring the argument regardless of its
default value -- incompatible with a signal providing none, and &Class::slot alone won't compile against these signals at all.
When migrating to the modern member pointer connect, replaced with a lambda that calls the slot with no arguments, letting
the default apply exactly as before.
- SelectAutonumW::applyEnable(bool = true), connected to each NumPartEditorW's changed() signal in both setContext() and
on_add_button_clicked(). The corresponding disconnect() in on_remove_button_clicked() is removed rather than reimplemented: a
lambda-based connection can't be matched and removed by a separately-written disconnect() call, and the explicit disconnect
was already redundant -- the very next line deletes the part object, which Qt automatically disconnects on destruction (the same
guarantee setContext()'s own qDeleteAll() cleanup already relies on).
- PartText::adjustItemPosition(int = 0), connected to QTextDocument::contentsChanged().
- ExportDialog::slot_changeFilesExtension(bool = false), connected to ExportPropertiesWidget::formatChanged().
since QComboBox::activated(QString) still exists pre-Qt6 and makes &QComboBox::activated alone ambiguous:
- StyleEditor: outline_color/line_style/size_weight/filling_color,
both connect (activeConnections(true)) and disconnect
(activeConnections(false)) branches. antialiasing's stateChanged(int)
connect modernized alongside them (single signal, no disambiguation
needed).
- TitleBlockTemplateCellWidget: cell_type_input_ (two connects to
different slots), horiz_align_input_, vert_align_input_, logo_input_.
Also modernises QETApp's system tray connect.
In two cases stateChanged already replaced with version guarded checkStateChanged for future proofing.
setTransformOriginPoint() was only applied inside
parentElementRotationChanged(), so loading an already-rotated element,
or enabling keep_visual_rotation while rotation_point_center was
already true, left the origin at (0, 0) until the next parent rotation.
Apply the origin directly in both setters so it's always in sync.
Added .ts files (de and en) to the commit
QComboBox::currentIndexChanged(QString) still exists pre-Qt6.
TitleBlockTemplateLocationChooser: collections_ -> updateTemplates() (a virtual method; pointer-to-member dispatch still resolves to the
TitleBlockTemplateLocationSaver override at runtime as expected)
TitleBlockTemplateLocationSaver: templates_ -> updateNewName()
TitleBlockPropertiesWidget: m_tbt_cb -> changeCurrentTitleBlockTemplate(int)
XRefPropertiesWidget: m_type_cb -> typeChanged(), m_snap_to_cb ->enableOffsetSB(int), both connect (constructor) and disconnect(destructor)
Two QButtonGroup::buttonClicked overload-ambiguity fixes, plus cleanup of the connects sitting alongside them:
TitleBlockDimensionWidget: switched from the deprecated buttonClicked(int) id-based overload to buttonClicked(QAbstractButton*),
disambiguated via qOverload. The slot doesn't use the argument either way, so this is a pure modernization with no behavior change.
ExportPropertiesWidget: same buttonClicked fix for exported_content_choices, plus modernized the adjacent
currentIndexChanged(int) relay (disambiguated via qOverload, since QComboBox::currentIndexChanged(QString) still exists pre-Qt6) and
six QCheckBox::stateChanged(int) relays (single signal, no disambiguation needed).
QCheckBox::stateChanged(int) is deprecated as of Qt 6.7 in favor of checkStateChanged(Qt::CheckState), but this project has no Qt6 minor
version floor pinned in CMakeLists.txt, so stateChanged(int) remains the correct unconditional choice for now. QT_VERSION_CHECK(6, 7, 0) guarded
checkStateChanged was introduced to avoid future warnings.
Bug #331: "Il serait intéressant de pouvoir directement dans la fenêtre
'Sélection numérotation auto' modifier la valeur d'incrément et visualiser
la prochaine numérotation qui sera appliquée. Ceci sans être obligé
d'ouvrir la page de configuration."
The dock (AutoNumberingDockWidget) already let you see and edit a rule's
*current* value inline (added in 52c8ef6b4/031710b5f/ee4ba82d2). The
increment itself, and any preview of where the numbering is headed, was
reachable only through Configurer -> the full project-properties dialog.
Two new widgets per row (conductor/element/folio):
- An increment spin box, read from and written to the same NumerotationContext
field NumPartEditorW's increase_spinBox already edits in the full dialog --
same data, second place to reach it.
- A read-only next-value field, computed via
NumerotationContextCommands::next() -- the identical engine the "Suivant"
button in the full dialog already uses to step a whole context. Reusing it
rather than reimplementing the arithmetic means wrap-and-carry between parts
comes out identical to what actually happens when the number is next
consumed, and zero-padding matches real rendering
(NumerotationContext::formatValue(), mirroring
autonum::setSequentialToList()'s padding rule by hand since that function is
local to assignvariables.cpp).
NumerotationContext gains replaceIncrease(index, increase), a sibling to the
existing replaceValue() that touches only the increment field.
Every refresh call site in the file (13 of them) previously refreshed just the
value field; they now go through a new refreshRow(category), which refreshes
value + increment + next-value-preview together via a small per-row widget
bundle (rowFor()). This also let resetAutoNum()'s three-way switch collapse to
one line, and refreshValueFields()'s three near-identical blocks collapse to a
loop -- both existing before this change, not new here.
Verified live under Xvfb: created an element numbering rule "K" (Chiffre 1,
value 1, increment 1) via the full dialog, confirmed the dock showed
Valeur=1/Incrément=1/Suivant=2. Changed the dock's own Incrément to 3 --
Suivant updated live to 4, no dialog needed. Changed Valeur to 10 -- Suivant
became 13. Reopened the full configuration dialog and confirmed it read back
the same value_field=10/increase_spinBox=3, i.e. the round trip through
replaceIncrease()/storeContext() does not disturb type, initial value, modulus
or format.
Builds clean, CMake/Ninja Release, Qt 5.15, 820/820, no new warnings.
Fixes: https://qelectrotech.org/bugtracker/view.php?id=331
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bugtracker #308: the "current date" preset for a project's default
title block doesn't persist. A later comment on the report pinpointed
it exactly: the setting falls back to "No date" unless the folio tab
remains active when saving settings, and the same happens in Project
Properties.
TitleBlockPropertiesWidget::properties() (and its near-duplicate
sibling propertiesAutoNum(), copy-pasted with the same bug) reads the
date radio buttons like this:
else if (ui->m_current_date_rb->isVisible() && ui->m_current_date_rb->isChecked()) {
prop.useDate = TitleBlockProperties::CurrentDate;
...
Both the New Project settings page and Project Properties embed this
widget as one page of a QTabWidget (NewDiagramPage, in
configpage/configpages.cpp). QWidget::isVisible() depends on the
whole ancestor chain being visible, not just the widget's own state --
switch to any other tab before clicking OK/Apply and this radio
button's isVisible() goes false even though it's still checked
underneath, silently falling through all three branches. The function
returns a default-constructed TitleBlockProperties for the date
fields (useDate = UseDateValue, date = QDate(), i.e. "no date"),
matching exactly what was reported.
Fix: use isHidden() instead, which reflects only this widget's own
explicit state and mirrors the read side's own check in
setProperties()/initDialog() just above it in the same file -- that
side already uses isHidden(), not isVisible(), for the identical
"is the current-date option even offered here" question.
Verified directly: a standalone Qt program constructing the real
NewDiagramPage, checking "current date", switching the tab widget
away from Folio to Conducteur (reproducing the report's exact
trigger), then calling applyConf() and reading back the QSettings
value. Against the original code this saves date="null"; with the
fix, date="now" -- the same scenario, same tab switch, only the one
line differs. Also confirmed a full Release build (504/504, CMake/
Ninja, Qt 5.15.18) with no new warnings.
columns summed to exactly 100% (e.g. the shipped A4_1.titleblock), this produced qRound(NaN), which fatally aborted under Qt6's stricter qCheckedFPConversionToInteger assertion -- reached via
double-clicking a title block template to edit it.
Introduce TitleBlockTemplate::classifyWidthConstraint(), shared by minimumWidth() and maximumWidth(), returning std::optional WidthConstraintCase> to distinguish three non-finite outcomes:
Unconstrained (RTT columns == 100%, no absolute columns -- an ordinary, valid template), RelativeWidthExceeds100Percent (RTT alone exceeds 100%), and AbsoluteColumnsExceedRemainingWidth (RTT == 100%
with at least one absolute column also present) -- the latter two meaning the template's columns cannot be laid out at any width.
maximumWidth() previously only checked "are all columns absolute", which incorrectly reported "no upper bound" for the two unsatisfiable cases above; it now shares the same classification, so both functions
agree.
Update TitleBlockTemplateView::updateDisplayedMinMaxWidth() to show distinct, accurate tooltip text for all four cases instead of printing the old std::numeric_limits<int>::max()
sentinel or a misleading "no constraint" message for an unsatisfiable template.
Manually verified all four cases: a normal template (finite width), A4_1.titleblock (Unconstrained), an over-100% RTT template
(RelativeWidthExceeds100Percent), and RTT==100% with an absolute column present (AbsoluteColumnsExceedRemainingWidth).
Translations still partially missing.
Bugtracker #335: element library icons are black and nearly invisible
under a dark desktop theme (reported on KDE Plasma / Fedora 43).
The main elements panel (ElementsCollectionWidget) already forces a
fixed light palette on its tree views via ElementsTreeView, added in
a8e2a7acf and completed in bb61dde81 -- element icons are rendered
with colors read from each .elmt file (almost always black linework,
matching printed-schematic convention) onto a transparent
background, so any view showing them needs to stay light regardless
of the OS theme. ElementsTreeView's own class doc already says
"This class must be used when the tree view have an
ElementsCollectionModel as model" -- but two other dialogs showing
the exact same model were still using a plain QTreeView and missed
that fix: the Open/Save Element/Category/Template dialog
(ElementDialog) and the New Element Wizard's parent-category picker
(NewElementWizard). Same underlying ElementsCollectionModel, same
black-on-transparent icons, same invisibility on a dark theme.
Fix: use ElementsTreeView in both, matching the main panel and the
class's own documented contract. No other behavior changes --
ElementsTreeView only additionally overrides startDrag() to use a
nicer drag pixmap, which is inert unless drag-out is enabled.
Verified with a full Release build (504/504, no new warnings) and a
standalone Qt program that shows the real ElementDialog under a
forced dark QPalette (simulating a dark OS theme, since neither this
build environment nor QET itself forces the palette one way or the
other): screenshots down through nested collection categories
(Electric > IEC 60617 > Conductors and connecting devices) confirm
the tree view keeps a white background against the dark dialog
chrome around it.
Now a new checkbox in the dynamictextfieldeditor is available (by default set to false for legacy) to make it possible to turn dynamic text fields around its own center.
This resolves an undesirable behaviour that occurs when the text alignment is retained
Including translation to english and german
Bugtracker #323: crash changing a label's color, but only when
confirmed via Enter -- clicking the dialog's own OK button with the
mouse doesn't crash. Reported on Windows 11 and Debian, with
"QObject::installEventFilter(): Cannot filter events for objects in
a different thread" immediately before the segfault.
Root cause: DynamicTextItemDelegate::createEditor()'s color case
constructed a QColorDialog and returned it directly as the item
view's editor widget for the color cell -- unlike every other case
in this same function, which returns a small inline widget
(QSpinBox, QComboBox, or, for the adjacent font case, a plain
placeholder). A QColorDialog is not designed to be used this way: it
is not one of the objectNames this delegate's own eventFilter()
special-cases, so Enter is handled by the base
QStyledItemDelegate::eventFilter() as an ordinary "commit and
destroy this small editor" trigger -- racing the dialog's own
internal OK-button accept/close path, which on Windows can hand off
to the native color picker. Clicking OK with the mouse doesn't go
through the same key-press path, which is why only Enter crashed.
Verified structurally: an embedded QColorDialog editor is a *child*
widget of the view's viewport rather than a proper top-level dialog
(confirmed with a standalone Qt program driving the real delegate
through QAbstractItemView::edit() -- searching QApplication's
top-level widgets never found it, only a search of the viewport's
children did), which is the same "used as something it isn't"
pattern, just observed a different way.
Fix: mirror the font case immediately above -- resolve the color via
the static, blocking QColorDialog::getColor() inside createEditor(),
and hand back a plain QWidget with the result stashed in two
properties (mirroring the font case's "ok" property) for
setModelData() to read. By the time the view processes any commit
trigger, the "editor" is an inert placeholder with no dialog state
left to race.
Verified end-to-end with the same standalone program: creates the
model item, triggers editing, finds the real (top-level, this time)
QColorDialog, clicks its actual OK button, confirms the color lands
on the placeholder's properties, sends the editor a synthetic Enter
keypress (the exact trigger from the bug report), and confirms the
final committed value in the model matches the picked color. Also
confirmed a full Release build (333/333) with no new warnings.
Raised by plc-user in discussion #618: the diagram editor allows moving
elements by as little as 1px, and asked that rotation not undershoot
that. Checking the actual constraint (Settings -> DiagramEditor_xGrid_sb
/ _yGrid_sb, both independently configurable, minimum 1, maximum 30)
turned up a real, verified gap this PR's existing fractional-pivot fix
doesn't cover: an ASYMMETRIC grid (xGrid != yGrid).
Swapping X/Y deltas for a 90-degree turn -- the exact-arithmetic path
already in this file -- only stays on the configured grid if
xGrid == yGrid. With an asymmetric grid, a delta that was a clean
multiple of xGrid lands on the Y axis after the swap, where the grid
unit is yGrid, and one is not generally a multiple of the other.
Verified on a real build (not just derived): two elements at (100,210)
and (150,420), both on-grid under xGrid=10/yGrid=7, selected and
group-rotated 90 degrees via a temporary local CLI harness.
before this change: (242,292) and (32,342) -- off-grid on both axes
after this change: (240,294) and ( 30,343) -- exactly on-grid
Confirmed the same drift is present without this change too (i.e. not
something introduced elsewhere) and that xGrid==yGrid, the common case,
is unaffected: snapping an already-on-grid point is a no-op.
Fix: re-snap the final computed position to Diagram::snapToGrid(), not
just the shared pivot, for the exact-90-degree path. Left the
arbitrary-angle trig fallback alone -- it has no caller today (the
diagram editor only ever passes multiples of 90) and "on-grid" doesn't
have a clean meaning for an arbitrary angle regardless of grid shape.
Does not attempt to fix a separate, pre-existing property surfaced
while testing this: four consecutive 90-degree turns do not reliably
return a selection to its exact starting position, even on a symmetric
grid, because each RotateSelectionCommand recomputes the pivot fresh
from the selection's current sceneBoundingRect(), and an item whose
bounding box isn't rotationally symmetric reports a different box (and
therefore a different centre) at 0 and 90 degrees. Verified this drift
is identical with and without this change, so it is not a regression --
just a different, harder guarantee this change does not attempt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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
Following up on the earlier division-by-zero fix: return -1 from the bad denominator branch of minimumWidth(), matching the "no
constraint" convention maximumWidth() already uses, instead of std::numeric_limits<int>::max() or 0. Update
TitleBlockTemplateView::updateDisplayedMinMaxWidth() to skip the "Longueur minimale" line when minimumWidth() reports -1, mirroring
its existing handling of maximumWidth() == -1.
Rotating a selection around the raw bounding-box centre moved
grid-aligned elements off the grid permanently. sceneBoundingRect()
comes from font metrics and pen widths, so the centre is almost never
a round number: with a pivot of (132.67, 101.11), an element sitting
at x=100 landed at x=133.78, and no further rotation brought it back.
Positions are written with QString::number() (%.6g), which hides the
floating-point noise but keeps the offset, so the diagram ends up
subtly misaligned with no way to repair it from the UI.
Snap the pivot with Diagram::snapToGrid(), which also follows the
user's configured X/Y grid rather than assuming the 10 px default.
Also compute the rotated offset exactly for multiples of 90 degrees
instead of going through qCos()/qSin(). The rotate actions only ever
pass right angles, and qCos(90 deg) is 6.12e-17 rather than 0, so the
trig path added error for no benefit -- four 90 degree steps did not
return a point to where it started. A quadrant is an axis swap, which
is exact; trig is kept as the fallback for any other angle.
With both, four 90 degree rotations of a grid-aligned element return
it exactly to its original position and every intermediate step stays
on the grid.
Reported by plc-user, who hit the same problem rotating graphical
primitives in the Element Editor -- discussion #618.
BorderTitleBlock::slot_setAutoPageNum was removed in 471f876 ("Remove unused signal", 2023-10-17) without noticing autonumberingdockwidget.cpp still referenced it via old-style
SIGNAL()/SLOT() macros, which fail silently at runtime instead of producing a compile error. This has remained broken on master ever since; a fix (57572a2, "Fix two broken signal
connections") exists on the unmerged qt6-cmake-elevatormind-merged branch but that one only commented the lines out without solving the underlying issue.
Removed the dead code entirely and call BorderTitleBlock::importTitleBlock() directly on the active diagram in on_m_folio_cb_activated() instead. The same mechanism is already
used elsewhere in the codebase (undo command, new-diagram creation, XML loading) to push TitleBlockProperties into a diagram and trigger a folio numbering recompute via needFolioData().
27dcd5e renamed BorderTitleBlock::diagramTitleChanged to informationChanged and updated diagram.cpp accordingly, but diagramview.cpp still used the old string-based SIGNAL()/SLOT()
macro referencing the removed signal name, which failed silently at runtime instead of at compile time. The diagram/window title never refreshed after title block changes.
Observed when going through the warnings.
Updated the connect to use informationChanged with modern pointer-to-member syntax, matching diagram.cpp's own connect to the same signal.
removed in Qt6, replaced by mappedInt/mappedWidget/mappedString.
What was broken:
* the logo-conflict rename dialog
* the system tray show/hide toggle
* the Window menu
* export dialog's per-diagram preview controls
Switched to the modern mappedInt/mappedWidget signals with pointer-to-member connect(), guarded for Qt < 5.15 until Qt5 can be dropped.
connect() used the string-based currentIndexChanged(QString) signal, which was removed from QComboBox in Qt6. Selecting a conductor/element/folio auto-numbering context in the
combo box in the project properties dialog never updated the other fields.
Switch to currentTextChanged with the modern pointer-to-member connect() syntax, which also catches signal/slot mismatches at compile time. This is still backward compatible with Qt5 (as long as this is needed).
`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.
RotateSelectionCommand's existing "Pivoter" action (Space) only ever
bumps each selected item's own rotation property -- QGraphicsItem's
setRotation() spins an item around its own local origin and never
touches pos(). Select three elements arranged in a row and rotate:
each spins 90 degrees individually, but the row stays a row. That's
"rotate each item," not "rotate the group."
Add a rotate_as_group parameter to RotateSelectionCommand (default
false, so the existing action and its one call site are unchanged).
When set, it computes a shared pivot once -- the bounding-box center
of the whole selection -- and queues a second, parallel "pos"
QPropertyUndoCommand alongside the existing "rotation" one, rotating
each item's position around that pivot by the same angle.
Scoped the position change to Element/IndependentTextItem/
DiagramImageItem only: these are the only selectable types with
scene-space pos(). ConductorTextItem, DynamicElementTextItem and
ElementTextItemGroup are all parent-relative children (confirmed by
reading their constructors), so when their owning Element is also
selected and gets its own pos() rotated, they're carried along for
free by Qt's normal parent/child transform propagation -- exactly
what the existing "skip rotation if parent is also selected" guard
already assumes for those three cases.
Exposed as a new, separate action ("Pivoter le groupe", Shift+Space)
next to the existing one rather than changing Space's behavior, since
some workflows may rely on the current per-item rotation.
Drawing tools on the diagram canvas always started new shapes and free
text from a fixed hardcoded default (Qt's plain QPen()/QBrush(), and
the static Preferences font) -- changing a shape's color or a text's
font had zero effect on what the next new item of that type got, even
within the same editing session.
Add LastUsedStyle: a small in-memory, session-scoped static helper
(no QSettings, no persistence across restarts -- this is a live "what
did I just use" value, not a new app-wide default). Write side hooks
capture the value right where the properties editors already apply a
change (ShapeGraphicsItemPropertiesWidget::associatedUndo(), both the
live-edit dock path and the modal editProperty() dialog path; and
IndiTextPropertiesWidget::on_m_font_pb_clicked() right after the font
dialog returns). Read side hooks apply the stored value, if any, to a
newly created item: DiagramEventAddShape::mousePressEvent for shapes,
IndependentTextItem's constructor for free text (falling back to the
existing QETApp::indiTextsItemFont() Preferences default otherwise).
Doesn't touch the element/symbol editor's own drawing tools (a
separate subsystem) or add last-used text color (no color control
exists in the text properties UI to originate it from yet).
Slice 1 of discussion #503 (from-to wiring list built on projectDataBase).
Conductor is the one item type on a diagram without a stable identity
of its own -- Element and Diagram both have a uuid, Conductor didn't.
This is the prerequisite the wiring-list tables need: a conductor
table keyed by uuid, the same way the existing element table is keyed
by Element::uuid().
- Conductor gets a QUuid m_uuid, generated in the constructor, with
uuid()/newUuid() accessors mirroring Element's exact pattern.
- toXml()/fromXml() read/write a "uuid" attribute the same way
Element already does, including the same generate-on-missing
fallback (QUuid(e.attribute("uuid", QUuid::createUuid().toString())))
for projects saved before this change.
- PasteDiagramCommand::redo() calls newUuid() on every pasted
conductor (content.conductors(), all three categories), mirroring
the existing per-element newUuid() call right above it -- otherwise
copy-paste would duplicate a conductor's uuid.
Backward compatibility: Conductor::valideXml() doesn't require the
"uuid" attribute, so old files parse unchanged. Verified by opening a
genuinely pre-uuid project (examples/industrial.qet, 150 folios, 671
conductors, legacy integer terminal1/terminal2 references with no
uuid attribute at all) -- loads and renders correctly, gets uuids
assigned on load, and those uuids are stable across a second
load/save cycle (byte-identical uuid values). Verified paste
separately: copying a selection with conductors and pasting produces
distinct new uuids for every pasted conductor, none colliding with
the originals or each other.
On macOS in particular there's currently no way to tell from the window
chrome alone whether the active project has unsaved changes. The main
window's title is set once in the constructor to a static string and
never updated afterward, and QET never sets Qt's windowModified
property anywhere -- so the native "document modified" indicator
(the dot in the close button on macOS; an asterisk in the title on
platforms that render it as text) never appears.
Add QETDiagramEditor::updateWindowModifiedState(), which sets the
window title to "<project>[*] - QElectroTech" (the "[*]" is Qt's own
placeholder convention for this) and calls setWindowModified() with
the active project's own modified flag. Call it from two places:
- subWindowActivated(), the single existing choke point already used
whenever the visible MDI tab changes, so switching projects
immediately reflects the newly active one's own state.
- A new per-project connection to QETProject::projectModified, added
in addProject() alongside the existing undo-stack registration,
filtered to only act when the modified project is the currently
active one.
With no project open, the title and modified flag both revert to the
original static, unmodified state.
Implements https://github.com/qelectrotech/qelectrotech-source-mirror/discussions/596
2026-08-02 07:30:14 +12:00
344 changed files with 119975 additions and 54997 deletions
echo "Old Qt6 .exe and .zip assets deleted (legacy Qt5 assets left untouched)."
- name:Update nightly release
uses:softprops/action-gh-release@v3
@@ -759,8 +482,7 @@ jobs:
> ⚠️ This is a development version; it introduces new features you want, but may cause bugs that have not yet been identified yet.
> For stable releases, see the [Releases page](https://github.com/${{ github.repository }}/releases).
> 🧪 **Qt6 builds are experimental.** Files tagged `-qt6-` are built against Qt6 and are not yet
> as tested as the Qt5 track. Expect rough edges; report issues clearly labelled "Qt6".
> 🗄️ Files without `-qt6-` in the name are the last Qt5 build ever published (frozen, unmaintained). Files tagged `-qt6-` are the actively maintained build.
prerelease:true
make_latest:false
files:|
@@ -769,4 +491,4 @@ jobs:
token:${{ secrets.GITHUB_TOKEN }}
# GitHub Pages is generated and deployed by windows-msi.yml
# after the MSI upload, so that all URLs (exe/zip/msi, Qt5+Qt6) are known.
# after the MSI upload, so that all URLs (exe/zip/msi) are known.
d="M 4 2 L 4 3 L 5 3 L 5 2 L 4 2 z M 11 2 L 11 3 L 12 3 L 12 2 L 11 2 z M 2 4 L 2 12 L 7 12 L 7 10 L 9 10 L 9 12 L 14 12 L 14 4 L 9 4 L 9 6 L 7 6 L 7 4 L 2 4 z M 3 5 L 6 5 L 6 11 L 3 11 L 3 5 z M 10 5 L 13 5 L 13 11 L 10 11 L 10 5 z M 11 6 L 11 10 L 12 10 L 12 6 L 11 6 z M 7 7 L 9 7 L 9 9 L 7 9 L 7 7 z M 4 13 L 4 14 L 5 14 L 5 13 L 4 13 z M 11 13 L 11 14 L 12 14 L 12 13 L 11 13 z "
d="M 4 2 L 4 7 L 6 7 L 6 9 L 4 9 L 4 14 L 12 14 L 12 9 L 10 9 L 10 7 L 12 7 L 12 2 L 4 2 z M 5 3 L 11 3 L 11 6 L 5 6 L 5 3 z M 2 4 L 2 5 L 3 5 L 3 4 L 2 4 z M 13 4 L 13 5 L 14 5 L 14 4 L 13 4 z M 7 7 L 9 7 L 9 9 L 7 9 L 7 7 z M 5 10 L 11 10 L 11 13 L 5 13 L 5 10 z M 2 11 L 2 12 L 3 12 L 3 11 L 2 11 z M 6 11 L 6 12 L 10 12 L 10 11 L 6 11 z M 13 11 L 13 12 L 14 12 L 14 11 L 13 11 z "
<pathstyle="fill:currentColor;fill-opacity:1;stroke:none"d="M 3 3 L 3 5 L 3 6 L 6 6 L 6 5 L 6 3 L 4 3 L 3 3 z M 16 3 L 16 4 L 16 5 L 16 6 L 17 6 L 18 6 L 19 6 L 19 5 L 19 4 L 19 3 L 16 3 z M 4 4 L 5 4 L 5 5 L 4 5 L 4 4 z M 17 4 L 18 4 L 18 5 L 17 5 L 17 4 z M 7 5 L 7 6 L 9 6 L 9 5 L 7 5 z M 10 5 L 10 6 L 12 6 L 12 5 L 10 5 z M 13 5 L 13 6 L 15 6 L 15 5 L 13 5 z M 5 7 L 5 9 L 6 9 L 6 7 L 5 7 z M 16 7 L 16 9 L 17 9 L 17 7 L 16 7 z M 5 10 L 5 12 L 6 12 L 6 10 L 5 10 z M 16 10 L 16 12 L 17 12 L 17 10 L 16 10 z M 5 13 L 5 15 L 6 15 L 6 13 L 5 13 z M 16 13 L 16 15 L 17 15 L 17 13 L 16 13 z M 3 16 L 3 17 L 3 19 L 4 19 L 5 19 L 6 19 L 6 16 L 3 16 z M 7 16 L 7 17 L 9 17 L 9 16 L 7 16 z M 10 16 L 10 17 L 12 17 L 12 16 L 10 16 z M 13 16 L 13 17 L 15 17 L 15 16 L 13 16 z M 16 16 L 16 17 L 16 18 L 16 19 L 17 19 L 18 19 L 19 19 L 19 18 L 19 17 L 19 16 L 17 16 L 16 16 z M 4 17 L 5 17 L 5 18 L 4 18 L 4 17 z M 17 17 L 18 17 L 18 18 L 17 18 L 17 17 z "class="ColorScheme-Text"/>
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.