Commit Graph

71 Commits

Author SHA1 Message Date
Andre Rummler cc8be46c6e For the richtext widget the compiled uic was check in since QT4(?) times although it was declared to the AUTOUIC. Removed the pre-compiled version and it still works. 2026-08-07 14:16:20 +02:00
Laurent Trinques 55fdae918b Merge pull request #642 from ispyisail/feature-custom-element-properties
Add user-defined custom properties on elements (discussion #611)
2026-08-07 12:12:31 +02:00
ispyisail 4caefc048f Add optional precompiled headers behind QET_ENABLE_PCH (default OFF)
Building QET is dominated by re-parsing Qt's headers. A 214-line source
file expands to roughly 198,000 preprocessed lines, and compiling one
translation unit costs ~4.1 s, of which only ~0.35 s is optimisation --
switching -O3 to -O0 saves just 8%, so the usual "build Debug for faster
compiles" advice does not help here. A precompiled header caches the
parsed header state, which is the part that actually costs.

Measured on a 24-thread Xeon E5-2650 v4 with Qt 5.15.18 and GCC 15.2,
same build tree, only the option differing:

  compile one translation unit   4.12 s -> 1.21 s
  edit one .cpp -> linked binary 5.22 s -> 1.65 s

Deliberately OFF by default. A PCH satisfies includes that a source file
neglected to make for itself, so code written with it enabled can fail to
compile for everyone else. Leaving the default off keeps CI and
contributors on the strict behaviour; only developers who opt in trade
that away for the speed.

Two details in the implementation are load-bearing:

- The generator expressions are not decoration. This target also compiles
  the 18 C files of the bundled LZMA decoder, and an unguarded header list
  applies to every language in the target, so the Qt headers would be fed
  to the C compiler and fail with "unknown type name 'namespace'".
  $<ANGLE-R> is needed because a literal '>' would end the generator
  expression.
- target_precompile_headers() requires CMake 3.16 while the project still
  declares a 3.5 minimum, so the block warns and skips rather than raising
  the project-wide requirement for an opt-in developer feature.

Verified both ways: with the option off no PCH artefacts are generated and
the build is byte-for-byte the previous behaviour; with it on, all 18 C
files still compile, the generated PCH is C++-only (cmake_pch.hxx, with no
cmake_pch.h), the C compile commands carry no PCH, and the resulting
binary runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 21:10:46 +12:00
ispyisail da3a976b60 Add an event-loop responsiveness watchdog (discussion #644 follow-up)
QetLogger (discussion #644, steps 1-3) captures whatever an explicit
qDebug()/qInfo()/qWarning() call already decided to report. Most of a
session -- painting, dragging, a slow synchronous operation -- produces
no log output at all, so a silent multi-second gap in the log is
indistinguishable from the user simply not doing anything. That gap
came up directly: investigating a user-reported "the program lagged"
required inferring stalls from timestamp gaps between unrelated log
lines, which can't tell a real freeze apart from normal idle time.

EventLoopWatchdog closes that gap directly instead of inferring it. A
QTimer::PreciseTimer repeating tick (every 50ms) measures the *actual*
elapsed time since the previous tick via QElapsedTimer (monotonic,
unaffected by system clock/NTP adjustments). Qt does not queue up
missed fires for a normal repeating timer, so if the main thread is
blocked for 600ms, the timer fires once as soon as the loop frees up,
with ~600ms measured since the last tick -- that gap is the stall,
measured at its source. Only logs (via the existing qWarning() path,
so it reuses QetLogger's file/ring/rotation with no new plumbing) when
a tick is late by more than 200ms, so a healthy session produces zero
output from this class, in keeping with QetLogger's bounded-log design.

Same QET_WATCHDOG_DISABLE=1 escape-hatch convention as QetLogger's own
QET_LOG_DISABLE=1.

Deliberately not included: attributing a stall to what caused it. This
tells you a stall happened and how long -- pairing that timestamp with
gdb attached to a running session (as used for the CLI hang, PR #661)
is still how you get from "it stalled" to a root cause.

Stacked on #647 (feature-diagnostic-logging-crash) for QetLogger/
qWarning() plumbing this depends on -- diff includes its commits until
that merges.

Verified against the compiled binary, not just read: temporarily
injected a QThread::msleep(600) via a one-shot QTimer 2s after
startup, confirmed the exact expected warning
("EventLoopWatchdog: main thread stalled for 620 ms") at the right
severity through the real qWarning()/QetLogger path, then removed the
test hook and reconfirmed a normal run produces no output from this
class at all.
2026-08-06 23:05:05 +12:00
ispyisail 5dec36cb29 Add crash-time ring flush and a diagnostics export UI (discussion #644, steps 4-5)
Stacked on the steps 1-3 branch (feature-diagnostic-logging, PR #646).
Kept as its own PR rather than folded into that one, matching the
discussion's own framing: step 4 is explicitly "the highest-risk piece
... lands last, behind its own switch."

## Step 4 -- crash-time ring flush (CrashHandler)

Installs a handler for SIGSEGV/SIGABRT/SIGBUS/SIGFPE/SIGILL (POSIX) /
SetUnhandledExceptionFilter (Windows) that flushes the in-memory ring to
a fixed crash_dump.log before the process dies.

This required reworking LogRing (step 3) to be genuinely lock-free, not
just mutex-protected: a signal handler that blocks on a lock the
crashing thread (or another thread) already holds turns a clean crash
into a hang -- no ring dump *and* no core dump, worse than doing
nothing. append() now claims a slot with a single atomic fetch-add;
dumpToFd() reads the preallocated entries directly and writes them with
write(2) only, looping on EINTR/short writes. Accepted tradeoff: at most
one entry can be read torn if a crash lands mid-append into that exact
slot -- documented in logring.h, and the alternative (a seqlock to
detect and retry) wasn't judged worth the complexity for that window.

Other invariants implemented per the discussion:
- sigaltstack with a static 64 KiB buffer, SA_ONSTACK -- a stack-
  overflow SIGSEGV has no usable stack for a handler without one.
- Nothing under the actual handler touches Qt, QString or the
  allocator: the dump path and a small header (version/git/OS/Qt) are
  precomputed into fixed char buffers by install(), which runs once at
  startup in normal context.
- Atomic test-and-set so only the first crash writes a dump; a second
  concurrent/nested fault goes straight to restore-and-re-raise.
- After writing, the handler restores SIG_DFL and re-raises (POSIX) /
  returns EXCEPTION_CONTINUE_SEARCH (Windows) so the OS's own crash
  path -- core dump, Windows Error Reporting -- still runs. A handler
  that "fixed" the crash by swallowing the signal would destroy exactly
  the post-mortem evidence this whole design exists to preserve.

Tested in this environment: POSIX/Linux only, all five signals. Sent
each directly to a running process and confirmed (a) crash_dump.log is
written with the correct header and ring contents, mode 0600, and (b)
the process still terminates via the signal with the kernel's own
"core dumped" flag set (exit code 128+signal, confirmed for all five).
The Windows path is implemented per the discussion's guidance but is
untested -- no Windows build available in this sandbox.

## Step 5 -- getting the data back out

- QETApp::checkCrashDump(), called from checkBackupFiles() only when
  there's no stale project file to recover this run (so the two
  prompts never both show, per the discussion), offers an unretrieved
  crash dump via DiagnosticsReportDialog and then deletes it regardless
  of the user's choice -- offered exactly once.
- A new "Aide > Enregistrer un rapport de diagnostic..." action
  (QETMainWindow) builds the same kind of report from the *current*
  session (QetLogger::buildDiagnosticsReport(): header + this session's
  log file) for a manual "attach this to a bug report" flow, not tied
  to a crash.
- Both go through QetLogger::redact() before ever reaching the user:
  the one redaction implemented is a literal replace of the home
  directory with "~", since an absolute path under it leaks the
  account name. The discussion's fancier "optionally redact project
  filenames too" isn't attempted -- reliably telling a project path
  apart from arbitrary log text is a much fuzzier problem than a
  literal prefix match.
- DiagnosticsReportDialog shows the full (already-redacted) content
  before saving, per the discussion: "the user is about to attach this
  to a public tracker."

Verified in a real GUI session (Xvfb): triggered a SIGSEGV, relaunched,
confirmed the crash-report dialog appears with the right header/content,
confirmed it does not reappear on a second relaunch, and confirmed the
manual "Save report" action produces a correctly-formatted report and
saves it to a chosen path.

Built clean, no new warnings.

## Build systems

Registered in both: cmake/qet_compilation_vars.cmake, and
qelectrotech.pro. The .pro needed explicit globs for the new
sources/logging/ui/ subfolder -- sources/logging/*.{h,cpp} was already
globbed, but unlike the other ui/ subfolders that one had no entry of
its own, so diagnosticsreportdialog.{h,cpp} would not have been built
under qmake.
2026-08-06 23:04:27 +12:00
Laurent Trinques a7d504aaa8 Merge pull request #646 from ispyisail/feature-diagnostic-logging
Rework diagnostic logging: fix file writer, add rotation and a ring buffer (discussion #644, steps 1-3)
2026-08-06 10:42:20 +02:00
ispyisail ff812f221a Rework diagnostic logging: fix the file writer, add rotation and a ring buffer
Implements steps 1-3 of discussion #644 (deliberately not steps 4/5 --
no signal handler / crash flush, no diagnostics UI; see below).

## Step 1 -- fix the existing logger (bugs, no new behavior)

- One QFile handle held open for the whole session under a mutex,
  instead of opening and closing the log file on every single message.
- The log directory and the session's date-stamped filename are
  resolved exactly once, in the new QetLogger::init() called explicitly
  from main() immediately before qInstallMessageHandler() -- not
  recomputed per message, so a session that runs past midnight now
  stays in one file instead of silently splitting.
- Age-based retention now uses lastModified() instead of lastRead():
  opening a log to attach it to a bug report no longer resets its
  retention clock.
- stderr and file output both encode UTF-8 explicitly (toUtf8()),
  replacing stderr's toLocal8Bit() and the file stream's previously
  Qt5/Qt6-inconsistent default encoding.

## Step 2 -- size-capped rotation + hardening

- The previously-unbounded daily file is now capped at 2 MiB and
  rotated (kMaxFileBytes/kRotationKeep in QetLogger), keeping
  <date>.log plus <date>.1.log .. <date>.4.log; oldest is dropped.
- Each message is truncated to 4 KB with a "...[truncated N bytes]"
  marker before it reaches the ring or the file.
- Control characters (newlines, tabs, other non-printables) in message
  content are escaped, since much of what QET logs is externally
  controlled (file paths, element names, font strings out of a .qet
  file) -- left unescaped, an embedded '\n' could forge log lines.
- The log file is refused if a symlink already exists at that path,
  and is created/rotated owner-read/write only.

## Step 3 -- in-memory ring buffer

- LogRing (sources/logging/logring.h) is a fixed-capacity, always-on
  ring of the last 4096 log lines, preallocated once at construction
  (4096 * 512 B = 2 MiB) so append() never allocates. Entries are
  stored as plain pre-formatted bytes in fixed-size slots -- the shape
  discussion #644 specifies so a *future* crash handler could dump it
  with nothing but write(2), even though no such handler exists yet.
  Thread-safe via a plain QMutex (the lock-free requirement in the
  discussion applies specifically to a signal-handler read path, which
  this step doesn't add).

## Escape hatch

QET_LOG_DISABLE=1 in the environment at startup bypasses all of the
above -- no ring, no file, no rotation -- falling back to a minimal,
self-contained stderr passthrough that doesn't share any code with the
new formatting/sanitization path, so it stays usable even if that path
is what's misbehaving.

## Deliberately not included (per the discussion's own phasing)

- No signal handler / crash-time ring flush (step 4) -- the discussion
  flags this as the highest-risk piece, explicitly meant to land last
  and behind its own switch once the rest is proven.
- No diagnostics export UI (step 5).
- No log categories, session header, repeat collapsing or rate
  limiting -- listed under "best practices worth building in", not
  part of steps 1-3.

## Testing

Built clean, no new warnings.

Verified with real runs (QT_QPA_PLATFORM=offscreen, isolated HOME):
- Log file created at the expected dataDir()/YYYYMMDD.log path, mode
  0600.
- A full startup's worth of real messages (translations, MachineInfo's
  system dump, collection loading) written correctly; every one of the
  231 lines in one run starts with a proper timestamp -- confirmed the
  sanitizer correctly escapes the raw embedded newlines/tabs in
  MachineInfo's multi-line CPU/GPU description fields into visible
  \n/\t sequences rather than letting them fragment the log.
- QET_LOG_DISABLE=1: zero log files created, stderr still worked via
  the independent legacy path.
- Rotation: pre-filled a log to just under the 2 MiB cap, ran a normal
  session, confirmed it rotated to <date>.1.log (still 0600) with a
  byte-clean split (no truncated/duplicated line at the boundary) and
  a fresh <date>.log picked up from the next line.
2026-08-03 14:25:42 +12:00
ispyisail 2e0fe44174 Cover auto-numbering counter changes with undo/redo
Placing an auto-numbered element or conductor advances a shared
NumerotationContext counter (QETProject::addConductorAutoNum/
addElementAutoNum) as a side effect that sat entirely outside the undo
stack. Undoing the placement removed the visible number but left the
counter advanced, so every undo of an auto-numbered placement silently
burned a number, with no way to get it back short of a manual reset.

Adds SetAutoNumContextCommand, a small QUndoCommand storing the old/new
NumerotationContext and calling the matching add*AutoNum() setter on
undo()/redo() -- the same shape QPropertyUndoCommand already uses next
to it in ConductorAutoNumerotation::applyText().

Wires it into the two conductor call sites (the static newProperties(),
and numerateNewConductor(), both in ConductorAutoNumerotation) and the
element call site (Element::setUpFormula(), called from
DiagramEventAddElement::addElement() when a new element is dropped onto
a diagram). setUpFormula() now takes an optional parent QUndoCommand;
addElement() calls it before pushing its own undo_object so the counter
change lands in the same undo macro as the element's placement -- one
Ctrl+Z reverts both together, instead of leaving the counter adrift.

The project-properties config dialog's own add*AutoNum() calls (editing
the numbering rule itself, not a side effect of placing something) are
deliberately left untouched, as are the load-time folio-sequential
bookkeeping calls in Diagram::loadElmtFolioSeq()/loadCndFolioSeq() and
the bulk folio-renumbering passes in QETProject -- none of those run as
part of an undoable user gesture.

Implements the scope proposed in discussion #608.
2026-08-03 13:19:51 +12:00
ispyisail 6b9cb0a220 Add user-defined custom properties on elements
ElementInfoWidget's fixed ~40 predefined ELMT_* keys had no way for a
user to add a genuinely new element-info key, even though DiagramContext
already stores/round-trips arbitrary keys generically via toXml()/fromXml().

Adds an "Ajouter une propriété personnalisée" button that appends a
CustomElementInfoPartWidget row (both key and value user-editable,
unlike the fixed ElementInfoPartWidget rows bound to one predefined
key). The typed key is validated live against the existing
DiagramContext::isKeyAcceptable() and flagged with a red border when
it doesn't match, instead of silently dropping it. Any key already
present on the element that isn't one of the predefined/special keys
is re-displayed as a custom row on next selection.

Implements the scope proposed in discussion #611.
2026-08-03 10:28:26 +12:00
Laurent Trinques a4d75c4d8c Merge pull request #586 from ispyisail/feature-jump-to-element
Add Ctrl+G "jump to element" quick-open popup
2026-08-01 10:58:33 +02:00
Laurent Trinques 6cdf60e2e7 Merge pull request #587 from ispyisail/feature-configurable-shortcuts
Add configurable shortcuts: Shortcuts preferences page + app-wide registry
2026-08-01 07:23:56 +02:00
ispyisail 55886e5e8a Add undo/redo support for folio add, delete, and reorder (#575)
Three new QUndoCommand subclasses (AddDiagramCommand, RemoveDiagramCommand,
MoveDiagramCommand) pushed onto the project's existing (already
project-scoped) undo stack, so folio structure edits are undoable
alongside every item-level edit already on that stack.

- QETProject::addDiagram()/detachDiagram() are the shared attach/detach
  primitives: they mutate the diagram list, connect/disconnect the two
  per-diagram signals set up at add time, and emit diagramAdded/
  diagramRemoved. AddDiagramCommand and RemoveDiagramCommand call these
  (via friend access) for both redo and undo, so a removed diagram is
  parked rather than destroyed -- it's only actually deleted if the
  command itself falls out of undo history while still detached.
- ProjectView reacts to diagramRemoved the same way it already reacted to
  diagramAdded (tearing down/rebuilding the tab), so both directions of
  both commands go through the same reactive path every other diagram
  listener (project database, cross-references, generic panel) already
  relies on.
- MoveDiagramCommand wraps a new ProjectView::setDiagramPosition(), which
  performs the tab move and the project's diagramOrderChanged() list
  reorder synchronously in one step, instead of relying on the queued
  tabMoved connection (needed for interactive drag-and-drop) to catch up
  later -- avoiding a second, redundant reorder from that queued call.
- Multi-folio delete and multi-folio move (QETDiagramEditor::removeDiagrams()
  and the moveDiagram*(QList<Diagram*>) batch slots) wrap their per-diagram
  loop in QUndoStack::beginMacro()/endMacro(), so a multi-select action is
  one undo step, matching current UX.
- Softened the delete confirmation's "this change is irreversible" wording
  now that it no longer is.

Verified headlessly (Xvfb + xdotool + scrot): add/undo/redo, delete/undo/
redo (single and multi-select, single undo step for the batch), and
move/undo/redo all behave correctly against a 7-folio project.
2026-08-01 17:03:39 +12:00
ispyisail 5275fb44fe Add configurable shortcuts: ShortcutManager registry + Shortcuts config page (#574)
Implements the first pillar of #574: a "Shortcuts" preferences page letting
users rebind, search and reset every keyboard shortcut in the app.

What it does
- New ShortcutManager singleton: every one of the ~95 setShortcut()/
  setShortcuts() call sites across qet.cpp, qetmainwindow.cpp,
  elementspanelwidget.cpp, autonumberingdockwidget.cpp, richtexteditor.cpp,
  qetdiagrameditor.cpp, qettemplateeditor.cpp and qetelementeditor.cpp now
  calls registerAction(target, id, category, default_sequence) instead,
  which applies the user's saved override (or the default) and remembers
  the target for later editing.
- New ShortcutsConfigPage, added to the existing "Configurer QElectroTech"
  dialog: a filterable table of every registered shortcut, grouped by
  category, each with a QKeySequenceEdit and a per-row reset button, plus a
  "reset all" button. Bindings are only persisted (via
  ShortcutManager::setSequence()) when the dialog is accepted.
- Conflict detection: rows whose currently-edited sequence collides with
  another row are highlighted with a tooltip naming the conflicting action.
- Overrides are stored under a "shortcuts/" QSettings group, one key per
  id, keyed to match the id (not persisted at all when equal to the
  hardcoded default), so a future QET version can safely raise a default
  for anyone who never customized it.

Design notes
- Targets are handled generically via QObject rather than QAction, since one
  call site (autonumberingdockwidget's "Configurer" button) is a
  QPushButton, not a QAction. Both declare an identical "shortcut"
  QKeySequence Q_PROPERTY, so registerAction() reads/writes it through the
  property system instead of needing a separate code path.
- Several live targets can share one id at once -- QET allows multiple
  windows of the same kind (diagram editor, element editor...) open
  simultaneously, each constructing its own QAction with the same id.
  setSequence() updates every live target for that id in one call, so a
  rebind takes effect in all open windows immediately, without restart.
- A shortcut's description is captured from its target's text() the first
  time that id is registered, then cached -- so the config page stays
  correct even after the owning window is closed. One consequence: a
  shortcut belonging to an on-demand window (element editor, title block
  editor, rich text editor) only appears in the list once that window has
  been opened at least once in the current session, since nothing has
  registered its id yet otherwise.

Testing
Full CMake build (qmake CONFIG+=no_kf5, Qt 5.15) compiles clean with zero
errors and zero new warnings. Verified end-to-end in a real running session
(Xvfb + xdotool):
- The Shortcuts page appears in Configure QElectroTech with the right icon,
  lists every always-registered shortcut with correct category/action name/
  current binding.
- The filter box correctly narrows the list, and correctly returns nothing
  for an action whose owning window hasn't been constructed yet this
  session (confirming the on-demand-registration behavior above is working
  as designed, not silently broken).
- Conflict detection correctly flagged a real pre-existing same-key overlap
  between "Supprimer" (delete selection, Del) and "Supprimer ce folio"
  (delete diagram from panel, Del) -- both highlighted with explanatory
  tooltips.
- Rebound "Manuel en ligne" to Ctrl+Shift+M, clicked OK: persisted under
  [shortcuts] in QElectroTech.conf, and the Aide menu's entry showed the new
  binding immediately, no restart needed.
- Reopened the dialog: the rebind was still shown. Clicked its per-row
  reset button, then OK: the settings key was removed entirely (not stored
  as "F1"), correctly falling back to the hardcoded default.

Retrofitting the Tab/Shift+Tab, select-all (#585) and Ctrl+G jump-to-element
(#586) shortcuts through this registry is left for a follow-up once those
PRs land, to avoid re-merging still-open branches into this one.

Developed with assistance from Claude (Anthropic).
2026-08-01 01:35:51 +12:00
ispyisail 80fcd4283d Add Ctrl+G "jump to element" type-ahead popup (#574)
Implements the third pillar of #574: a lightweight quick-open popup
for jumping straight to an element on the current diagram, rather
than scrolling/scanning visually.

New JumpToElementDialog (sources/ui/): a small QDialog with a filter
QLineEdit and a live-filtered QListWidget beneath it. Built from
every Element on the diagram, searchable against its label
(elementInformations().value("label")), type name (Element::name()),
and every other element information value, joined into one
lowercased search string per candidate. Up/Down move through the
filtered list, Enter selects the highlighted element on the diagram
(clearing the rest of the selection) and scrolls it into view via
ensureVisible(), Escape cancels without changing the current
selection. All three are handled via an event filter on the line
edit, so the user never has to leave the text field to navigate or
confirm.

Triggered by a new Ctrl+G action in QETDiagramEditor, added next to
the existing Ctrl+F "search and replace" action and to the Edit
menu. Confirmed free: not used anywhere in qetdiagrameditor.cpp or
qetmainwindow.cpp today.

Explicitly not a duplicate of the existing SearchAndReplace module
(also on this menu, via Ctrl+F): that's a bulk property search/replace
tool across whole diagrams; this is a single-item navigational
popup with no editing capability.

Verified end-to-end in a real running session (Xvfb + xdotool)
against a multi-transistor schematic: Ctrl+G opens the popup listing
every element; typing "Q16" live-filters down to the one match;
arrow keys move the highlighted row through the filtered list;
Enter selects the highlighted element (confirmed via the properties
panel showing its label) and closes the popup; Escape closes it
without changing the selection.

See discussion #574.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-01 01:05:43 +12:00
Laurent Trinques 717c89fa66 Merge pull request #562 from Kellermorph/PLC-Manager
PLC Manager
2026-07-31 09:05:30 +02:00
ispyisail 464d516537 Add a local, per-project time-spent tracker (#576)
Adds a ProjectUsageTracker (sources/project/) that accumulates how
long a project has been the active tab, using QElapsedTimer so the
value is computed on demand rather than via polling. It's hosted on
ProjectPropertiesHandler per that class's own stated design intent
("all new properties should be managed by this class").

The accumulated time is persisted as a new <usage time_spent="N"
enabled="true|false"/> element, a sibling of <properties> in the
project XML, written/read by new QETProject::writeUsageXml()/
readUsageXml(). It rides along on the existing autosave path for
free, since writeBackup() already serializes the full project via
toXml().

QETDiagramEditor::subWindowActivated() now pauses every open
project's tracker except the one whose tab just became current, so
switching between several open projects keeps each project's tracked
time isolated.

Surfaced in the existing Project Properties "Général" page: a
"Temps passé sur ce projet" display, a "Réinitialiser" button, and
an opt-out checkbox ("uniquement enregistré localement dans ce
fichier" - this is local-only, never transmitted anywhere).

Verified beyond compiling: full CMake build, then an actual runtime
session confirming the saved XML's time_spent value, that closing
and reopening the project round-trips and resumes timing, that the
reset button works, and - the key correctness check - that with two
projects open, the inactive one's time_spent stays frozen while the
active one accumulates real elapsed time, over the same interval.

See discussion #576.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 09:52:32 +12:00
Kellermorph b3a8ae898e PLC Manager 2026-07-27 22:16:52 +02:00
ispyisail 3ffa0194c7 cmake: Windows translations live in lang/, not l10n/
QET_LANG_PATH was "l10n/" for WIN32, a value that appears nowhere else in
the tree: the MSI shortcuts pass --lang-dir="[INSTALLDIR]lang/", and the
windows-build workflow copies the .qm files into files/lang/. So the
compiled-in default pointed at a directory no packaging creates, leaving
the command-line argument to do all the work.

Align it with what is actually shipped, so the binary-relative lookup
added in the previous commit can find the translations on its own.
2026-07-26 22:41:44 +12:00
Laurent Trinques 42d2c824d2 Merge pull request #513 from ispyisail/feature/edz-import
Import EPLAN Data Portal parts (.edz) into element collections
2026-07-23 13:58:56 +02:00
Laurent Trinques 7a87754024 Merge pull request #545 from Kellermorph/terminal-name
Implement slave contact groups — label transfer, terminal assignment and UI fixes
2026-07-18 08:00:23 +02:00
Dieter Mayer 683eefdac1 Request the GuiPrivate component only on Qt6
Listing GuiPrivate unconditionally in QET_COMPONENTS breaks the whole
Qt5 configure: find_package(Qt5 COMPONENTS GuiPrivate) looks for a
Qt5GuiPrivateConfig.cmake that has never existed - Qt5 creates the
Qt5::GuiPrivate target implicitly together with Gui. Only Qt6 requires
(and provides) the explicit component.

Move the request into a QT_VERSION_MAJOR-guarded find_package after the
main one, both for the application and for tests/catch (whose targets
link Qt::GuiPrivate via QET_PRIVATE_LIBRARIES). Fixes the msys2/Qt5
Windows CI configure failure:
"Could not find a package configuration file provided by Qt5GuiPrivate".

Verified: Qt 6.11 configure passes and the Qt6::GuiPrivate target is
created (the private-module warning now fires from the guarded call).
The Qt5 path simply no longer requests the component, restoring the
pre-existing implicit behaviour.
2026-07-17 19:25:26 +02:00
Laurent Trinques 424a1c945e Try to fix compile with cmake .. -G Ninja -DQT_VERSION_MAJOR=6 -DBUILD_WITH_KF5=OFF -DPACKAGE_TESTS=OFF 2026-07-17 10:25:30 +02:00
Kellermorph b025bd205d Implement slave contact groups — label transfer, terminal assignment and UI fixes 2026-07-17 09:47:32 +02:00
Kellermorph 2db6c3968a feat: add backup dialog when opening an existing project 2026-07-07 21:42:45 +02:00
Laurent Trinques 14253930ae Merge pull request #509 from Kellermorph/master
Add Custom Guides
2026-07-07 16:34:56 +02:00
Gerhard Schwanzer e6124e941a Add Qt-only autosave recovery fallback
Provide a small KAutoSaveFile-compatible implementation for the no-KF5 build path and use it to keep the existing crash-recovery code active when BUILD_WITH_KF5=OFF.

The normal KF5 build still uses the KDE KAutoSaveFile implementation.

Assisted-by: pi coding agent / Mika (OpenAI GPT-5.5)
2026-07-05 13:23:49 +02:00
Gerhard Schwanzer d0cea474a6 Fix Qt-only build without KF5
The BUILD_WITH_KF5 option was checked with DEFINED, so passing -DBUILD_WITH_KF5=OFF still entered the KF5 setup path.

Skip the KF5 setup when disabled and provide small Qt-only replacements for the KDE color widgets used by .ui files in that build mode.

Assisted-by: pi coding agent / Mika (OpenAI GPT-5.5)
2026-07-05 11:10:44 +02:00
Shane Ringrose 734391eabf Merge master: add cli_export, pdf_links; EDZ: 10-slot grid, group headers, ToS notice
- Resolve cmake/qet_compilation_vars.cmake conflict: keep both upstream's
  cli_export.cpp/h and pdf_links.cpp/h and the EDZ source additions.

- 10-position grid alignment: pin_y values are multiples of 10 so terminals
  snap cleanly to QET's default grid.  group_gap raised to 10 (one full slot).

- Named connector groups get a header label (group name) placed in the gap
  above the first pin, so the electrician sees block names (XDI, XPOW, …)
  without reading individual terminal designations.

- Device-tag dynamic_text now uses 9pt LABEL_FONT and y = min_y - 9 so it
  clears the element body and is legible at normal zoom.

- Add EPLAN Data Portal Terms of Use disclaimer to sources/import/edz/README.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 21:00:49 +12:00
Shane Ringrose e4b4ba875b Bundle LZMA SDK for .edz extraction; drop 7z CLI dependency (M3)
Replaces the QProcess->7z shim with the public-domain LZMA SDK (23.01) 7-Zip
reader, vendored under sources/import/edz/lzma/ (decode-only subset). New
edzsevenzip.cpp wraps SzArEx_Open/SzArEx_Extract and writes entries via Qt, so
EdzArchive no longer needs an external 7-Zip at runtime. Enables the C language
in CMake for the vendored sources.

Verified: the bundled decoder extracts all three ifm sample .edz and the
generated elements still match the Python oracle exactly (byte-correct decode),
with no 7z on the path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 07:49:14 +12:00
Shane Ringrose f4ff6c81f9 Add EdzImporter + collection-panel import action (M2)
Wires EPLAN .edz import into the elements panel. EdzImporter orchestrates
EdzArchive -> EdzPart -> EdzElementBuilder and writes the generated .elmt into a
destination collection folder (named by order number). A right-click
"Importer une piece EPLAN (.edz)..." action on a writable collection directory
opens a file picker, runs the importer into that folder's fileSystemPath() and
reloads the panel; errors surface via QetMessageBox. Modeled on newElement().

EdzImporter verified headless against the Python oracle for KG6000/MFH200/
R1D200; the panel wiring is built/tested via the WSL Qt5+KF5 build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 07:34:52 +12:00
Shane Ringrose 7cc5790f7c Add EdzPart + EdzElementBuilder: .edz part -> .elmt (M1)
Port of the edz2qet.py prototype's mapping to C++. EdzPart parses an EPLAN
part.xml into a portable model (identity/metadata, localized names, connection
list); EdzElementBuilder turns that into a QET element (generic symbol: body
rectangle + one west-facing terminal per pin, per-pin labels, localized <name>s,
and elementInformations for the BOM).

Pins are natural-sorted by designation so they stack 1,2,3,4 regardless of the
order EPLAN lists them (MFH200 lists 1,3,4,2). Output verified structurally
identical (uuids aside) to the Python oracle for three ifm samples — KG6000
(4-pin), MFH200 (4-pin, reordered) and R1D200 (5-pin, incl. Dutch name) — and
the generated element loads in the QET editor with correct UTF-8.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 07:11:09 +12:00
Shane Ringrose 82884d1cf7 Add EdzArchive: extract EPLAN .edz packages (M0)
First piece of native EPLAN Data Portal (.edz) import. A .edz is a 7-Zip
archive; QET previously had no archive handling. EdzArchive unpacks one to a
temporary directory and locates the contained part.xml.

The extraction backend is isolated behind extractWithSevenZipCli() so it can be
replaced with a bundled decompressor later without touching callers; this M0
step shells out to a 7-Zip CLI via QProcess. Verified on three ifm sample
parts (KG6000, MFH200, R1D200 — all 7z), plus the corrupt/missing-file paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 07:05:42 +12:00
Kellermorph 4e0c075575 Fix grid and guide startup behavior and update german translation 2026-06-16 17:41:03 +02:00
Shane Ringrose 6b3b55b0e1 PDF links: extract pdf_links.{cpp,h} shared helper
Move the PDF cross-reference hyperlink logic out of ProjectPrintWindow
into a standalone translation unit so it can be reused (the CLI PDF
export will call it next):

- injectCrossRefLinks(): emits the URI link annotations for a diagram's
  cross-references and folio reports. The scene-to-page mapping is passed
  in as a PageGeometry (transform + devToPdf + source-rect lookup) so each
  caller supplies its own correct geometry, rather than the helper assuming
  a QPrinter.
- convertUriToGoTo(): the PDF post-processor, moved verbatim.

ProjectPrintWindow stays a pure caller: it builds its PageGeometry from the
printer page layout exactly as before and calls the helper. No behavioural
change to GUI PDF export; no class-structure changes.

Per review guidance on #483.
2026-06-11 23:48:14 +12:00
Shane Ringrose 1070179617 Add headless command-line export (PDF/PNG/SVG)
Implements the long-requested batch/headless export
(bugtracker #171, GitHub #309): render a project's diagrams to files
without opening the GUI.

  qelectrotech --export-pdf <project.qet> <output.pdf>   one multi-page PDF
  qelectrotech --export-png <project.qet> <output_dir>   one PNG per diagram
  qelectrotech --export-svg <project.qet> <output_dir>   one SVG per diagram

main.cpp detects an export request before SingleApplication is created (so the
arguments are not forwarded to a running instance), spins up a plain
QApplication for rendering, and exits with the export's status code.

Rendering reuses Diagram::render() over
BorderTitleBlock::borderAndTitleBlockRect(), the same geometry the GUI
print/export path uses, so output matches the editor. Image files are named
NN_Title.<ext>.

New files: sources/cli_export.{h,cpp}, registered in
cmake/qet_compilation_vars.cmake.
2026-06-11 11:10:09 +12:00
Laurent Trinques 6c4711a8d0 Update qet_compilation_vars.cmake 2026-06-07 15:11:06 +02:00
Laurent Trinques 5cb8930732 Update fetch_pugixml.cmake 2026-06-07 14:57:29 +02:00
Laurent Trinques a24acfac24 Update fetch_pugixml.cmake 2026-06-07 14:57:05 +02:00
Laurent Trinques cd76b6a1d6 This adds native, clickable hyperlinks to PDF exports: cross-references jump
directly to the related component on its folio, framing the target element.

When a project is exported to PDF, every cross-reference becomes an internal
link. Four kinds are covered:

- **Master → contact**: the contact list on a coil/relay (`CrossRefItem`)
- **Folio report → report**: report element labels (`DynamicElementTextItem`)
- **Slave → master**: the `(folio-position)` reference shown on a slave
  (both standalone `DynamicElementTextItem` and grouped `ElementTextItemGroup`)

Clicking a link navigates **inside** the open document (no new viewer
instance) and zooms to frame the target element.

1. **Injection** (`printDiagram`, only when the paint engine is a `QPdfEngine`):
   link rectangles are added with `QPdfEngine::drawHyperlink()`. The scene→page
   mapping is rebuilt to match exactly what `QGraphicsScene::render()` does
   (top-left anchored, `KeepAspectRatio`, **no centering**), and rectangles are
   passed in device pixels — `pageMatrix()` already applies the 72/resolution
   scale and Y-flip internally.

2. Each link URL encodes the target page and the target element's rectangle, in
   PDF points on its own page: `#page=N&fitr=L_B_R_T`.

3. **Post-processing** (`pdfConvertUriToGoTo`, run after the painter is closed):
   the `/S /URI` annotations are rewritten to native `/S /GoTo` actions with a
   `/D [pageObj 0 R /FitR L B R T]` destination, and the xref table is rebuilt.
   Pages are enumerated from the `/Pages /Kids` tree (reliable), not by scanning
   for `/Type /Page` in raw bytes.

- `sources/print/projectprintwindow.{cpp,h}` — injection + post-processing
- `sources/qetgraphicsitem/crossrefitem.{cpp,h}` — `hoveredContactsMap()` accessor; store text rect for hit area
- `sources/qetgraphicsitem/dynamicelementtextitem.h` — `slaveXrefItem()` / `masterElement()` accessors
- `sources/qetgraphicsitem/elementtextitemgroup.h` — `slaveXrefItem()` accessor
- `qelectrotech.pro`, `cmake/qet_compilation_vars.cmake` — enable Qt gui-private headers (`<private/qpdf_p.h>`)

- **Fit-to-page mode only.** Links are not injected in tiled mode (multiple
  pages per folio), which would require a per-tile transform.
- Uses Qt private API (`QPdfEngine::drawHyperlink`), stable since Qt 4 but not
  part of the public API; the build links against `gui-private`.
- Page-tree enumeration assumes the flat `/Kids` array Qt produces (no nested
  page trees).
- The frame zoom is controlled by two constants in `destRectPdf` (`pad`,
  `minSide`) and can be tuned.
- Tested on Qt5; the `/Kids` parsing and `pageMatrix` behaviour are identical on
  Qt6.
2026-05-30 18:48:28 +02:00
Kellermorph 0118d94d4e makro 2026-04-26 10:48:47 +02:00
Kellermorph eb8f859038 Terminal numbering 2026-04-09 08:36:54 +02:00
Kellermorph 51725d9d8e Replace conductornumexport.cpp with wiringlistexport.h 2026-04-05 18:32:35 +02:00
Kellermorph bc9173d726 Add RAM-based wiring list export 2026-04-05 08:33:37 +02:00
Andre Rummler 67b80364e1 CMAKE/FIX: BE was missing as it had been renamed to nl_BE. RS and UK were not included. 2025-09-08 23:52:27 +02:00
Magnus Hellströmer 82d7bbda68 Added swedish language 2025-02-16 20:54:15 +01:00
plc-user d971da5ac7 add commandline-parameter "--data-dir" 2025-01-27 21:48:36 +01:00
plc-user 5d725f6d55 fix typo 2024-08-19 09:48:11 +02:00
plc-user 85d10fc0e6 integrate "QET_ElementScaler" as external software 2024-08-19 09:19:28 +02:00
plc-user 73f7d44dcf changed some remaining "pt_br" to "pt_BR" 2024-03-28 17:43:15 +01:00
Simon De Backer d35b1623a4 Fix Cmake build
When creating new files.
Add the new files to: "cmake/qet_compilation_vars.cmake".

So that Cmake stays up to date.

thank you.
2023-12-10 22:58:31 +01:00