Compare commits

...

259 Commits

Author SHA1 Message Date
Laurent Trinques 70c665199f Merge pull request #809 from arummler/feature-graphics-primitives-squashed
Add editable resize/rotate/skew handles for shapes and images
2026-09-05 19:13:48 +02:00
Andre Rummler 6b577ee757 Add editable resize/rotate/skew handles for shapes and images
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.
2026-09-05 15:48:28 +02:00
Laurent Trinques 74e878207a Merge pull request #731 from philippeagray/cli-export-hide-terminals
CLI export: hide terminal markers and names in rendered output (add --show-terminals)
2026-09-05 15:14:12 +02:00
plc-user 3e66d403c8 adjust whitespace 2026-09-05 00:22:35 +02:00
plc-user a7fa4dfdcf FIX language-setting
Set language to system-default on start-up, if for
whatever reason the setting in file or registry
for language is empty.
2026-09-04 21:46:23 +02:00
Laurent Trinques b98589d3c7 CI(windows): drop Qt5 build, sign Qt6 MSI, freeze legacy Qt5 nightly assets
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).
2026-09-04 10:57:10 +02:00
Laurent Trinques f997550e07 Merge pull request #805 from Kellermorph/fix-marked-site
Select newly added diagram in project tree
2026-09-04 08:30:41 +02:00
Kellermorph d9f148ab2f fix 2026-09-03 15:05:22 +02:00
Kellermorph 9fdd7b5fc0 Select newly added diagram in project tree 2026-09-02 21:30:10 +02:00
Laurent Trinques 823468826d Merge pull request #803 from ispyisail/feat/lock-element-position
Add position-lock checkbox to element properties (#801)
2026-09-01 21:10:49 +02:00
Laurent Trinques b659c4acad Update translation nl-be, thanks Ronny 2026-09-01 16:11:03 +02:00
ispyisail c69e5747a1 Add position-lock checkbox to element properties (issue #801)
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
2026-09-01 07:16:32 +12:00
Laurent Trinques 899f10533d Merge pull request #799 from ispyisail/fix/element-editor-zoom-clamp
Fix issue #798: element editor crash on scroll-wheel zoom
2026-08-30 21:22:48 +02:00
ispyisail 5b2fdaeb00 ChangeLog: add entry for issue #798 zoom-clamp fix
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdWpDp3TrPKbHnv7YUcNJj
2026-08-31 05:49:05 +12:00
ispyisail 5027ffda9b Apply the same zoom clamp to DiagramView::zoom()
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
2026-08-31 05:47:04 +12:00
ispyisail 3ca5d4ab29 Fix GitHub issue #798: element editor crash on scroll-wheel zoom
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
2026-08-31 05:45:35 +12:00
Laurent Trinques e47dcec7cb Rty to fix icon app on macOS 2026-08-30 12:33:10 +02:00
Laurent Trinques a32aef2246 Fix macOS app icon showing as generic placeholder
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.
2026-08-30 11:55:25 +02:00
Laurent Trinques a5d9cc37e1 Try to fix macOS bundle 2026-08-29 13:10:38 +02:00
Laurent Trinques 0dde1eaf73 Two blockers hit while porting the macOS packaging script (MacQetDeploy_arm64_cmake.sh) from qmake/Qt5 to CMake/Qt6:
**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.
2026-08-29 12:15:36 +02:00
Laurent Trinques e3fe633267 build(cmake): mark macOS executable as MACOSX_BUNDLE
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.
2026-08-29 12:08:53 +02:00
Laurent Trinques 3fa73e945e fix(ui): remove legacy Qt5 font weight tags in autonumberingmanagementw.ui
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.
2026-08-29 11:02:59 +02:00
Laurent Trinques 4038122f99 Merge pull request #797 from arummler/fix-load-empty-project
Fix: avoid crash when opening a project without a folio (diagram).
2026-08-29 10:15:04 +02:00
Laurent Trinques 860a12b970 Merge pull request #796 from Kellermorph/fix-crossref-plc
Fix PLC cross-reference links and make cross-ref column clickable
2026-08-29 10:14:25 +02:00
Andre Rummler 0a98cce8de Fix: avoid crash when opening a project without a folio (diagram). 2026-08-28 19:55:29 +02:00
Kellermorph cceddba44e Fix PLC cross-reference links and make cross-ref column clickable 2026-08-28 12:36:03 +02:00
Laurent Trinques 487b22f483 Merge pull request #769 from Kellermorph/layout-fix
Fix dock widget size/position not being restored on Qt6
2026-08-28 09:25:00 +02:00
plc-user da1d7df662 update German language-files 2026-08-27 20:12:23 +02:00
Laurent Trinques 617619c92c Merge pull request #795 from Kellermorph/fix-templates-qt6
Fix crash when expanding templates/macros tree (Qt6 regression)
2026-08-27 08:14:36 +02:00
Laurent Trinques 61fa130d6d Merge pull request #794 from Kellermorph/german-translation
German Translation
2026-08-27 08:12:43 +02:00
Kellermorph c9899372d3 Fix crash when expanding templates/macros tree (Qt6 regression) 2026-08-27 07:27:25 +02:00
Laurent Trinques e6f1a18844 Merge pull request #793 from jindongjie/master
Improve zh-CN translation
2026-08-27 07:16:52 +02:00
Kellermorph b8b9fa086a fix 2026-08-26 09:49:03 +02:00
Kellermorph 34270a961d German Translation 2026-08-26 09:33:51 +02:00
ar0m 48715051bb Merge pull request #1 from jindongjie/copilot/update-zh-cn-translation 2026-08-26 12:22:42 +08:00
copilot-swe-agent[bot] efab108709 Complete all remaining zh-CN translations
Co-authored-by: jindongjie <141336798+jindongjie@users.noreply.github.com>
2026-08-26 03:24:53 +00:00
copilot-swe-agent[bot] ea6f0f893a Update zh-CN UI translations in qet_zh.ts
Co-authored-by: jindongjie <141336798+jindongjie@users.noreply.github.com>
2026-08-26 03:02:39 +00:00
Laurent Trinques bfe2aa26a0 Merge pull request #786 from ispyisail/fix/remove-dead-exclude-bom-clause
Remove dead exclude_from_bom clause from ElementQueryWidget
2026-08-25 06:52:29 +02:00
Laurent Trinques 4bde81937d Merge pull request #789 from ispyisail/fix/removediagram-safe-teardown
Fix segfault when a project is destroyed with a diagram still pending deleteLater()
2026-08-25 06:50:50 +02:00
Laurent Trinques 91116f7044 Merge pull request #784 from plc-user/master
fix problem with PDF-links in files with brackets in name
2026-08-25 06:47:35 +02:00
Laurent Trinques 8246c8aaba Merge pull request #790 from cezlom/pt_BR-translation-update
Complete the Brazilian Portuguese translation
2026-08-25 06:12:34 +02:00
Cezar Machado 8ca62c4a17 Fix remaining degree sign mistranslations in pt_BR (º → °)
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
2026-08-25 02:06:35 +00:00
Cezar Machado 0394503c0b Complete the Brazilian Portuguese translation
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.
2026-08-24 21:44:16 +00:00
ispyisail e5935c75d1 Fix segfault when a project is destroyed with a diagram still pending deleteLater()
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>
2026-08-25 06:59:02 +12:00
plc-user 2e49f4588a fix problem with PDF-links in files with brackets in name 2026-08-24 18:24:30 +02:00
ispyisail ccf545d30c Remove dead exclude_from_bom clause from ElementQueryWidget's query builder
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>
2026-08-24 22:28:00 +12:00
Laurent Trinques 26d7c03a76 Merge pull request #775 from ispyisail/fix/jump-to-element-shortcutmanager
Register jump-to-element (Ctrl+G) with ShortcutManager
2026-08-24 04:50:07 +02:00
Laurent Trinques bf23967470 Remove name experimental for Qt6packages 2026-08-24 04:47:23 +02:00
Kellermorph 8f6f41ed19 Clean up readSettingsState() in all three editors
- 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())
2026-08-23 18:25:23 +02:00
scorpio810 5e898c4661 Encourage users to switch to and test the Qt6 track
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.
2026-08-23 14:27:43 +02:00
Laurent Trinques 79251c8ed6 Update translations files 2026-08-23 13:51:09 +02:00
Laurent Trinques 03b552c601 Merge pull request #776 from ispyisail/remove/diagramselection-dead-code
Remove dead code: sources/ui/diagramselection.*
2026-08-23 13:50:00 +02:00
Laurent Trinques 1195474f88 Update translations files 2026-08-23 13:44:30 +02:00
plc-user 7bfa2a56df PDF-import: adjust variable-name 2026-08-23 12:31:28 +02:00
plc-user 47558f8b0a load PDFs from "documents" instead of "images", even though PDF page is imported as image 2026-08-23 12:23:39 +02:00
Laurent Trinques e44219778e Make QtPdf detection optional at configure time
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.
2026-08-23 12:05:52 +02:00
ispyisail 8d175754cd Remove dead code: sources/ui/diagramselection.*
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.
2026-08-23 21:59:39 +12:00
ispyisail 9d590eaa6f Register jump-to-element (Ctrl+G) with ShortcutManager
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.
2026-08-23 21:26:00 +12:00
Laurent Trinques 759b197dc2 Merge pull request #770 from Kellermorph/copy-fix
Fix paste breaking existing conductors
2026-08-23 11:07:31 +02:00
Laurent Trinques e542724953 Merge pull request #774 from Kellermorph/fix-plc-warnings
Fix PLC table dangling reference warning and row deletion
2026-08-23 11:05:26 +02:00
Kellermorph 61a160fa62 Fix restoreState() for QETElementEditor and QETTitleBlockTemplateEditor on Qt6
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
2026-08-23 10:37:52 +02:00
Laurent Trinques 8af4bc8eee Update translation to Hungarian, thanks Gábor 2026-08-23 08:40:48 +02:00
Laurent Trinques 45afc7e6aa Merge pull request #772 from Kellermorph/pdf-import
Add PDF page import as image
2026-08-22 19:30:33 +02:00
Kellermorph ff25a77159 Improve PDF import: DPI selection, page preview, Qt5 compat, custom icon
- 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
2026-08-22 18:53:36 +02:00
Kellermorph 4edd4932bb Fix PLC table dangling reference warning and row deletion 2026-08-22 16:42:13 +02:00
Kellermorph baf95338a3 Add PDF page import as image 2026-08-22 14:27:50 +02:00
plc-user 56464629bf FIX compile-warnings 2026-08-22 13:34:29 +02:00
Kellermorph b7f8086166 Fix paste breaking existing conductors 2026-08-22 11:25:47 +02:00
Kellermorph f6afd87522 Fix dock widget size/position not being restored on Qt6 2026-08-22 11:08:02 +02:00
plc-user c536dcf30f Merge pull request #746 from ispyisail/fix/issue413-paste-conductor-underscore-label
Fix pasted conductors getting an unwanted "_" label
2026-08-22 10:53:10 +02:00
plc-user 5db77fe40c Merge pull request #747 from ispyisail/fix/parttext-real-font-size-desync
Fix real_font_size_ desyncing from the actual font in PartText
2026-08-22 10:27:14 +02:00
Laurent Trinques bf816cbc44 Merge pull request #761 from ChuckNr11/master
Change element editor coordinates display
2026-08-22 10:11:40 +02:00
Laurent Trinques 315889baf4 Merge pull request #766 from Kellermorph/plc-fix
PLC terminal names: fix transfer to slaves and display in properties widgets
2026-08-22 10:00:34 +02:00
Kellermorph 2f6fb7808a fix 2026-08-22 09:52:01 +02:00
Laurent Trinques f951c20586 Update snapcraft.yaml 2026-08-22 09:20:31 +02:00
Laurent Trinques 033c3fc112 Fixes the Launchpad Snap build failures on core24 2026-08-22 07:05:12 +02:00
Laurent Trinques 480d2757e7 Snap swapped the deprecated architectures key for platforms, 2026-08-22 06:44:00 +02:00
Laurent Trinques 92db329f40 Merge pull request #768 from Kellermorph/fix-text-editor
Fix text editor toolbar and add text alignment support for Qt6
2026-08-22 05:50:41 +02:00
Kellermorph c488d8437e Fix text editor toolbar and add text alignment support for Qt6 2026-08-21 18:11:14 +02:00
Laurent Trinques 7f7185172f Merge pull request #628 from ispyisail/feature-wiring-db-tables
Add terminal and conductor tables to projectDataBase (discussion #503, slice 2)
2026-08-21 14:03:02 +02:00
Kellermorph dbe2bcb599 PLC terminal names: fix transfer to slaves and display in properties widgets 2026-08-21 12:10:50 +02:00
ispyisail ab159404a3 Give every terminal an identity, not only uuid-aware ones
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>
2026-08-21 20:42:21 +12:00
ispyisail bc79a5df7a Keep a conductor's row in step, and index the columns the view scans
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>
2026-08-21 19:26:40 +12:00
ispyisail 43da912aad Add terminal and conductor tables to projectDataBase
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.
2026-08-21 19:07:49 +12:00
ispyisail 0fdcd1e1e8 Regenerate conductor uuids when a folio is duplicated
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>
2026-08-21 19:07:40 +12:00
Laurent Trinques f172e0740b Merge pull request #760 from ispyisail/s6-shortcut-browsing
Shortcuts page: group by category and make search actually work
2026-08-21 08:38:46 +02:00
Laurent Trinques 5263d78dce Merge pull request #741 from Kellermorph/update-terminal-numbering
Extend terminal numbering dialog with letter numbering and strip selection
2026-08-21 08:37:48 +02:00
Laurent Trinques 265ec12351 Merge pull request #739 from Kellermorph/info-box-pdf
Export component info as invisible PDF text annotations
2026-08-20 23:31:09 +02:00
Kellermorph 4572fca31f fix2 2026-08-20 13:30:22 +02:00
Kellermorph b8df298953 fix 2026-08-20 11:21:22 +02:00
ChuckNr11 9d81a9788f move position of element editor coord display to center of statusbar
for better visibility only
2026-08-19 12:00:43 +02:00
ChuckNr11 2803dcf8b8 Change live cursor-coordinate readout on running ESEvent
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.
2026-08-18 14:19:33 +02:00
Laurent Trinques 5f83679756 Update paths_compilation_installation.cmake 2026-08-17 18:58:11 +02:00
plc-user bb1f457763 remove #include that's already in own header-file 2026-08-17 16:46:49 +02:00
plc-user e2e0df784b fix deprecation-warning about "setContent" with qt >= 6.5 2026-08-17 10:25:44 +02:00
plc-user cfc64dfad0 fix whitespace 2026-08-17 10:23:28 +02:00
Laurent Trinques eb095f9a10 Update start_options.cmake restore -DQET_ALLOW_OVERRIDE_DD_OPTION 2026-08-17 06:57:18 +02:00
ispyisail e9ee8b600b Make the shortcut list browsable and searchable
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>
2026-08-17 12:30:23 +12:00
Laurent Trinques 4b981668ca Update org.qelectrotech.QElectroTech.json 2026-08-16 13:46:57 +02:00
Laurent Trinques 91380b0695 Update org.qelectrotech.QElectroTech.json 2026-08-16 13:39:51 +02:00
Laurent Trinques dc1f0ffa4d Update org.qelectrotech.QElectroTech.json 2026-08-16 13:20:20 +02:00
Laurent Trinques 5b9da15982 Update org.qelectrotech.QElectroTech.json 2026-08-16 11:50:58 +02:00
Laurent Trinques e18d5b1aa0 Update org.qelectrotech.QElectroTech.json 2026-08-16 11:50:33 +02:00
Laurent Trinques f8634a83fc Update org.qelectrotech.QElectroTech.json 2026-08-16 11:46:46 +02:00
Laurent Trinques 99fa186d30 Update snap to Qt6 2026-08-16 08:45:44 +02:00
Laurent Trinques c8ff06459f Flapak update to runtime-version": "6.11" 2026-08-16 08:39:03 +02:00
Laurent Trinques d5c75ad19d Update the Flatpak manifest to Qt6/KF6 and cmake
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
2026-08-16 07:50:13 +02:00
Laurent Trinques 17eac03ca3 Update QVersionNumber to 0, 200, 1 2026-08-16 07:16:33 +02:00
Laurent Trinques e90cb66686 ci(windows-build): change cmake flag for -DQET_EXPORT_PROJECT_DB=ON 2026-08-16 07:10:20 +02:00
Laurent Trinques 610001a847 Enable project database export via CMake option 2026-08-15 21:06:08 +02:00
Laurent Trinques 2419faf931 Merge pull request #707 from ispyisail/fix/conductor-text-rotation-not-saved-bug312
Fix bugtracker #312: wire text rotation not preserved on reload
2026-08-15 17:10:28 +02:00
Laurent Trinques ddbd1d8d75 cmake: fix QET_MIME_PACKAGE_PATH escaping CMAKE_INSTALL_PREFIX
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).
2026-08-15 16:42:10 +02:00
Laurent Trinques afb7442e8a cmake: use vendored SingleApplication submodule when available
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.
2026-08-15 16:17:10 +02:00
Kellermorph 1780fde458 Fix component-info annotation index collision across pages 2026-08-15 11:57:19 +02:00
Laurent Trinques f0b16fe650 Merge pull request #750 from ispyisail/feature/dxf-paint-device
Export the master-side cross-reference table to DXF
2026-08-15 06:19:34 +02:00
ispyisail d988054aca Export the master-side cross-reference table to DXF
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.
2026-08-15 08:39:55 +12:00
Laurent Trinques 1743f342ce Merge pull request #748 from Kellermorph/Fix-Master-Slave
Fix slave contact groups being trimmed to one when reopening element properties
2026-08-14 12:55:12 +02:00
Laurent Trinques 2c69bf0dad Update windows-build.yml
Fix   error: target not found: mingw-w64-ucrt-x86_64-kwidgetsaddons-qt5
error: target not found: mingw-w64-ucrt-x86_64-kcoreaddons-qt5
2026-08-14 11:04:54 +02:00
Laurent Trinques 8bda0a1821 CI(windows): enable KF6 in Qt6 job, fix KF package mismatch in Qt5 job
- 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
2026-08-14 10:43:21 +02:00
Kellermorph fe6191f26d Fix slave contact groups being trimmed to one when reopening element properties 2026-08-14 10:31:15 +02:00
ispyisail 3077527601 Fix real_font_size_ desyncing from the actual font in PartText
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.
2026-08-14 20:21:16 +12:00
Laurent Trinques 8a71649e87 Merge pull request #744 from ispyisail/fix/bugtracker-335-dark-theme-collection-icons
Fix bugtracker #335: element icons invisible on dark themes
2026-08-14 10:02:39 +02:00
Laurent Trinques 46aed59b8b Merge pull request #743 from ispyisail/fix/bugtracker-291-cancel-during-collection-load
Fix bugtracker #291: crash on cancelling open-element dialog before collection load finishes
2026-08-14 09:55:10 +02:00
Kellermorph 5acf201067 fix 2026-08-14 09:43:39 +02:00
ispyisail 4c7d4e7c53 Fix pasted conductors getting an unwanted "_" label
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.
2026-08-14 19:08:21 +12:00
Kellermorph f0348a7cd8 fix 2026-08-14 08:58:36 +02:00
ispyisail 6326cb3789 Fix bugtracker #335: element icons invisible on dark themes
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.
2026-08-14 14:22:01 +12:00
ispyisail 39ac5716c7 Fix crash on cancelling the open-element dialog before collection load finishes
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.
2026-08-14 13:08:45 +12:00
Laurent Trinques f83ed508e3 Merge pull request #705 from arummler/master-update-more-signal-slot
Continued signal/slot migration
2026-08-13 21:40:30 +02:00
Laurent Trinques ab88937ac9 git submodule update --remote elements 2026-08-13 21:35:58 +02:00
plc-user 1cf58e3caf Merge pull request #740 from ispyisail/fix/dxf-export-slave-xref
Export slave cross-reference labels to DXF
2026-08-13 18:40:08 +02:00
plc-user 26f6f6d36e Merge pull request #727 from zi-mozhuang/zh
Fix print window clipping diagram when titleblock on right edge is hidden.

Fixes a frequently made mistake: confusing width and height when rotating something...   😉
2026-08-13 18:04:19 +02:00
plc-user 293dc92b41 Merge pull request #738 from ispyisail/fix/exclude-from-bom-ghost-row
Fix nameless "false" row in the element Informations panel
2026-08-13 17:56:40 +02:00
Andre Rummler 6d05bc2f21 Remove all Qt version checks and branches for <5.15.12 as such versions are no longer supported. 2026-08-13 16:20:30 +02:00
Andre Rummler 7ba295a339 Remove all Qt version checks and branches for <5.14.0 as such versions are no longer supported. 2026-08-13 15:45:15 +02:00
Kellermorph 458c9921c2 Extend terminal numbering dialog with letter numbering and strip selection 2026-08-13 13:51:13 +02:00
Andre Rummler deba8d0e4e Merge branch 'master' into master-update-more-signal-slot 2026-08-13 13:07:53 +02:00
ispyisail 2e1ba46430 Export slave cross-reference labels to DXF
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.
2026-08-13 22:01:37 +12:00
Kellermorph 67b1665b36 Export component info as invisible PDF text annotations 2026-08-13 11:54:33 +02:00
ispyisail 61a509e5d8 Fix nameless "false" row in the element Informations panel
"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.
2026-08-13 21:24:50 +12:00
Laurent Trinques 2186d2733f Merge pull request #709 from arummler/master-continue-qt6-migration
Qt6 migration beyond signal/slot
2026-08-13 10:55:47 +02:00
plc-user b7efbdc323 Merge pull request #726 from ispyisail/fix/bugtracker-333-multiselect-text-color
Fix bugtracker #333: selecting several dynamic texts overwrites their colours
2026-08-13 09:39:36 +02:00
plc-user b00e053dab Merge pull request #725 from ispyisail/fix/pdf-export-filename-multiple-dots-forum3005
Fix PDF export truncating project filenames at the first dot
2026-08-13 09:22:48 +02:00
子墨庄 b0172f4dd7 Change comment style for title block edge method
Updated comment to use Doxygen style for documentation.
2026-08-13 08:50:24 +08:00
Andre Rummler 1de9f7eaf0 The Qt6 CMake signatures for translation handling changed multiple times with minor versions. 2026-08-12 22:37:46 +02:00
philippeagray b72794bea1 CLI export: document --show-terminals in cli_export.h 2026-08-12 10:33:45 -06:00
philippeagray 11357eb504 CLI export: add --show-terminals instead of hardcoding markers off 2026-08-12 10:33:37 -06:00
philippeagray 85346305e5 CLI export: hide terminal markers and names in rendered output
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.
2026-08-12 10:22:51 -06:00
Andre Rummler d2a921ea80 Fixing MacOS only GUI adjustment relying on QWheelEvent->delta() which was removed in Qt6. Some simplification was possible but no test due to lack of OS.
Discovered due to new CI.
2026-08-12 12:29:56 +02:00
Andre Rummler a4d23fe312 QUuid and QHash no longer transititve. Adding includes explicitly. 2026-08-12 12:29:56 +02:00
Andre Rummler 8cfb777fe1 Fixing the translation installation for Qt5. Messed up the order of two lines. 2026-08-12 12:29:56 +02:00
Andre Rummler 8d08c3fd56 Replacing depreciated qAsConst with std::as_const 2026-08-12 12:29:56 +02:00
Andre Rummler 766a6b981d QUuid not transitively included in Qt6; added explicit includes. 2026-08-12 12:29:56 +02:00
Andre Rummler 8b40134a47 sources/qetgraphicsitem/ViewItem/projectdbmodel.cpp
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}.
2026-08-12 12:29:56 +02:00
Andre Rummler cc032c9c76 Fixing comment removing the long dash that AIs tend to use. 2026-08-12 12:29:56 +02:00
Andre Rummler 92b2608bfb Comments in CMake with # and not //... always remember the language you are currently using. 2026-08-12 12:29:56 +02:00
Andre Rummler af87d28e33 Fix several parameters after Qt6 migration. 2026-08-12 12:29:56 +02:00
Andre Rummler c5d714089f Replacing KF5 missed in shell print out. 2026-08-12 12:29:56 +02:00
Shane Ringrose 206c37f620 cmake(qt6): verify Qt6::GuiPrivate at configure time, drop #warning
<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>
2026-08-12 12:29:56 +02:00
Andre Rummler c14d6a6dd6 In order to migrate to Qt6 all options for KF6 were added:
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.
2026-08-12 12:29:56 +02:00
zi-mozhuang e32e5595c0 syn 2026-08-12 15:20:03 +08:00
zi-mozhuang 1684d2cfe6 syn 2026-08-12 15:13:52 +08:00
zi-mozhuang 6f66745ce3 Apply minimal title block fix to zh (from c77f78e4a) 2026-08-12 15:07:52 +08:00
ispyisail 762bd7febf Fix bugtracker #333: selecting several dynamic texts overwrites their colours
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.
2026-08-12 15:53:30 +12:00
ispyisail 162d43e1c9 Fix PDF export truncating project filenames at the first dot
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.
2026-08-12 10:31:04 +12:00
plc-user 10cc162c4e Merge pull request #715 from ispyisail/fix/properties-dialog-mac-sheet-bug275
Fix bugtracker #275: element properties window stuck centered on macOS

That addition definitely makes sense!
2026-08-11 17:33:56 +02:00
plc-user 3a3182862c move "diagnostics_action_" upwards in help-menu 2026-08-11 14:11:00 +02:00
plc-user 0d432f6159 Merge pull request #714 from ispyisail/fix/newpart-wizard-window-focus-bug281
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!
2026-08-11 13:02:22 +02:00
plc-user 45458c2f2a Merge pull request #717 from ispyisail/fix/titleblock-invalid-name-bug251
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!
2026-08-11 12:52:52 +02:00
plc-user 472786fd23 Merge pull request #712 from ispyisail/fix/xref-default-alignment-bug296
Fix bugtracker #296: cross-reference text overlaps element label by default
Sounds and looks reasonable!
2026-08-11 12:47:42 +02:00
plc-user 0732bf64a7 Merge pull request #716 from ispyisail/fix/saveas-qet-extension-bug270
Fix bugtracker #270: Save As on Snap produces file with no .qet extension

Absolutely correct implementation for portability!
2026-08-11 12:42:24 +02:00
ispyisail 2d889568a5 Fix bugtracker #251: title block template with slash in name fails silently
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.
2026-08-11 12:36:31 +12:00
ispyisail 55250a6de9 Fix bugtracker #270: Save As on Snap produces file with no .qet extension
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.
2026-08-11 12:01:24 +12:00
ispyisail 325b895d0b Fix bugtracker #275: element properties window stuck centered on macOS
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.
2026-08-11 11:48:58 +12:00
ispyisail 6ad2a63575 Fix bugtracker #281: new-part wizard's element editor opens behind main window
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.
2026-08-11 11:33:28 +12:00
ispyisail 99dac17327 Fix bugtracker #296: cross-reference text overlaps element label by default
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.
2026-08-11 10:27:52 +12:00
plc-user 13e245b104 Merge pull request #706 from ispyisail/fix/search-hit-scroll-bug309
Scroll the diagram view to the selected search hit (bugtracker #309)
2026-08-10 22:08:31 +02:00
plc-user 6bf6a17dc9 update German translations 2026-08-10 21:33:24 +02:00
plc-user 550e085b82 Merge pull request #693 from ispyisail/fix/color-editor-crash-bug323
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!
2026-08-10 21:24:04 +02:00
Laurent Trinques 9fd951152a Merge pull request #708 from Kellermorph/fix-german-translation
Update German from Folie to Seite
2026-08-10 18:28:38 +02:00
Kellermorph 7debaa5504 Update German from Folie to Seite 2026-08-10 17:55:59 +02:00
ispyisail 6c76b1f6a8 Fix bugtracker #312: wire text rotation not preserved on reload
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).
2026-08-10 22:37:23 +12:00
ispyisail 31ab7538a4 Scroll the diagram view to the selected search hit
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.
2026-08-10 21:19:56 +12:00
ispyisail b0b5345e15 Commit font/color edits immediately instead of waiting for an unrelated click
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.
2026-08-10 20:30:05 +12:00
Andre Rummler c25c400c93 Some of the newly guarded (Qt5 path) signal/slot connects could be still converted to method pointers. Mostly for completeness as they will go away anyhow soon.
Remaining: richtext editor.
2026-08-10 09:08:57 +02:00
plc-user d9638ad746 adjust some German texts 2026-08-09 22:47:13 +02:00
plc-user ea8eeb02c3 adjust some German texts 2026-08-09 21:46:53 +02:00
Laurent Trinques 5e4c423a7d Merge pull request #700 from Kellermorph/update-auto-break
Follow up Auto-break conductors
2026-08-09 21:33:30 +02:00
Laurent Trinques 7f75500023 Merge pull request #694 from IBSYSLevi/feature/center-rotation-option-for-textfields
Feature added: Rotation point center property for dynamic text fields
2026-08-09 21:09:19 +02:00
Laurent Trinques cdf5cad1e5 Merge pull request #698 from arummler/master-modernize-signal-slot
Migrate string based signal/slot to method pointer
2026-08-09 21:08:28 +02:00
plc-user 3a3f23b7a8 swap position of ComboBox and static text in config-page 2026-08-09 20:55:01 +02:00
plc-user 1f7107a7b6 Merge pull request #703 from Kellermorph/fix-plc-warnings
Compile-warnings have been fixed as requested.
2026-08-09 20:36:56 +02:00
Andre Rummler 7cb1e7394e Revert accidental German translation changes 2026-08-09 19:35:55 +02:00
Andre Rummler b91aaacad9 Re-adding connects (migrated) which got lost unfortunately during the migration and repeated merging. 2026-08-09 19:25:56 +02:00
Andre Rummler 1f3c28992c Merge remote-tracking branch 'origin/master' into master-modernize-signal-slot 2026-08-09 19:03:29 +02:00
Kellermorph 5e02111600 Fix-PLC-Warnings 2026-08-09 18:27:20 +02:00
Levi Jetzer a3d348fdcd Updated .ts files to resolve potential merge conflicts 2026-08-09 15:37:46 +02:00
Levi Jetzer c1d892c496 Merge remote-tracking branch 'origin/master' into feature/center-rotation-option-for-textfields
# Conflicts:
#	lang/qet_de.ts
#	lang/qet_en.ts
2026-08-09 15:31:52 +02:00
plc-user 5b33c044c5 Merge pull request #660 from ispyisail/feature-rotate-group
Add "rotate group" to actually rotate a selection as a whole
2026-08-09 12:51:37 +02:00
Andre Rummler c7ed3229d0 Migrating more SLOT() macros. 2026-08-09 12:30:15 +02:00
Andre Rummler 79cd91bd1c Merge branch 'master' into master-modernize-signal-slot 2026-08-09 12:20:46 +02:00
Laurent Trinques b7fc0c79cb Merge pull request #679 from arummler/master-fix-division-by-zero
Fixing minimum width calculation for title block
2026-08-09 12:15:29 +02:00
Andre Rummler fca1e0993b Remaining signal/slot migration and clean-up after the latest merge. 2026-08-09 11:51:23 +02:00
Andre Rummler 9f283322a2 Merge branch 'master' into master-modernize-signal-slot 2026-08-09 11:30:27 +02:00
Laurent Trinques 09983efe05 ci(windows-msi): switch cron to monthly, bump artifact retention 2026-08-09 11:23:13 +02:00
Laurent Trinques 76ff69e912 ci(windows-build): switch cron to monthly, bump artifact retention
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.
2026-08-09 11:19:48 +02:00
Laurent Trinques 779602372a Merge pull request #662 from arummler/master-fix-slot
Fix broken signal/slot relations
2026-08-09 10:55:42 +02:00
Kellermorph d95e744494 autoBreakConductors: share conductors_handled/used_terminals across batch
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.
2026-08-09 09:11:40 +02:00
Laurent Trinques 1317f137c0 Merge pull request #699 from Kellermorph/german-translation
Update German translation
2026-08-09 07:39:52 +02:00
Andre Rummler 201bd4c5f6 Migration of signal/slot to method pointer continued. Mostly simple cases. 2026-08-09 01:34:22 +02:00
Andre Rummler c12137c5a0 Fixing the connect for requestForNewDiagramAt -- typo during on-the-fly migration during merge. 2026-08-09 00:13:51 +02:00
Andre Rummler 5adf61936b Merge branch 'master' into master-modernize-signal-slot 2026-08-08 23:13:42 +02:00
Kellermorph 39b1e836bf Follow up Auto-break conductors 2026-08-08 22:10:47 +02:00
Andre Rummler f076df77da Migrating more signal/slot connects to modern system.
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.
2026-08-08 21:57:53 +02:00
Andre Rummler b3de01d171 Migrating more signal/slot to the new member pointer system. Unlike the earlier signal-side overload fixes (QComboBox/QSpinBox
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.
2026-08-08 21:50:28 +02:00
Andre Rummler 90950075bf Three connects paired a zero-argument signal with a slot that has a default-valued parameter (e.g. void applyEnable(bool = true)).
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().
2026-08-08 21:39:31 +02:00
Andre Rummler a668ccfa90 Migrating the remaining signal/slot connects with an ambigious activated(int) via qOverload<int>,
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.
2026-08-08 21:29:17 +02:00
Kellermorph ad200e28c7 Update German translation 2026-08-08 20:15:07 +02:00
Levi Jetzer 3424ba50bd Fix transform origin not applied on load or delayed activation
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
2026-08-08 20:06:32 +02:00
Andre Rummler 79da321ddc Missed a necessary overload in the previous commit. 2026-08-08 18:55:01 +02:00
Andre Rummler 293e61abeb Modernizes the signal/slot connect and solves the disambiguities of the remaining currentIndexChanged(int) connects via qOverload<int>, since
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)
2026-08-08 18:37:53 +02:00
Andre Rummler 438e2ade3a Modernize and fix buttonClicked connects.
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.
2026-08-08 18:25:08 +02:00
Andre Rummler bfee5b1cdb Merge branch 'master' into master-fix-slot 2026-08-08 15:16:48 +02:00
Laurent Trinques 3eafe840f1 Merge pull request #656 from ispyisail/feature-last-used-style
Remember last-used shape/text style for new items this session
2026-08-08 14:22:52 +02:00
Laurent Trinques e38493c308 Merge pull request #697 from ispyisail/feature/autonum-inline-increment
Edit increment and preview the next number in the auto-numbering dock (bugtracker #331)
2026-08-08 13:53:23 +02:00
Laurent Trinques 5abf890b24 Merge pull request #695 from ispyisail/fix/dark-theme-element-icons-bug335
Fix invisible element icons on dark themes in two dialogs missed by the earlier fix (bugtracker #335)
2026-08-08 13:50:36 +02:00
Laurent Trinques 037d5a13cd Merge pull request #696 from ispyisail/fix/current-date-preset-bug308
Fix "use current date" preset lost unless the Folio tab is active on save (bugtracker #308)
2026-08-08 13:45:48 +02:00
ispyisail cd7388985e Edit increment and preview the next number in the auto-numbering dock
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>
2026-08-08 23:10:54 +12:00
ispyisail 5ba08284f5 Fix "use current date" preset being lost unless the Folio tab is active on save
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.
2026-08-08 23:02:07 +12:00
Andre Rummler 4a95efbfe6 TitleBlockTemplate::minimumWidth() divided by (100.0 - sum(RelativeToTotalLength)) without guarding against a zero or negative denominator. When a template's relative-to-total-length
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.
2026-08-08 12:31:45 +02:00
ispyisail bab5bf58f8 Fix invisible element icons on dark themes in Open/Save Element and New Element Wizard dialogs
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.
2026-08-08 21:53:20 +12:00
Laurent Trinques fca945f90e Merge pull request #624 from ispyisail/feature-window-modified-indicator
Show unsaved-changes state in the main window title (macOS modified dot)
2026-08-08 11:50:46 +02:00
Levi Jetzer 615e40dee2 Feature added: Rotation point center property for dynamic text fields
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
2026-08-08 11:10:36 +02:00
ispyisail 4bd9b6b212 Fix crash changing a dynamic text's color and confirming with Enter
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.
2026-08-08 21:06:05 +12:00
Laurent Trinques d2b2a2eadb Update translations files 2026-08-08 10:23:06 +02:00
Laurent Trinques e5e2f2efbd Merge pull request #673 from Kellermorph/show-terminal-names-export
show terminalnames in export
2026-08-08 10:04:55 +02:00
Laurent Trinques 5c39518921 drop one dead member 2026-08-08 08:14:06 +02:00
Kellermorph 6d2995ad68 set to false 2026-08-08 08:09:45 +02:00
Laurent Trinques 1198cf73a9 Update translations files 2026-08-08 07:23:17 +02:00
Laurent Trinques e8f80697f3 Reapply "Auto-break conductor"
This reverts commit 905afc1bbc.
2026-08-08 07:03:50 +02:00
Andre Rummler 62ad49a6d3 Update old fashioned SIGNAL/SLOT to point-to-member. Only simple and clear cases. 2026-08-08 00:32:31 +02:00
ispyisail 41e83bbc09 Keep group rotation on-grid when X and Y grid sizes differ
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>
2026-08-08 09:40:25 +12:00
plc-user b6e43f78d0 fix whitespace 2026-08-07 22:09:06 +02:00
Laurent Trinques 016bb3636e Merge pull request #685 from qelectrotech/revert-639-Replace-automatic-conductors
Revert "Auto-break conductor"
2026-08-07 16:25:22 +02:00
Kellermorph 57d0b4b5a3 fix whitespace 2026-08-07 13:48:40 +02:00
Andre Rummler 99151b9c04 Report "no constraint" (still needs the translations; to be added in the next translation round I guess) from minimumWidth() for a title template instead of an arbitrary value.
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.
2026-08-07 11:39:08 +02:00
Andre Rummler 43e4603c55 Fixing minimum width calculation for title block which could lead to division by zero and subsequent program crash if opening a template with only relative sized columns. 2026-08-07 07:59:39 +02:00
ispyisail 3cf5cb945e Keep group rotation on the grid
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.
2026-08-07 12:18:17 +12:00
Kellermorph b3a4a41ad9 new Checkbox 2026-08-06 21:37:40 +02:00
Kellermorph 979df376d1 show terminalnames in export 2026-08-06 12:48:11 +02:00
Andre Rummler 484ab9ed87 Fix folio auto-num context not applying to the active diagram
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().
2026-08-05 12:17:49 +02:00
Andre Rummler ee36073428 Fix DiagramView window title not updating (missed rename from 27dcd5e)
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.
2026-08-05 10:54:31 +02:00
Andre Rummler ab61e00cdf Fix QSignalMapper connects silently broken under Qt6 (using the old string based system). QSignalMapper::mapped(int/QWidget*) was deprecated in Qt 5.15 and
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.
2026-08-05 10:29:31 +02:00
Andre Rummler fb51489436 Fix of previous commit: use textActivated signal instead. 2026-08-05 09:39:15 +02:00
Andre Rummler 1216855cfa Fix broken auto-numbering conductor/element/folio selection in project properties
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).
2026-08-04 23:10:18 +02:00
ispyisail 32dd686144 Add "rotate group" to actually rotate a selection as a whole
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.
2026-08-04 18:51:22 +12:00
ispyisail 29ba7c9636 Remember last-used shape/text style for new items this session
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).
2026-08-04 10:32:25 +12:00
ispyisail a3511e8694 Give Conductor its own persisted uuid
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.
2026-08-02 08:12:22 +12:00
ispyisail 76fda3f0e6 Reflect unsaved-changes state in the main window title
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
252 changed files with 52714 additions and 35265 deletions
+30 -369
View File
@@ -2,7 +2,7 @@ name: Windows Build
on:
schedule:
- cron: '0 2 * * 1' # Every Monday at 2:00 UTC
- cron: '0 2 1 * *' #
workflow_dispatch: # Manual trigger available at any time
concurrency:
@@ -11,356 +11,7 @@ concurrency:
jobs:
# =============================================================================
# Job 1: Qt5 build (stable track)
# =============================================================================
build-windows:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
submodules: recursive
fetch-depth: 0
- name: Install MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: UCRT64
update: true
cache: true
install: >-
git
mingw-w64-ucrt-x86_64-ccache
mingw-w64-ucrt-x86_64-gcc
mingw-w64-ucrt-x86_64-cmake
mingw-w64-ucrt-x86_64-ninja
mingw-w64-ucrt-x86_64-qt5-base
mingw-w64-ucrt-x86_64-qt5-svg
mingw-w64-ucrt-x86_64-qt5-tools
mingw-w64-ucrt-x86_64-qt5-translations
mingw-w64-ucrt-x86_64-qt5-pdf
mingw-w64-ucrt-x86_64-sqlite3
mingw-w64-ucrt-x86_64-pkg-config
mingw-w64-ucrt-x86_64-kwidgetsaddons
mingw-w64-ucrt-x86_64-kcoreaddons
mingw-w64-ucrt-x86_64-extra-cmake-modules
mingw-w64-ucrt-x86_64-nsis
mingw-w64-ucrt-x86_64-angleproject
- name: Cache ccache
uses: actions/cache@v5
with:
path: C:\Users\runneradmin\AppData\Local\ccache
key: ccache-windows-${{ github.ref_name }}-${{ github.sha }}
restore-keys: |
ccache-windows-${{ github.ref_name }}-
ccache-windows-
- name: Configure ccache
shell: msys2 {0}
run: |
/ucrt64/bin/ccache --set-config=max_size=500M
/ucrt64/bin/ccache --set-config=compression=true
/ucrt64/bin/ccache -z
echo "=== ccache config ==="
/ucrt64/bin/ccache -p
- name: Patch NSIS Welcome page — fix title font size
shell: msys2 {0}
run: |
set -euo pipefail
WELCOME_NSH=$(find /ucrt64 -path "*/Modern UI 2/Pages/Welcome.nsh" | head -1)
if [ -z "$WELCOME_NSH" ]; then
echo "WARNING: Welcome.nsh not found, skipping font patch"
else
echo "Patching: $WELCOME_NSH"
sed -i '/WelcomePage\.Title\.Font/s/"[0-9]\+" "700"/"10" "700"/' "$WELCOME_NSH"
grep 'WelcomePage.Title.Font' "$WELCOME_NSH"
echo " OK font size patched to 10"
fi
FINISH_NSH=$(find /ucrt64 -path "*/Modern UI 2/Pages/Finish.nsh" | head -1)
if [ -z "$FINISH_NSH" ]; then
echo "WARNING: Finish.nsh not found, skipping font patch"
else
echo "Patching: $FINISH_NSH"
sed -i '/FinishPage\.Title\.Font/s/"[0-9]\+" "700"/"10" "700"/' "$FINISH_NSH"
grep 'FinishPage.Title.Font' "$FINISH_NSH"
echo " OK font size patched to 10"
fi
- name: Force Qt5 — remove Qt6 cmake + tools
shell: msys2 {0}
run: |
set -euo pipefail
rm -rf /ucrt64/lib/cmake/Qt6
pacman -R --noconfirm mingw-w64-ucrt-x86_64-qt6-tools 2>/dev/null || true
echo "=== windeployqt binaries ==="
ls /ucrt64/bin/windeployqt* || echo "NO windeployqt found!"
- name: Build with cmake
shell: msys2 {0}
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE"
mkdir build && cd build
NPROC=$(nproc)
echo "Available CPUs: $NPROC"
cmake -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH=/ucrt64 \
-DQt5_DIR=/ucrt64/lib/cmake/Qt5 \
-DQT_VERSION_MAJOR=5 \
-DCMAKE_DISABLE_FIND_PACKAGE_Qt6=ON \
-DBUILD_TESTING=OFF \
-DCMAKE_POLICY_DEFAULT_CMP0077=NEW \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DCMAKE_CXX_FLAGS="-DQET_EXPORT_PROJECT_DB" \
-DCMAKE_C_COMPILER_LAUNCHER=/ucrt64/bin/ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=/ucrt64/bin/ccache \
-DSQLite3_INCLUDE_DIR=/ucrt64/include \
-DSQLite3_LIBRARY=/ucrt64/lib/libsqlite3.dll.a \
..
ninja -j"$NPROC"
- name: Show ccache stats
shell: msys2 {0}
run: |
echo "=== ccache statistics ==="
/ucrt64/bin/ccache -s
- name: Verify exe was built
shell: msys2 {0}
run: |
set -euo pipefail
EXE=$(find "$GITHUB_WORKSPACE/build" -maxdepth 3 -iname "qelectrotech.exe" | head -1)
if [ -z "$EXE" ]; then
echo "ERROR: no qelectrotech.exe found in build/"
find "$GITHUB_WORKSPACE/build" -maxdepth 3 -name "*.exe" || true
exit 1
fi
SIZE=$(stat -c%s "$EXE")
echo "Exe found: $EXE ($SIZE bytes)"
[ "$SIZE" -gt 100000 ] || { echo "ERROR: exe too small"; exit 1; }
- name: Deploy — copy exe + windeployqt + DLLs
shell: msys2 {0}
run: |
set -euo pipefail
NSIS_ROOT="$GITHUB_WORKSPACE/nsis_root"
FILES="$NSIS_ROOT/files"
BIN="$FILES/bin"
mkdir -p "$BIN"
EXE=$(find "$GITHUB_WORKSPACE/build" -maxdepth 3 -iname "qelectrotech.exe" | head -1)
echo "Copying exe: $EXE -> $BIN/QElectroTech.exe"
cp "$EXE" "$BIN/QElectroTech.exe"
cd "$BIN"
/ucrt64/bin/windeployqt-qt5 \
--release \
--no-translations \
--no-compiler-runtime \
./QElectroTech.exe || true
echo "=== 3-pass transitive DLL scan ==="
set +e
for PASS in 1 2 3; do
echo "-- Pass $PASS --"
for bin_file in "$BIN"/*.dll "$BIN"/*.exe "$BIN"/sqldrivers/*.dll "$BIN"/platforms/*.dll "$BIN"/imageformats/*.dll; do
[ -f "$bin_file" ] || continue
while IFS= read -r line; do
dll_path=$(echo "$line" | awk '{print $3}')
[ -f "$dll_path" ] || continue
dll_name=$(basename "$dll_path")
dst="$BIN/$dll_name"
if [ ! -f "$dst" ]; then
cp "$dll_path" "$dst"
echo " Copied (pass $PASS): $dll_name"
fi
done < <(ldd "$bin_file" 2>/dev/null | grep -i '/ucrt64/bin/')
done
done
set -e
DLL_COUNT=$(find "$BIN" -name "*.dll" | wc -l)
echo "=== $DLL_COUNT DLLs present after scan ==="
ls -lh "$BIN/QElectroTech.exe" || { echo "ERROR: exe missing from bin/"; exit 1; }
[ "$DLL_COUNT" -gt 5 ] || { echo "ERROR: too few DLLs"; exit 1; }
cd "$GITHUB_WORKSPACE"
cp /ucrt64/bin/libgcc_s_seh-1.dll "$BIN/"
cp /ucrt64/bin/libstdc++-6.dll "$BIN/"
cp /ucrt64/bin/libwinpthread-1.dll "$BIN/"
SQLITE=$(find /ucrt64/bin -name "libsqlite3*.dll" | head -1)
if [ -n "$SQLITE" ]; then
cp "$SQLITE" "$BIN/"
echo "SQLite3 copied: $(basename $SQLITE)"
else
echo "WARNING: libsqlite3 not found in /ucrt64/bin/"
fi
cp "$GITHUB_WORKSPACE/build-aux/windows/QET64.nsi" "$NSIS_ROOT/"
cp "$GITHUB_WORKSPACE/build-aux/windows/lang_extra.nsh" "$NSIS_ROOT/"
cp "$GITHUB_WORKSPACE/build-aux/windows/lang_extra_fr.nsh" "$NSIS_ROOT/"
cp "$GITHUB_WORKSPACE/build-aux/windows/lang_extra_missing.nsh" "$NSIS_ROOT/"
cp -r "$GITHUB_WORKSPACE/build-aux/windows/nsis_base/." "$NSIS_ROOT/"
curl -fsSL \
"https://raw.githubusercontent.com/qelectrotech/qelectrotech-source-mirror/refs/heads/master/misc/Lancer%20QET.bat" \
-o "$FILES/Lancer QET.bat"
cp -r "$GITHUB_WORKSPACE/elements" "$FILES/elements" || true
cp -r "$GITHUB_WORKSPACE/titleblocks" "$FILES/titleblocks" || true
cp -r "$GITHUB_WORKSPACE/examples" "$FILES/examples" || true
cp -r "$GITHUB_WORKSPACE/fonts" "$FILES/fonts" || true
cp -r "$GITHUB_WORKSPACE/lang" "$FILES/lang" || true
find "$GITHUB_WORKSPACE/build" -name "*.qm" -exec cp {} "$FILES/lang/" \; 2>/dev/null || true
echo "=== .qm files in files/lang/ ==="
ls "$FILES/lang/"*.qm 2>/dev/null | wc -l || echo "0 .qm files"
for f in LICENSE ChangeLog CREDIT README ELEMENTS.LICENSE; do
cp "$GITHUB_WORKSPACE/$f" "$FILES/$f" 2>/dev/null || true
done
echo "=== Verification of key files in files/ ==="
for f in LICENSE ChangeLog CREDIT README ELEMENTS.LICENSE \
qet_uninstall_file_associations.reg register_filetypes.bat "Lancer QET.bat"; do
[ -f "$FILES/$f" ] \
&& echo " OK : $f" \
|| echo " MISSING: $f"
done
for d in ico elements lang titleblocks fonts examples bin; do
[ -d "$FILES/$d" ] \
&& echo " OK : $d/" \
|| echo " MISSING: $d/"
done
- name: Extract version for installer name
shell: msys2 {0}
id: qet_version
run: |
set -euo pipefail
GITCOMMIT=$(git -C "$GITHUB_WORKSPACE" rev-parse --short HEAD)
A=$(git -C "$GITHUB_WORKSPACE" rev-list HEAD --count)
HEAD=$(( A + 473 ))
VERSION=$(grep 'return QVersionNumber{' "$GITHUB_WORKSPACE/sources/qetversion.cpp" \
| head -1 \
| awk -F '{' '{ print $2 }' \
| awk -F '}' '{ print $1 }' \
| sed -e 's/,/./g' -e 's/ //g')
[ -z "$VERSION" ] && VERSION="dev"
FULL_VERSION="${VERSION}-r${HEAD}-${GITCOMMIT}_x86_64-win64"
echo "version=$FULL_VERSION" >> "$GITHUB_OUTPUT"
echo "base_version=$VERSION" >> "$GITHUB_OUTPUT"
echo "gitcommit=$GITCOMMIT" >> "$GITHUB_OUTPUT"
echo "head=$HEAD" >> "$GITHUB_OUTPUT"
echo "VERSION : $VERSION"
echo "GITCOMMIT : $GITCOMMIT"
echo "HEAD (rev) : $HEAD"
echo "FULL : $FULL_VERSION"
- name: Patch QET64.nsi — version + exe name + absolute paths
shell: msys2 {0}
run: |
set -euo pipefail
VERSION="${{ steps.qet_version.outputs.version }}"
NSI="$GITHUB_WORKSPACE/nsis_root/QET64.nsi"
FILES_WIN=$(cygpath -w "$GITHUB_WORKSPACE/nsis_root/files")
SCRIPT="$GITHUB_WORKSPACE/build-aux/windows/patch_nsi.py"
python3 "$SCRIPT" "$NSI" "$VERSION" "$FILES_WIN"
echo "=== Verification ==="
grep 'SOFT_VERSION' "$NSI" | head -1
grep -m2 'nsis_root' "$NSI" | head -2
echo "=== Contents of nsis_root/files/ ==="
ls "$GITHUB_WORKSPACE/nsis_root/files/"
- name: Build NSIS installer
shell: msys2 {0}
run: |
set -euo pipefail
NSIS_ROOT="$GITHUB_WORKSPACE/nsis_root"
cd "$NSIS_ROOT"
echo "=== CWD : $(pwd) ==="
MSYS2_ARG_CONV_EXCL="*" makensis /V4 QET64.nsi
RC=$?
echo "=== Contents of nsis_root after makensis ==="
ls "$NSIS_ROOT/"
[ $RC -eq 0 ] || { echo "ERROR: makensis failed (exit $RC)"; exit 1; }
- name: Move installer to dist/
shell: msys2 {0}
run: |
set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/dist"
INSTALLER=$(find "$GITHUB_WORKSPACE/nsis_root" -maxdepth 1 -iname "installer_*.exe" | head -1)
if [ -z "$INSTALLER" ]; then
echo "ERROR: no installer .exe found in nsis_root/"
ls "$GITHUB_WORKSPACE/nsis_root/"
exit 1
fi
echo "Moving: $INSTALLER -> dist/"
mv "$INSTALLER" "$GITHUB_WORKSPACE/dist/"
- name: Upload build logs on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: build-logs
path: |
build/CMakeFiles/*.log
nsis_root/files/bin/
if-no-files-found: warn
- name: Zip portable (readytouse)
id: zip_portable
shell: pwsh
run: |
$version = "${{ steps.qet_version.outputs.base_version }}"
$head = "${{ steps.qet_version.outputs.head }}"
$zipName = "qelectrotech-${version}+git${head}-x86-win64-readytouse.zip"
$src = "$env:GITHUB_WORKSPACE\nsis_root\files"
$dst = "$env:GITHUB_WORKSPACE\dist\$zipName"
$7z = "C:\Program Files\7-Zip\7z.exe"
New-Item -ItemType Directory -Force -Path "$env:GITHUB_WORKSPACE\dist" | Out-Null
& $7z a -tzip -mx=5 -mmt=on $dst "$src\*"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$sizeMB = [math]::Round((Get-Item $dst).Length / 1MB, 1)
Write-Output "ZIP created: $zipName ($sizeMB MB)"
"zip_name=$zipName" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
- name: Upload portable (files/ without installer)
uses: actions/upload-artifact@v7
with:
name: qelectrotech-${{ steps.qet_version.outputs.base_version }}+git${{ steps.qet_version.outputs.head }}-x86-win64-readytouse
path: dist/${{ steps.zip_portable.outputs.zip_name }}
retention-days: 14
- name: Upload NSIS installer
uses: actions/upload-artifact@v7
with:
name: qelectrotech-windows-installer
path: dist/Installer_*.exe
retention-days: 14
- name: Upload portable (nom fixe pour le workflow MSI)
uses: actions/upload-artifact@v7
with:
name: qelectrotech-windows-portable
path: nsis_root/files/
retention-days: 14
# =============================================================================
# Job 2: Qt6 build (EXPERIMENTAL track)
# Job 1: Windows build (Qt6/KF6 — sole track since the Qt5 track was removed)
#
# Version label: the C++ source (sources/qetversion.cpp) intentionally stays
# Qt-agnostic — QT_VERSION-based detection inside the binary proved unreliable
@@ -368,6 +19,9 @@ jobs:
# certainty which Qt major version it configured (-DQT_VERSION_MAJOR=6), so it
# is safe to label the *package* "0.200.1" at this level, without touching
# QetVersion::currentVersion() or making the binary self-detect its Qt build.
#
# Job id kept as "build-windows-qt6" (not renamed to "build-windows") in case
# branch protection / required status checks reference this exact job name.
# =============================================================================
build-windows-qt6:
runs-on: windows-latest
@@ -397,6 +51,9 @@ jobs:
mingw-w64-ucrt-x86_64-qt6-pdf
mingw-w64-ucrt-x86_64-sqlite3
mingw-w64-ucrt-x86_64-pkg-config
mingw-w64-ucrt-x86_64-kwidgetsaddons
mingw-w64-ucrt-x86_64-kcoreaddons
mingw-w64-ucrt-x86_64-extra-cmake-modules
mingw-w64-ucrt-x86_64-nsis
mingw-w64-ucrt-x86_64-angleproject
@@ -456,11 +113,12 @@ jobs:
-DCMAKE_PREFIX_PATH=/ucrt64 \
-DQt6_DIR=/ucrt64/lib/cmake/Qt6 \
-DQT_VERSION_MAJOR=6 \
-DBUILD_WITH_KF5=OFF \
-DBUILD_WITH_KF=ON \
-DBUILD_KF=OFF \
-DPACKAGE_TESTS=OFF \
-DCMAKE_POLICY_DEFAULT_CMP0077=NEW \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DCMAKE_CXX_FLAGS="-DQET_EXPORT_PROJECT_DB" \
-DQET_EXPORT_PROJECT_DB=ON \
-DCMAKE_C_COMPILER_LAUNCHER=/ucrt64/bin/ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=/ucrt64/bin/ccache \
-DSQLite3_INCLUDE_DIR=/ucrt64/include \
@@ -594,8 +252,8 @@ jobs:
# Deliberately NOT parsed from sources/qetversion.cpp: the CI job
# already knows for certain it configured Qt6 (-DQT_VERSION_MAJOR=6),
# so the "0.200.1" experimental-track label is set here explicitly
# rather than relying on in-binary Qt version detection.
# so the "0.200.1" version label is set here explicitly rather than
# relying on in-binary Qt version detection.
VERSION="0.200.1"
FULL_VERSION="${VERSION}-qt6-r${HEAD}-${GITCOMMIT}_x86_64-win64"
@@ -687,60 +345,64 @@ jobs:
with:
name: qelectrotech-${{ steps.qet_version.outputs.base_version }}+git${{ steps.qet_version.outputs.head }}-x86-win64-readytouse
path: dist/${{ steps.zip_portable.outputs.zip_name }}
retention-days: 14
retention-days: 40
- name: Upload NSIS installer
uses: actions/upload-artifact@v7
with:
name: qelectrotech-windows-installer-qt6
path: dist/Installer_*.exe
retention-days: 14
retention-days: 40
- name: Upload portable (nom fixe pour le workflow MSI)
uses: actions/upload-artifact@v7
with:
name: qelectrotech-windows-portable-qt6
path: nsis_root/files/
retention-days: 14
retention-days: 40
# ---------------------------------------------------------------------------
# Job 3 : Publie les assets nightly (exe + zip, Qt5 et Qt6) sur la release
# Job 2 : Publie les assets nightly (exe + zip) sur la release
# Ne tourne que sur push master (pas sur les PRs)
# ---------------------------------------------------------------------------
publish-nightly-assets:
needs: [build-windows, build-windows-qt6]
needs: [build-windows-qt6]
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
permissions:
contents: write
steps:
- name: Download installer artifacts (Qt5 + Qt6)
- name: Download installer artifact
uses: actions/download-artifact@v8
with:
pattern: qelectrotech-windows-installer*
path: downloaded/installer/
merge-multiple: true
- name: Download portable artifacts (Qt5 + Qt6)
- name: Download portable artifact
uses: actions/download-artifact@v8
with:
pattern: qelectrotech-*-readytouse
path: downloaded/portable/
merge-multiple: true
- name: Delete old nightly assets (.exe and .zip)
# Only Qt6-tagged assets are deleted/replaced here. Assets without "qt6"
# in the name are the frozen Qt5 legacy build (last one ever published,
# before the Qt5 CI job was removed) — deliberately left untouched so
# they stay downloadable indefinitely instead of disappearing.
- name: Delete old nightly Qt6 assets (.exe and .zip)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
run: |
gh release view nightly --repo "$REPO" --json assets \
--jq '.assets[] | select(.name | test("\\.(exe|zip)$")) | .name' \
--jq '.assets[] | select(.name | test("qt6")) | select(.name | test("\\.(exe|zip)$")) | .name' \
| while read -r name; do
echo "Deleting old asset: $name"
echo "Deleting old Qt6 asset: $name"
gh release delete-asset nightly "$name" --repo "$REPO" --yes
done
echo "Old .exe and .zip assets deleted."
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 +421,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 +430,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.
+44 -49
View File
@@ -25,18 +25,17 @@ jobs:
strategy:
fail-fast: false
# Single-entry matrix kept on purpose (rather than flattening the job):
# matrix.flavor is used as part of the MSI ProductCode seed, so changing
# it would generate a new ProductCode and break upgrade detection for
# existing installs. Keeping "qt6" here preserves continuity.
matrix:
include:
- flavor: qt5
portable_artifact: qelectrotech-windows-portable
version_source: qetversion # parsed from sources/qetversion.cpp
label_suffix: ""
experimental: false
- flavor: qt6
portable_artifact: qelectrotech-windows-portable-qt6
version_source: hardcoded # see note in "Extract version" step
label_suffix: "-qt6-EXPERIMENTAL"
experimental: true
label_suffix: "-qt6"
experimental: false
permissions:
contents: write
@@ -44,7 +43,6 @@ jobs:
id-token: write # Required by SignPath
outputs:
qt5_msi: ${{ steps.export.outputs.msi_name_qt5 }}
qt6_msi: ${{ steps.export.outputs.msi_name_qt6 }}
steps:
@@ -72,14 +70,12 @@ jobs:
# ----------------------------------------------------------------
# 3. Extract version
# Qt5: parsed from sources/qetversion.cpp (single source of truth
# for the software's own reported version).
# Qt6: hardcoded "0.200.1" here — deliberately NOT parsed from the
# binary/source, since Qt-version self-detection inside the
# compiled code proved unreliable (see commit e1aa65f). The CI
# matrix entry already knows for certain which flavor it is
# packaging, so the experimental-track label is set explicitly
# at the packaging level instead.
# packaging, so the version label is set explicitly at the
# packaging level instead.
# ----------------------------------------------------------------
- name: Extract version
id: version
@@ -125,10 +121,9 @@ jobs:
Write-Host "Version MSI : $verMsi"
Write-Host "Version display : $verDisplay"
# Qt platform argument: only Qt5 needs the QTBUG-83161 font-rendering
# workaround (GDI backend). Qt6 uses DirectWrite by default and does
# not need it. Computed once here so both the bundled "Lancer QET.bat"
# and the MSI shortcuts (wix build -d QtPlatformArgs=...) stay in sync.
# Qt platform argument: kept from the Qt5 era (QTBUG-83161
# font-rendering workaround, GDI backend). Qt6 uses DirectWrite by
# default and does not need it, so this always resolves empty now.
if ("${{ matrix.flavor }}" -eq "qt5") {
$qtArgs = "-platform windows:fontengine=freetype"
} else {
@@ -228,10 +223,10 @@ jobs:
Write-Host "Lancer QET.bat replaced for MSI installation (qtArgs: '$qtArgs')."
# ----------------------------------------------------------------
# 9. Build the MSI (unsigned)
# Qt6 (experimental) uses a distinct ProductCode seed so it never
# collides with / upgrades over the Qt5 MSI — they must be able to
# coexist as clearly separate installs.
# 9. Build the MSI (unsigned at this stage — signing happens below)
# Qt6 keeps its own ProductCode seed (distinct from the retired Qt5
# MSI), so a machine that still has the old Qt5 MSI installed gets a
# separate, coexisting install rather than an unexpected upgrade.
# ----------------------------------------------------------------
- name: Build MSI
shell: pwsh
@@ -289,12 +284,13 @@ jobs:
retention-days: 1
if-no-files-found: error
# Qt6 stays on the unsigned/experimental track (no signing request for
# this flavor), and forks never have the SignPath secrets, so guard on
# both: only qt5, and only in the upstream repo.
# Qt6 is now signed too (previously excluded while Qt5 was the stable
# track and Qt6 was experimental-only). The remaining guard is the fork
# check: forks never have the SignPath secrets, and a fork-originated
# PR/run must never attempt a signing request.
# (cf. DieterMayerOSS:fix/msi-signing-fork-guard, d3f60c88)
- name: Sign MSI via SignPath
if: matrix.flavor == 'qt5' && github.repository == 'qelectrotech/qelectrotech-source-mirror'
if: github.repository == 'qelectrotech/qelectrotech-source-mirror'
uses: signpath/github-action-submit-signing-request@v2
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
@@ -311,18 +307,17 @@ jobs:
with:
name: qelectrotech-windows-msi-${{ matrix.flavor }}
path: dist\*.msi
retention-days: 14
retention-days: 40
if-no-files-found: error
- name: Delete old nightly .msi asset for this flavor
- name: Delete old nightly .msi asset
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
run: |
$pattern = if ("${{ matrix.flavor }}" -eq "qt6") { "-qt6-EXPERIMENTAL.*\\.msi$" } else { "\\.msi$" }
$pattern = "-qt6.*\\.msi$"
$names = gh release view nightly --repo $env:REPO --json assets --jq ".assets[] | select(.name | test(`"$pattern`")) | .name"
foreach ($name in ($names -split "`n" | Where-Object { $_ })) {
if ("${{ matrix.flavor }}" -eq "qt5" -and $name -match "-qt6-EXPERIMENTAL") { continue }
Write-Host "Deleting old asset: $name"
gh release delete-asset nightly $name --repo $env:REPO --yes
}
@@ -342,11 +337,7 @@ jobs:
shell: pwsh
run: |
$name = "$env:MSI_NAME"
if ("${{ matrix.flavor }}" -eq "qt6") {
echo "msi_name_qt6=$name" >> $env:GITHUB_OUTPUT
} else {
echo "msi_name_qt5=$name" >> $env:GITHUB_OUTPUT
}
echo "msi_name_qt6=$name" >> $env:GITHUB_OUTPUT
- name: Summary
if: always()
@@ -354,11 +345,11 @@ jobs:
run: |
Write-Host "=== MSI build summary (${{ matrix.flavor }}) ==="
Write-Host "Version : ${{ steps.version.outputs.VERSION_DISPLAY }}"
Write-Host "Experimental : ${{ matrix.experimental }}"
Write-Host "Signed : true"
# ---------------------------------------------------------------------------
# Job 2 : Génère et déploie la page GitHub Pages une fois les DEUX MSI
# (Qt5 + Qt6) publiés, pour que toutes les URLs soient connues.
# Job 2 : Génère et déploie la page GitHub Pages une fois le MSI publié,
# pour que toutes les URLs soient connues.
# ---------------------------------------------------------------------------
deploy-pages:
needs: build-msi
@@ -388,13 +379,17 @@ jobs:
ASSETS=$(gh release view nightly --repo "$REPO" --json assets --jq '.assets[].name')
EXE_NAME=$(echo "$ASSETS" | grep -v 'qt6' | grep '\.exe$' | head -1)
ZIP_NAME=$(echo "$ASSETS" | grep -v 'qt6' | grep '\.zip$' | head -1)
MSI_NAME=$(echo "$ASSETS" | grep -v 'qt6' | grep '\.msi$' | head -1 || echo "")
EXE_NAME=$(echo "$ASSETS" | grep 'qt6' | grep '\.exe$' | head -1)
ZIP_NAME=$(echo "$ASSETS" | grep 'qt6' | grep '\.zip$' | head -1)
MSI_NAME=$(echo "$ASSETS" | grep 'qt6' | grep '\.msi$' | head -1 || echo "")
EXE_QT6_NAME=$(echo "$ASSETS" | grep 'qt6' | grep '\.exe$' | head -1 || echo "")
ZIP_QT6_NAME=$(echo "$ASSETS" | grep 'qt6' | grep '\.zip$' | head -1 || echo "")
MSI_QT6_NAME=$(echo "$ASSETS" | grep 'qt6' | grep '\.msi$' | head -1 || echo "")
# Legacy Qt5 assets (no "qt6" in the name): last ever published,
# frozen — windows-build.yml/windows-msi.yml no longer delete or
# replace these, so they keep pointing at the same files release
# after release. Rendered as a separate "legacy" section if present.
LEGACY_EXE_NAME=$(echo "$ASSETS" | grep -v 'qt6' | grep '\.exe$' | head -1 || echo "")
LEGACY_ZIP_NAME=$(echo "$ASSETS" | grep -v 'qt6' | grep '\.zip$' | head -1 || echo "")
LEGACY_MSI_NAME=$(echo "$ASSETS" | grep -v 'qt6' | grep '\.msi$' | head -1 || echo "")
BASE="https://github.com/$REPO/releases/download/nightly"
INSTALLER_URL="$BASE/$EXE_NAME"
@@ -402,12 +397,12 @@ jobs:
MSI_URL=""
[ -n "$MSI_NAME" ] && MSI_URL="$BASE/$MSI_NAME"
INSTALLER_QT6_URL=""
PORTABLE_QT6_URL=""
MSI_QT6_URL=""
[ -n "$EXE_QT6_NAME" ] && INSTALLER_QT6_URL="$BASE/$EXE_QT6_NAME"
[ -n "$ZIP_QT6_NAME" ] && PORTABLE_QT6_URL="$BASE/$ZIP_QT6_NAME"
[ -n "$MSI_QT6_NAME" ] && MSI_QT6_URL="$BASE/$MSI_QT6_NAME"
LEGACY_INSTALLER_URL=""
LEGACY_PORTABLE_URL=""
LEGACY_MSI_URL=""
[ -n "$LEGACY_EXE_NAME" ] && LEGACY_INSTALLER_URL="$BASE/$LEGACY_EXE_NAME"
[ -n "$LEGACY_ZIP_NAME" ] && LEGACY_PORTABLE_URL="$BASE/$LEGACY_ZIP_NAME"
[ -n "$LEGACY_MSI_NAME" ] && LEGACY_MSI_URL="$BASE/$LEGACY_MSI_NAME"
SHA="${{ github.event.workflow_run.head_sha || github.sha }}"
SHORT="${SHA:0:7}"
@@ -417,7 +412,7 @@ jobs:
export DATE SHORT REPO SHA RUN_URL RUN_NUMBER
export INSTALLER_URL PORTABLE_URL MSI_URL
export INSTALLER_QT6_URL PORTABLE_QT6_URL MSI_QT6_URL
export LEGACY_INSTALLER_URL LEGACY_PORTABLE_URL LEGACY_MSI_URL
python3 source/build-aux/generate-page.py
+138 -31
View File
@@ -48,19 +48,11 @@ endif()
# INTERFACE_QT_MAJOR_VERSION mismatch at generate time.
set(QT_DEFAULT_MAJOR_VERSION ${QT_VERSION_MAJOR} CACHE STRING "Qt version to use (5 or 6)" FORCE)
# Add sub directories
option(PACKAGE_TESTS "Build the tests" ON)
if(PACKAGE_TESTS)
message("Add sub directory tests")
add_subdirectory(tests)
endif()
include(cmake/paths_compilation_installation.cmake)
include(cmake/start_options.cmake)
include(cmake/developer_options.cmake)
include(cmake/git_update_submodules.cmake)
include(cmake/git_last_commit_sha.cmake)
include(cmake/fetch_kdeaddons.cmake)
include(cmake/fetch_singleapplication.cmake)
include(cmake/fetch_pugixml.cmake)
include(cmake/qet_compilation_vars.cmake)
@@ -78,12 +70,43 @@ find_package(
${QET_COMPONENTS}
REQUIRED)
# Qt6 only creates the Qt::GuiPrivate target (used for QPdfEngine::drawHyperlink)
# when the GuiPrivate component is explicitly requested. Qt5 has no such
# component package and creates the target implicitly with Gui, so only
# request it on Qt6 - requesting it on Qt5 fails the whole configure.
# <private/qpdf_p.h> (QPdfEngine::drawHyperlink) needs Qt's private GUI module.
# Qt >= 6.7 ships it as a proper find_package component, but some distro
# packages (e.g. Ubuntu's qt6-base-private-dev) omit Qt6GuiPrivateConfig.cmake
# and only provide the implicit Qt6::GuiPrivate target created alongside
# Qt6::Gui. Try the component quietly, then verify the target below so a
# missing private-headers package fails here instead of at compile time.
# Qt5 has no such component as its GuiPrivate target always exists once Gui is found.
if(QT_VERSION_MAJOR GREATER_EQUAL 6)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS GuiPrivate)
find_package(Qt6 QUIET COMPONENTS GuiPrivate)
endif()
if(QT_VERSION_MAJOR GREATER_EQUAL 6 AND NOT TARGET Qt6::GuiPrivate)
message(FATAL_ERROR
"Qt6::GuiPrivate was not found. It is required for PDF hyperlink "
"support (<private/qpdf_p.h>). Install the Qt6 private headers "
"(e.g. 'qt6-base-private-dev' on Debian/Ubuntu) or use a Qt build "
"that provides the GuiPrivate component.")
endif()
# PDF page import (toolbar "Ajouter un PDF") needs the QtPdf module and,
# specifically, QPdfDocument::pagePointSize() which only exists since Qt 6.4.
# Unlike GuiPrivate above, a missing QtPdf module is NOT fatal here: some
# Qt6 distributions (e.g. the Flatpak org.kde.Platform runtime) don't ship
# it at all, since it lives in the qtwebengine source tree rather than Qt6
# core. When it's missing, or too old, the feature is silently disabled -
# see the QT_VERSION_CHECK / QET_HAS_QTPDF guards in diagrameventaddpdf.*
# and pdfpagesdialog.*.
set(QET_HAS_QTPDF FALSE)
if(QT_VERSION_MAJOR GREATER_EQUAL 6)
find_package(Qt6 QUIET COMPONENTS Pdf)
if(TARGET Qt6::Pdf AND NOT Qt6_VERSION VERSION_LESS 6.4.0)
set(QET_HAS_QTPDF TRUE)
list(APPEND QET_PRIVATE_LIBRARIES Qt::Pdf)
add_compile_definitions(QET_HAS_QTPDF)
else()
message(STATUS "QtPdf module not available (or Qt < 6.4): PDF page import feature disabled")
endif()
endif()
find_package(SQLite3 REQUIRED)
@@ -100,25 +123,19 @@ endif()
set(CMAKE_AUTOUIC_SEARCH_PATHS ${QET_DIR}/sources/ui)
# The default build only compiles the tracked .ts files to .qm (lrelease).
# Refreshing the .ts from the sources (lupdate) is a developer action behind
# the explicit "update_translations" target below: running lupdate on every
# build rewrote tracked files as a side effect, and under high parallelism
# lupdate rewriting a .ts while lrelease read the same file made the build
# fail with "Premature end of document".
set_source_files_properties(${TS_FILES} PROPERTIES OUTPUT_LOCATION "${QET_DIR}/lang")
if(QT_VERSION_MAJOR EQUAL 6)
qt6_add_translation(QM_FILES ${TS_FILES})
set(KF_MAJOR_VERSION 6)
else()
qt5_add_translation(QM_FILES ${TS_FILES})
set(KF_MAJOR_VERSION 5)
endif()
include(cmake/fetch_kdeaddons.cmake)
add_custom_target(update_translations
COMMAND $<TARGET_FILE:Qt${QT_VERSION_MAJOR}::lupdate> ${CMAKE_SOURCE_DIR}/sources -ts ${TS_FILES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Updating .ts files from sources/ (lupdate) - developer target, run explicitly"
VERBATIM
)
# Add sub directories
option(PACKAGE_TESTS "Build the tests" ON)
if(PACKAGE_TESTS)
message("Add sub directory tests")
add_subdirectory(tests)
endif()
# als laatse
include(cmake/define_definitions.cmake)
@@ -145,6 +162,94 @@ else()
)
endif()
if(APPLE)
set_target_properties(${PROJECT_NAME} PROPERTIES MACOSX_BUNDLE TRUE)
endif()
# The default build only compiles the tracked .ts files to .qm (lrelease).
# Refreshing the .ts from the sources (lupdate) is a developer action behind
# the explicit "update_translations" target below: running lupdate on every
# build rewrote tracked files as a side effect, and under high parallelism
# lupdate rewriting a .ts while lrelease read the same file made the build
# fail with "Premature end of document".
set_source_files_properties(
${TS_FILES}
PROPERTIES OUTPUT_LOCATION "${QET_DIR}/lang"
)
if(QT_VERSION_MAJOR EQUAL 6)
if(Qt6_VERSION VERSION_LESS "6.2")
# Qt 6.06.1
qt6_add_translation(QM_FILES ${TS_FILES})
# qt6_add_translation() only creates custom commands. Something must
# depend on their outputs for them to run during the default build.
add_custom_target(${PROJECT_NAME}_lrelease ALL
DEPENDS ${QM_FILES}
)
add_custom_target(update_translations
COMMAND
$<TARGET_FILE:Qt6::lupdate>
"${CMAKE_SOURCE_DIR}/sources"
-ts ${TS_FILES}
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
COMMENT
"Updating .ts files from sources/ (lupdate) - developer target"
VERBATIM
)
elseif(Qt6_VERSION VERSION_LESS "6.7")
# Qt 6.26.6: old target-based signature
qt_add_lrelease(
${PROJECT_NAME}
TS_FILES ${TS_FILES}
QM_FILES_OUTPUT_VARIABLE QM_FILES
)
# Automatically creates the update_translations umbrella target.
qt_add_lupdate(
${PROJECT_NAME}
TS_FILES ${TS_FILES}
)
else()
# Qt 6.7+: new signature
qt_add_lrelease(
TS_FILES ${TS_FILES}
LRELEASE_TARGET ${PROJECT_NAME}_lrelease
QM_FILES_OUTPUT_VARIABLE QM_FILES
)
qt_add_lupdate(
SOURCE_TARGETS ${PROJECT_NAME}
TS_FILES ${TS_FILES}
LUPDATE_TARGET update_translations
NO_GLOBAL_TARGET
)
endif()
else()
# Qt 5
qt5_add_translation(QM_FILES ${TS_FILES})
# Likewise, qt5_add_translation() needs a target depending on its outputs,
# unless QM_FILES are already consumed elsewhere.
add_custom_target(${PROJECT_NAME}_lrelease ALL
DEPENDS ${QM_FILES}
)
add_custom_target(update_translations
COMMAND
$<TARGET_FILE:Qt5::lupdate>
"${CMAKE_SOURCE_DIR}/sources"
-ts ${TS_FILES}
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
COMMENT
"Updating .ts files from sources/ (lupdate) - developer target"
VERBATIM
)
endif()
# Optional precompiled headers -- see QET_ENABLE_PCH in
# cmake/developer_options.cmake for what this trades away.
#
@@ -180,7 +285,7 @@ target_link_libraries(
pugixml::pugixml
SingleApplication::SingleApplication
SQLite3::SQLite3
${KF5_PRIVATE_LIBRARIES}
${KF_PRIVATE_LIBRARIES}
${QET_PRIVATE_LIBRARIES}
)
@@ -221,7 +326,7 @@ target_include_directories(
${QET_DIR}/sources/svg
)
if(NOT BUILD_WITH_KF5)
if(NOT BUILD_WITH_KF)
target_include_directories(
${PROJECT_NAME}
PRIVATE
@@ -229,7 +334,9 @@ if(NOT BUILD_WITH_KF5)
)
endif()
install(TARGETS ${PROJECT_NAME})
install(TARGETS ${PROJECT_NAME}
BUNDLE DESTINATION .
)
if (NOT MINGW)
install(DIRECTORY ico/breeze-icons/16x16 DESTINATION ${QET_ICONS_PATH})
+1
View File
@@ -20,6 +20,7 @@ All notable changes to QElectroTech are documented here.
### 🐛 Bug Fixes
- Fix #798: clamp element-editor and diagram-view zoom to prevent view-transform overflow crash on scroll-wheel zoom ([3ca5d4a](../../commit/3ca5d4ab2))
- Fix(windows-msi): inject rev into MSI Version Build field ([e19f523](../../commit/e19f5232277efb37435cb65a83563d73333d62ec))
- Fix #391: use wide-char path for pugixml on Windows to handle Unicode paths ([31edf30](../../commit/31edf30c619213368e9b592b51be6ca8190db831))
- Fix(#283): restore center alignment when loading table config ([f55ba56](../../commit/f55ba568f68293e06436899bcc831431e9d27295))
@@ -1,7 +1,7 @@
{
"id": "org.qelectrotech.QElectroTech",
"runtime": "org.kde.Platform",
"runtime-version": "5.15-25.08",
"runtime-version": "6.11",
"sdk": "org.kde.Sdk",
"command": "qelectrotech",
"rename-desktop-file": "org.qelectrotech.qelectrotech.desktop",
@@ -30,7 +30,20 @@
"pypi-dependencies.json",
{
"name": "qelectrotech",
"buildsystem": "qmake",
"//qt6-private-headers-note": "qt6-base-private-dev has no Flatpak build-depends equivalent — private Qt6 headers ship inside org.kde.Sdk itself. If the cmake build fails looking for QtCore/private/*.h, the SDK/runtime branch is mismatched with the source tree, not a missing package.",
"//sqlite-driver-note": "libqt6sql6-sqlite / libsqlite3-dev have no Flatpak build-depends equivalent either. QET_EXPORT_PROJECT_DB=ON assumes the KDE runtime ships the Qt6 SQLite plugin (libqsqlite.so) already built into QtSql — verified at Debian packaging time this needed an explicit runtime dep there (see debian/control). Test the in-app database export after building; if it silently fails, the runtime's QtSql plugin set needs checking, not a package to add here.",
"buildsystem": "cmake",
"config-opts": [
"-DCMAKE_INSTALL_PREFIX=/app",
"-DQT_VERSION_MAJOR=6",
"-DBUILD_WITH_KF=ON",
"-DBUILD_KF=OFF",
"-DPACKAGE_TESTS=OFF",
"-DBUILD_PUGIXML=ON",
"-DFETCHCONTENT_FULLY_DISCONNECTED=ON",
"-DFETCHCONTENT_SOURCE_DIR_PUGIXML=/run/build/qelectrotech/pugixml",
"-DQET_EXPORT_PROJECT_DB=ON"
],
"post-install": [
"mv ${FLATPAK_DEST}/share/mime/packages/qelectrotech.xml ${FLATPAK_DEST}/share/mime/packages/org.qelectrotech.QElectroTech.xml"
],
@@ -38,12 +51,6 @@
{
"type": "dir",
"path": "../.."
},
{
"type": "patch",
"paths": [
"patches/fix-the-installation-paths.patch"
]
}
]
}
+42 -33
View File
@@ -2,11 +2,19 @@
"""
generate-page.py — Generates gh-pages/index.html for QElectroTech nightly builds.
Called from windows-msi.yml deploy-pages job.
Environment variables required:
DATE, SHORT, REPO, SHA, RUN_URL, RUN_NUMBER,
INSTALLER_URL, PORTABLE_URL, MSI_URL (optional)
Optional (Qt6 experimental track — omitted entirely if empty):
INSTALLER_QT6_URL, PORTABLE_QT6_URL, MSI_QT6_URL
Optional (frozen Qt5 legacy build — omitted entirely if empty):
LEGACY_INSTALLER_URL, LEGACY_PORTABLE_URL, LEGACY_MSI_URL
NOTE: Windows Qt5 CI build removed (Qt6 is now the sole track built and
signed going forward). INSTALLER_URL / PORTABLE_URL / MSI_URL point to the
Qt6 build artifacts. The LEGACY_* variables, when set, point to the last
Qt5 nightly assets that were ever published — kept downloadable but frozen
(windows-build.yml no longer regenerates or deletes them).
"""
import os
@@ -20,9 +28,9 @@ installer_url = os.environ.get("INSTALLER_URL", "")
portable_url = os.environ.get("PORTABLE_URL", "")
msi_url = os.environ.get("MSI_URL", "")
installer_qt6_url = os.environ.get("INSTALLER_QT6_URL", "")
portable_qt6_url = os.environ.get("PORTABLE_QT6_URL", "")
msi_qt6_url = os.environ.get("MSI_QT6_URL", "")
legacy_installer_url = os.environ.get("LEGACY_INSTALLER_URL", "")
legacy_portable_url = os.environ.get("LEGACY_PORTABLE_URL", "")
legacy_msi_url = os.environ.get("LEGACY_MSI_URL", "")
msi_block = ""
if msi_url:
@@ -32,43 +40,44 @@ if msi_url:
<span class="btn-text">Windows Installer .msi<small>.msi &mdash; for enterprise / GPO deployment</small></span>
</a>"""
# Qt6 experimental section — only rendered if at least one Qt6 asset exists.
qt6_block = ""
if installer_qt6_url or portable_qt6_url or msi_qt6_url:
qt6_msi_btn = ""
if msi_qt6_url:
qt6_msi_btn = f"""
<a class="btn btn-msi" href="{msi_qt6_url}">
# Legacy Qt5 section — only rendered if at least one legacy asset exists.
legacy_block = ""
if legacy_installer_url or legacy_portable_url or legacy_msi_url:
legacy_installer_btn = ""
if legacy_installer_url:
legacy_installer_btn = f"""
<a class="btn btn-secondary" href="{legacy_installer_url}">
<span class="btn-icon">&#11015;</span>
<span class="btn-text">Windows Installer .msi (Qt6)<small>.msi &mdash; experimental, for enterprise / GPO deployment</small></span>
<span class="btn-text">Windows Installer (Qt5, legacy)<small>.exe &mdash; frozen, no longer updated</small></span>
</a>"""
qt6_installer_btn = ""
if installer_qt6_url:
qt6_installer_btn = f"""
<a class="btn btn-primary" href="{installer_qt6_url}">
legacy_msi_btn = ""
if legacy_msi_url:
legacy_msi_btn = f"""
<a class="btn btn-secondary" href="{legacy_msi_url}">
<span class="btn-icon">&#11015;</span>
<span class="btn-text">Windows Installer (Qt6)<small>.exe &mdash; experimental, includes all dependencies</small></span>
<span class="btn-text">Windows Installer .msi (Qt5, legacy)<small>.msi &mdash; frozen, no longer updated</small></span>
</a>"""
qt6_portable_btn = ""
if portable_qt6_url:
qt6_portable_btn = f"""
<a class="btn btn-secondary" href="{portable_qt6_url}">
legacy_portable_btn = ""
if legacy_portable_url:
legacy_portable_btn = f"""
<a class="btn btn-secondary" href="{legacy_portable_url}">
<span class="btn-icon">&#128230;</span>
<span class="btn-text">Windows Portable (Qt6)<small>.zip &mdash; experimental, no installation required</small></span>
<span class="btn-text">Windows Portable (Qt5, legacy)<small>.zip &mdash; frozen, no longer updated</small></span>
</a>"""
qt6_block = f"""
legacy_block = f"""
<div class="card">
<h2>&#129514; Windows &mdash; x86_64 &mdash; Qt6 track</h2>
<h2>&#128451; Windows &mdash; x86_64 &mdash; Qt5 (legacy, unmaintained)</h2>
<div class="warning">
&#9888;&#65039; <strong>Experimental.</strong> These Qt6-based installer, portable and MSI
builds are new and not as thoroughly tested as the Qt5 track above. Expect rough
edges; please report issues and mention &quot;Qt6&quot; explicitly.
&#128451; <strong>Legacy build &mdash; frozen, no longer updated.</strong> This is the
last Qt5 build published before Windows CI switched to Qt6 only. Kept available for
anyone who still needs it, but it will not receive further fixes or security updates.
Please migrate to the Qt6 build above when you can.
</div>
<div class="downloads">
{qt6_installer_btn}
{qt6_msi_btn}
{qt6_portable_btn}
{legacy_installer_btn}
{legacy_msi_btn}
{legacy_portable_btn}
</div>
</div>"""
@@ -126,7 +135,7 @@ For production use, download a <a href="https://github.com/{repo}/releases">stab
</div>
</div>
<div class="card">
<h2>&#127993; Windows &mdash; x86_64 &mdash; Qt5 track</h2>
<h2>&#127993; Windows &mdash; x86_64</h2>
<div class="downloads">
<a class="btn btn-primary" href="{installer_url}">
<span class="btn-icon">&#11015;</span>
@@ -143,7 +152,7 @@ For production use, download a <a href="https://github.com/{repo}/releases">stab
</a>
</div>
</div>
{qt6_block}
{legacy_block}
</main>
<footer>
Auto-generated by GitHub Actions &nbsp;&middot;&nbsp;
+40 -37
View File
@@ -1,48 +1,39 @@
name: qelectrotech
title: QElectroTech
base: core22
base: core24
adopt-info: qelectrotech
license: GPL-2.0
summary: Electrical diagram editor
description: |
QElectroTech, or QET in short, is a libre and open source desktop application
QElectroTech, or QET in short, is a libre and open source desktop application
to create diagrams and schematics.
grade: stable
confinement: strict
compression: lzo
architectures:
- build-on: amd64
build-for: amd64
- build-on: arm64
build-for: arm64
layout:
/usr/local/share/qelectrotech:
symlink: $SNAP/usr/local/share/qelectrotech
platforms:
amd64:
arm64:
apps:
qelectrotech:
command: usr/local/bin/qelectrotech
command: usr/bin/qelectrotech
common-id: qelectrotech.desktop
extensions:
- kde-neon
extensions:
- kde-neon-6
plugs: &plugs [opengl, unity7, home, removable-media, gsettings, network, cups-control, wayland, x11]
environment: &env
TCL_LIBRARY: $SNAP/usr/share/tcltk/tcl8.6
HOME: $SNAP_USER_COMMON
PYTHONPATH: $SNAP:$SNAP/lib/python3.10/site-packages:$SNAP/usr/lib/python3.10:$SNAP/usr/lib/python3.10/lib-dynload
PYTHONPATH: $SNAP:$SNAP/lib/python3.12/site-packages:$SNAP/usr/lib/python3.12:$SNAP/usr/lib/python3.12/lib-dynload
qet-tb-generator:
command: bin/qet_tb_generator
extensions:
- kde-neon
extensions:
- kde-neon-6
plugs: *plugs
environment: *env
parts:
launchers:
plugin: dump
@@ -55,7 +46,7 @@ parts:
source: https://github.com/raulroda/qet_tb_generator-plugin.git
source-tag: v1.31
python-packages: [PySimpleGUI]
stage-packages:
stage-packages:
- python3-lxml
- python3-tk
- libtk8.6
@@ -63,16 +54,17 @@ parts:
kde-sdk-setup:
plugin: nil
build-snaps:
- kf5-5-110-qt-5-15-11-core22-sdk
- kf6-core24-sdk
- kde-qt6-core24-sdk
build-packages:
- g++
- mesa-common-dev
- libglvnd-dev
- rsync
override-build: |
rsync -a --ignore-existing /snap/kf5-5-110-qt-5-15-11-core22-sdk/current/ /
rsync -a --ignore-existing /snap/kf6-core24-sdk/current/ /
rsync -a --ignore-existing /snap/kde-qt6-core24-sdk/current/ /
qelectrotech:
after: [kde-sdk-setup]
plugin: nil
@@ -81,37 +73,48 @@ parts:
- git
- sqlite3
- xdg-user-dirs
# libxcb-cursor0 was added as a hard dependency of the Qt5 xcb platform
# plugin in Qt 5.15.x. The kf5-5-110-qt-5-15-11-core22 content snap does
# not bundle it, so dlopen() of libqxcb.so fails on Ubuntu 24.04 hosts
# with "found but could not load" (issue #373). Staging the 22.04 build
# of this library satisfies the dep without changing base or Qt version.
# libxcb-cursor0 workaround was needed against the Qt5/KF5 core22 content
# snap (issue #373). Not yet re-verified against kf6-core24 — remove this
# if the Qt6 xcb platform plugin loads cleanly without it.
- libxcb-cursor0
build-packages:
- git
- cmake
- ninja-build
- libsqlite3-dev
- qt6-tools-dev
- qt6-base-private-dev
- pkgconf
override-build: |
displayed_version=$(cat sources/qetversion.cpp | grep "return QVersionNumber{"| head -n 1| awk -F "{" '{ print $2 }' | awk -F "}" '{ print $1 }' | sed -e 's/,/./g' -e 's/ //g')
snap_version="${displayed_version}-g$(git rev-parse --short=8 HEAD)"
modified_displayed_version="${snap_version}.snap"
sed -i -E "s|const QString displayedVersion =.*|const QString displayedVersion =\"$modified_displayed_version\";|" sources/qet.h
craftctl set version="$snap_version"
qmake "$CRAFT_PART_SRC/qelectrotech.pro"
make -j${CRAFT_PARALLEL_BUILD_COUNT}
make install INSTALL_ROOT="$CRAFT_PART_INSTALL"
cmake -G Ninja -B build -S "$CRAFT_PART_SRC" \
-DCMAKE_INSTALL_PREFIX=/usr \
-DCMAKE_BUILD_TYPE=Release \
-DQT_VERSION_MAJOR=6 \
-DBUILD_WITH_KF=ON \
-DBUILD_KF=OFF \
-DPACKAGE_TESTS=OFF \
-DBUILD_PUGIXML=ON \
-DQET_EXPORT_PROJECT_DB=ON
ninja -C build -j${CRAFT_PARALLEL_BUILD_COUNT}
DESTDIR="$CRAFT_PART_INSTALL" ninja -C build install
override-stage: |
craftctl default
# patch desktop file with correct icon path
SED_CMD="sed -i -E s|^Icon=(.*)|Icon=\${SNAP}/usr/local/share/icons/hicolor/128x128/apps/\1.png|g"
$SED_CMD usr/local/share/applications/org.qelectrotech.qelectrotech.desktop
SED_CMD="sed -i -E s|^Icon=(.*)|Icon=\${SNAP}/usr/share/icons/hicolor/128x128/apps/\1.png|g"
$SED_CMD usr/share/applications/org.qelectrotech.qelectrotech.desktop
cleanup:
after: [qelectrotech, qet-tb-generator]
plugin: nil
build-snaps: [kf5-5-110-qt-5-15-11-core22]
build-snaps: [kf6-core24]
override-prime: |
set -eux
for snap in "kf5-5-110-qt-5-15-11-core22"; do # List all content-snaps you're using here
for snap in "kf6-core24"; do # List all content-snaps you're using here
cd "/snap/$snap/current" && find . -type f,l -exec rm -f "$CRAFT_PRIME/{}" "$CRAFT_PRIME/usr/{}" \;
done
for cruft in bug lintian man; do
+2 -2
View File
@@ -40,8 +40,8 @@ git clone --recursive https://github.com/qelectrotech/qelectrotech-source-mirror
Here are the technical choices made for the software development:
* Integrated development environment: [Qt Framework](https://www.qt.io/ide/)
* Libraries: Qt 5.x
* [KF5 Framework](https://github.com/KDE)
* Libraries: Qt 5.x / Qt 6.x
* [KF5/6 Framework](https://github.com/KDE)
[Cmake](https://cmake.org/install/)
[kcoreaddons](https://github.com/KDE/kcoreaddons/tree/kf5)
[kwidgetsaddons](https://github.com/KDE/kwidgetsaddons/tree/kf5).
+11 -11
View File
@@ -25,24 +25,24 @@ message("INSTALL_PREFIX " ${INSTALL_PREFIX})
message("QET_BINARY_PATH " ${QET_BINARY_PATH})
if(${QET_COMMON_COLLECTION_PATH} STRGREATER "")
message("QET_COMMON_COLLECTION_PATH " ${INSTALL_PREFIX}${QET_COMMON_COLLECTION_PATH})
add_definitions(-DQET_COMMON_COLLECTION_PATH=${INSTALL_PREFIX}${QET_COMMON_COLLECTION_PATH})
message("QET_COMMON_COLLECTION_PATH " ${COMPIL_PREFIX}${QET_COMMON_COLLECTION_PATH})
add_definitions(-DQET_COMMON_COLLECTION_PATH=${COMPIL_PREFIX}${QET_COMMON_COLLECTION_PATH})
endif()
if(${QET_COMMON_TBT_PATH} STRGREATER "")
message("QET_COMMON_TBT_PATH " ${INSTALL_PREFIX}${QET_COMMON_TBT_PATH})
add_definitions(-DQET_COMMON_TBT_PATH=${INSTALL_PREFIX}${QET_COMMON_TBT_PATH})
message("QET_COMMON_TBT_PATH " ${COMPIL_PREFIX}${QET_COMMON_TBT_PATH})
add_definitions(-DQET_COMMON_TBT_PATH=${COMPIL_PREFIX}${QET_COMMON_TBT_PATH})
endif()
if(${QET_LANG_PATH_RELATIVE_TO_BINARY_PATH})
add_definitions(-DQET_LANG_PATH_RELATIVE_TO_BINARY_PATH)
endif()
if(${QET_LANG_PATH} STRGREATER "")
message("QET_LANG_PATH " ${INSTALL_PREFIX}${QET_LANG_PATH})
add_definitions(-DQET_LANG_PATH=${INSTALL_PREFIX}${QET_LANG_PATH})
message("QET_LANG_PATH " ${COMPIL_PREFIX}${QET_LANG_PATH})
add_definitions(-DQET_LANG_PATH=${COMPIL_PREFIX}${QET_LANG_PATH})
endif()
if (NOT MINGW)
if(${QET_EXAMPLES_PATH} STRGREATER "")
message("QET_EXAMPLES_PATH " ${INSTALL_PREFIX}${QET_EXAMPLES_PATH})
add_definitions(-DQET_EXAMPLES_PATH=${INSTALL_PREFIX}${QET_EXAMPLES_PATH})
message("QET_EXAMPLES_PATH " ${COMPIL_PREFIX}${QET_EXAMPLES_PATH})
add_definitions(-DQET_EXAMPLES_PATH=${COMPIL_PREFIX}${QET_EXAMPLES_PATH})
endif()
endif()
@@ -62,10 +62,10 @@ message("PROJECT_SOURCE_DIR :" ${PROJECT_SOURCE_DIR})
message("QET_DIR :" ${QET_DIR})
message("GIT_COMMIT_SHA :" ${GIT_COMMIT_SHA})
if(BUILD_WITH_KF5)
message("KF5_GIT_TAG :" ${KF5_GIT_TAG})
if(BUILD_WITH_KF)
message("KF_GIT_TAG :" ${KF_GIT_TAG})
else()
add_definitions(-DBUILD_WITHOUT_KF5)
add_definitions(-DBUILD_WITHOUT_KF)
endif()
message("QET_COMPONENTS :" ${QET_COMPONENTS})
message("QT_VERSION_MAJOR :" ${QT_VERSION_MAJOR})
+5 -2
View File
@@ -31,8 +31,11 @@ add_definitions(-DQT_MESSAGELOGCONTEXT)
# In order to do so, uncomment the following line.
#add_definitions(-DTODO_LIST)
# Build with KF5
option(BUILD_WITH_KF5 "Build with KF5" ON)
# Build with KDE Frameworks. The major version (KF5/KF6) is derived
# automatically from QT_VERSION_MAJOR -- KDE Frameworks deliberately
# mirrors Qt's own major version numbering, so there is no independent
# choice to make here. See cmake/fetch_kdeaddons.cmake.
option(BUILD_WITH_KF "Build with KDE Frameworks" ON)
# Precompiled headers for the Qt umbrella headers.
#
+58 -36
View File
@@ -16,52 +16,74 @@
message(" - fetch_kdeaddons")
if(BUILD_WITH_KF5)
# TODO remove path as soon as Qt5 gets retired
if(BUILD_WITH_KF)
Include(FetchContent)
option(BUILD_KF5 "Build KF5 libraries, use system ones otherwise" YES)
option(BUILD_KF "Build KF libraries, use system ones otherwise" YES)
if(BUILD_KF5)
if(BUILD_KF)
if(NOT DEFINED KF5_GIT_TAG)
#https://qelectrotech.org/forum/viewtopic.php?pid=13924#p13924
set(KF5_GIT_TAG v5.77.0)
if(KF_MAJOR_VERSION EQUAL 5)
if(NOT DEFINED KF_GIT_TAG)
#https://qelectrotech.org/forum/viewtopic.php?pid=13924#p13924
set(KF_GIT_TAG v5.77.0)
endif()
else()
if(NOT DEFINED KF_GIT_TAG)
# this is a more or less random version, taken as an conservative approach
set(KF_GIT_TAG v6.10.0)
endif()
endif()
# using a function in order to limit the scope of the variables
# with CMake >=3.25 we could use a block()
function(qet_make_kf_available)
# Fix stop the run autotests of kcoreaddons
# see
# https://invent.kde.org/frameworks/kcoreaddons/-/blob/master/CMakeLists.txt#L98
# issue:
# CMake Error at /usr/share/ECM/modules/ECMAddTests.cmake:89 (add_executable):
# Cannot find source file:
# see
# https://qelectrotech.org/forum/viewtopic.php?pid=13929#p13929
set(KDE_SKIP_TEST_SETTINGS ON)
set(BUILD_TESTING OFF)
# QElectroTech is a plain QtWidgets application with no QML anywhere in
# it; these disable optional features of the fetched KF modules that
# would otherwise pull in extra Qt6 components (e.g. Qt6Qml) we don't
# have and don't need.
set(BUILD_DESIGNERPLUGIN OFF)
set(KCOREADDONS_USE_QML OFF)
set(BUILD_QCH OFF)
set(BUILD_SHARED_LIBS OFF)
# Fix stop the run autotests of kcoreaddons
# see
# https://invent.kde.org/frameworks/kcoreaddons/-/blob/master/CMakeLists.txt#L98
# issue:
# CMake Error at /usr/share/ECM/modules/ECMAddTests.cmake:89 (add_executable):
# Cannot find source file:
# see
# https://qelectrotech.org/forum/viewtopic.php?pid=13929#p13929
set(KDE_SKIP_TEST_SETTINGS "TRUE")
set(BUILD_TESTING "0")
FetchContent_Declare(
ecm
GIT_REPOSITORY https://invent.kde.org/frameworks/extra-cmake-modules.git
GIT_TAG ${KF5_GIT_TAG})
FetchContent_MakeAvailable(ecm)
FetchContent_Declare(
ecm
GIT_REPOSITORY https://invent.kde.org/frameworks/extra-cmake-modules.git
GIT_TAG ${KF_GIT_TAG})
FetchContent_MakeAvailable(ecm)
FetchContent_Declare(
kcoreaddons
GIT_REPOSITORY https://invent.kde.org/frameworks/kcoreaddons.git
GIT_TAG ${KF5_GIT_TAG})
FetchContent_MakeAvailable(kcoreaddons)
FetchContent_Declare(
kcoreaddons
GIT_REPOSITORY https://invent.kde.org/frameworks/kcoreaddons.git
GIT_TAG ${KF_GIT_TAG})
FetchContent_MakeAvailable(kcoreaddons)
FetchContent_Declare(
kwidgetsaddons
GIT_REPOSITORY https://invent.kde.org/frameworks/kwidgetsaddons.git
GIT_TAG ${KF5_GIT_TAG})
FetchContent_MakeAvailable(kwidgetsaddons)
FetchContent_Declare(
kwidgetsaddons
GIT_REPOSITORY https://invent.kde.org/frameworks/kwidgetsaddons.git
GIT_TAG ${KF_GIT_TAG})
FetchContent_MakeAvailable(kwidgetsaddons)
endfunction()
qet_make_kf_available()
else()
find_package(KF5CoreAddons REQUIRED)
find_package(KF5WidgetsAddons REQUIRED)
find_package(KF${KF_MAJOR_VERSION}CoreAddons REQUIRED)
find_package(KF${KF_MAJOR_VERSION}WidgetsAddons REQUIRED)
endif()
set(KF5_PRIVATE_LIBRARIES
KF5::WidgetsAddons
KF5::CoreAddons
set(KF_PRIVATE_LIBRARIES
KF${KF_MAJOR_VERSION}::WidgetsAddons
KF${KF_MAJOR_VERSION}::CoreAddons
)
endif()
+8
View File
@@ -23,6 +23,14 @@ set(QAPPLICATION_CLASS QApplication)
Include(FetchContent)
if(EXISTS "${CMAKE_SOURCE_DIR}/SingleApplication/CMakeLists.txt")
# Submodule deja present dans l'arbre source (clone --recursive, tarball
# de distro deja peuple via "git submodule update", etc.) : on l'utilise
# tel quel, sans acces reseau. Necessaire pour les builds hors-ligne
# (pbuilder/sbuild avec FETCHCONTENT_FULLY_DISCONNECTED=ON, Launchpad PPA...).
set(FETCHCONTENT_SOURCE_DIR_SINGLEAPPLICATION "${CMAKE_SOURCE_DIR}/SingleApplication")
endif()
FetchContent_Declare(
SingleApplication
GIT_REPOSITORY https://github.com/itay-grudev/SingleApplication.git
+3 -3
View File
@@ -20,15 +20,15 @@ message(" - paths_compilation_installation")
if(UNIX AND NOT APPLE)
# for Linux, BSD, Solaris, Minix
set(COMPIL_PREFIX "/usr/local/")
set(INSTALL_PREFIX "/usr/local/")
set(COMPIL_PREFIX "${CMAKE_INSTALL_PREFIX}/")
set(INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}/")
set(QET_BINARY_PATH "bin/")
set(QET_COMMON_COLLECTION_PATH "share/qelectrotech/elements/")
set(QET_COMMON_TBT_PATH "share/qelectrotech/titleblocks/")
set(QET_LANG_PATH "share/qelectrotech/lang/")
set(QET_EXAMPLES_PATH "share/qelectrotech/examples/")
set(QET_LICENSE_PATH "doc/qelectrotech/")
set(QET_MIME_PACKAGE_PATH "../share/mime/packages/")
set(QET_MIME_PACKAGE_PATH "share/mime/packages/")
set(QET_DESKTOP_PATH "share/applications/")
set(QET_ICONS_PATH "share/icons/hicolor/")
set(QET_MAN_PATH "man/")
+38 -4
View File
@@ -33,6 +33,13 @@ set(QET_COMPONENTS
Widgets
Concurrent)
# Note: Pdf is intentionally NOT in this list. Some Qt6 distributions
# (notably the Flatpak org.kde.Platform runtime) don't ship the QtPdf
# module at all - it lives in the qtwebengine source tree, not Qt6 core.
# Requesting it here as a REQUIRED component would fail the whole
# configure on those setups. It is probed separately, QUIET and
# non-fatal, right after the main find_package() call below.
set(QET_PRIVATE_LIBRARIES
Qt::PrintSupport
Qt::Gui
@@ -45,6 +52,9 @@ set(QET_PRIVATE_LIBRARIES
Qt::Concurrent
)
# Qt::Pdf is appended conditionally in CMakeLists.txt, once we know
# whether the module was actually found (see QET_HAS_QTPDF).
set(QET_RES_FILES
${QET_DIR}/sources/autoNum/ui/autonumberingdockwidget.ui
${QET_DIR}/sources/autoNum/ui/autonumberingmanagementw.ui
@@ -91,7 +101,6 @@ set(QET_RES_FILES
${QET_DIR}/sources/ui/configsaveloaderwidget.ui
${QET_DIR}/sources/ui/diagramcontextwidget.ui
${QET_DIR}/sources/ui/diagrameditorhandlersizewidget.ui
${QET_DIR}/sources/ui/diagramselection.ui
${QET_DIR}/sources/ui/dialogwaiting.ui
${QET_DIR}/sources/ui/dynamicelementtextitemeditor.ui
${QET_DIR}/sources/ui/elementinfopartwidget.ui
@@ -177,6 +186,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/configdialog.h
${QET_DIR}/sources/createdxf.cpp
${QET_DIR}/sources/createdxf.h
${QET_DIR}/sources/dxfpaintdevice.cpp
${QET_DIR}/sources/dxfpaintdevice.h
${QET_DIR}/sources/diagramcommands.cpp
${QET_DIR}/sources/diagramcommands.h
${QET_DIR}/sources/diagramcontent.cpp
@@ -215,6 +226,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/exportpropertieswidget.h
${QET_DIR}/sources/genericpanel.cpp
${QET_DIR}/sources/genericpanel.h
${QET_DIR}/sources/lastusedstyle.cpp
${QET_DIR}/sources/lastusedstyle.h
${QET_DIR}/sources/machine_info.cpp
${QET_DIR}/sources/machine_info.h
${QET_DIR}/sources/main.cpp
@@ -296,12 +309,17 @@ set(QET_SRC_FILES
${QET_DIR}/sources/dataBase/ui/summaryquerywidget.cpp
${QET_DIR}/sources/dataBase/ui/summaryquerywidget.h
${QET_DIR}/sources/autobreakconductor.cpp
${QET_DIR}/sources/autobreakconductor.h
${QET_DIR}/sources/diagramevent/diagrameventaddelement.cpp
${QET_DIR}/sources/diagramevent/diagrameventaddelement.h
${QET_DIR}/sources/diagramevent/diagrameventaddimage.cpp
${QET_DIR}/sources/diagramevent/diagrameventaddimage.h
${QET_DIR}/sources/diagramevent/diagrameventaddshape.cpp
${QET_DIR}/sources/diagramevent/diagrameventaddshape.h
${QET_DIR}/sources/diagramevent/diagrameventaddpath.cpp
${QET_DIR}/sources/diagramevent/diagrameventaddpath.h
${QET_DIR}/sources/diagramevent/diagrameventaddtext.cpp
${QET_DIR}/sources/diagramevent/diagrameventaddtext.h
${QET_DIR}/sources/diagramevent/diagrameventinterface.cpp
@@ -502,6 +520,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/qetgraphicsitem/qetgraphicsitem.h
${QET_DIR}/sources/qetgraphicsitem/qetshapeitem.cpp
${QET_DIR}/sources/qetgraphicsitem/qetshapeitem.h
${QET_DIR}/sources/qetgraphicsitem/shapetransform.cpp
${QET_DIR}/sources/qetgraphicsitem/shapetransform.h
${QET_DIR}/sources/qetgraphicsitem/qgraphicsitemutility.cpp
${QET_DIR}/sources/qetgraphicsitem/qgraphicsitemutility.h
${QET_DIR}/sources/qetgraphicsitem/reportelement.cpp
@@ -682,8 +702,6 @@ set(QET_SRC_FILES
${QET_DIR}/sources/ui/diagrampropertiesdialog.h
${QET_DIR}/sources/ui/diagrampropertieseditordockwidget.cpp
${QET_DIR}/sources/ui/diagrampropertieseditordockwidget.h
${QET_DIR}/sources/ui/diagramselection.cpp
${QET_DIR}/sources/ui/diagramselection.h
${QET_DIR}/sources/ui/backupdialog.cpp
${QET_DIR}/sources/ui/backupdialog.h
${QET_DIR}/sources/ui/dialogwaiting.cpp
@@ -704,8 +722,12 @@ set(QET_SRC_FILES
${QET_DIR}/sources/ui/elementpropertieswidget.h
${QET_DIR}/sources/ui/formulaassistantdialog.cpp
${QET_DIR}/sources/ui/formulaassistantdialog.h
${QET_DIR}/sources/ui/imagecropdialog.cpp
${QET_DIR}/sources/ui/imagecropdialog.h
${QET_DIR}/sources/ui/imagepropertieswidget.cpp
${QET_DIR}/sources/ui/imagepropertieswidget.h
${QET_DIR}/sources/ui/imagetransparentcolordialog.cpp
${QET_DIR}/sources/ui/imagetransparentcolordialog.h
${QET_DIR}/sources/ui/importelementdialog.cpp
${QET_DIR}/sources/ui/importelementdialog.h
${QET_DIR}/sources/ui/importelementtextpatterndialog.cpp
@@ -776,6 +798,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/undocommand/setautonumcontextcommand.h
${QET_DIR}/sources/undocommand/rotateselectioncommand.cpp
${QET_DIR}/sources/undocommand/rotateselectioncommand.h
${QET_DIR}/sources/undocommand/promoteshapecommand.cpp
${QET_DIR}/sources/undocommand/promoteshapecommand.h
${QET_DIR}/sources/undocommand/rotatetextscommand.cpp
${QET_DIR}/sources/undocommand/rotatetextscommand.h
${QET_DIR}/sources/undocommand/movegraphicsitemcommand.cpp
@@ -796,7 +820,7 @@ set(QET_SRC_FILES
${QET_DIR}/sources/xml/terminalstriplayoutpatternxml.h
)
if(NOT BUILD_WITH_KF5)
if(NOT BUILD_WITH_KF)
list(APPEND QET_SRC_FILES
${QET_DIR}/sources/ui/nokde/kautosavefile.cpp
${QET_DIR}/sources/ui/nokde/kautosavefile.h
@@ -807,6 +831,16 @@ if(NOT BUILD_WITH_KF5)
)
endif()
# Qt6-only: PDF page import files
if(QT_VERSION_MAJOR GREATER_EQUAL 6)
list(APPEND QET_SRC_FILES
${QET_DIR}/sources/diagramevent/diagrameventaddpdf.cpp
${QET_DIR}/sources/diagramevent/diagrameventaddpdf.h
${QET_DIR}/sources/ui/pdfpagesdialog.cpp
${QET_DIR}/sources/ui/pdfpagesdialog.h
)
endif()
set(TS_FILES
${QET_DIR}/lang/qet_ar.ts
${QET_DIR}/lang/qet_ca.ts
+6 -2
View File
@@ -28,5 +28,9 @@ add_definitions(-DQET_ALLOW_OVERRIDE_CD_OPTION)
# Comment the line below to deactivate the --data-dir option
add_definitions(-DQET_ALLOW_OVERRIDE_DD_OPTION)
#comment the line below to disable the project database export
#add_definitions(-DQET_EXPORT_PROJECT_DB) #error Todo
# Enable project database export when requested by the build system.
option(QET_EXPORT_PROJECT_DB "Enable project database export" OFF)
if(QET_EXPORT_PROJECT_DB)
add_compile_definitions(QET_EXPORT_PROJECT_DB)
endif()
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#232629;
}
</style>
</defs>
<g transform="translate(1,1)">
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="m18.5 4c-.65424 0-1.202197.418077-1.408203 1h-13.0918v1c3.601 0 6.5 2.899 6.5 6.5 0 1.905514-.822115 3.601777-2.121094 4.787109-.245727-.180239-.548776-.287109-.878906-.287109-.831 0-1.5.669-1.5 1.5 0 .831.669 1.5 1.5 1.5.65424 0 1.202197-.418077 1.408203-1h4.091797v1h3v-3h-3v1h-3.912109c1.479147-1.367931 2.412109-3.317097 2.412109-5.5 0-2.788667-1.510668-5.207469-3.757813-6.5h9.349609c.206006.581923.753963 1 1.408203 1 .831 0 1.5-.669 1.5-1.5 0-.831-.669-1.5-1.5-1.5m0 1c.277 0 .5.223.5.5 0 .277-.223.5-.5.5-.06925 0-.135453-.013828-.195312-.039063-.179579-.075703-.304688-.253188-.304688-.460938 0-.06925.013828-.135453.039062-.195313.075704-.179578.253188-.304688.460938-.304688m-11 13c.277 0 .5.223.5.5 0 .277-.223.5-.5.5-.277 0-.5-.223-.5-.5 0-.277.223-.5.5-.5m6.5 0h1v1h-1v-1z" transform="translate(-.99999-.99999)" class="ColorScheme-Text"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+1234 -998
View File
File diff suppressed because it is too large Load Diff
+1226 -999
View File
File diff suppressed because it is too large Load Diff
+1228 -998
View File
File diff suppressed because it is too large Load Diff
+1226 -999
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1553 -1320
View File
File diff suppressed because it is too large Load Diff
+1226 -999
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1231 -1000
View File
File diff suppressed because it is too large Load Diff
+1226 -999
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1226 -997
View File
File diff suppressed because it is too large Load Diff
+1227 -996
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1763 -1482
View File
File diff suppressed because it is too large Load Diff
+1226 -998
View File
File diff suppressed because it is too large Load Diff
+1224 -999
View File
File diff suppressed because it is too large Load Diff
+1223 -996
View File
File diff suppressed because it is too large Load Diff
+1226 -999
View File
File diff suppressed because it is too large Load Diff
+1226 -998
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1612 -1348
View File
File diff suppressed because it is too large Load Diff
+1225 -996
View File
File diff suppressed because it is too large Load Diff
+1228 -999
View File
File diff suppressed because it is too large Load Diff
+1225 -999
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1613 -1354
View File
File diff suppressed because it is too large Load Diff
+1227 -996
View File
File diff suppressed because it is too large Load Diff
+1227 -996
View File
File diff suppressed because it is too large Load Diff
+1228 -1001
View File
File diff suppressed because it is too large Load Diff
+1227 -996
View File
File diff suppressed because it is too large Load Diff
+1229 -996
View File
File diff suppressed because it is too large Load Diff
+1227 -996
View File
File diff suppressed because it is too large Load Diff
+1226 -999
View File
File diff suppressed because it is too large Load Diff
+1224 -999
View File
File diff suppressed because it is too large Load Diff
+1228 -999
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1490 -1244
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -40,7 +40,7 @@
<key>CFBundleExecutable</key>
<string>qelectrotech</string>
<key>CFBundleIconFile</key>
<string>qelectrotech.icns</string>
<string>qelectrotech</string>
<key>CFBundleIdentifier</key>
<string>org.qelectrotech</string>
<key>CFBundleInfoDictionaryVersion</key>
+428
View File
@@ -0,0 +1,428 @@
#!/bin/sh
# Copyright 2026 The QElectroTech Team
# This file is part of QElectroTech.
# QElectroTech is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
# Suppose que l'environnement a ete prepare via macos_homebrew_setup.sh
# (Homebrew, Qt6, cmake, ninja, kf6-kwidgetsaddons, kf6-kcoreaddons).
#
# Remplace l'ancien misc/MacQetDeploy_arm64.sh (qmake/Qt5) par un
# build CMake/Qt6/KF6. La chaine de signature/notarization/DMG est
# reprise a l'identique de l'ancien script (eprouvee, ne pas y toucher
# sans raison).
# configuration
APPNAME='qelectrotech'
BUNDLE=$APPNAME.app
IDENTITY="Developer ID Application: Laurent TRINQUES (Y73WZ6WZ5X)"
QT_MAJOR="${QT_MAJOR:-6}"
BUILD_WITH_KF="${BUILD_WITH_KF:-ON}"
BUILD_DIR="build-macos-arm64"
# Temp paths
RW_DMG="/tmp/qet_rw.dmg"
MOUNT_POINT="/tmp/qet_dmg_mount"
STAGING="/tmp/qet_dmg_staging"
# Script location
current_dir=$(dirname "$0")
cd "${current_dir}/../"
current_dir=$(PWD)
### get system configuration ########################################
echo
echo "______________________________________________________________"
echo "This script prepares a Qt6/KF6 application bundle for deployment."
echo "This script :"
echo "\t - update the git depot"
echo "\t - configure and build via CMake,"
echo "\t - copy over required Qt frameworks,"
echo "\t - copy additional files: translations, titleblocks and elements,"
echo "\t - notarize the .app, then create a signed DMG."
echo
QT_PREFIX=$(brew --prefix qt 2>/dev/null || true)
if [ -z "$QT_PREFIX" ] || [ ! -d "$QT_PREFIX/lib/QtCore.framework" ] ; then
echo "ERROR: cannot find Qt6 via Homebrew. Run macos_homebrew_setup.sh first."
exit 1
fi
export CMAKE_PREFIX_PATH="$QT_PREFIX:$CMAKE_PREFIX_PATH"
### GIT ####################################################
echo
echo "______________________________________________________________"
echo "Run GIT:"
git submodule init
git submodule update
git pull --recurse-submodules
git pull
GITCOMMIT=$(git rev-parse --short HEAD)
A=$(git rev-list HEAD --count)
HEAD=$(($A+473))
VERSION=$(cat sources/qetversion.cpp | grep "return QVersionNumber{"| head -n 1| awk -F "{" '{ print $2 }' | awk -F "}" '{ print $1 }' | sed -e 's/,/./g' -e 's/ //g')
DMG_NAME="${APPNAME}-$VERSION-r$HEAD-arm64.dmg"
DMG_PATH="build-aux/mac-osx/$DMG_NAME"
if [ -e "$DMG_PATH" ] ; then
echo "There are not new updates, make disk image can"
echo "take a lot of time (5 min). Can you continu?"
echo "[y/n]"
read userinput
if [ "$userinput" == "n" ] ; then
echo
echo "Process is stopped."
echo
exit
fi
fi
### build with CMake #################################################
echo
echo "______________________________________________________________"
echo "Run CMake configure + build (Qt${QT_MAJOR}, BUILD_WITH_KF=${BUILD_WITH_KF}):"
if [ -d $BUNDLE ] ; then
echo "Removing old bundle..."
rm -rf $BUNDLE
fi
if [ -d "$BUILD_DIR" ] ; then
echo "Removing old build directory..."
rm -rf "$BUILD_DIR"
fi
cmake -S . -B "$BUILD_DIR" -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DQT_VERSION_MAJOR=$QT_MAJOR \
-DBUILD_WITH_KF=$BUILD_WITH_KF \
-DBUILD_KF=OFF \
-DQET_EXPORT_PROJECT_DB=ON \
-DPACKAGE_TESTS=OFF
if [ $? -ne 0 ]; then
echo "ERROR: cmake configure failed."
exit 1
fi
START_TIME=$SECONDS
coeur=$(sysctl hw.ncpu | awk '{print $2}')
cmake --build "$BUILD_DIR" -j$(($coeur + 1))
if [ $? -ne 0 ]; then
ELAPSED_TIME=$(($SECONDS - $START_TIME))
echo
echo "cmake build failed - $(($ELAPSED_TIME/60)) min $(($ELAPSED_TIME%60)) sec"
exit 1
fi
ELAPSED_TIME=$(($SECONDS - $START_TIME))
echo
echo "The time of compilation is $(($ELAPSED_TIME/60)) min $(($ELAPSED_TIME%60)) sec"
# TODO: confirmer le chemin exact de sortie du .app selon CMakeLists.txt
echo "Copying built bundle into place..."
cp -R "$BUILD_DIR/qelectrotech.app" "./$BUNDLE"
if [ ! -d "$BUNDLE" ] ; then
echo "ERROR: expected bundle \"$BUNDLE\" not found after cmake build."
exit 1
fi
### copy over frameworks ############################################
echo
echo "______________________________________________________________"
echo "Copy Qt libraries and private frameworks:"
if [ ! -d $BUNDLE ] ; then
echo "ERROR: cannot find application bundle \"$BUNDLE\" in current directory"
exit 1
fi
macdeployqt $BUNDLE
### install Info.plist and app icon #################################
# NOTE: this must run AFTER macdeployqt, not before. macdeployqt
# rewrites/regenerates parts of Contents/Resources, and files copied
# there beforehand (e.g. the .icns icons) do not reliably survive it
# and end up missing from the final bundle, causing the app to show
# the generic placeholder icon instead of the real one. Info.plist
# itself lives directly in Contents/ and happens to survive either
# way, but keep it here too so this whole "final metadata" step stays
# in one place, after macdeployqt is done touching the bundle.
echo
echo "______________________________________________________________"
echo "Install Info.plist and app icon:"
cp -R ${current_dir}/misc/Info.plist $BUNDLE/Contents/
cp -R ${current_dir}/ico/mac_icon/*.icns $BUNDLE/Contents/Resources/
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION r$HEAD" "$BUNDLE/Contents/Info.plist"
### add missing files ###############################################
echo
echo "______________________________________________________________"
echo "Copy missing files:"
QET_ELMT_DIR="${current_dir}/elements/"
QET_TBT_DIR="${current_dir}/titleblocks/"
QET_LANG_DIR="${current_dir}/lang/"
QET_EXAMPLES_DIR="${current_dir}/examples/"
QET_FONTS_DIR="${current_dir}/fonts/"
QET_LICENSES_DIR="${current_dir}/licenses/"
LANG_DIR="${current_dir}/lang1/"
if [ -d "${QET_ELMT_DIR}" ]; then
cp -R ${QET_ELMT_DIR} $BUNDLE/Contents/Resources/elements
fi
if [ -d "${QET_TBT_DIR}" ]; then
cp -R ${QET_TBT_DIR} $BUNDLE/Contents/Resources/titleblocks
fi
if [ -d "${QET_LANG_DIR}" ]; then
mkdir $BUNDLE/Contents/Resources/lang
cp ${current_dir}/lang/*.qm $BUNDLE/Contents/Resources/lang
fi
if [ -d "${LANG_DIR}" ]; then
cp ${current_dir}/lang1/*.qm $BUNDLE/Contents/Resources/lang
fi
if [ -d "${QET_EXAMPLES_DIR}" ]; then
mkdir $BUNDLE/Contents/Resources/examples
cp ${current_dir}/examples/*.qet $BUNDLE/Contents/Resources/examples
fi
if [ -d "${QET_FONTS_DIR}" ]; then
mkdir $BUNDLE/Contents/Resources/fonts
cp ${current_dir}/fonts/*.ttf $BUNDLE/Contents/Resources/fonts
fi
if [ -d "${QET_LICENSES_DIR}" ]; then
cp -R -L ${QET_LICENSES_DIR} $BUNDLE/Contents/Resources/licenses
fi
### Sign the bundle #################################################
# Sign in correct order: all dylibs first (including flat libs copied
# by macdeployqt from Homebrew), then frameworks, plugins, bundle last.
echo
echo "______________________________________________________________"
echo "Code signing (dylibs -> frameworks -> plugins -> bundle):"
echo "-- Signing dylibs in Frameworks/..."
find "$BUNDLE/Contents/Frameworks" -name "*.dylib" | while read lib; do
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$lib"
done
echo "-- Signing .framework bundles..."
find "$BUNDLE/Contents/Frameworks" -maxdepth 1 -name "*.framework" | while read fw; do
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$fw"
done
echo "-- Signing plugins..."
find "$BUNDLE/Contents/PlugIns" \( -name "*.dylib" -o -name "*.so" \) | while read lib; do
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$lib"
done
echo "-- Signing dylibs in MacOS/..."
find "$BUNDLE/Contents/MacOS" -name "*.dylib" | while read lib; do
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$lib"
done
echo "-- Signing main executable..."
codesign --force --sign "$IDENTITY" --timestamp --options=runtime \
"$BUNDLE/Contents/MacOS/$APPNAME"
echo "-- Signing bundle..."
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$BUNDLE"
echo
echo "Verifying bundle signature..."
codesign --verify --deep --strict --verbose=2 "$BUNDLE"
if [ $? -ne 0 ]; then
echo "ERROR: bundle signature verification failed, aborting."
exit 1
fi
spctl -a -vv "$BUNDLE"
echo "Bundle signature OK."
### Notarize the .app (via temporary ZIP) ###########################
echo
echo "______________________________________________________________"
echo "Create temporary ZIP for notarization:"
NOTARIZE_ZIP="/tmp/${APPNAME}-$VERSION-r$HEAD-arm64-notarize.zip"
/usr/bin/ditto -c -k --keepParent "$BUNDLE" "$NOTARIZE_ZIP"
echo -e "\033[1;31mWould you like to notarize the .app \"${APPNAME}-${VERSION}-r${HEAD}\", n/Y?\033[m"
read a
if [[ $a == "Y" || $a == "y" ]]; then
echo
echo "______________________________________________________________"
echo "Notarizing .app:"
xcrun notarytool submit "$NOTARIZE_ZIP" --keychain-profile "org.qelectrotech" --wait
if [ $? -ne 0 ]; then
echo "ERROR: notarization failed. Check the log with:"
echo " xcrun notarytool log <submission-id> --keychain-profile org.qelectrotech"
rm -f "$NOTARIZE_ZIP"
exit 1
fi
else
echo -e "\033[1;33mExit.\033[m"
fi
rm -f "$NOTARIZE_ZIP"
### Staple the .app #################################################
echo -e "\033[1;31mWould you like to staple the .app \"${APPNAME}-${VERSION}-r${HEAD}\", n/Y?\033[m"
read a
if [[ $a == "Y" || $a == "y" ]]; then
xcrun stapler staple -v "$BUNDLE"
if [ $? -ne 0 ]; then
echo "ERROR: stapling .app failed."
exit 1
fi
xcrun stapler validate -v "$BUNDLE"
spctl -a -vv "$BUNDLE"
echo ".app stapled OK."
else
echo -e "\033[1;33mExit.\033[m"
fi
### Create staging folder with Applications symlink #################
echo
echo "______________________________________________________________"
echo "Preparing DMG staging folder:"
rm -rf "$STAGING"
mkdir -p "$STAGING"
cp -R "$BUNDLE" "$STAGING/"
ln -s /Applications "$STAGING/Applications"
### Create writable DMG (UDRW) ######################################
echo
echo "______________________________________________________________"
echo "Create writable DMG (UDRW) and re-sign .app inside:"
rm -f "$RW_DMG"
hdiutil create \
-volname "QElectroTech $VERSION" \
-srcfolder "$STAGING" \
-ov \
-format UDRW \
-fs HFS+ \
"$RW_DMG"
if [ $? -ne 0 ]; then
echo "ERROR: hdiutil failed to create writable DMG."
rm -rf "$STAGING"
exit 1
fi
rm -rf "$MOUNT_POINT"
mkdir -p "$MOUNT_POINT"
hdiutil attach "$RW_DMG" -mountpoint "$MOUNT_POINT" -nobrowse -noverify
if [ $? -ne 0 ]; then
echo "ERROR: failed to mount writable DMG."
rm -f "$RW_DMG"
rm -rf "$STAGING"
exit 1
fi
echo "-- Re-signing dylibs inside DMG..."
find "$MOUNT_POINT/$BUNDLE/Contents/Frameworks" -name "*.dylib" | while read lib; do
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$lib"
done
find "$MOUNT_POINT/$BUNDLE/Contents/Frameworks" -maxdepth 1 -name "*.framework" | while read fw; do
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$fw"
done
find "$MOUNT_POINT/$BUNDLE/Contents/PlugIns" \( -name "*.dylib" -o -name "*.so" \) | while read lib; do
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$lib"
done
codesign --force --sign "$IDENTITY" --timestamp --options=runtime \
"$MOUNT_POINT/$BUNDLE/Contents/MacOS/$APPNAME"
codesign --force --sign "$IDENTITY" --timestamp --options=runtime \
"$MOUNT_POINT/$BUNDLE"
echo "Verifying bundle signature inside DMG..."
codesign --verify --deep --strict --verbose=2 "$MOUNT_POINT/$BUNDLE"
if [ $? -ne 0 ]; then
echo "ERROR: bundle signature invalid inside DMG, aborting."
hdiutil detach "$MOUNT_POINT"
rm -f "$RW_DMG"
rm -rf "$STAGING" "$MOUNT_POINT"
exit 1
fi
echo "Bundle signature inside DMG OK."
hdiutil detach "$MOUNT_POINT"
### Convert UDRW to final compressed UDZO ###########################
echo
echo "______________________________________________________________"
echo "Convert to final compressed DMG (UDZO):"
mkdir -p "build-aux/mac-osx"
rm -f "$DMG_PATH"
hdiutil convert "$RW_DMG" \
-format UDZO \
-o "$DMG_PATH"
if [ $? -ne 0 ]; then
echo "ERROR: hdiutil convert failed."
rm -f "$RW_DMG"
rm -rf "$STAGING" "$MOUNT_POINT"
exit 1
fi
rm -f "$RW_DMG"
rm -rf "$STAGING" "$MOUNT_POINT"
### Sign the final DMG ##############################################
echo "Signing final DMG..."
codesign --sign "$IDENTITY" --timestamp "$DMG_PATH"
### Notarize and staple the final DMG ###############################
echo -e "\033[1;31mWould you like to notarize the DMG \"${DMG_NAME}\", n/Y?\033[m"
read a
if [[ $a == "Y" || $a == "y" ]]; then
echo
echo "______________________________________________________________"
echo "Notarizing DMG:"
xcrun notarytool submit "$DMG_PATH" --keychain-profile "org.qelectrotech" --wait
if [ $? -ne 0 ]; then
echo "ERROR: DMG notarization failed. Check the log with:"
echo " xcrun notarytool log <submission-id> --keychain-profile org.qelectrotech"
exit 1
fi
echo "Stapling DMG..."
xcrun stapler staple "$DMG_PATH"
if [ $? -ne 0 ]; then
echo "ERROR: stapling DMG failed."
exit 1
fi
echo "DMG notarized and stapled OK."
spctl -a -vv "$DMG_PATH"
else
echo -e "\033[1;33mExit.\033[m"
fi
### Clean up bundle #################################################
rm -rf "$BUNDLE"
echo
echo "______________________________________________________________"
echo "The process is done."
echo "DMG is in the folder 'build-aux/mac-osx'."
### Upload via rsync ################################################
echo -e "\033[1;31mWould you like to upload MacOS package \"${DMG_NAME}\", n/Y?\033[m"
read a
if [[ $a == "Y" || $a == "y" ]]; then
cp -Rf "$DMG_PATH" /Users/laurent/MAC_OS_X/
rsync -e ssh -av --delete-after --no-owner --no-g --chmod=g+w \
--progress --exclude='.DS_Store' \
/Users/laurent/MAC_OS_X/ \
server:download.qelectrotech.org/qet/builds/MAC_OS_X/arm64/
if [ $? != 0 ]; then
echo "RSYNC ERROR: problem syncing ${DMG_NAME}, retrying..."
rsync -e ssh -av --delete-after --no-owner --no-g --chmod=g+w \
--progress --exclude='.DS_Store' \
/Users/laurent/MAC_OS_X/ \
server:download.qelectrotech.org/qet/builds/MAC_OS_X/arm64/
fi
else
echo -e "\033[1;33mExit.\033[m"
fi
+2
View File
@@ -140,6 +140,7 @@
<file>ico/22x22/go-up.png</file>
<file>ico/22x22/hotspot.png</file>
<file>ico/22x22/insert-image.png</file>
<file>ico/22x22/pdf-import.png</file>
<file>ico/22x22/label.png</file>
<file>ico/22x22/landscape.png</file>
<file>ico/22x22/line.png</file>
@@ -547,6 +548,7 @@
<file>ico/breeze-icons/scalable/mimetypes/small/48x48/application-x-qet-element.svgz</file>
<file>ico/breeze-icons/scalable/mimetypes/small/48x48/application-x-qet-project.svgz</file>
<file>ico/breeze-icons/scalable/mimetypes/small/48x48/application-x-qet-titleblock.svgz</file>
<file>ico/breeze-icons/scalable/apps/hidef/draw-bezier-curves.svg</file>
<file>ico/16x16/object-group.png</file>
<file>ico/mac_icon/elmt.icns</file>
<file>ico/mac_icon/qelectrotech.icns</file>
@@ -38,6 +38,20 @@ ElementsCollectionModel::ElementsCollectionModel(QObject *parent) :
{
}
/**
@brief ElementsCollectionModel::~ElementsCollectionModel
Destructor. loadCollections() may still have background threads
(via QtConcurrent::map()) running setUpData() on this model's items
when the model is destroyed (e.g. the user cancels the dialog before
loading finishes). Wait for them here so QStandardItemModel's
destructor doesn't free items out from under them, which used to
crash the whole application (bugtracker #291).
*/
ElementsCollectionModel::~ElementsCollectionModel()
{
m_future.waitForFinished();
}
/**
@brief ElementsCollectionModel::data
Reimplemented from QStandardItemModel
@@ -49,6 +63,7 @@ QVariant ElementsCollectionModel::data(const QModelIndex &index, int role) const
{
if (role == Qt::DecorationRole) {
QStandardItem *item = itemFromIndex(index);
if (!item) return QStandardItemModel::data(index, role);
if (item->type() == FileElementCollectionItem::Type)
static_cast<FileElementCollectionItem*>(item)->setUpIcon();
@@ -316,7 +331,6 @@ void ElementsCollectionModel::loadMacrosCollection()
void ElementsCollectionModel::addMacrosCollection(bool set_data)
{
QString macrosPath = QETApp::userMacrosDir();
qDebug() << "=== MAKRO PFAD CHECK ===" << macrosPath;
if (macrosPath.endsWith("/")) {
macrosPath.remove(macrosPath.length() - 1, 1);
}
@@ -35,6 +35,7 @@ class ElementsCollectionModel : public QStandardItemModel
public:
ElementsCollectionModel(QObject *parent = Q_NULLPTR);
~ElementsCollectionModel() override;
QVariant data(const QModelIndex &index, int role) const override;
QMimeData *mimeData(const QModelIndexList &indexes) const override;
@@ -949,14 +949,7 @@ void ElementsCollectionWidget::search()
}
hideCollection(true);
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
const QStringList text_list = text.split("+", QString::SkipEmptyParts);
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.14 or later")
#endif
const QStringList text_list = text.split("+", Qt::SkipEmptyParts);
#endif
QModelIndexList match_index;
for (QString txt : text_list) {
match_index << m_model->match(m_showed_index.isValid()
@@ -143,11 +143,7 @@ void ElementsTreeView::startElementDrag(const ElementsLocation &location)
QString file_name = (last_slash != -1) ? path.mid(last_slash + 1) : path;
if (!dir_path.isEmpty()) {
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
QStringList parts = dir_path.split('/', QString::SkipEmptyParts);
#else
QStringList parts = dir_path.split('/', Qt::SkipEmptyParts);
#endif
QString current_path = "";
for (const QString &part : parts) {
QString parent_path = current_path;
@@ -430,7 +430,7 @@ void FileElementCollectionItem::setUpData()
*/
void FileElementCollectionItem::setUpIcon()
{
// Must return unconditionally once an icon is set: setIcon() calls
// Must return unconditionally once setUpIcon has run: setIcon() calls
// setData(), which emits dataChanged() regardless of whether the new
// icon differs from the old one (QIcon has no meaningful equality).
// QTreeView responds to dataChanged() by recomputing the row's size
@@ -438,13 +438,18 @@ void FileElementCollectionItem::setUpIcon()
// guard, any repeated setIcon() here recurses until the stack
// overflows. Confirmed by crash report on PR #633.
//
// We use a dedicated bool instead of icon().isNull() because
// some items intentionally keep a null icon (e.g. .qetmak),
// which would bypass a null-based guard and still recurse.
//
// This item's m_qet_directory_unreadable is already final by the time
// this can run at all: ElementsCollectionModel only attaches itself
// to the tree view (making data() reachable) from loadingFinished(),
// which fires after the QtConcurrent::map over every item -- this one
// included -- has completed. So there is no race to work around here.
if (!icon().isNull())
if (m_icon_initialized)
return;
m_icon_initialized = true;
if (isCollectionRoot()) {
QString macrosPath = QETApp::userMacrosDir();
@@ -69,6 +69,7 @@ class FileElementCollectionItem : public ElementCollectionItem
/// rather than acted on in localName(), because setUpData() resets
/// the tooltip afterwards and would otherwise discard it.
bool m_qet_directory_unreadable = false;
bool m_icon_initialized = false;
};
#endif // FILEELEMENTCOLLECTIONITEM2_H
@@ -46,6 +46,18 @@ class PropertiesEditorDialog : public QDialog
PropertiesEditorDialog(T editor, QWidget *parent = nullptr) :
QDialog (parent)
{
// Without this, a QWidget-modal QDialog with a parent falls
// back to macOS's automatic sheet presentation, which some
// Qt/Cocoa versions render stuck centered on screen and
// non-draggable rather than as a proper attached sheet
// (bugtracker #275). Other dialogs in this codebase already
// opt in explicitly (see ElementDialog, DiagramPropertiesDialog)
// -- this one was missing it.
setWindowModality(Qt::WindowModal);
#ifdef Q_OS_MACOS
setWindowFlags(Qt::Sheet);
#endif
//Set dialog title
setWindowTitle(editor->title());
// Reparent the editor,
@@ -1020,6 +1020,7 @@ void SearchAndReplaceWidget::on_m_tree_widget_currentItemChanged(
{
m_highlighted_element = elmt;
elmt.data()->setHighlighted(true);
elmt.data()->ensureVisible();
}
}
else if (m_text_hash.contains(current))
@@ -1029,6 +1030,7 @@ void SearchAndReplaceWidget::on_m_tree_widget_currentItemChanged(
{
text.data()->setSelected(true);
m_last_selected = text;
text.data()->ensureVisible();
}
}
else if (m_conductor_hash.contains(current))
@@ -1038,6 +1040,7 @@ void SearchAndReplaceWidget::on_m_tree_widget_currentItemChanged(
{
cond.data()->setSelected(true);
m_last_selected = cond;
cond.data()->ensureVisible();
}
}
@@ -19,6 +19,7 @@
#define TERMINALSTRIPDRAWER_H
#include <QPointer>
#include <QUuid>
#include "properties/terminalstriplayoutpattern.h"
@@ -130,12 +130,7 @@ bool PhysicalTerminal::setLevelOf(const QSharedPointer<RealTerminal> &terminal,
const int i = m_real_terminal.indexOf(terminal);
if (i >= 0)
{
#if QT_VERSION >= QT_VERSION_CHECK(5,14,0)
m_real_terminal.swapItemsAt(i, std::min<int>(level, static_cast<int>(m_real_terminal.size())-1));
#else
auto j = std::min(level, m_real_terminal.size()-1);
std::swap(m_real_terminal.begin()[i], m_real_terminal.begin()[j]);
#endif
return true;
}
return false;
+1 -4
View File
@@ -64,11 +64,8 @@ bool TerminalStripData::fromXml(const QDomElement &xml_element)
"due to wrong tag name. Expected " << this->xmlTagName() << " used " << xml_element.tagName();
return false;
}
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
m_uuid = QUuid::fromString(xml_element.attribute(QStringLiteral("uuid")));
#else
m_uuid = QUuid(xml_element.attribute(QStringLiteral("uuid")));
#endif
for (auto &xml_info :
QETXML::findInDomElement(xml_element.firstChildElement(QStringLiteral("informations")),
@@ -28,6 +28,8 @@
#include "../terminalstrip.h"
#include "../../qetinformation.h"
#include <QUuid>
TerminalStripTreeDockWidget::TerminalStripTreeDockWidget(QETProject *project, QWidget *parent) :
QDockWidget(parent),
ui(new Ui::TerminalStripTreeDockWidget)
@@ -35,11 +37,7 @@ TerminalStripTreeDockWidget::TerminalStripTreeDockWidget(QETProject *project, QW
ui->setupUi(this);
setProject(project);
#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
ui->m_tree_view->expandRecursively(ui->m_tree_view->rootIndex());
#else
ui->m_tree_view->expandAll();
#endif
}
TerminalStripTreeDockWidget::~TerminalStripTreeDockWidget()
@@ -93,11 +91,7 @@ void TerminalStripTreeDockWidget::reload()
buildTree();
#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
ui->m_tree_view->expandRecursively(ui->m_tree_view->rootIndex());
#else
ui->m_tree_view->expandAll();
#endif
//Reselect the tree widget item of the current edited strip
auto item = m_item_strip_H.key(current_);
@@ -20,6 +20,7 @@
#include <QDockWidget>
#include <QPointer>
#include <QHash>
class QETProject;
class QTreeWidgetItem;
+45
View File
@@ -220,6 +220,24 @@ void NumerotationContext::replaceValue(int index, QString content) {
content_[index] = type + "|" + value + "|" + increase + "|" + initvalue + "|" + modulus + "|" + format;
}
/**
@brief NumerotationContext::replaceIncrease
Change how much this part advances per step, leaving its current value,
initial value, modulus and format untouched. Sibling to replaceValue(),
which deliberately never touches this field.
@param index of NC item
@param increase new increase for that item
*/
void NumerotationContext::replaceIncrease(int index, int increase) {
QStringList strl = content_[index].split("|");
QString type = strl.at(0);
QString value = strl.at(1);
QString initvalue = strl.at(3);
QString modulus = strl.size() > 4 ? strl.at(4) : QStringLiteral("0");
QString format = strl.size() > 5 ? strl.at(5) : QString();
content_[index] = type + "|" + value + "|" + QString::number(increase) + "|" + initvalue + "|" + modulus + "|" + format;
}
/**
@brief NumerotationContext::formatOf
@param item : a context item as returned by itemAt()
@@ -230,3 +248,30 @@ QString NumerotationContext::formatOf(const QStringList &item)
{
return item.size() > 5 ? item.at(5) : QString();
}
/**
@brief NumerotationContext::formatValue
@param item : a context item as returned by itemAt()
@return the part's value, zero-padded exactly as
autonum::setSequentialToList() pads it when composing a real label: an
explicit format mask wins, then "ten"/"hundred" parts get their
implicit 2/3-digit width, "alpha" is used as-is, everything else is a
plain number. Kept in step with that function by hand since the two
cannot share code without exposing an assignvariables.cpp-local helper.
*/
QString NumerotationContext::formatValue(const QStringList &item)
{
const QString &type = item.at(0);
const QString &value = item.at(1);
if (type == QLatin1String("alpha"))
return value;
const QString mask = formatOf(item);
if (!mask.isEmpty())
return QString("%1").arg(value.toInt(), mask.length(), 10, QChar('0'));
if (type == QLatin1String("ten") || type == QLatin1String("tenfolio"))
return QString("%1").arg(value.toInt(), 2, 10, QChar('0'));
if (type == QLatin1String("hundred") || type == QLatin1String("hundredfolio"))
return QString("%1").arg(value.toInt(), 3, 10, QChar('0'));
return QString::number(value.toInt());
}
+5
View File
@@ -54,6 +54,11 @@ class NumerotationContext
QDomElement toXml(QDomDocument &, const QString&);
void fromXml(QDomElement &);
void replaceValue(int, QString);
void replaceIncrease(int, int);
/// Zero-pad a part's value the same way the real numbering engine
/// does (autonum::setSequentialToList in assignvariables.cpp), so a
/// UI preview of a part's value matches what actually gets rendered.
static QString formatValue(const QStringList &item);
private:
QStringList content_;
+198 -75
View File
@@ -24,10 +24,14 @@
#include "../../titleblockproperties.h"
#include "../../ui/projectpropertiesdialog.h"
#include "../numerotationcontext.h"
#include "../numerotationcontextcommands.h"
#include "ui_autonumberingdockwidget.h"
#include "../../undocommand/changetitleblockcommand.h"
#include <QComboBox>
#include <QLineEdit>
#include <QSignalBlocker>
#include <QSpinBox>
/**
@brief AutoNumberingDockWidget::AutoNumberingDockWidget
@@ -64,6 +68,29 @@ void AutoNumberingDockWidget::clear()
ui->m_conductor_value_le->clear();
ui->m_element_value_le->clear();
ui->m_folio_value_le->clear();
ui->m_conductor_next_le->clear();
ui->m_element_next_le->clear();
ui->m_folio_next_le->clear();
}
/**
@brief AutoNumberingDockWidget::rowFor
@return the combo/value/increase/next widgets that make up category's row.
*/
AutoNumberingDockWidget::Row AutoNumberingDockWidget::rowFor(AutoNumCategory category) const
{
switch (category) {
case AutoNumCategory::Conductor:
return {ui->m_conductor_cb, ui->m_conductor_value_le,
ui->m_conductor_increase_sb, ui->m_conductor_next_le};
case AutoNumCategory::Element:
return {ui->m_element_cb, ui->m_element_value_le,
ui->m_element_increase_sb, ui->m_element_next_le};
case AutoNumCategory::Folio:
return {ui->m_folio_cb, ui->m_folio_value_le,
ui->m_folio_increase_sb, ui->m_folio_next_le};
}
return {nullptr, nullptr, nullptr, nullptr};
}
void AutoNumberingDockWidget::projectClosed()
@@ -86,33 +113,19 @@ void AutoNumberingDockWidget::setProject(QETProject *project,
//Disconnect previous project
if (m_project && m_project_view)
{
//Conductor Signals
disconnect(m_project, SIGNAL(conductorAutoNumChanged()),
this,SLOT(conductorAutoNumChanged()));
disconnect (m_project,SIGNAL(conductorAutoNumRemoved()),
this,SLOT(conductorAutoNumChanged()));
disconnect (m_project,SIGNAL(conductorAutoNumAdded()),
this,SLOT(conductorAutoNumChanged()));
disconnect(m_project_view,SIGNAL(diagramActivated(DiagramView*)),
this,SLOT(setConductorActive(DiagramView*)));
//Element Signals
disconnect (m_project,SIGNAL(elementAutoNumRemoved(QString)),
this,SLOT(elementAutoNumChanged()));
disconnect (m_project,SIGNAL(elementAutoNumAdded(QString)),
this,SLOT(elementAutoNumChanged()));
disconnect(m_project, &QETProject::conductorAutoNumChanged, this, &AutoNumberingDockWidget::conductorAutoNumChanged);
disconnect(m_project, &QETProject::conductorAutoNumRemoved, this, &AutoNumberingDockWidget::conductorAutoNumChanged);
disconnect(m_project, &QETProject::conductorAutoNumAdded, this, &AutoNumberingDockWidget::conductorAutoNumChanged);
disconnect(m_project_view, &ProjectView::diagramActivated, this, &AutoNumberingDockWidget::setConductorActive);
//Element Signals
disconnect(m_project, &QETProject::elementAutoNumRemoved, this, &AutoNumberingDockWidget::elementAutoNumChanged);
disconnect(m_project, &QETProject::elementAutoNumAdded, this, &AutoNumberingDockWidget::elementAutoNumChanged);
//Folio Signals
disconnect (m_project,SIGNAL(folioAutoNumRemoved()),
this,SLOT(folioAutoNumChanged()));
disconnect (m_project,SIGNAL(folioAutoNumAdded()),
this,SLOT(folioAutoNumChanged()));
disconnect (this,
SIGNAL(folioAutoNumChanged(QString)),
&m_project_view->currentDiagram()->diagram()->border_and_titleblock,
SLOT (slot_setAutoPageNum(QString)));
disconnect(m_project, SIGNAL(defaultTitleBlockPropertiesChanged()),
this,SLOT(setActive()));
disconnect(m_project, &QETProject::folioAutoNumRemoved, this, qOverload<>(&AutoNumberingDockWidget::folioAutoNumChanged));
disconnect(m_project, &QETProject::folioAutoNumAdded, this, qOverload<>(&AutoNumberingDockWidget::folioAutoNumChanged));
disconnect(m_project, &QETProject::defaultTitleBlockPropertiesChanged, this, &AutoNumberingDockWidget::setActive);
//Conductor, Element and Folio Signals
disconnect(m_project, &QETProject::autoNumContextUpdated,
@@ -125,33 +138,19 @@ void AutoNumberingDockWidget::setProject(QETProject *project,
m_project_view = projectview;
this->setEnabled(true);
//Conductor Signals
connect(m_project, SIGNAL(conductorAutoNumChanged()),
this,SLOT(conductorAutoNumChanged()));
connect(m_project,SIGNAL(conductorAutoNumRemoved()),
this,SLOT(conductorAutoNumChanged()));
connect(m_project,SIGNAL(conductorAutoNumAdded()),
this,SLOT(conductorAutoNumChanged()));
connect(m_project_view,SIGNAL(diagramActivated(DiagramView*)),
this,SLOT(setConductorActive(DiagramView*)));
connect(m_project, &QETProject::conductorAutoNumChanged, this, &AutoNumberingDockWidget::conductorAutoNumChanged);
connect(m_project, &QETProject::conductorAutoNumRemoved, this, &AutoNumberingDockWidget::conductorAutoNumChanged);
connect(m_project, &QETProject::conductorAutoNumAdded, this, &AutoNumberingDockWidget::conductorAutoNumChanged);
connect(m_project_view, &ProjectView::diagramActivated, this, &AutoNumberingDockWidget::setConductorActive);
//Element Signals
connect (m_project,SIGNAL(elementAutoNumRemoved(QString)),
this,SLOT(elementAutoNumChanged()));
connect (m_project,SIGNAL(elementAutoNumAdded(QString)),
this,SLOT(elementAutoNumChanged()));
//Element Signals
connect(m_project, &QETProject::elementAutoNumRemoved, this, &AutoNumberingDockWidget::elementAutoNumChanged);
connect(m_project, &QETProject::elementAutoNumAdded, this, &AutoNumberingDockWidget::elementAutoNumChanged);
//Folio Signals
connect (m_project,SIGNAL(folioAutoNumRemoved()),
this,SLOT(folioAutoNumChanged()));
connect (m_project,SIGNAL(folioAutoNumAdded()),
this,SLOT(folioAutoNumChanged()));
connect (this,
SIGNAL(folioAutoNumChanged(QString)),
&m_project_view->currentDiagram()->diagram()->border_and_titleblock,
SLOT (slot_setAutoPageNum(QString)));
connect(m_project, SIGNAL(defaultTitleBlockPropertiesChanged()),
this,SLOT(setActive()));
connect(m_project, &QETProject::folioAutoNumRemoved, this, qOverload<>(&AutoNumberingDockWidget::folioAutoNumChanged));
connect(m_project, &QETProject::folioAutoNumAdded, this, qOverload<>(&AutoNumberingDockWidget::folioAutoNumChanged));
connect(m_project, &QETProject::defaultTitleBlockPropertiesChanged, this, &AutoNumberingDockWidget::setActive);
//Conductor, Element and Folio Signals
connect(m_project, &QETProject::autoNumContextUpdated,
@@ -201,9 +200,9 @@ void AutoNumberingDockWidget::setContext()
//The combo boxes have just been repopulated, so the value fields next
//to them are showing whatever the previous project left there.
refreshValueField(ui->m_conductor_cb, ui->m_conductor_value_le, AutoNumCategory::Conductor);
refreshValueField(ui->m_element_cb, ui->m_element_value_le, AutoNumCategory::Element);
refreshValueField(ui->m_folio_cb, ui->m_folio_value_le, AutoNumCategory::Folio);
refreshRow(AutoNumCategory::Conductor);
refreshRow(AutoNumCategory::Element);
refreshRow(AutoNumCategory::Folio);
this->setActive();
}
@@ -279,7 +278,7 @@ void AutoNumberingDockWidget::on_m_conductor_cb_activated(int)
m_project->setCurrentConductorAutoNum(current_autonum);
m_project_view->currentDiagram()->diagram()->setConductorsAutonumName(current_autonum);
m_project_view->currentDiagram()->diagram()->loadCndFolioSeq();
refreshValueField(ui->m_conductor_cb, ui->m_conductor_value_le, AutoNumCategory::Conductor);
refreshRow(AutoNumCategory::Conductor);
}
/**
@@ -308,7 +307,7 @@ void AutoNumberingDockWidget::on_m_element_cb_activated(int)
{
m_project->setCurrrentElementAutonum(ui->m_element_cb->currentText());
m_project_view->currentDiagram()->diagram()->loadElmtFolioSeq();
refreshValueField(ui->m_element_cb, ui->m_element_value_le, AutoNumCategory::Element);
refreshRow(AutoNumCategory::Element);
}
/**
@@ -345,8 +344,17 @@ void AutoNumberingDockWidget::on_m_folio_cb_activated(int) {
ip.folio = "%id/%total";
m_project->setDefaultTitleBlockProperties(ip);
}
emit(folioAutoNumChanged(current_autonum));
refreshValueField(ui->m_folio_cb, ui->m_folio_value_le, AutoNumCategory::Folio);
if (m_project_view && m_project_view->currentDiagram()) {
Diagram *diagram = m_project_view->currentDiagram()->diagram();
TitleBlockProperties old_properties = diagram->border_and_titleblock.exportTitleBlock();
TitleBlockProperties new_properties = old_properties;
new_properties.auto_page_num = ip.auto_page_num;
new_properties.folio = ip.folio;
if (new_properties != old_properties)
diagram->undoStack().push(new ChangeTitleBlockCommand(diagram, old_properties, new_properties));
}
refreshRow(AutoNumCategory::Folio);
}
void AutoNumberingDockWidget::on_m_configure_pb_clicked()
@@ -389,6 +397,21 @@ void AutoNumberingDockWidget::on_m_folio_value_le_editingFinished()
applyValueField(ui->m_folio_cb, ui->m_folio_value_le, AutoNumCategory::Folio);
}
void AutoNumberingDockWidget::on_m_conductor_increase_sb_valueChanged(int)
{
applyIncreaseField(ui->m_conductor_cb, ui->m_conductor_increase_sb, AutoNumCategory::Conductor);
}
void AutoNumberingDockWidget::on_m_element_increase_sb_valueChanged(int)
{
applyIncreaseField(ui->m_element_cb, ui->m_element_increase_sb, AutoNumCategory::Element);
}
void AutoNumberingDockWidget::on_m_folio_increase_sb_valueChanged(int)
{
applyIncreaseField(ui->m_folio_cb, ui->m_folio_increase_sb, AutoNumCategory::Folio);
}
/**
@brief AutoNumberingDockWidget::contextFor
@return the numerotation context named by combo_box, for category
@@ -455,17 +478,21 @@ int AutoNumberingDockWidget::counterIndex(const NumerotationContext &context)
*/
void AutoNumberingDockWidget::refreshValueFields()
{
//Leave alone a field the user is typing in: numbering an element
//refreshes all three, and overwriting a half-typed value under the
//cursor is worse than showing it a moment out of date. Only this
//automatic path skips; an explicit refresh after a reset or an edit
//still writes, so the field always ends up canonical.
if (!ui->m_conductor_value_le->hasFocus())
refreshValueField(ui->m_conductor_cb, ui->m_conductor_value_le, AutoNumCategory::Conductor);
if (!ui->m_element_value_le->hasFocus())
refreshValueField(ui->m_element_cb, ui->m_element_value_le, AutoNumCategory::Element);
if (!ui->m_folio_value_le->hasFocus())
refreshValueField(ui->m_folio_cb, ui->m_folio_value_le, AutoNumCategory::Folio);
//Leave alone a row the user is typing in: numbering an element
//refreshes all three rows, and overwriting a half-typed value or
//increment under the cursor is worse than showing it a moment out
//of date. Only this automatic path skips; an explicit refresh after
//a reset or an edit still writes, so the row always ends up
//canonical. The next-value preview has no such guard: it is
//read-only, so there is nothing a refresh could clobber.
for (AutoNumCategory category : {AutoNumCategory::Conductor,
AutoNumCategory::Element,
AutoNumCategory::Folio})
{
const Row row = rowFor(category);
if (!row.value->hasFocus() && !row.increase->hasFocus())
refreshRow(category);
}
}
/**
@@ -507,13 +534,114 @@ void AutoNumberingDockWidget::applyValueField(QComboBox *combo_box, QLineEdit *l
const QString typed = line_edit->text();
if (typed.isEmpty() || typed == context.itemAt(index).at(1))
{
refreshValueField(combo_box, line_edit, category);
refreshRow(category);
return;
}
context.replaceValue(index, typed);
storeContext(combo_box, category, context);
refreshValueField(combo_box, line_edit, category);
refreshRow(category);
}
/**
@brief AutoNumberingDockWidget::refreshIncreaseField
Show the counter's current step size (bug #331: previously only
reachable from the full configuration dialog, via "Configurer").
*/
void AutoNumberingDockWidget::refreshIncreaseField(QComboBox *combo_box, QSpinBox *increase_sb, AutoNumCategory category)
{
//QSpinBox::setValue() emits valueChanged() even when called
//programmatically. Without blocking it, this refresh would
//immediately re-trigger on_..._increase_sb_valueChanged() ->
//applyIncreaseField() -> storeContext() -> the project's
//autoNumContextUpdated signal -> refreshValueFields() -> back here.
const QSignalBlocker blocker(increase_sb);
if (!m_project || combo_box->currentText().isEmpty())
{
increase_sb->setEnabled(false);
increase_sb->setValue(increase_sb->minimum());
return;
}
const NumerotationContext context = contextFor(combo_box, category);
const int index = counterIndex(context);
increase_sb->setEnabled(index >= 0);
increase_sb->setValue(index >= 0 ? context.itemAt(index).at(2).toInt()
: increase_sb->minimum());
}
/**
@brief AutoNumberingDockWidget::applyIncreaseField
Write the spin box's step size to the counter it displays (bug #331).
*/
void AutoNumberingDockWidget::applyIncreaseField(QComboBox *combo_box, QSpinBox *increase_sb, AutoNumCategory category)
{
if (!m_project || combo_box->currentText().isEmpty())
return;
NumerotationContext context = contextFor(combo_box, category);
const int index = counterIndex(context);
if (index < 0)
return;
if (increase_sb->value() == context.itemAt(index).at(2).toInt())
return;
context.replaceIncrease(index, increase_sb->value());
storeContext(combo_box, category, context);
refreshRow(category);
}
/**
@brief AutoNumberingDockWidget::refreshNextField
Show what this counter will read after one more step (bug #331: "visualiser
la prochaine numérotation qui sera appliquée"). Advances a copy of the
whole context through NumerotationContextCommands -- the same engine the
Suivant button in the full configuration dialog uses to step a context --
so wrap-and-carry into this part from a following part, or out of it into
a preceding one, comes out identical to what will actually happen when the
number is next consumed.
*/
void AutoNumberingDockWidget::refreshNextField(QComboBox *combo_box, QLineEdit *next_edit, AutoNumCategory category)
{
if (!m_project || combo_box->currentText().isEmpty())
{
next_edit->clear();
next_edit->setEnabled(false);
return;
}
const NumerotationContext context = contextFor(combo_box, category);
const int index = counterIndex(context);
if (index < 0)
{
next_edit->clear();
next_edit->setEnabled(false);
return;
}
Diagram *diagram = (m_project_view && m_project_view->currentDiagram())
? m_project_view->currentDiagram()->diagram()
: nullptr;
NumerotationContextCommands ncc(context, diagram);
const NumerotationContext next_context = ncc.next();
next_edit->setEnabled(true);
next_edit->setText(NumerotationContext::formatValue(next_context.itemAt(index)));
}
/**
@brief AutoNumberingDockWidget::refreshRow
Refresh a category's value, increment and next-value preview together --
every call site that used to refresh just the value field needs the
other two kept in step with it as well.
*/
void AutoNumberingDockWidget::refreshRow(AutoNumCategory category)
{
const Row row = rowFor(category);
refreshValueField(row.combo, row.value, category);
refreshIncreaseField(row.combo, row.increase, category);
refreshNextField(row.combo, row.next, category);
}
/**
@@ -557,10 +685,5 @@ void AutoNumberingDockWidget::resetAutoNum(QComboBox *combo_box, AutoNumCategory
}
storeContext(combo_box, category, context);
switch (category) {
case AutoNumCategory::Conductor: refreshValueField(combo_box, ui->m_conductor_value_le, category); break;
case AutoNumCategory::Element: refreshValueField(combo_box, ui->m_element_value_le, category); break;
case AutoNumCategory::Folio: refreshValueField(combo_box, ui->m_folio_value_le, category); break;
}
refreshRow(category);
}
+34 -1
View File
@@ -25,6 +25,7 @@
class QComboBox;
class QLineEdit;
class QSpinBox;
namespace Ui {
class AutoNumberingDockWidget;
@@ -66,12 +67,27 @@ class AutoNumberingDockWidget : public QDockWidget
void on_m_element_value_le_editingFinished();
void on_m_folio_value_le_editingFinished();
void on_m_conductor_increase_sb_valueChanged(int);
void on_m_element_increase_sb_valueChanged(int);
void on_m_folio_increase_sb_valueChanged(int);
signals:
void folioAutoNumChanged(QString);
private:
enum class AutoNumCategory { Conductor, Element, Folio };
/// The four widgets that make up one category's row, bundled so
/// refreshRow() can be called with just a category instead of
/// four pointers that must always be passed in matching sets.
struct Row
{
QComboBox *combo;
QLineEdit *value;
QSpinBox *increase;
QLineEdit *next;
};
Row rowFor(AutoNumCategory category) const;
/**
@brief resetAutoNum
Reset the numerotation context currently selected in combo_box
@@ -93,6 +109,23 @@ class AutoNumberingDockWidget : public QDockWidget
void refreshValueField(QComboBox *combo_box, QLineEdit *line_edit, AutoNumCategory category);
void applyValueField(QComboBox *combo_box, QLineEdit *line_edit, AutoNumCategory category);
/// Refresh/apply the increment spin box the same way.
void refreshIncreaseField(QComboBox *combo_box, QSpinBox *increase_sb, AutoNumCategory category);
void applyIncreaseField(QComboBox *combo_box, QSpinBox *increase_sb, AutoNumCategory category);
/// Show what the counter will read after one more step, using
/// the same NumerotationContextCommands engine the Suivant
/// button in the full configuration dialog already advances
/// the whole context with -- so the preview can never disagree
/// with what actually happens when the number is next consumed.
void refreshNextField(QComboBox *combo_box, QLineEdit *next_edit, AutoNumCategory category);
/// Refresh a whole row -- value, increment and next-value
/// preview -- in one call. Every refresh call site needs the
/// increment and preview kept in step with the value now, so
/// this replaces refreshValueField() at each of them.
void refreshRow(AutoNumCategory category);
Ui::AutoNumberingDockWidget *ui;
QETProject* m_project = nullptr;
ProjectView* m_project_view = nullptr;
@@ -15,6 +15,36 @@
</property>
<widget class="QWidget" name="dockWidgetContents">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="3">
<widget class="QLabel" name="value_header_label">
<property name="text">
<string>Valeur</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QLabel" name="increase_header_label">
<property name="text">
<string>Incrément</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="0" column="5">
<widget class="QLabel" name="next_header_label">
<property name="text">
<string>Suivant</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="m_element_cb"/>
</item>
@@ -61,6 +91,47 @@
</property>
</widget>
</item>
<item row="2" column="4">
<widget class="QSpinBox" name="m_conductor_increase_sb">
<property name="maximumSize">
<size>
<width>55</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Incrément : valeur ajoutée au compteur à chaque nouvelle numérotation</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="accelerated">
<bool>true</bool>
</property>
<property name="minimum">
<number>0</number>
</property>
</widget>
</item>
<item row="2" column="5">
<widget class="QLineEdit" name="m_conductor_next_le">
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Prochaine valeur qui sera appliquée avec cet incrément</string>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label">
<property name="text">
@@ -108,6 +179,47 @@
</property>
</widget>
</item>
<item row="3" column="4">
<widget class="QSpinBox" name="m_element_increase_sb">
<property name="maximumSize">
<size>
<width>55</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Incrément : valeur ajoutée au compteur à chaque nouvelle numérotation</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="accelerated">
<bool>true</bool>
</property>
<property name="minimum">
<number>0</number>
</property>
</widget>
</item>
<item row="3" column="5">
<widget class="QLineEdit" name="m_element_next_le">
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Prochaine valeur qui sera appliquée avec cet incrément</string>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="m_folio_cb"/>
</item>
@@ -144,6 +256,47 @@
</property>
</widget>
</item>
<item row="4" column="4">
<widget class="QSpinBox" name="m_folio_increase_sb">
<property name="maximumSize">
<size>
<width>55</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Incrément : valeur ajoutée au compteur à chaque nouvelle numérotation</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="accelerated">
<bool>true</bool>
</property>
<property name="minimum">
<number>0</number>
</property>
</widget>
</item>
<item row="4" column="5">
<widget class="QLineEdit" name="m_folio_next_le">
<property name="maximumSize">
<size>
<width>70</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Prochaine valeur qui sera appliquée avec cet incrément</string>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="6" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
@@ -141,7 +141,6 @@
</property>
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
@@ -163,7 +162,6 @@
<widget class="QLabel" name="label_2">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
@@ -238,7 +236,6 @@
<widget class="QLabel" name="label_3">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
@@ -328,7 +325,6 @@
<widget class="QWidget" name="folioWidget" native="true">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
<kerning>true</kerning>
</font>
@@ -338,7 +334,6 @@
<widget class="QLabel" name="label_4">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
+3 -3
View File
@@ -112,7 +112,7 @@ void SelectAutonumW::setContext(const NumerotationContext &context)
else {
for (int i=0; i<m_context.size(); ++i) { //build with the content of @context
NumPartEditorW *part= new NumPartEditorW(m_context, i, m_edited_type, this);
connect (part, SIGNAL(changed()), this, SLOT(applyEnable()));
connect(part, &NumPartEditorW::changed, this, [this]() { applyEnable(); });
num_part_list_ << part;
ui -> editor_layout -> addWidget(part);
}
@@ -145,7 +145,7 @@ void SelectAutonumW::on_add_button_clicked()
{
applyEnable(false);
NumPartEditorW *part = new NumPartEditorW(m_edited_type, this);
connect (part, SIGNAL(changed()), this, SLOT(applyEnable()));
connect(part, &NumPartEditorW::changed, this, [this]() { applyEnable(); });
num_part_list_ << part;
ui -> editor_layout -> addWidget(part);
ui -> remove_button -> setEnabled(true);
@@ -160,7 +160,7 @@ void SelectAutonumW::on_remove_button_clicked()
//remove if @num_part_list contains more than one item
if (num_part_list_.size() > 1) {
NumPartEditorW *part = num_part_list_.takeLast();
disconnect(part, SIGNAL(changed()), this, SLOT(applyEnable()));
// deliberately not disconnecting as not possible to resolve with lambda and will happen automatically when the object "part" is destroyed.
delete part;
if (num_part_list_.size() == 1) {
ui -> remove_button -> setDisabled(true);
+237
View File
@@ -0,0 +1,237 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "autobreakconductor.h"
#include "diagram.h"
#include "qetproject.h"
#include "conductorautonumerotation.h"
#include "qetgraphicsitem/element.h"
#include "qetgraphicsitem/terminal.h"
#include "qetgraphicsitem/conductor.h"
#include "undocommand/addgraphicsobjectcommand.h"
#include "undocommand/deleteqgraphicsitemcommand.h"
#include <QPainterPath>
#include <QMap>
#include <limits>
namespace {
/**
@brief distanceToSegment
@param point : point to measure from
@param segment : the finite segment (not the infinite line through it)
@return the distance from @a point to the closest point actually on
@a segment. Unlike QET::orthogonalProjection(), which reports a hit
for any point on the segment's infinite extension, this clamps the
projection to the segment itself: a point collinear with a wire but
past its actual drawn end is correctly reported as far away, not "on"
the wire.
*/
qreal distanceToSegment(const QPointF &point, const QLineF &segment)
{
const QPointF a = segment.p1();
const QPointF b = segment.p2();
const QPointF ab = b - a;
const qreal len2 = QPointF::dotProduct(ab, ab);
if (len2 <= 0.0)
return QLineF(point, a).length();
qreal t = QPointF::dotProduct(point - a, ab) / len2;
t = qBound(0.0, t, 1.0);
return QLineF(point, a + t * ab).length();
}
} // anonymous namespace
/**
@brief autoBreakConductors
For each terminal of @a element, check if its dock point lies on an
existing conductor. If so, break the conductor and reconnect through
the element's terminal. Broken conductors from the same circuit
(sharing the same far-end endpoint) are broken together; independent
crossing conductors are left untouched.
*/
QSet<Terminal *> autoBreakConductors(
Diagram *diagram,
Element *element,
QUndoCommand *parent,
QList<Conductor *> &conductors_handled,
QSet<Terminal *> &used_terminals)
{
QSet<Terminal *> broken_endpoints;
if (!diagram->project()->autoBreakConductor())
return broken_endpoints;
foreach (Terminal *t, element->terminals())
{
if (used_terminals.contains(t))
continue;
QPointF t_dock = t->dockConductor();
struct ConductorMatch {
Conductor *conductor;
Terminal *connect_to;
Terminal *other;
};
QList<ConductorMatch> all_matches;
foreach (Conductor *c, diagram->conductors())
{
if (conductors_handled.contains(c) ||
c->terminal1->parentElement() == element ||
c->terminal2->parentElement() == element)
continue;
QPointF local_dock = c->mapFromScene(t_dock);
bool point_on_conductor = false;
QPainterPath path = c->path();
for (int i = 0; i < path.elementCount() - 1; ++i)
{
const QPainterPath::Element &e1 = path.elementAt(i);
const QPainterPath::Element &e2 = path.elementAt(i + 1);
QLineF segment(QPointF(e1.x, e1.y), QPointF(e2.x, e2.y));
if (distanceToSegment(local_dock, segment) < 5.0)
{
point_on_conductor = true;
break;
}
}
if (!point_on_conductor)
continue;
Terminal *c1 = c->terminal1;
Terminal *c2 = c->terminal2;
QPointF c1_dock = c1->dockConductor();
QPointF c2_dock = c2->dockConductor();
Terminal *connect_to = nullptr;
Terminal *other = nullptr;
switch (t->orientation()) {
case Qet::North:
if (c1_dock.y() < t_dock.y()) { connect_to = c1; other = c2; }
else if (c2_dock.y() < t_dock.y()) { connect_to = c2; other = c1; }
break;
case Qet::South:
if (c1_dock.y() > t_dock.y()) { connect_to = c1; other = c2; }
else if (c2_dock.y() > t_dock.y()) { connect_to = c2; other = c1; }
break;
case Qet::East:
if (c1_dock.x() > t_dock.x()) { connect_to = c1; other = c2; }
else if (c2_dock.x() > t_dock.x()) { connect_to = c2; other = c1; }
break;
case Qet::West:
if (c1_dock.x() < t_dock.x()) { connect_to = c1; other = c2; }
else if (c2_dock.x() < t_dock.x()) { connect_to = c2; other = c1; }
break;
}
if (!connect_to) {
qreal d1 = QLineF(t_dock, c1_dock).length();
qreal d2 = QLineF(t_dock, c2_dock).length();
if (d1 <= d2) { connect_to = c1; other = c2; }
else { connect_to = c2; other = c1; }
}
all_matches.append({c, connect_to, other});
}
if (all_matches.isEmpty())
continue;
QMap<Terminal *, QList<ConductorMatch>> groups;
for (const auto &m : all_matches) {
groups[m.other].append(m);
}
Terminal *best_other = nullptr;
int best_count = 0;
for (auto it = groups.constBegin(); it != groups.constEnd(); ++it) {
if (it.value().size() > best_count) {
best_count = it.value().size();
best_other = it.key();
}
}
const QList<ConductorMatch> &matches = groups[best_other];
used_terminals.insert(t);
DiagramContent content;
for (const auto &m : matches) {
content.m_other_conductors.append(m.conductor);
conductors_handled.append(m.conductor);
}
new DeleteQGraphicsItemCommand(diagram, content, parent);
for (const auto &m : matches) {
Conductor *new_c = new Conductor(m.connect_to, t);
new AddGraphicsObjectCommand(new_c, diagram, QPointF(), parent);
ConductorAutoNumerotation can(new_c, diagram, parent);
can.numerate();
if (diagram->freezeNewConductors() || diagram->project()->isFreezeNewConductors())
new_c->setFreezeLabel(true);
broken_endpoints.insert(m.connect_to);
}
QPointF other_dock = best_other->dockConductor();
Terminal *other_terminal = nullptr;
qreal best_dist = std::numeric_limits<qreal>::max();
foreach (Terminal *ot, element->terminals())
{
if (used_terminals.contains(ot))
continue;
QPointF ot_dock = ot->dockConductor();
bool orientation_ok = false;
switch (ot->orientation()) {
case Qet::North: orientation_ok = other_dock.y() < ot_dock.y(); break;
case Qet::South: orientation_ok = other_dock.y() > ot_dock.y(); break;
case Qet::East: orientation_ok = other_dock.x() > ot_dock.x(); break;
case Qet::West: orientation_ok = other_dock.x() < ot_dock.x(); break;
}
if (!orientation_ok)
continue;
qreal dist = QLineF(ot_dock, other_dock).length();
if (dist < best_dist) {
best_dist = dist;
other_terminal = ot;
}
}
if (other_terminal) {
Conductor *new_c2 = new Conductor(best_other, other_terminal);
new AddGraphicsObjectCommand(new_c2, diagram, QPointF(), parent);
ConductorAutoNumerotation can2(new_c2, diagram, parent);
can2.numerate();
if (diagram->freezeNewConductors() || diagram->project()->isFreezeNewConductors())
new_c2->setFreezeLabel(true);
broken_endpoints.insert(best_other);
used_terminals.insert(other_terminal);
}
}
return broken_endpoints;
}
+57
View File
@@ -0,0 +1,57 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef AUTOBREAKCONDUCTOR_H
#define AUTOBREAKCONDUCTOR_H
#include <QSet>
#include <QList>
class Diagram;
class Element;
class Terminal;
class Conductor;
class QUndoCommand;
/**
@brief autoBreakConductors
For each terminal of @a element, check if its dock point lies on an
existing conductor. If so, break the conductor and reconnect through the
element's terminal. Broken conductors from the same circuit (sharing the
same far-end endpoint) are broken together; independent crossing
conductors are left untouched.
@param diagram the diagram containing the conductors
@param element the element whose terminals trigger breaks
@param parent undo command under which break/reconnect
sub-commands are created
@param conductors_handled shared list of conductors already claimed by
a previous call in the same batch (prevents
double-processing when multiple elements are
broken in one pass)
@param used_terminals shared set of terminals already used as
connect_to or other_terminal in the same batch
@return set of terminals that were connected to a broken conductor's
far-end (useful for preventing duplicate auto-connect)
*/
QSet<Terminal *> autoBreakConductors(
Diagram *diagram,
Element *element,
QUndoCommand *parent,
QList<Conductor *> &conductors_handled,
QSet<Terminal *> &used_terminals);
#endif // AUTOBREAKCONDUCTOR_H
+2
View File
@@ -73,6 +73,8 @@ class BorderTitleBlock : public QObject
return(rows_count_ * rows_height_); }
/// @return la rows header width, in pixels
qreal rowsHeaderWidth() const { return(rows_header_width_); }
/// @return the edge where title block is docked
Qt::Edge titleBlockEdge() const { return(m_edge); }
// border - title block = diagram
/**
+37 -13
View File
@@ -109,7 +109,16 @@ QString diagramStem(Diagram *diagram, int index)
}
/// Render @p diagram into @p painter, fitting @p target to the page rect.
void renderDiagram(Diagram *diagram, QPainter &painter, const QRectF &target)
/// @p showTerminals: paint terminal markers (red stroke + blue docking
/// dot) and terminal names, as the interactive editor does. Off by
/// default — Terminal::paint() draws them whenever the diagram's
/// drawTerminals()/drawTerminalNames() flags are set (default true, so
/// headless export used to ship them as editor UI); the GUI export
/// dialog already defaults to clearing them via
/// Diagram::applyProperties(). Pass true (--show-terminals) to keep
/// them, e.g. to visually debug an unconnected pin.
void renderDiagram(Diagram *diagram, QPainter &painter, const QRectF &target,
bool showTerminals = false)
{
const QRect source = diagramRect(diagram);
// Export without the editor grid: drawBackground() only paints it when
@@ -117,14 +126,21 @@ void renderDiagram(Diagram *diagram, QPainter &painter, const QRectF &target)
// and restore it afterwards.
const bool was_drawing_grid = diagram->displayGrid();
const bool was_drawing_guides = diagram->displayGuides();
const bool was_drawing_terminals = diagram->drawTerminals();
const bool was_drawing_terminal_names = diagram->drawTerminalNames();
diagram->setDisplayGrid(false);
diagram->setDisplayGuides(false);
diagram->setDrawTerminals(showTerminals);
diagram->setDrawTerminalNames(showTerminals);
diagram->render(&painter, target, source, Qt::KeepAspectRatio);
diagram->setDisplayGrid(was_drawing_grid);
diagram->setDisplayGuides(was_drawing_guides);
diagram->setDrawTerminals(was_drawing_terminals);
diagram->setDrawTerminalNames(was_drawing_terminal_names);
}
int exportPdf(QETProject &project, const QString &output)
int exportPdf(QETProject &project, const QString &output,
bool showTerminals = false)
{
const QList<Diagram *> diagrams = project.diagrams();
if (diagrams.isEmpty()) {
@@ -164,7 +180,7 @@ int exportPdf(QETProject &project, const QString &output)
}
const QRectF target(0, 0,
writer.width(), writer.height());
renderDiagram(diagram, painter, target);
renderDiagram(diagram, painter, target, showTerminals);
// Inject clickable cross-reference / folio-report hyperlinks for this
// page. The geometry is rebuilt from the QPdfWriter (not a QPrinter):
@@ -214,7 +230,7 @@ int exportPdf(QETProject &project, const QString &output)
}
int exportImages(QETProject &project, const QString &format,
const QString &out_dir)
const QString &out_dir, bool showTerminals = false)
{
const QList<Diagram *> diagrams = project.diagrams();
if (diagrams.isEmpty()) {
@@ -237,14 +253,16 @@ int exportImages(QETProject &project, const QString &format,
gen.setViewBox(QRect(0, 0, r.width(), r.height()));
gen.setTitle(diagram->title());
QPainter painter(&gen);
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()));
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()),
showTerminals);
painter.end();
} else { // png
QImage image(r.size(), QImage::Format_ARGB32);
image.fill(Qt::white);
QPainter painter(&image);
painter.setRenderHint(QPainter::Antialiasing, true);
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()));
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()),
showTerminals);
painter.end();
if (!image.save(path)) {
err << "Failed to write '" << path << "'.\n";
@@ -768,13 +786,19 @@ bool isExportRequest(const QStringList &args)
int run(const QStringList &args)
{
// --show-terminals is a standalone switch (not tied to a position),
// so pull it out before the positional project/output arguments are
// collected below.
QStringList filtered = args;
const bool showTerminals = filtered.removeAll("--show-terminals") > 0;
QString flag;
QStringList rest;
for (int i = 0; i < args.size(); ++i) {
if (exportFlags().contains(args.at(i))) {
flag = args.at(i);
for (int j = i + 1; j < args.size(); ++j)
rest << args.at(j);
for (int i = 0; i < filtered.size(); ++i) {
if (exportFlags().contains(filtered.at(i))) {
flag = filtered.at(i);
for (int j = i + 1; j < filtered.size(); ++j)
rest << filtered.at(j);
break;
}
}
@@ -818,7 +842,7 @@ int run(const QStringList &args)
return 2;
}
if (format == "pdf")
return exportPdf(project, output);
return exportPdf(project, output, showTerminals);
if (format == "cables" || format == "wires")
return exportCsv(project, format, output);
if (format == "bom")
@@ -831,7 +855,7 @@ int run(const QStringList &args)
return resaveProject(project, output);
if (format == "settb")
return setTitleBlock(project, output, rest.mid(2));
return exportImages(project, format, output);
return exportImages(project, format, output, showTerminals);
}
} // namespace CLIExport
+7 -3
View File
@@ -42,9 +42,9 @@ namespace CLIExport {
@return process exit code (0 on success).
Usage:
qelectrotech --export-pdf <project.qet> <output.pdf>
qelectrotech --export-png <project.qet> <output_dir>
qelectrotech --export-svg <project.qet> <output_dir>
qelectrotech --export-pdf <project.qet> <output.pdf> [--show-terminals]
qelectrotech --export-png <project.qet> <output_dir> [--show-terminals]
qelectrotech --export-svg <project.qet> <output_dir> [--show-terminals]
qelectrotech --export-cables <project.qet> <output.csv>
qelectrotech --export-wires <project.qet> <output.csv>
qelectrotech --export-bom <project.qet> <output.csv>
@@ -57,6 +57,10 @@ namespace CLIExport {
PDF: one multi-page document (one diagram per page).
PNG/SVG: one file per diagram, named <output_dir>/<NN>_<title>.<ext>.
--show-terminals: also paint terminal markers (red stroke + blue
docking dot) and terminal names, as the interactive editor
does; off by default, matching the GUI export dialog's
default. Has no effect on the non-image export modes.
cables: wiring list (one row per conductor) as CSV.
wires: list of distinct wire numbers as CSV.
bom: bill of materials (one row per element) as CSV.
+1 -7
View File
@@ -72,14 +72,8 @@ bool ConductorNumExport::toCsv()
if (file.open(QIODevice::WriteOnly | QIODevice::Text))
{
QTextStream stream(&file);
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // ### Qt 6: remove
stream << wiresNum() << endl;
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.15 or later")
#endif
stream << wiresNum() << &Qt::endl(stream);
#endif
}
else {
return false;
-7
View File
@@ -811,14 +811,7 @@ void ConductorProperties::readStyle(const QString &style_string) {
if (style_string.isEmpty()) return;
// recupere la liste des couples style / valeur
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
QStringList styles = style_string.split(";", QString::SkipEmptyParts);
#else
#if TODO_LIST
#pragma message("@TODO remove code QString::SkipEmptyParts for QT 5.14 or later")
#endif
QStringList styles = style_string.split(";", Qt::SkipEmptyParts);
#endif
QRegularExpression Rx("^(?<name>[a-z-]+): (?<value>[a-z-]+)$");
if (!Rx.isValid())
+3 -4
View File
@@ -74,10 +74,9 @@ ConfigDialog::ConfigDialog(QWidget *parent) : QDialog(parent) {
setLayout(dialog_layout);
// connexion signaux / slots
connect(buttons, SIGNAL(accepted()), this, SLOT(applyConf()));
connect(buttons, SIGNAL(rejected()), this, SLOT(reject()));
connect(pages_list, SIGNAL(currentRowChanged(int)),
pages_widget, SLOT(setCurrentIndex(int)));
connect(buttons, &QDialogButtonBox::accepted, this, &ConfigDialog::applyConf);
connect(buttons, &QDialogButtonBox::rejected, this, &ConfigDialog::reject);
connect(pages_list, &QListWidget::currentRowChanged, pages_widget, &QStackedWidget::setCurrentIndex);
// set maximum a bit smaller than available size = (screen-size - Task-Bar):
setMaximumSize((int)(0.94 * MachineInfo::instance()->i_max_available_width()),
+249
View File
@@ -21,7 +21,9 @@
#include "../diagramposition.h"
#include "../elementprovider.h"
#include "../qetapp.h"
#include "../qetgraphicsitem/conductor.h"
#include "../qetgraphicsitem/element.h"
#include "../qetgraphicsitem/terminal.h"
#include "../qetinformation.h"
#include "../qetproject.h"
@@ -87,6 +89,7 @@ void projectDataBase::updateDB()
populateDiagramInfoTable();
populateElementTable();
populateElementInfoTable();
populateConductorTable();
emit dataBaseUpdated();
}
@@ -245,6 +248,132 @@ void projectDataBase::diagramOrderChanged()
{
}
/**
@brief projectDataBase::addConductor
@param conductor
*/
void projectDataBase::addConductor(Conductor *conductor)
{
if (!conductor || !conductor->diagram()) {
qDebug() << "projectDataBase::addConductor: null conductor or diagram";
return;
}
//Both endpoints must belong to an element: the terminal table is keyed
//on (terminal, element) and a terminal with no parent has no identity
//to key on. Terminals whose *definition* predates terminal uuids are
//fine -- Terminal::stableUuid() derives one from the terminal's local
//position, which is what the project format itself matches on.
if (!conductor->terminal1->parentElement()
|| !conductor->terminal2->parentElement()) {
return;
}
insertTerminal(conductor->terminal1);
insertTerminal(conductor->terminal2);
watchConductor(conductor);
bindConductorValues(m_insert_conductor_query, conductor, conductor->diagram());
if (!m_insert_conductor_query.exec()) {
qDebug() << "projectDataBase::addConductor insert error : " << m_insert_conductor_query.lastError();
} else {
emit dataBaseUpdated();
}
}
/**
@brief projectDataBase::removeConductor
@param conductor
*/
void projectDataBase::removeConductor(Conductor *conductor)
{
m_remove_conductor_query.bindValue(":uuid", conductor->uuid().toString());
if (!m_remove_conductor_query.exec()) {
qDebug() << "projectDataBase::removeConductor delete error : " << m_remove_conductor_query.lastError();
} else {
emit dataBaseUpdated();
}
}
/**
@brief projectDataBase::updateConductor
Refresh the mutable columns of an already-inserted conductor.
Only the text (the wire number) can change without the conductor being
removed and re-added: its endpoints are fixed for its lifetime. Without
this, renaming a wire left the database holding the old number and the
wiring list showed a stale value until the next full repopulate.
@param conductor
*/
void projectDataBase::updateConductor(Conductor *conductor)
{
if (!conductor) {
return;
}
m_update_conductor_query.bindValue(QStringLiteral(":uuid"), conductor->uuid().toString());
m_update_conductor_query.bindValue(QStringLiteral(":text"), conductor->properties().text);
if (!m_update_conductor_query.exec()) {
qDebug() << "projectDataBase::updateConductor update error : " << m_update_conductor_query.lastError();
}
//Deliberately no dataBaseUpdated() here, unlike add/remove. The only
//column this touches is the wire text, which no view watched by
//ProjectDBModel displays -- the nomenclature shows elements, and its
//wire_count changes when a conductor appears or disappears, not when
//it is renamed. Emitting would make every ProjectDBModel re-run its
//query, and auto-numbering renames every conductor in the project in
//one pass.
}
/**
@brief projectDataBase::watchConductor
Keep this conductor's row in step with its properties.
Conductor::setProperties() has a dozen call sites (auto-numbering, the
properties dialog, element moves, deletion re-links...), so listening to
the signal it already emits is the only way to catch them all -- and the
only way to catch the ones added later. Qt::UniqueConnection makes a
repeated insert or a full repopulate harmless.
@param conductor
*/
void projectDataBase::watchConductor(Conductor *conductor)
{
connect(conductor, &Conductor::propertiesChange,
this, &projectDataBase::conductorPropertiesChanged,
Qt::UniqueConnection);
}
/**
@brief projectDataBase::conductorPropertiesChanged
*/
void projectDataBase::conductorPropertiesChanged()
{
if (auto *conductor = qobject_cast<Conductor *>(sender())) {
updateConductor(conductor);
}
}
/**
@brief projectDataBase::bindConductorValues
One binder for both insert paths, so a conductor added to a live diagram
and one read from a file can never drift apart -- the same reason
bindElementValues() exists for elements.
@param query
@param conductor
@param diagram : the diagram the conductor belongs to
*/
void projectDataBase::bindConductorValues(QSqlQuery &query, Conductor *conductor, Diagram *diagram)
{
query.bindValue(QStringLiteral(":uuid"), conductor->uuid().toString());
query.bindValue(QStringLiteral(":diagram_uuid"), diagram->uuid().toString());
query.bindValue(QStringLiteral(":terminal1_uuid"), conductor->terminal1->stableUuid().toString());
query.bindValue(QStringLiteral(":terminal1_element_uuid"), conductor->terminal1->parentElement()->uuid().toString());
query.bindValue(QStringLiteral(":terminal2_uuid"), conductor->terminal2->stableUuid().toString());
query.bindValue(QStringLiteral(":terminal2_element_uuid"), conductor->terminal2->parentElement()->uuid().toString());
query.bindValue(QStringLiteral(":text"), conductor->properties().text);
}
/**
@brief projectDataBase::createDataBase
Create the data base
@@ -323,6 +452,57 @@ bool projectDataBase::createDataBase()
qDebug() << " element_info_table query : " << query_.lastError();
}
//Create the terminal table.
//Terminal::uuid() is the terminal-position id baked into the catalog
//.elmt definition (e.g. "the top terminal") -- identical across every
//placed instance of that catalog element, not a per-instance id. A
//terminal instance is only uniquely identified by (uuid, element_uuid)
//together, so that pair is the primary key here, not uuid alone.
QString terminal_table("CREATE TABLE terminal"
"( "
"uuid VARCHAR(50) NOT NULL, "
"element_uuid VARCHAR(50) NOT NULL,"
"name VARCHAR(50),"
"PRIMARY KEY (uuid, element_uuid),"
"FOREIGN KEY (element_uuid) REFERENCES element (uuid)"
")");
if (!query_.exec(terminal_table)) {
qDebug() << "terminal_table query : "<< query_.lastError();
}
//Create the conductor table
QString conductor_table("CREATE TABLE conductor"
"( "
"uuid VARCHAR(50) PRIMARY KEY NOT NULL, "
"diagram_uuid VARCHAR(50) NOT NULL,"
"terminal1_uuid VARCHAR(50) NOT NULL,"
"terminal1_element_uuid VARCHAR(50) NOT NULL,"
"terminal2_uuid VARCHAR(50) NOT NULL,"
"terminal2_element_uuid VARCHAR(50) NOT NULL,"
"text VARCHAR(100),"
"FOREIGN KEY (diagram_uuid) REFERENCES diagram (uuid),"
"FOREIGN KEY (terminal1_uuid, terminal1_element_uuid) REFERENCES terminal (uuid, element_uuid),"
"FOREIGN KEY (terminal2_uuid, terminal2_element_uuid) REFERENCES terminal (uuid, element_uuid)"
")");
if (!query_.exec(conductor_table)) {
qDebug() << "conductor_table query : "<< query_.lastError();
}
//The element-facing columns are looked up per element row, not per
//conductor row: element_nomenclature_view carries a correlated
//subquery counting the wires touching each element. Without these
//indexes each element row full-scans the conductor table, which grows
//as elements x conductors.
for (const QString &index_ : {
QStringLiteral("CREATE INDEX idx_conductor_terminal1_element ON conductor (terminal1_element_uuid)"),
QStringLiteral("CREATE INDEX idx_conductor_terminal2_element ON conductor (terminal2_element_uuid)"),
QStringLiteral("CREATE INDEX idx_conductor_diagram ON conductor (diagram_uuid)") })
{
if (!query_.exec(index_)) {
qDebug() << "conductor index query : " << query_.lastError();
}
}
createElementNomenclatureView();
createSummaryView();
prepareQuery();
@@ -534,6 +714,58 @@ void projectDataBase::populateDiagramInfoTable()
}
}
/**
@brief projectDataBase::populateConductorTable
Populate the terminal and conductor tables. Terminals only matter here
in the context of a conductor referencing them, so their population is
folded into this method rather than tracked independently.
*/
void projectDataBase::populateConductorTable()
{
QSqlQuery query(m_data_base);
query.exec(QStringLiteral("DELETE FROM conductor"));
query.exec(QStringLiteral("DELETE FROM terminal"));
for (auto *diagram : m_project->diagrams())
{
const auto conductor_list = diagram->conductors();
for (auto *conductor : conductor_list)
{
//See addConductor(): only a terminal with no parent element is
//skipped. A missing terminal uuid is handled by stableUuid().
if (!conductor->terminal1->parentElement()
|| !conductor->terminal2->parentElement()) {
continue;
}
insertTerminal(conductor->terminal1);
insertTerminal(conductor->terminal2);
watchConductor(conductor);
bindConductorValues(m_insert_conductor_query, conductor, diagram);
if (!m_insert_conductor_query.exec()) {
qDebug() << "projectDataBase::populateConductorTable insert error : " << m_insert_conductor_query.lastError();
}
}
}
}
/**
@brief projectDataBase::insertTerminal
Insert (or, if already present -- e.g. a junction shared by several
conductors -- silently keep) @terminal in the terminal table.
@param terminal
*/
void projectDataBase::insertTerminal(Terminal *terminal)
{
m_insert_terminal_query.bindValue(":uuid", terminal->stableUuid().toString());
m_insert_terminal_query.bindValue(":element_uuid", terminal->parentElement()->uuid().toString());
m_insert_terminal_query.bindValue(":name", terminal->name());
if (!m_insert_terminal_query.exec()) {
qDebug() << "projectDataBase::insertTerminal insert error : " << m_insert_terminal_query.lastError();
}
}
void projectDataBase::prepareQuery()
{
//INSERT DIAGRAM
@@ -606,6 +838,23 @@ void projectDataBase::prepareQuery()
update_str.append(" WHERE element_uuid = :uuid");
m_update_element_query = QSqlQuery(m_data_base);
m_update_element_query.prepare(update_str);
//INSERT TERMINAL
m_insert_terminal_query = QSqlQuery(m_data_base);
m_insert_terminal_query.prepare("INSERT OR IGNORE INTO terminal (uuid, element_uuid, name) VALUES (:uuid, :element_uuid, :name)");
//INSERT CONDUCTOR
m_insert_conductor_query = QSqlQuery(m_data_base);
m_insert_conductor_query.prepare("INSERT INTO conductor (uuid, diagram_uuid, terminal1_uuid, terminal1_element_uuid, terminal2_uuid, terminal2_element_uuid, text) "
"VALUES (:uuid, :diagram_uuid, :terminal1_uuid, :terminal1_element_uuid, :terminal2_uuid, :terminal2_element_uuid, :text)");
//UPDATE CONDUCTOR
m_update_conductor_query = QSqlQuery(m_data_base);
m_update_conductor_query.prepare(QStringLiteral("UPDATE conductor SET text = :text WHERE uuid = :uuid"));
//REMOVE CONDUCTOR
m_remove_conductor_query = QSqlQuery(m_data_base);
m_remove_conductor_query.prepare("DELETE FROM conductor WHERE uuid=:uuid");
}
/**
+21 -1
View File
@@ -27,6 +27,8 @@
class Element;
class QETProject;
class Diagram;
class Conductor;
class Terminal;
class sqlite3;
/**
@@ -58,6 +60,16 @@ class projectDataBase : public QObject
void diagramInfoChanged (Diagram *diagram);
void diagramOrderChanged();
void addConductor (Conductor *conductor);
void removeConductor (Conductor *conductor);
void updateConductor (Conductor *conductor);
private slots:
//Refresh the sender()'s row after Conductor::setProperties().
void conductorPropertiesChanged();
public:
signals:
void dataBaseUpdated();
@@ -69,6 +81,10 @@ class projectDataBase : public QObject
void populateElementTable();
void populateElementInfoTable();
void populateDiagramInfoTable();
void populateConductorTable();
void bindConductorValues(QSqlQuery &query, Conductor *conductor, Diagram *diagram);
void watchConductor(Conductor *conductor);
void insertTerminal(Terminal *terminal);
void prepareQuery();
static QHash<QString, QString> elementInfoToString(
Element *elmt);
@@ -86,7 +102,11 @@ class projectDataBase : public QObject
m_insert_diagram_info_query,
m_update_diagram_info_query,
m_diagram_order_changed,
m_diagram_info_order_changed;
m_diagram_info_order_changed,
m_insert_terminal_query,
m_insert_conductor_query,
m_update_conductor_query,
m_remove_conductor_query;
#ifdef QET_EXPORT_PROJECT_DB
public:
+5 -11
View File
@@ -48,14 +48,8 @@ ElementQueryWidget::ElementQueryWidget(QWidget *parent) :
m_button_group.addButton(ui->m_protection_cb, 5);
m_button_group.addButton(ui->m_thumbnail_cb, 6);
m_button_group.addButton(ui->m_plc_cb, 7);
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // ### Qt 6: remove
connect(&m_button_group, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::buttonClicked), [this](int id)
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.15 or later")
#endif
connect(&m_button_group, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::idClicked), [this](int id)
#endif
{
auto check_box = static_cast<QCheckBox *>(m_button_group.button(0));
if (id == 0)
@@ -384,10 +378,10 @@ QString ElementQueryWidget::queryStr() const
where.clear();
}
QString exclude_condition = "(exclude_from_bom IS NULL OR exclude_from_bom != '1')";
filter_ += " AND " + exclude_condition;
// -------------------------------------------------------------
// exclude_from_bom is already filtered by element_nomenclature_view
// (see createElementNomenclatureView() in projectdatabase.cpp); this
// widget's query reads FROM that view, so a flagged element never
// reaches this point in the first place.
if (where.isEmpty() && !filter_.isEmpty()) {
filter_.remove(0, 4); //Remove the first " AND" of filter.
+21 -10
View File
@@ -67,6 +67,7 @@ Diagram::Diagram(QETProject *project) :
m_project (project),
use_border_ (true),
draw_terminals_ (true),
draw_terminal_names_ (true),
draw_colored_conductors_ (true),
m_event_interface (nullptr),
m_freeze_new_elements (false),
@@ -1555,14 +1556,6 @@ bool Diagram::fromXml(QDomElement &document,
if (content_ptr) {
content_ptr -> m_elements = added_elements;
content_ptr -> m_conductors_to_move = added_conductors;
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ### Qt 6: remove
content_ptr -> m_text_fields = added_texts.toSet();
content_ptr -> m_images = added_images.toSet();
content_ptr -> m_shapes = added_shapes.toSet();
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.14 or later")
#endif
content_ptr -> m_text_fields = QSet<IndependentTextItem *>(
added_texts.begin(),
added_texts.end());
@@ -1573,7 +1566,6 @@ bool Diagram::fromXml(QDomElement &document,
added_shapes.begin(),
added_shapes.end());
content_ptr->m_terminal_strip.swap(added_strips);
#endif
content_ptr->m_tables.swap(added_tables);
}
@@ -1674,6 +1666,7 @@ void Diagram::addItem(QGraphicsItem *item)
conductor->terminal1->addConductor(conductor);
conductor->terminal2->addConductor(conductor);
conductor->calculateTextItemPosition();
m_project->dataBase()->addConductor(conductor);
break;
}
default: {break;}
@@ -1704,6 +1697,7 @@ void Diagram::removeItem(QGraphicsItem *item)
Conductor *conductor = static_cast<Conductor *>(item);
conductor->terminal1->removeConductor(conductor);
conductor->terminal2->removeConductor(conductor);
m_project->dataBase()->removeConductor(conductor);
break;
}
default: {break;}
@@ -2319,6 +2313,7 @@ ExportProperties Diagram::applyProperties(
old_properties.draw_border = border_and_titleblock.borderIsDisplayed();
old_properties.draw_titleblock = border_and_titleblock.titleBlockIsDisplayed();
old_properties.draw_terminals = drawTerminals();
old_properties.draw_terminal_names = drawTerminalNames();
old_properties.draw_colored_conductors = drawColoredConductors();
old_properties.exported_area = useBorder() ? QET::BorderArea
: QET::ElementsArea;
@@ -2327,6 +2322,7 @@ ExportProperties Diagram::applyProperties(
// applique les nouvelles options de rendu
setUseBorder (new_properties.exported_area == QET::BorderArea);
setDrawTerminals (new_properties.draw_terminals);
setDrawTerminalNames (new_properties.draw_terminal_names);
setDrawColoredConductors (new_properties.draw_colored_conductors);
setDisplayGrid (new_properties.draw_grid);
setDisplayGuides (new_properties.draw_guides);
@@ -2397,9 +2393,24 @@ QPointF Diagram::snapToGrid(const QPointF &p)
\~French true pour afficher les bornes, false sinon
*/
void Diagram::setDrawTerminals(bool dt) {
draw_terminals_ = dt;
foreach(QGraphicsItem *qgi, items()) {
if (Terminal *t = qgraphicsitem_cast<Terminal *>(qgi)) {
t -> setVisible(dt);
t -> update();
}
}
}
/**
@brief Diagram::setDrawTerminalNames
Defines whether or not to display the terminal names/labels
@param dt : true to display the terminal names, false otherwise
*/
void Diagram::setDrawTerminalNames(bool dt) {
draw_terminal_names_ = dt;
foreach(QGraphicsItem *qgi, items()) {
if (Terminal *t = qgraphicsitem_cast<Terminal *>(qgi)) {
t -> update();
}
}
}
+12
View File
@@ -127,6 +127,7 @@ class Diagram : public QGraphicsScene
bool draw_guides_;
QList<Diagram::Guide> m_guides_list;
bool draw_terminals_;
bool draw_terminal_names_;
bool draw_colored_conductors_;
QString m_conductors_autonum_name;
@@ -226,6 +227,8 @@ class Diagram : public QGraphicsScene
bool drawTerminals() const;
void setDrawTerminals(bool);
bool drawTerminalNames() const;
void setDrawTerminalNames(bool);
bool drawColoredConductors() const;
void setDrawColoredConductors(bool);
@@ -426,6 +429,15 @@ inline bool Diagram::drawTerminals() const
return(draw_terminals_);
}
/**
@brief Diagram::drawTerminalNames
@return true if terminal names are rendered, false otherwise
*/
inline bool Diagram::drawTerminalNames() const
{
return(draw_terminal_names_);
}
/**
@brief Diagram::drawColoredConductors
@return true if conductors colors are rendered, false otherwise.
+15 -2
View File
@@ -75,6 +75,13 @@ void PasteDiagramCommand::redo()
{
first_redo = false;
//make new uuid for every pasted conductor, because old uuid are
//the uuid of the copied conductor
const QList <Conductor *> all_pasted_conductors = content.conductors();
for (Conductor *c : all_pasted_conductors) {
c -> newUuid();
}
//this is the first paste, we do some actions for the new element
const QList <Element *> elmts_list = content.m_elements;
for (Element *e : elmts_list)
@@ -92,12 +99,18 @@ void PasteDiagramCommand::redo()
dc.addValue("location", "");
e->setElementInformations(dc);
//Reset the text of conductors
//Reset the text of conductors, the same way the label/comment/
//location above are reset to "" rather than to some other
//value - "erase on copy" means erase, not "replace with the
//project's default new-conductor text" (which happens to
//default to a literal "_" character, unrelated to whether the
//user wanted this copy's old label kept or cleared; see
//issue #413).
const QList <Conductor *> conductors_list = content.m_conductors_to_move;
for (Conductor *c : conductors_list)
{
ConductorProperties cp = c -> properties();
cp.text = c->diagram() ? c -> diagram() -> defaultConductorProperties.text : "_";
cp.text = "";
c -> setProperties(cp);
}
}
@@ -20,11 +20,17 @@
#include "../conductorautonumerotation.h"
#include "../diagram.h"
#include "../undocommand/addgraphicsobjectcommand.h"
#include "../undocommand/deleteqgraphicsitemcommand.h"
#include "../factory/elementfactory.h"
#include "../qetapp.h"
#include "../qetdiagrameditor.h"
#include "../qetgraphicsitem/element.h"
#include "../qetgraphicsitem/conductor.h"
#include "../qetgraphicsitem/terminal.h"
#include "../qet.h"
#include "../autobreakconductor.h"
#include <QPainterPath>
#include <limits>
/**
@brief DiagramEventAddElement::DiagramEventAddElement
@@ -248,16 +254,32 @@ void DiagramEventAddElement::addElement()
QUndoCommand *undo_object = new QUndoCommand(tr("Ajouter %1").arg(element->name()));
new AddGraphicsObjectCommand(element, m_diagram, m_element -> pos(), undo_object);
//When we search for free aligned terminal we temporally remove m_element to
//avoid any interaction with the function Element::AlignedFreeTerminals
//This is useful when an element has two (or more) terminals on opposite sides,
//because m_element is exactly at the same pos of the new element
//added to the scene so new conductor are created between terminal of the new element
//and the opposite terminal of m_element.
//When we search for free aligned terminal we temporally remove m_element to
//avoid any interaction with the function Element::AlignedFreeTerminals
//This is useful when an element has two (or more) terminals on opposite sides,
//because m_element is exactly at the same pos of the new element
//added to the scene so new conductor are created between terminal of the new element
//and the opposite terminal of m_element.
m_diagram->removeItem(m_element);
while (!element -> AlignedFreeTerminals().isEmpty() && m_diagram -> project() -> autoConductor())
//Auto break conductor: if a terminal of the new element lies on an existing
//conductor, break the conductor and reconnect through the new element's terminal.
//Track the endpoints of broken conductors so auto-connect doesn't create duplicates.
QList<Conductor *> conductors_handled;
QSet<Terminal *> used_terminals;
QSet<Terminal *> broken_endpoints = autoBreakConductors(m_diagram, element, undo_object,
conductors_handled, used_terminals);
//Auto-connect: collect all aligned pairs first, then filter and process.
QList<QPair<Terminal *, Terminal *>> aligned_pairs;
if (m_diagram->project()->autoConductor())
aligned_pairs = element->AlignedFreeTerminals();
for (const QPair<Terminal *, Terminal *> &pair : aligned_pairs)
{
QPair <Terminal *, Terminal *> pair = element -> AlignedFreeTerminals().takeFirst();
//Skip if the other terminal was an endpoint of a broken conductor
if (broken_endpoints.contains(pair.second))
continue;
Conductor *conductor = new Conductor(pair.first, pair.second);
new AddGraphicsObjectCommand(conductor, m_diagram, QPointF(), undo_object);
+139 -21
View File
@@ -19,10 +19,14 @@
#include "diagrameventaddimage.h"
#include "../qetapp.h"
#include "../qetdiagrameditor.h"
#include "../diagram.h"
#include "../undocommand/addgraphicsobjectcommand.h"
#include "../qetgraphicsitem/diagramimageitem.h"
#include <QStatusBar>
#include <QTimer>
/**
@brief DiagramEventAddImage::DiagramEventAddImage
Default constructor
@@ -34,6 +38,16 @@ DiagramEventAddImage::DiagramEventAddImage(Diagram *diagram) :
m_is_added (false)
{
openDialog();
if (m_running)
{
// Deferred for the same reason as the shape tools' own
// constructor-time hint: Diagram::setEventInterface() destroys
// whatever tool was previously active *after* this constructor
// returns, and that tool's own destructor clears the status bar
// -- an immediate show here would just get wiped out moments
// later by that cleanup.
QTimer::singleShot(0, this, [this]() { showHint(); });
}
}
/**
@@ -47,33 +61,61 @@ DiagramEventAddImage::~DiagramEventAddImage()
delete m_image;
}
if (!m_diagram->views().isEmpty())
{
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
editor->statusBar()->clearMessage();
}
foreach (QGraphicsView *view, m_diagram->views())
view->setContextMenuPolicy((Qt::DefaultContextMenu));
}
/**
@brief DiagramEventAddImage::showHint
Re-asserted on every move (see mouseMoveEvent), not just once at
activation: Qt's own built-in "show an action's statusTip on hover"
has its own internal "restore whatever was there before" logic for
when the hover ends. Since this message is first shown *during* that
same hover session (the user is still over the toolbar icon when the
deferred constructor-time call above fires), Qt's hover-tracking has
no idea this code changed the status bar in the meantime -- the
moment the mouse leaves the icon for the canvas, it silently
restores whatever it remembers being there before its own tip
started, overwriting this one. Re-showing it on every move within
the canvas simply outlasts that one-time restore -- the exact same
issue already found and fixed for the shape tools.
*/
void DiagramEventAddImage::showHint() const
{
if (m_diagram->views().isEmpty())
return;
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
editor->statusBar()->showMessage(tr("Clic : positionner à la taille d'origine. "
"Cliquer-glisser : positionner et redimensionner. "
"Clic droit : pivoter de 90°. Ctrl+molette : ajuster la taille."));
}
/**
@brief DiagramEventAddImage::mousePressEvent
Action when mouse is pressed
Left button: starts a potential drag-to-resize, anchored here -- but
doesn't commit to anything yet. A quick click-release (see
mouseMoveEvent's threshold check) still places the image at its
original size, matching the previous behavior exactly; only an
actual drag switches to resizing. Right button still rotates in 90
degree steps, unchanged, and only while not already left-dragging.
@param event : event of mouse pressed
*/
void DiagramEventAddImage::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
if (m_image && event -> button() == Qt::LeftButton)
if (m_image && event->button() == Qt::LeftButton)
{
QPointF pos = event->scenePos();
pos.rx() -= m_image->boundingRect().width()/2;
pos.ry() -= m_image->boundingRect().height()/2;
m_diagram -> undoStack().push (new AddGraphicsObjectCommand(m_image, m_diagram, pos));
for (QGraphicsView *view : m_diagram->views()) {
view->setContextMenuPolicy((Qt::DefaultContextMenu));
}
m_running = false;
emit finish();
m_pressed = true;
m_resize_engaged = false;
m_press_pos = event->scenePos();
event->setAccepted(true);
}
else if (m_image && event -> button() == Qt::RightButton)
else if (m_image && !m_pressed && event->button() == Qt::RightButton)
{
m_image->setRotation(m_image->rotation() + 90);
event->setAccepted(true);
@@ -87,26 +129,95 @@ void DiagramEventAddImage::mousePressEvent(QGraphicsSceneMouseEvent *event)
*/
void DiagramEventAddImage::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
if (!m_image || event->buttons() != Qt::NoButton) {
if (!m_image) {
return;
}
showHint();
QPointF pos = event->scenePos();
if (!m_is_added)
{
for (QGraphicsView *view : m_diagram->views()) {
view->setContextMenuPolicy((Qt::NoContextMenu));
}
m_diagram->addItem(m_image);
m_is_added = true;
}
m_image->setPos(pos - m_image->boundingRect().center());
if (m_pressed)
{
// Anchored on m_press_pos, not the item's own current position:
// dragging in any direction has to visibly grow the image from
// where the click started, not from wherever the "no button
// held" preview phase happened to leave it centered.
const QPointF delta = pos - m_press_pos;
if (!m_resize_engaged && QLineF(m_press_pos, pos).length() >= 4.0)
m_resize_engaged = true; // latched: crossing back within the threshold afterward must not un-engage it
if (!m_resize_engaged)
{
// Still just a (so far) plain click -- keep behaving like
// the pre-drag preview: original size, centered here, so
// releasing right now reproduces the old click-to-place
// behavior exactly.
m_image->setPos(m_press_pos - m_image->boundingRect().center());
}
else
{
const QSizeF naturalSize = m_image->boundingRect().size();
if (naturalSize.width() > 0 && naturalSize.height() > 0)
{
const qreal scaleX = qAbs(delta.x()) / naturalSize.width();
const qreal scaleY = qAbs(delta.y()) / naturalSize.height();
// The larger of the two, not a per-axis stretch: images
// only support a single uniform scale today (see
// boundingRect()/paint(), which never touch aspect
// ratio), so this is a diagonal-drag size, not a
// free-form one -- breaking aspect ratio on purpose is
// its own, separate, larger piece of work.
const qreal newScale = qBound(0.01, qMax(scaleX, scaleY), 50.0);
m_image->setScale(newScale);
}
m_image->setPos(qMin(m_press_pos.x(), pos.x()), qMin(m_press_pos.y(), pos.y()));
}
}
else
{
m_image->setPos(pos - m_image->boundingRect().center());
}
event->setAccepted(true);
}
/**
@brief DiagramEventAddImage::mouseReleaseEvent
Left button release commits whatever mouseMoveEvent last set --
original size and centered if the press never turned into a real
drag, or the dragged-out size and position otherwise. Either way,
this is the only place placement is actually finalized now; a plain
click no longer finishes inside mousePressEvent itself, since it has
to wait and see whether a drag follows.
@param event : event of mouse release
*/
void DiagramEventAddImage::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
if (m_image && m_pressed && event->button() == Qt::LeftButton)
{
m_diagram->undoStack().push(new AddGraphicsObjectCommand(m_image, m_diagram, m_image->pos()));
for (QGraphicsView *view : m_diagram->views()) {
view->setContextMenuPolicy((Qt::DefaultContextMenu));
}
m_running = false;
emit finish();
event->setAccepted(true);
}
}
/**
@brief DiagramEventAddImage::mouseDoubleClickEvent
This method is used only to overwrite double click.
@@ -124,7 +235,14 @@ void DiagramEventAddImage::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event
*/
void DiagramEventAddImage::wheelEvent(QGraphicsSceneWheelEvent *event)
{
if (!m_is_added || !m_image || event -> modifiers() != Qt::CTRL) {
// !m_pressed added alongside the modifier fix: without it, wheel
// scaling could fight with an active drag-resize, both trying to
// set scale() from different sources in the same gesture.
// event->modifiers() & Qt::ControlModifier, not != Qt::CTRL: the
// same exact-equality bug already found and fixed several times
// this session elsewhere -- Ctrl held together with any other
// modifier would silently fail to register as Ctrl at all.
if (!m_is_added || !m_image || m_pressed || !(event->modifiers() & Qt::ControlModifier)) {
return;
}
@@ -20,6 +20,8 @@
#include "diagrameventinterface.h"
#include <QPointF>
class Diagram;
class DiagramImageItem;
@@ -37,15 +39,20 @@ class DiagramEventAddImage : public DiagramEventInterface
void mousePressEvent (QGraphicsSceneMouseEvent *event) override;
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent (QGraphicsSceneMouseEvent *event) override;
void mouseDoubleClickEvent (QGraphicsSceneMouseEvent *event) override;
void wheelEvent (QGraphicsSceneWheelEvent *event) override;
bool isNull () const;
private:
void openDialog();
void showHint() const;
DiagramImageItem *m_image;
bool m_is_added;
bool m_pressed = false; // left button held: dragging out a size, not just positioning
bool m_resize_engaged = false; // latched once the drag threshold is crossed, matching the pen tool's own curve-drag threshold convention -- so dragging out and back near the start point doesn't "snap back" to original size before release
QPointF m_press_pos; // scene position of the left-button press, the resize anchor
};
#endif // DIAGRAMEVENTADDIMAGE_H
@@ -43,11 +43,7 @@ m_preview_item(nullptr)
QString file_name = (last_slash != -1) ? path.mid(last_slash + 1) : path;
if (!dir_path.isEmpty()) {
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
QStringList parts = dir_path.split('/', QString::SkipEmptyParts);
#else
QStringList parts = dir_path.split('/', Qt::SkipEmptyParts);
#endif
QString current_path = "";
for (const QString &part : parts) {
QString parent_path = current_path;
@@ -202,11 +198,7 @@ bool DiagramEventAddMacro::loadMacro()
QString file_name = (last_slash != -1) ? path.mid(last_slash + 1) : path;
if (!dir_path.isEmpty()) {
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
QStringList parts = dir_path.split('/', QString::SkipEmptyParts);
#else
QStringList parts = dir_path.split('/', Qt::SkipEmptyParts);
#endif
QString current_path = "";
for (const QString &part : parts) {
QString parent_path = current_path;
@@ -0,0 +1,427 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "diagrameventaddpath.h"
#include "../diagram.h"
#include "../lastusedstyle.h"
#include "../qetapp.h"
#include "../qetdiagrameditor.h"
#include "../undocommand/addgraphicsobjectcommand.h"
#include <QGraphicsLineItem>
#include <QGraphicsSceneMouseEvent>
#include <QKeyEvent>
#include <QLineF>
#include <QStatusBar>
#include <QTimer>
/**
@brief DiagramEventAddPath::DiagramEventAddPath
@param diagram : the diagram where this event must operate
*/
DiagramEventAddPath::DiagramEventAddPath(Diagram *diagram) :
DiagramEventInterface(diagram),
m_shape_item (nullptr),
m_help_horiz (nullptr),
m_help_verti (nullptr)
{
m_running = true;
init();
// Deferred for the same reason as DiagramEventAddShape's own
// constructor-time hint: Diagram::setEventInterface() destroys
// whatever tool was previously active *after* this constructor
// returns, and that tool's own destructor clears the status bar --
// an immediate show here would just get wiped out moments later.
QTimer::singleShot(0, this, [this]() { showHint(); });
}
DiagramEventAddPath::~DiagramEventAddPath()
{
if ((m_running || m_abort) && m_shape_item)
{
m_diagram->removeItem(m_shape_item);
delete m_shape_item;
}
delete m_help_horiz;
delete m_help_verti;
if (m_diagram && !m_diagram->views().isEmpty())
{
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
editor->statusBar()->clearMessage();
}
foreach (QGraphicsView *v, m_diagram->views())
v->setContextMenuPolicy(Qt::DefaultContextMenu);
}
/**
@brief DiagramEventAddPath::showHint
Re-asserted on every move within the canvas (see mouseMoveEvent), not
just once at activation: Qt's own built-in "show an action's
statusTip on hover" has its own internal "restore whatever was there
before" logic for when the hover ends. Since this message is first
shown *during* that same hover session (the user is still over the
toolbar icon when the deferred constructor-time call above fires),
Qt's hover-tracking has no idea this code changed the status bar in
the meantime -- the moment the mouse leaves the icon for the canvas,
it silently restores whatever it remembers being there before its
own tip started, overwriting this one. Re-showing it on every move
within the canvas simply outlasts that one-time restore.
*/
void DiagramEventAddPath::showHint() const
{
if (!m_diagram || m_diagram->views().isEmpty())
return;
if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst()))
editor->statusBar()->showMessage(tr("Clic: point anguleux. Cliquer-glisser: point courbe. "
"Clic sur le premier point: fermer. Échap/Entrée: terminer. "
"Clic droit: annuler le dernier point."));
}
void DiagramEventAddPath::init()
{
foreach (QGraphicsView *v, m_diagram->views())
v->setContextMenuPolicy(Qt::NoContextMenu);
}
QPointF DiagramEventAddPath::snapped(const QPointF &scenePos, Qt::KeyboardModifiers mods) const
{
return mods == Qt::ControlModifier ? scenePos : Diagram::snapToGrid(scenePos);
}
int DiagramEventAddPath::confirmedNodeCount() const
{
// The trailing element is always the live preview while m_shape_item
// exists; with no shape yet there are no nodes of any kind.
return m_shape_item ? qMax(0, m_nodes.size() - 1) : 0;
}
/**
@brief DiagramEventAddPath::mousePressEvent
Left click: on the very first click, creates the shape with a real
node *and* an immediate preview node at the same spot, so a segment
exists (even if zero-length) from the start rather than requiring a
second click before anything is visible. On later clicks: either
confirms the live preview into a real point and appends a fresh one
for the next segment, or -- if close enough to the first node --
closes the path.
*/
void DiagramEventAddPath::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
if (Q_UNLIKELY(m_diagram->isReadOnly()))
return;
if (event->button() != Qt::LeftButton)
{
// Accept every button while this tool is running, not just the
// one it actually acts on -- Diagram::mousePressEvent falls
// through to Qt's own default scene handling for anything left
// unaccepted, which is exactly the kind of competing control
// this tool can't afford while it's supposed to have exclusive
// ownership of input.
event->setAccepted(true);
return;
}
const QPointF pos = snapped(event->scenePos(), event->modifiers());
if (!m_shape_item)
{
m_shape_item = new QetShapeItem(pos, pos, QetShapeItem::Path);
if (LastUsedStyle::hasShapePen())
m_shape_item->setPen(LastUsedStyle::shapePen());
if (LastUsedStyle::hasShapeBrush())
m_shape_item->setBrush(LastUsedStyle::shapeBrush());
m_diagram->addItem(m_shape_item);
// Handles only ever get built for a selected item.
m_shape_item->setSelected(true);
QetShapeItem::PathNode node;
node.anchor = pos;
m_nodes << node;
m_nodes << node; // live preview, tracks the mouse from here on
m_shape_item->setPathNodes(m_nodes);
m_shape_item->enableNodeEditMode();
m_dragging_node = 0;
event->setAccepted(true);
return;
}
if (confirmedNodeCount() >= 2 && nearFirstNode(pos))
{
finishPath(true);
event->setAccepted(true);
return;
}
// Confirm the preview node as a real point, then append a fresh
// preview (a plain Corner, not a copy of the just-confirmed node's
// kind/handles) for the segment after it.
m_dragging_node = m_nodes.size() - 1;
m_nodes[m_dragging_node].anchor = pos;
QetShapeItem::PathNode preview;
preview.anchor = pos;
m_nodes << preview;
m_shape_item->setPathNodes(m_nodes);
m_shape_item->enableNodeEditMode();
event->setAccepted(true);
}
/**
@brief DiagramEventAddPath::mouseMoveEvent
Two mutually exclusive behaviours, matching whether a button is held:
with the left button down on a just-placed node, dragging shapes that
node's handles (same convention as editing an existing node -- see
QetShapeItem::dragPathControlHandle()). With no button held, the
trailing preview node instead tracks the mouse, giving the live
rubber-band segment.
*/
void DiagramEventAddPath::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
updateHelpCross(event->scenePos());
showHint();
if (m_shape_item)
{
const QPointF pos = snapped(event->scenePos(), event->modifiers());
if (m_dragging_node >= 0 && (event->buttons() & Qt::LeftButton))
{
QetShapeItem::PathNode &node = m_nodes[m_dragging_node];
const QPointF delta = pos - node.anchor;
// A small threshold so an accidental few-pixel wobble on
// what was meant to be a plain click doesn't silently add
// curve handles the user never intended.
if (QLineF(QPointF(), delta).length() > 3.0)
{
node.kind = QetShapeItem::NodeKind::Smooth;
node.outHandle = delta;
node.inHandle = -delta;
}
else
{
node.kind = QetShapeItem::NodeKind::Corner;
node.outHandle.reset();
node.inHandle.reset();
}
m_shape_item->setPathNodes(m_nodes);
}
else if (!(event->buttons() & Qt::LeftButton) && !m_nodes.isEmpty())
{
m_nodes.last().anchor = pos;
m_shape_item->setPathNodes(m_nodes);
}
}
// Ours unconditionally while running: a stray, unaccepted move event
// falling through to Qt's default handling risks it dragging our
// selected, movable in-progress shape out from under the tool.
event->setAccepted(true);
}
/**
@brief DiagramEventAddPath::mouseReleaseEvent
Left release just ends the current node's drag phase (the trailing
preview resumes tracking the mouse on the next move). Right release
steps back one *confirmed* point (the preview is left alone), or
cancels outright once only one remains, or exits the tool entirely if
nothing is in progress at all.
*/
void DiagramEventAddPath::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
{
m_dragging_node = -1;
}
else if (event->button() == Qt::RightButton)
{
if (m_shape_item)
{
if (confirmedNodeCount() > 1)
{
m_nodes.remove(m_nodes.size() - 2); // the last *confirmed* node; keep the trailing preview
m_shape_item->setPathNodes(m_nodes);
}
else
{
cancelPath();
}
}
else
{
m_running = false;
emit finish();
}
}
event->setAccepted(true);
}
/**
@brief DiagramEventAddPath::mouseDoubleClickEvent
A double-click is a press, release, press, release, doubleclick
sequence -- the second press already confirmed the preview into a
duplicate point (mousePressEvent can't distinguish a double-click
from two single clicks in the same place) and appended a fresh
preview after it. Dropping the last node here removes that fresh
preview; finishPath()'s own trailing-preview removal then removes the
duplicate underneath it, leaving only the genuinely-placed points.
*/
void DiagramEventAddPath::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
if (m_shape_item && event->button() == Qt::LeftButton && !m_nodes.isEmpty())
{
m_nodes.removeLast();
finishPath(false);
}
event->setAccepted(true);
}
/**
@brief DiagramEventAddPath::keyPressEvent
Escape or Enter finish the path open once at least two real points
exist; Escape with fewer (or none placed at all) cancels/exits
instead, since there's nothing meaningful to keep.
*/
void DiagramEventAddPath::keyPressEvent(QKeyEvent *event)
{
if (event->key() == Qt::Key_Escape)
{
if (m_shape_item && confirmedNodeCount() >= 2)
finishPath(false);
else if (m_shape_item)
cancelPath();
else
{
m_running = false;
emit finish();
}
event->accept();
}
else if ((event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
&& m_shape_item && confirmedNodeCount() >= 2)
{
finishPath(false);
event->accept();
}
}
/**
@brief DiagramEventAddPath::finishPath
Strips the trailing live-preview node, commits the in-progress path
onto the undo stack, and resets so the tool is ready to draw another
one immediately -- matching every other shape tool's own behaviour
after finishing a shape.
*/
void DiagramEventAddPath::finishPath(bool closed)
{
if (!m_shape_item)
return;
if (!m_nodes.isEmpty())
m_nodes.removeLast();
if (m_nodes.size() < 2)
{
cancelPath();
return;
}
if (closed)
m_shape_item->setClosed(true);
m_shape_item->setPathNodes(m_nodes);
m_diagram->undoStack().push(new AddGraphicsObjectCommand(m_shape_item, m_diagram));
m_shape_item = nullptr;
m_nodes.clear();
m_dragging_node = -1;
}
/**
@brief DiagramEventAddPath::cancelPath
Discards the in-progress path entirely -- nothing worth keeping (an
empty or single-point path isn't a usable shape).
*/
void DiagramEventAddPath::cancelPath()
{
if (m_shape_item)
{
m_diagram->removeItem(m_shape_item);
delete m_shape_item;
m_shape_item = nullptr;
}
m_nodes.clear();
m_dragging_node = -1;
}
/**
@brief DiagramEventAddPath::nearFirstNode
m_shape_item's pos()/transform() stay at their identity defaults for
its entire construction here -- nothing during drawing ever touches
them -- so the first node's anchor, stored in local coordinates, is
directly comparable to a scene position without any mapping.
*/
bool DiagramEventAddPath::nearFirstNode(const QPointF &scenePos) const
{
if (m_nodes.isEmpty())
return false;
return QLineF(m_nodes.first().anchor, scenePos).length() <= CLOSE_THRESHOLD;
}
/**
@brief DiagramEventAddPath::updateHelpCross
Same crosshair guide as every other shape tool (see
DiagramEventAddShape::updateHelpCross) -- duplicated rather than
shared, since the two classes don't otherwise share a common base
beyond DiagramEventInterface.
*/
void DiagramEventAddPath::updateHelpCross(const QPointF &p)
{
if (!m_help_horiz || !m_help_verti)
{
QPen pen;
pen.setWidthF(0.4);
pen.setCosmetic(true);
pen.setColor(Diagram::background_color == Qt::darkGray ? Qt::lightGray : Qt::darkGray);
QRectF rect = m_diagram->border_and_titleblock.insideBorderRect();
if (!m_help_horiz)
{
m_help_horiz = new QGraphicsLineItem(rect.topLeft().x(), 0, rect.topRight().x(), 0);
m_help_horiz->setPen(pen);
m_diagram->addItem(m_help_horiz);
}
if (!m_help_verti)
{
m_help_verti = new QGraphicsLineItem(0, rect.topLeft().y(), 0, rect.bottomLeft().y());
m_help_verti->setPen(pen);
m_diagram->addItem(m_help_verti);
}
}
QPointF point = Diagram::snapToGrid(p);
m_help_horiz->setY(point.y());
m_help_verti->setX(point.x());
}
@@ -0,0 +1,81 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef DIAGRAMEVENTADDPATH_H
#define DIAGRAMEVENTADDPATH_H
#include "../qetgraphicsitem/qetshapeitem.h"
#include "diagrameventinterface.h"
class QGraphicsLineItem;
/**
@brief The DiagramEventAddPath class
Pen tool: interactively draw a new Path (Bezier) shape, following the
same vocabulary every vector editor's pen tool uses:
- click places a Corner node;
- press-drag-release places a Smooth node, with the drag defining a
pair of mirrored handles (same convention as
QetShapeItem::dragPathControlHandle's "Smooth" mirroring);
- clicking back on the first node closes the path;
- double-click, Enter, or Escape (once 2+ nodes exist) finishes it
open;
- right-click steps back one node;
- right-click or Escape with nothing placed yet cancels the tool.
While running, m_nodes always carries one extra "preview" node at the
end, tracking the mouse so a live rubber-band segment is always
visible -- confirmedNodeCount() excludes it; every public gesture
handler is responsible for stripping it before treating the list as
"the path so far" (see finishPath(), which does this once for every
finishing gesture).
*/
class DiagramEventAddPath : public DiagramEventInterface
{
Q_OBJECT
public:
DiagramEventAddPath(Diagram *diagram);
~DiagramEventAddPath() override;
void mousePressEvent (QGraphicsSceneMouseEvent *event) override;
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent (QGraphicsSceneMouseEvent *event) override;
void mouseDoubleClickEvent (QGraphicsSceneMouseEvent *event) override;
void keyPressEvent (QKeyEvent *event) override;
void init() override;
private:
void updateHelpCross (const QPointF &p);
void showHint () const;
void finishPath (bool closed);
void cancelPath ();
bool nearFirstNode (const QPointF &scenePos) const;
int confirmedNodeCount () const; // m_nodes always carries one trailing "preview" node while running; this excludes it
QPointF snapped (const QPointF &scenePos, Qt::KeyboardModifiers mods) const;
QetShapeItem *m_shape_item;
QVector<QetShapeItem::PathNode> m_nodes;
int m_dragging_node = -1;
QGraphicsLineItem *m_help_horiz, *m_help_verti;
// Scene units within which a click on an existing path is
// treated as "on the first node" and closes the shape, rather
// than adding yet another node right next to it.
static constexpr qreal CLOSE_THRESHOLD = 12.0;
};
#endif // DIAGRAMEVENTADDPATH_H
+250
View File
@@ -0,0 +1,250 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "diagrameventaddpdf.h"
// Whole file is a no-op unless QtPdf is available (see diagrameventaddpdf.h).
#ifdef QET_HAS_QTPDF
#include "../qetapp.h"
#include "../diagram.h"
#include "../undocommand/addgraphicsobjectcommand.h"
#include "../qetgraphicsitem/diagramimageitem.h"
#include "../ui/pdfpagesdialog.h"
#include <QPdfDocument>
#include <QFileDialog>
#include <QMessageBox>
#include <QPainter>
/**
@brief DiagramEventAddPdf::DiagramEventAddPdf
Constructor
@param diagram the diagram where this event operates
*/
DiagramEventAddPdf::DiagramEventAddPdf(Diagram *diagram) :
DiagramEventInterface(diagram),
m_image(nullptr),
m_is_added(false)
{
openDialog();
}
/**
@brief DiagramEventAddPdf::~DiagramEventAddPdf
Destructor
*/
DiagramEventAddPdf::~DiagramEventAddPdf()
{
if (m_running || m_abort)
{
if (m_is_added) m_diagram->removeItem(m_image);
delete m_image;
}
foreach (QGraphicsView *view, m_diagram->views())
view->setContextMenuPolicy((Qt::DefaultContextMenu));
}
/**
@brief DiagramEventAddPdf::mousePressEvent
Action when mouse is pressed
@param event event of mouse pressed
*/
void DiagramEventAddPdf::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
if (m_image && event->button() == Qt::LeftButton)
{
QPointF pos = event->scenePos();
pos.rx() -= m_image->boundingRect().width()/2;
pos.ry() -= m_image->boundingRect().height()/2;
m_diagram->undoStack().push(new AddGraphicsObjectCommand(m_image, m_diagram, pos));
for (QGraphicsView *view : m_diagram->views()) {
view->setContextMenuPolicy((Qt::DefaultContextMenu));
}
m_running = false;
emit finish();
event->setAccepted(true);
}
else if (m_image && event->button() == Qt::RightButton)
{
m_image->setRotation(m_image->rotation() + 90);
event->setAccepted(true);
}
}
/**
@brief DiagramEventAddPdf::mouseMoveEvent
Action when mouse moves
@param event event of mouse move
*/
void DiagramEventAddPdf::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
if (!m_image || event->buttons() != Qt::NoButton) {
return;
}
QPointF pos = event->scenePos();
if (!m_is_added)
{
for (QGraphicsView *view : m_diagram->views()) {
view->setContextMenuPolicy((Qt::NoContextMenu));
}
m_diagram->addItem(m_image);
m_is_added = true;
}
m_image->setPos(pos - m_image->boundingRect().center());
event->setAccepted(true);
}
/**
@brief DiagramEventAddPdf::mouseDoubleClickEvent
Overwrite double click to prevent opening properties dialog.
@param event event of mouse double click
*/
void DiagramEventAddPdf::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) {
event->setAccepted(true);
}
/**
@brief DiagramEventAddPdf::wheelEvent
Action when mouse wheel is rotated (CTRL+wheel to scale)
@param event event of mouse wheel
*/
void DiagramEventAddPdf::wheelEvent(QGraphicsSceneWheelEvent *event)
{
if (!m_is_added || !m_image || event->modifiers() != Qt::CTRL) {
return;
}
qreal scaling = m_image->scale();
event->delta() > 1 ? scaling += 0.01 : scaling -= 0.01;
if (scaling > 0.01 && scaling <= 2) {
m_image->setScale(scaling);
}
event->setAccepted(true);
}
/**
@brief DiagramEventAddPdf::isNull
@return true if the PDF image couldn't be loaded, false otherwise
*/
bool DiagramEventAddPdf::isNull() const
{
if (!m_image) return true;
return false;
}
/**
@brief DiagramEventAddPdf::openDialog
Opens a file dialog to select a PDF file, then opens a page selection
dialog with DPI options. The selected page is rendered to a QImage at
the chosen DPI and converted to a DiagramImageItem.
*/
void DiagramEventAddPdf::openDialog()
{
if (m_diagram->isReadOnly()) return;
// Open file dialog to select a PDF file
QString pathPDFs = QETApp::documentDir();
QString fileName = QFileDialog::getOpenFileName(
m_diagram->views().isEmpty() ? nullptr : m_diagram->views().first(),
QObject::tr("Sélectionner un fichier PDF..."),
pathPDFs,
QObject::tr("Fichiers PDF (*.pdf)")
);
if (fileName.isEmpty()) return;
// Load the PDF document
QPdfDocument document;
document.load(fileName);
if (document.status() != QPdfDocument::Status::Ready)
{
QMessageBox::critical(
m_diagram->views().isEmpty() ? nullptr : m_diagram->views().first(),
QObject::tr("Erreur"),
QObject::tr("Impossible de charger le fichier PDF.")
);
return;
}
int pageCount = document.pageCount();
if (pageCount <= 0)
{
QMessageBox::critical(
m_diagram->views().isEmpty() ? nullptr : m_diagram->views().first(),
QObject::tr("Erreur"),
QObject::tr("Le fichier PDF ne contient aucune page.")
);
return;
}
// Always show the dialog so the user can choose page and DPI
PdfPagesDialog dialog(document,
m_diagram->views().isEmpty() ? nullptr : m_diagram->views().first());
if (dialog.exec() != QDialog::Accepted) return;
int pageIndex = dialog.selectedPage() - 1; // Convert to 0-based index
int dpi = dialog.selectedDpi();
// Calculate pixel size from PDF points at the user-selected DPI
// PDF point = 1/72 inch
QSizeF pageSize = document.pagePointSize(pageIndex);
int pixelWidth = qRound((pageSize.width() / 72.0) * dpi);
int pixelHeight = qRound((pageSize.height() / 72.0) * dpi);
if (pixelWidth <= 0 || pixelHeight <= 0)
{
QMessageBox::critical(
m_diagram->views().isEmpty() ? nullptr : m_diagram->views().first(),
QObject::tr("Erreur"),
QObject::tr("Impossible de déterminer la taille de la page PDF.")
);
return;
}
// Render PDF page to QImage
QImage image = document.render(pageIndex, QSize(pixelWidth, pixelHeight));
if (image.isNull())
{
QMessageBox::critical(
m_diagram->views().isEmpty() ? nullptr : m_diagram->views().first(),
QObject::tr("Erreur"),
QObject::tr("Impossible de rendre la page PDF.")
);
return;
}
// Fill white background to handle transparent PDFs
QImage background(image.size(), QImage::Format_ARGB32_Premultiplied);
background.fill(Qt::white);
QPainter painter(&background);
painter.drawImage(0, 0, image);
painter.end();
m_image = new DiagramImageItem(QPixmap::fromImage(background));
m_running = true;
}
#endif // QET_HAS_QTPDF

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