Compare commits

...

39 Commits

Author SHA1 Message Date
Laurent Trinques 905afc1bbc Revert "Auto-break conductor" 2026-08-07 16:25:06 +02:00
Laurent Trinques d0fcc9ed78 Merge pull request #639 from Kellermorph/Replace-automatic-conductors
Auto-break conductor
2026-08-07 16:15:29 +02:00
Laurent Trinques cb7b45e281 Merge branch 'master' into Replace-automatic-conductors 2026-08-07 14:58:42 +02:00
Laurent Trinques 725678a866 Merge pull request #684 from arummler/master-remove-richtext-uic
Remove pre-compiled richtext widget
2026-08-07 14:52:34 +02:00
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 e3964fb24a Update en and fr translations files 2026-08-07 13:12:28 +02:00
Laurent Trinques 5df9a987a2 Merge pull request #678 from ispyisail/fix/menu-orphaned-actions
Add two orphaned actions to the menus, drop two dead members
2026-08-07 12:42:30 +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
Laurent Trinques 14ef848c4a Merge pull request #681 from IBSYSLevi/fix/save-new-project-cross-references
Fix: Save the default cross-reference properties to QSettings
2026-08-07 11:55:45 +02:00
Laurent Trinques dcc64462f6 Merge pull request #680 from ispyisail/feature/optional-pch
Add optional precompiled headers behind QET_ENABLE_PCH (default OFF)
2026-08-07 11:51:57 +02:00
Levi Jetzer 97709bff6a Save the default cross-reference properties to QSettings
NewDiagramPage::applyConf() writes every other default to QSettings —
border, title block, conductors, folio reports and the guides — but the
cross-reference branch only fetched the properties into a local hash and
then dropped it on the floor. hash_xrp was never used.

The result: changing the cross-reference defaults under Settings > New
project has no effect. Nothing is written, no defaultxref* key ever
appears in the configuration file, and XRefProperties::defaultProperties()
keeps handing out the hardcoded fallbacks for every new project.

Write each of the four types (coil, protection, commutator, plc) with the
"diagrameditor/defaultxref" + key prefix that defaultProperties() already
reads back.
2026-08-07 11:42:20 +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 9891eef916 Add two orphaned actions to the menus, drop two dead members
Both actions already exist and work; they were simply only reachable from
a toolbar, and those toolbars are user-hideable via Configuration >
Afficher, so hiding one made the feature unreachable entirely.

- "Afficher les guides" (m_draw_guides) goes into the Affichage menu next
  to "Afficher la grille". The two are adjacent lines in the view toolbar
  and do the same kind of thing, but only the grid had a menu entry.

- "Creation automatique de conducteur(s)" (m_auto_conductor) goes into the
  Projet menu. It writes a project setting via
  QETProject::setAutoConductor(), so the Projet menu is where a user would
  look for it; it is placed with the project properties, above a separator
  that keeps the folio operations grouped as before.

Also removes conductor_default and m_project_folio_list from the header.
Both are declared but never allocated and never referenced anywhere in the
tree -- that the build still links is the proof they were dead.

No new strings: both actions already carry translated text.

Found while auditing every QAction against every menu, discussion #677.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 19:07:52 +12:00
Laurent Trinques 4ff2be3f43 Update translations files 2026-08-06 13:36:38 +02:00
Laurent Trinques 894287111c Merge pull request #665 from ispyisail/feature-eventloop-watchdog
Add an event-loop responsiveness watchdog (discussion #644 follow-up)
2026-08-06 13:30:43 +02:00
Laurent Trinques 3603feb5d8 Merge pull request #647 from ispyisail/feature-diagnostic-logging-crash
Add crash-time ring flush and diagnostics export UI (discussion #644, steps 4-5)
2026-08-06 13:26:14 +02: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 ea5117b148 ci(doxygen): remove stale branch guard blocking the tag-triggered job
The job trigger was narrowed to push:tags a while back, but the
job-level 'if: github.ref == refs/heads/master' was left in place.
A tag push never has github.ref == refs/heads/master, so the two
conditions are mutually exclusive: the job trigger fires only on
tag pushes, while the guard only allows master-branch refs, meaning
the job has been silently skipped on every run since the trigger
was narrowed.

This is also the likely source of the recent Git LFS bandwidth/
storage overage: while the trigger was still push-to-master (pre-
narrowing), this job ran on nearly every commit and committed a
new QElectroTech.qch (LFS-tracked) each time, via the auto-generated
update-qch PR -- accumulating one LFS object version per run."
2026-08-06 11:45:53 +02:00
Laurent Trinques 34078c6459 Merge pull request #670 from IBSYSLevi/fix/projectview_corner_Layout_crash
Fix crash when opening a project: invalid index in corner layout
2026-08-06 11:30:37 +02:00
Laurent Trinques f46b37dd3e Update .pro files 2026-08-06 10:50:27 +02:00
IBSYSLevi e3a488e2a8 Merge branch 'qelectrotech:master' into fix/projectview_corner_Layout_crash 2026-08-06 10:47:54 +02: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
Levi Jetzer a911d72756 Fix crash when opening a project: invalid index in corner layout
ProjectView::initWidgets() called insertSpacing(1, 10) on a QHBoxLayout
that was still empty, inserting past the end of the item list. The
corrupted layout crashed later in QWidget::setLayout() via
QLayoutPrivate::reparentChildWidgets() and QBoxLayout::itemAt().

Use addSpacing(10) instead, which is equivalent for an empty layout.
2026-08-06 10:40:03 +02:00
Laurent Trinques f95ff29c21 Rename ChangeLog.md -> ChangeLog_full.md
Because: Windows is not case-sensitive, thanks Achim
2026-08-06 10:24:37 +02:00
Laurent Trinques 06b20c483e Merge pull request #645 from ispyisail/feature-autonum-undo
Cover auto-numbering counter changes with undo/redo (discussion #608)
2026-08-06 08:49:33 +02:00
Laurent Trinques 1318dc4b8e Merge pull request #658 from ispyisail/feature-insert-folio-position
Add "Insert folio above/below" to the elements panel's folio menu
2026-08-06 08:13:41 +02:00
Laurent Trinques 9c3ba45822 Merge pull request #669 from Kellermorph/fix-plc-editor
PLC Fix scroll sync, data persistence, font defaults, copy/paste, and layout fixes
2026-08-06 08:03:22 +02:00
Laurent Trinques 37e18efc6c Merge pull request #667 from Kellermorph/text-settings-terminals
Use global font as default for terminal label font
2026-08-06 08:03:03 +02:00
Kellermorph 4f9474a05f PLC Fix scroll sync, data persistence, font defaults, copy/paste, and layout fixes 2026-08-06 07:33:39 +02:00
Kellermorph 150b1796a4 Use global font as default for terminal label font 2026-08-05 21:11:02 +02:00
Kellermorph 06d1d70142 bug 2 2026-08-04 16:48:52 +02:00
ispyisail c0896c7ba7 Add "Insert folio above/below" to the elements panel's folio menu
"Add folio" always appends to the end of the project, ignoring
whatever folio is currently selected in the left panel -- even though
the panel already tracks the selected diagram's position for its
existing move up/down/top actions, and QETProject::addNewDiagram(pos)
already accepts an arbitrary insertion index, pushed as an undoable
AddDiagramCommand (QetGraphicsTableFactory::create() already relies on
this exact mechanism to insert a folio right after a specific one).

Add two new context-menu actions that compute the target position from
the selected diagram's folioIndex() and pass it straight through the
existing machinery -- no changes needed to QETProject or
AddDiagramCommand. New requestForNewDiagramAt/addDiagramToProjectAt
signal/slot pair added alongside the existing
requestForNewDiagram/addDiagramToProject rather than changing it, so
the plain "Add folio" action's append-at-end behavior is untouched.
2026-08-04 16:36:23 +12:00
Kellermorph 5ec49eedda fix bug 2 2026-08-03 21:48:34 +02:00
Kellermorph d850241f91 fix 2026-08-03 19:17:38 +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
Kellermorph ca42a2b7ff Auto-break conductor 2026-08-02 15:18:57 +02:00
79 changed files with 31084 additions and 21671 deletions
-1
View File
@@ -11,7 +11,6 @@ jobs:
permissions:
contents: write
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master'
steps:
- uses: actions/checkout@v4
with:
+28
View File
@@ -145,6 +145,34 @@ else()
)
endif()
# Optional precompiled headers -- see QET_ENABLE_PCH in
# cmake/developer_options.cmake for what this trades away.
#
# target_precompile_headers() needs CMake 3.16; the project still declares a
# 3.5 minimum, so guard rather than raise it for an opt-in developer feature.
#
# The generator expressions are load-bearing, not decoration: this target also
# compiles the 18 C files of the bundled LZMA decoder
# (sources/import/edz/lzma/*.c), and an unguarded list applies to every
# language in the target, so the Qt headers would reach the C compiler and fail
# with "unknown type name 'namespace'". $<ANGLE-R> is required because a
# literal '>' would terminate the generator expression.
if(QET_ENABLE_PCH)
if(CMAKE_VERSION VERSION_LESS 3.16)
message(WARNING
"QET_ENABLE_PCH needs CMake 3.16 or newer (found ${CMAKE_VERSION}); "
"building without precompiled headers.")
else()
target_precompile_headers(${PROJECT_NAME} PRIVATE
"$<$<COMPILE_LANGUAGE:CXX>:<QtCore/QtCore$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<QtGui/QtGui$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<QtWidgets/QtWidgets$<ANGLE-R>>"
"$<$<COMPILE_LANGUAGE:CXX>:<QtXml/QtXml$<ANGLE-R>>"
)
message(STATUS "QET_ENABLE_PCH: precompiled headers enabled")
endif()
endif()
target_link_libraries(
${PROJECT_NAME}
PUBLIC
View File
+14
View File
@@ -33,3 +33,17 @@ add_definitions(-DQT_MESSAGELOGCONTEXT)
# Build with KF5
option(BUILD_WITH_KF5 "Build with KF5" ON)
# Precompiled headers for the Qt umbrella headers.
#
# Off by default and intended for local development only. Building QET is
# dominated by re-parsing Qt's headers: a 214-line .cpp expands to ~198,000
# preprocessed lines, and compiling one translation unit costs ~4.1 s of which
# only ~0.35 s is optimisation (-O0 instead of -O3 saves 8%). A PCH caches the
# parsed header state and takes that ~4.1 s down to ~1.2 s.
#
# It is deliberately NOT on by default: a PCH satisfies includes that a source
# file forgot to make itself, so code written with it enabled can fail to
# compile for everyone else. Leaving it off keeps CI and contributors on the
# strict behaviour, and only developers who opt in trade that for the speed.
option(QET_ENABLE_PCH "Use precompiled headers (developer build speed; may mask missing #includes)" OFF)
+14 -1
View File
@@ -116,6 +116,16 @@ set(QET_RES_FILES
set(QET_SRC_FILES
${QET_DIR}/sources/cli_export.cpp
${QET_DIR}/sources/cli_export.h
${QET_DIR}/sources/logging/crashhandler.cpp
${QET_DIR}/sources/logging/crashhandler.h
${QET_DIR}/sources/logging/eventloopwatchdog.cpp
${QET_DIR}/sources/logging/eventloopwatchdog.h
${QET_DIR}/sources/logging/logring.cpp
${QET_DIR}/sources/logging/logring.h
${QET_DIR}/sources/logging/qetlogger.cpp
${QET_DIR}/sources/logging/qetlogger.h
${QET_DIR}/sources/logging/ui/diagnosticsreportdialog.cpp
${QET_DIR}/sources/logging/ui/diagnosticsreportdialog.h
${QET_DIR}/sources/pdf_links.cpp
${QET_DIR}/sources/pdf_links.h
${QET_DIR}/sources/import/edz/edzarchive.cpp
@@ -530,7 +540,6 @@ set(QET_SRC_FILES
${QET_DIR}/sources/richtext/richtexteditor.cpp
${QET_DIR}/sources/richtext/richtexteditor_p.h
${QET_DIR}/sources/richtext/ui_addlinkdialog.h
${QET_DIR}/sources/SearchAndReplace/searchandreplaceworker.cpp
${QET_DIR}/sources/SearchAndReplace/searchandreplaceworker.h
@@ -683,6 +692,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/ui/dynamicelementtextitemeditor.h
${QET_DIR}/sources/ui/dynamicelementtextmodel.cpp
${QET_DIR}/sources/ui/dynamicelementtextmodel.h
${QET_DIR}/sources/ui/customelementinfopartwidget.cpp
${QET_DIR}/sources/ui/customelementinfopartwidget.h
${QET_DIR}/sources/ui/elementinfopartwidget.cpp
${QET_DIR}/sources/ui/elementinfopartwidget.h
${QET_DIR}/sources/ui/elementinfowidget.cpp
@@ -761,6 +772,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/undocommand/movediagramcommand.h
${QET_DIR}/sources/undocommand/removediagramcommand.cpp
${QET_DIR}/sources/undocommand/removediagramcommand.h
${QET_DIR}/sources/undocommand/setautonumcontextcommand.cpp
${QET_DIR}/sources/undocommand/setautonumcontextcommand.h
${QET_DIR}/sources/undocommand/rotateselectioncommand.cpp
${QET_DIR}/sources/undocommand/rotateselectioncommand.h
${QET_DIR}/sources/undocommand/rotatetextscommand.cpp
+923 -681
View File
File diff suppressed because it is too large Load Diff
+915 -681
View File
File diff suppressed because it is too large Load Diff
+917 -681
View File
File diff suppressed because it is too large Load Diff
+915 -681
View File
File diff suppressed because it is too large Load Diff
+905 -676
View File
File diff suppressed because it is too large Load Diff
+915 -681
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+906 -676
View File
File diff suppressed because it is too large Load Diff
+915 -681
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+940 -706
View File
File diff suppressed because it is too large Load Diff
+922 -686
View File
File diff suppressed because it is too large Load Diff
+913 -681
View File
File diff suppressed because it is too large Load Diff
+915 -681
View File
File diff suppressed because it is too large Load Diff
+913 -681
View File
File diff suppressed because it is too large Load Diff
+913 -681
View File
File diff suppressed because it is too large Load Diff
+915 -681
View File
File diff suppressed because it is too large Load Diff
+915 -681
View File
File diff suppressed because it is too large Load Diff
+915 -681
View File
File diff suppressed because it is too large Load Diff
+943 -709
View File
File diff suppressed because it is too large Load Diff
+917 -681
View File
File diff suppressed because it is too large Load Diff
+943 -709
View File
File diff suppressed because it is too large Load Diff
+915 -681
View File
File diff suppressed because it is too large Load Diff
+946 -710
View File
File diff suppressed because it is too large Load Diff
+945 -709
View File
File diff suppressed because it is too large Load Diff
+917 -681
View File
File diff suppressed because it is too large Load Diff
+945 -709
View File
File diff suppressed because it is too large Load Diff
+947 -709
View File
File diff suppressed because it is too large Load Diff
+945 -709
View File
File diff suppressed because it is too large Load Diff
+915 -681
View File
File diff suppressed because it is too large Load Diff
+913 -681
View File
File diff suppressed because it is too large Load Diff
+917 -681
View File
File diff suppressed because it is too large Load Diff
+913 -681
View File
File diff suppressed because it is too large Load Diff
+7 -2
View File
@@ -173,7 +173,9 @@ HEADERS += $$files(sources/*.h) \
$$files(sources/qet_elementscaler/*.h) \
$$files(sources/svg/*.h) \
$$files(sources/import/edz/*.h) \
$$files(sources/import/edz/lzma/*.h)
$$files(sources/import/edz/lzma/*.h) \
$$files(sources/logging/*.h) \
$$files(sources/logging/ui/*.h)
SOURCES += $$files(sources/*.cpp) \
$$files(sources/editor/*.cpp) \
@@ -219,7 +221,10 @@ SOURCES += $$files(sources/*.cpp) \
$$files(sources/qet_elementscaler/*.cpp) \
$$files(sources/svg/*.cpp) \
$$files(sources/import/edz/*.cpp) \
$$files(sources/import/edz/lzma/*.c)
$$files(sources/import/edz/lzma/*.c) \
$$files(sources/logging/*.cpp) \
$$files(sources/logging/ui/*.cpp)
# Needed for use promote QTreeWidget in terminalstripeditor.ui
INCLUDEPATH += sources/TerminalStrip/ui
+26 -2
View File
@@ -24,6 +24,7 @@
#include "qet.h"
#include "qetdiagrameditor.h"
#include "ui/potentialselectordialog.h"
#include "undocommand/setautonumcontextcommand.h"
/**
@brief ConductorAutoNumerotation::ConductorAutoNumerotation
@@ -156,7 +157,16 @@ void ConductorAutoNumerotation::newProperties(
autonum::setSequential(formula, seq, context, diagram, autoNum_name);
NumerotationContextCommands ncc (context, diagram);
diagram->project()->addConductorAutoNum(autoNum_name, ncc.next());
NumerotationContext new_context = ncc.next();
QETProject *project = diagram->project();
auto *undo = new SetAutoNumContextCommand(
[project](const QString &k, const NumerotationContext &c) {project->addConductorAutoNum(k, c);},
autoNum_name,
context,
new_context);
undo->setText(QObject::tr("Numéroter automatiquement un conducteur", "undo caption"));
diagram->undoStack().push(undo);
}
/**
@@ -245,7 +255,21 @@ void ConductorAutoNumerotation::numerateNewConductor()
autoNum_name);
NumerotationContextCommands ncc (context, m_diagram);
m_diagram->project()->addConductorAutoNum(autoNum_name, ncc.next());
NumerotationContext new_context = ncc.next();
QETProject *project = m_diagram->project();
auto setter = [project](const QString &k, const NumerotationContext &c) {project->addConductorAutoNum(k, c);};
if (m_parent_undo)
{
new SetAutoNumContextCommand(setter, autoNum_name, context, new_context, m_parent_undo);
}
else
{
auto *undo = new SetAutoNumContextCommand(setter, autoNum_name, context, new_context);
undo->setText(QObject::tr("Numéroter automatiquement un conducteur", "undo caption"));
m_diagram->undoStack().push(undo);
}
}
applyText(autonum::AssignVariables::formulaToLabel(
@@ -271,7 +271,12 @@ void DiagramEventAddElement::addElement()
}
m_diagram->addItem(m_element);
//Autonum the new element before pushing undo_object, so the counter
//change it triggers is part of the same undo macro as the element's
//own placement (one Ctrl+Z reverts both, instead of silently
//leaving the counter advanced).
element->setUpFormula(true, undo_object);
m_diagram -> undoStack().push(undo_object);
element->setUpFormula();
element->freezeNewAddedElement();
}
+3 -2
View File
@@ -18,6 +18,7 @@
#include "partplctable.h"
#include "../../QPropertyUndoCommand/qpropertyundocommand.h"
#include "../../qetapp.h"
#include "../../QetGraphicsItemModeler/qetgraphicshandleritem.h"
#include "../../QetGraphicsItemModeler/qetgraphicshandlerutility.h"
#include "../../properties/elementdata.h"
@@ -302,7 +303,7 @@ void PartPlcTable::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti
// Draw column headers
QFont header_font = plc_data.headerFont.family().isEmpty()
? painter->font() : plc_data.headerFont;
? QETApp::diagramTextsFont() : plc_data.headerFont;
header_font.setBold(true);
painter->setFont(header_font);
@@ -318,7 +319,7 @@ void PartPlcTable::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti
// Draw IO rows
QFont cell_font = plc_data.cellFont.family().isEmpty()
? painter->font() : plc_data.cellFont;
? QETApp::diagramTextsFont() : plc_data.cellFont;
painter->setFont(cell_font);
int start_idx = block_starts.at(block);
int end_idx = (block + 1 < block_starts.size())
@@ -28,6 +28,8 @@
#include <QSignalBlocker>
#include <QTableWidgetItem>
#include <QHeaderView>
#include <QScrollBar>
#include <QWheelEvent>
#include <QTableWidget>
#include <QCheckBox>
#include <QGroupBox>
@@ -41,6 +43,8 @@
#include <QFont>
#include <QLineEdit>
#include <QSplitter>
#include <QShortcut>
#include <QMenu>
/**
@brief The EditorDelegate class
@@ -666,10 +670,15 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
plc_layout->addLayout(toolbar);
// Tables side by side: IO table (left) + Terminal table (right)
auto *tables_splitter = new QSplitter(Qt::Horizontal, m_plc_gb);
// Both share a single vertical scrollbar on the right
auto *tables_container = new QWidget(m_plc_gb);
auto *tables_layout = new QHBoxLayout(tables_container);
tables_layout->setContentsMargins(0, 0, 0, 0);
auto *splitter = new QSplitter(Qt::Horizontal, tables_container);
// IO Table
m_plc_table = new QTableWidget(tables_splitter);
m_plc_table = new QTableWidget(splitter);
m_plc_table->setColumnCount(5);
m_plc_table->setHorizontalHeaderLabels({
tr("Type"), tr("Adresse"), tr("Fonction"),
@@ -686,10 +695,10 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectItems);
m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_plc_table->setMinimumHeight(200);
tables_splitter->addWidget(m_plc_table);
m_plc_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
// Terminal table (per IO: Nb + T1-T4)
m_plc_terminal_table = new QTableWidget(tables_splitter);
m_plc_terminal_table = new QTableWidget(splitter);
m_plc_terminal_table->setColumnCount(2);
m_plc_terminal_table->setHorizontalHeaderLabels({
tr("Nb."), tr("T1")
@@ -700,13 +709,61 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
m_plc_terminal_table->setSelectionBehavior(QAbstractItemView::SelectItems);
m_plc_terminal_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_plc_terminal_table->setMinimumHeight(200);
tables_splitter->addWidget(m_plc_terminal_table);
m_plc_terminal_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
tables_splitter->setStretchFactor(0, 3);
tables_splitter->setStretchFactor(1, 1);
tables_splitter->setSizes({500, 150});
// Shared vertical scrollbar
m_plc_shared_scrollbar = new QScrollBar(Qt::Vertical, tables_container);
m_plc_shared_scrollbar->setMinimum(0);
plc_layout->addWidget(tables_splitter);
splitter->addWidget(m_plc_table);
splitter->addWidget(m_plc_terminal_table);
splitter->setStretchFactor(0, 3);
splitter->setStretchFactor(1, 1);
tables_layout->addWidget(splitter);
tables_layout->addWidget(m_plc_shared_scrollbar);
// Bidirectional sync between tables and shared scrollbar
// Use flag to prevent infinite loops
connect(m_plc_shared_scrollbar, &QScrollBar::valueChanged,
this, [this](int value) {
if (m_plc_scroll_sync) return;
m_plc_scroll_sync = true;
m_plc_table->verticalScrollBar()->setValue(value);
m_plc_terminal_table->verticalScrollBar()->setValue(value);
m_plc_scroll_sync = false;
});
connect(m_plc_table->verticalScrollBar(), &QScrollBar::valueChanged,
this, [this](int value) {
if (m_plc_scroll_sync) return;
m_plc_scroll_sync = true;
m_plc_shared_scrollbar->setValue(value);
m_plc_terminal_table->verticalScrollBar()->setValue(value);
m_plc_scroll_sync = false;
});
connect(m_plc_terminal_table->verticalScrollBar(), &QScrollBar::valueChanged,
this, [this](int value) {
if (m_plc_scroll_sync) return;
m_plc_scroll_sync = true;
m_plc_shared_scrollbar->setValue(value);
m_plc_table->verticalScrollBar()->setValue(value);
m_plc_scroll_sync = false;
});
// Update shared scrollbar range from both tables
auto syncRange = [this]() {
int max = qMax(m_plc_table->verticalScrollBar()->maximum(),
m_plc_terminal_table->verticalScrollBar()->maximum());
m_plc_shared_scrollbar->blockSignals(true);
m_plc_shared_scrollbar->setMaximum(max);
m_plc_shared_scrollbar->blockSignals(false);
};
connect(m_plc_table->verticalScrollBar(), &QScrollBar::rangeChanged,
this, syncRange);
connect(m_plc_terminal_table->verticalScrollBar(), &QScrollBar::rangeChanged,
this, syncRange);
plc_layout->addWidget(tables_container);
// Font settings
auto *font_layout = new QHBoxLayout();
@@ -790,12 +847,25 @@ void ElementPropertiesEditorWidget::createPlcConfigWidgets()
}
plc_layout->addLayout(col_layout);
// Add to master group box
ui->m_master_gb->layout()->addWidget(m_plc_gb);
// Add to master group box - row 4, full width
auto *gl = qobject_cast<QGridLayout*>(ui->m_master_gb->layout());
gl->addWidget(m_plc_gb, 4, 0, 1, 2);
// Connect signals
connect(add_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcAddRow);
connect(remove_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcRemoveRow);
// Ctrl+V shortcut for paste
auto *paste_shortcut = new QShortcut(QKeySequence::Paste, m_plc_table);
connect(paste_shortcut, &QShortcut::activated, this, &ElementPropertiesEditorWidget::plcPasteFromClipboard);
// Context menu for the PLC table
m_plc_table->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_plc_table, &QTableWidget::customContextMenuRequested, this, [this](const QPoint &pos) {
QMenu menu;
menu.addAction(tr("Coller depuis le presse-papiers"), this, &ElementPropertiesEditorWidget::plcPasteFromClipboard);
menu.exec(m_plc_table->mapToGlobal(pos));
});
}
/**
@@ -877,12 +947,12 @@ void ElementPropertiesEditorWidget::populatePlcTable()
m_plc_header_font = plc_data.headerFont;
m_plc_cell_font = plc_data.cellFont;
if (m_plc_header_font.family().isEmpty()) {
m_plc_header_font = QFont(m_plc_table->font());
m_plc_header_font = QETApp::diagramTextsFont();
m_plc_header_font.setBold(true);
m_plc_header_font.setPointSize(8);
}
if (m_plc_cell_font.family().isEmpty()) {
m_plc_cell_font = QFont(m_plc_table->font());
m_plc_cell_font = QETApp::diagramTextsFont();
m_plc_cell_font.setPointSize(8);
}
m_plc_header_font_btn->setText(tr("Police des en-têtes: %1 %2pt")
@@ -920,6 +990,21 @@ void ElementPropertiesEditorWidget::populatePlcTable()
hdr->moveSection(hdr->visualIndex(logical), visual);
}
}
// Ensure scrollbars stay hidden and sync shared scrollbar range
if (m_plc_table) {
m_plc_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_plc_table->verticalScrollBar()->setVisible(false);
}
if (m_plc_terminal_table) {
m_plc_terminal_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_plc_terminal_table->verticalScrollBar()->setVisible(false);
}
if (m_plc_shared_scrollbar && m_plc_table) {
int max = qMax(m_plc_table->verticalScrollBar()->maximum(),
m_plc_terminal_table->verticalScrollBar()->maximum());
m_plc_shared_scrollbar->setMaximum(max);
}
}
/**
@@ -1158,53 +1243,80 @@ void ElementPropertiesEditorWidget::plcPasteFromClipboard()
return;
QStringList lines = clipboard_text.split('\n', Qt::SkipEmptyParts);
if (lines.isEmpty())
return;
int start_row = m_plc_table->rowCount();
m_plc_table->setRowCount(start_row + lines.size());
for (int i = 0; i < lines.size(); ++i) {
QStringList cells = lines.at(i).split('\t');
int row = start_row + i;
// Type combo
auto *type_cb = new QComboBox(m_plc_table);
QStringList plc_types = ElementData::plcIOTypeList();
for (int t = 0; t < plc_types.size(); ++t) {
type_cb->addItem(plc_types.at(t), t);
bool has_tabs = false;
for (const QString &line : lines) {
if (line.contains('\t')) {
has_tabs = true;
break;
}
}
// Try to match type from clipboard
if (!cells.isEmpty()) {
QString type_str = cells.at(0).trimmed();
int type_idx = -1;
if (!has_tabs) {
// Vertical paste: values go down the same column
int target_col = m_plc_table->currentColumn();
if (target_col < 0) target_col = 0;
int target_row = m_plc_table->currentRow();
if (target_row < 0) target_row = 0;
int max_rows = m_plc_table->rowCount();
for (int i = 0; i < lines.size(); ++i) {
int row = target_row + i;
if (row >= max_rows) break;
plcSetCellFromValue(row, target_col, lines.at(i).trimmed());
}
} else {
// Horizontal paste: each line is a separate IO row
int target_row = m_plc_table->currentRow();
if (target_row < 0) target_row = 0;
int max_rows = m_plc_table->rowCount();
for (int i = 0; i < lines.size(); ++i) {
int row = target_row + i;
if (row >= max_rows) break;
QStringList cells = lines.at(i).split('\t');
for (int c = 0; c < cells.size(); ++c) {
if (c > 4) break;
plcSetCellFromValue(row, c, cells.at(c).trimmed());
}
}
}
}
/**
* @brief ElementPropertiesEditorWidget::plcSetCellFromValue
* Set a single table cell value, respecting the column widget type.
*/
void ElementPropertiesEditorWidget::plcSetCellFromValue(int row, int col, const QString &val)
{
if (!m_plc_table || row < 0 || col < 0 || col > 4)
return;
if (col == 0) {
auto *type_cb = qobject_cast<QComboBox*>(m_plc_table->cellWidget(row, col));
if (!type_cb)
return;
if (!val.isEmpty()) {
QStringList plc_types = ElementData::plcIOTypeList();
for (int t = 0; t < plc_types.size(); ++t) {
if (plc_types.at(t).compare(type_str, Qt::CaseInsensitive) == 0) {
type_idx = t;
break;
if (plc_types.at(t).compare(val, Qt::CaseInsensitive) == 0) {
type_cb->setCurrentIndex(t);
return;
}
}
if (type_idx >= 0)
type_cb->setCurrentIndex(type_idx);
}
m_plc_table->setCellWidget(row, 0, type_cb);
// Address
m_plc_table->setItem(row, 1, new QTableWidgetItem(
cells.size() > 1 ? cells.at(1).trimmed() : QString()));
// Function text
m_plc_table->setItem(row, 2, new QTableWidgetItem(
cells.size() > 2 ? cells.at(2).trimmed() : QString()));
// Comment
m_plc_table->setItem(row, 3, new QTableWidgetItem(
cells.size() > 3 ? cells.at(3).trimmed() : QString()));
// CrossRef (read-only)
auto *crossref_item = new QTableWidgetItem(
cells.size() > 4 ? cells.at(4).trimmed() : QString());
crossref_item->setFlags(crossref_item->flags() & ~Qt::ItemIsEditable);
m_plc_table->setItem(row, 4, crossref_item);
}
else if (col == 4) {
// CrossRef - read-only
auto *item = new QTableWidgetItem(val);
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
m_plc_table->setItem(row, col, item);
}
else {
// Text columns: Address, Function, Comment
m_plc_table->setItem(row, col, new QTableWidgetItem(val));
}
}
@@ -30,6 +30,7 @@ class QCheckBox;
class QGroupBox;
class QPushButton;
class QLineEdit;
class QScrollBar;
namespace Ui {
class ElementPropertiesEditorWidget;
@@ -74,6 +75,7 @@ class ElementPropertiesEditorWidget : public QDialog
void plcTerminalCountChanged(int row, int count);
void plcSelectHeaderFont();
void plcSelectCellFont();
void plcSetCellFromValue(int row, int col, const QString &val);
//ATTRIBUTES
private:
@@ -92,9 +94,11 @@ class ElementPropertiesEditorWidget : public QDialog
QCheckBox *m_plc_show_headers_cb = nullptr;
QFont m_plc_header_font;
QFont m_plc_cell_font;
QList<QCheckBox *> m_plc_col_visibility_checkboxes;
QList<QSpinBox *> m_plc_col_width_spinboxes;
QList<QLineEdit *> m_plc_col_name_edits;
QList<QCheckBox *> m_plc_col_visibility_checkboxes;
QList<QSpinBox *> m_plc_col_width_spinboxes;
QList<QLineEdit *> m_plc_col_name_edits;
QScrollBar *m_plc_shared_scrollbar = nullptr;
bool m_plc_scroll_sync = false;
};
#endif // ELEMENTPROPERTIESEDITORWIDGET_H
@@ -39,7 +39,20 @@
<item>
<widget class="QComboBox" name="m_base_type_cb"/>
</item>
</layout>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="m_slave_gb">
@@ -93,17 +106,34 @@
<property name="title">
<string>Élément maître</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Type concret</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="m_master_type_cb"/>
</item>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0" colspan="2">
<layout class="QHBoxLayout" name="type_concret_layout">
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Type concret</string>
</property>
</widget>
</item>
<item>
<spacer name="type_concret_spacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QComboBox" name="m_master_type_cb"/>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="max_slaves_checkbox">
<property name="text">
@@ -202,20 +232,7 @@
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</layout>
</widget>
<widget class="QWidget" name="Informations">
<attribute name="title">
+34
View File
@@ -61,6 +61,8 @@ ElementsPanelWidget::ElementsPanelWidget(QWidget *parent) : QWidget(parent) {
prj_edit_prop = new QAction(QET::Icons::DialogInformation, tr("Propriétés du projet"), this);
prj_prop_diagram = new QAction(QET::Icons::DialogInformation, tr("Propriétés du folio"), this);
prj_add_diagram = new QAction(QET::Icons::DiagramAdd, tr("Ajouter un folio"), this);
prj_insert_diagram_above = new QAction(QET::Icons::DiagramAdd, tr("Insérer un folio au-dessus"), this);
prj_insert_diagram_below = new QAction(QET::Icons::DiagramAdd, tr("Insérer un folio en dessous"), this);
prj_duplicate_diagram = new QAction(QET::Icons::IC_CopyFile, tr("Copier et coller"), this);
prj_del_diagram = new QAction(QET::Icons::DiagramDelete, tr("Supprimer ce folio"), this);
prj_move_diagram_up = new QAction(QET::Icons::GoUp, tr("Remonter ce folio"), this);
@@ -101,6 +103,8 @@ ElementsPanelWidget::ElementsPanelWidget(QWidget *parent) : QWidget(parent) {
connect(prj_edit_prop, SIGNAL(triggered()), this, SLOT(editProjectProperties()));
connect(prj_prop_diagram, SIGNAL(triggered()), this, SLOT(editDiagramProperties()));
connect(prj_add_diagram, SIGNAL(triggered()), this, SLOT(newDiagram()));
connect(prj_insert_diagram_above, SIGNAL(triggered()), this, SLOT(insertDiagramAbove()));
connect(prj_insert_diagram_below, SIGNAL(triggered()), this, SLOT(insertDiagramBelow()));
connect(prj_del_diagram, SIGNAL(triggered()), this, SLOT(deleteDiagram()));
connect(prj_duplicate_diagram, SIGNAL(triggered()), this, SLOT(duplicateDiagram()));
connect(prj_move_diagram_up, SIGNAL(triggered()), this, SLOT(moveDiagramUp()));
@@ -245,6 +249,32 @@ void ElementsPanelWidget::newDiagram()
}
}
/**
@brief ElementsPanelWidget::insertDiagramAbove
Emit requestForNewDiagramAt with the position of the currently
selected diagram, inserting the new folio right before it.
*/
void ElementsPanelWidget::insertDiagramAbove()
{
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
QETProject *project = selected_diagram->project();
emit(requestForNewDiagramAt(project, project->folioIndex(selected_diagram)));
}
}
/**
@brief ElementsPanelWidget::insertDiagramBelow
Emit requestForNewDiagramAt with the position right after the
currently selected diagram, inserting the new folio right after it.
*/
void ElementsPanelWidget::insertDiagramBelow()
{
if (Diagram *selected_diagram = elements_panel -> selectedDiagram()) {
QETProject *project = selected_diagram->project();
emit(requestForNewDiagramAt(project, project->folioIndex(selected_diagram) + 1));
}
}
/**
* Emet le signal requestForDiagramsDeletion avec les schemas selectionnes
*/
@@ -451,6 +481,8 @@ void ElementsPanelWidget::updateButtons()
prj_del_diagram -> setEnabled(is_writable);
prj_duplicate_diagram -> setEnabled(is_writable);
prj_insert_diagram_above -> setEnabled(is_writable);
prj_insert_diagram_below -> setEnabled(is_writable);
prj_move_diagram_up -> setEnabled(is_writable && min_position > 0);
prj_move_diagram_down -> setEnabled(is_writable && max_position < project_diagrams_count - 1);
prj_move_diagram_top -> setEnabled(is_writable && min_position > 0);
@@ -504,6 +536,8 @@ void ElementsPanelWidget::handleContextMenu(const QPoint &pos) {
break;
case QET::Diagram:
context_menu -> addAction(prj_prop_diagram);
context_menu -> addAction(prj_insert_diagram_above);
context_menu -> addAction(prj_insert_diagram_below);
context_menu -> addAction(prj_del_diagram);
context_menu -> addAction(prj_duplicate_diagram);
context_menu -> addAction(prj_move_diagram_top);
+5
View File
@@ -46,6 +46,8 @@ class ElementsPanelWidget : public QWidget {
*prj_edit_prop,
*prj_prop_diagram,
*prj_add_diagram,
*prj_insert_diagram_above,
*prj_insert_diagram_below,
*prj_del_diagram,
*prj_duplicate_diagram,
*prj_move_diagram_up,
@@ -66,6 +68,7 @@ class ElementsPanelWidget : public QWidget {
signals:
void requestForProject(QETProject *);
void requestForNewDiagram(QETProject *);
void requestForNewDiagramAt(QETProject *, int);
void requestForProjectClosing(QETProject *);
void requestForProjectPropertiesEdition(QETProject *);
void requestForDiagramPropertiesEdition(Diagram *);
@@ -88,6 +91,8 @@ class ElementsPanelWidget : public QWidget {
void editProjectProperties();
void editDiagramProperties();
void newDiagram();
void insertDiagramAbove();
void insertDiagramBelow();
void deleteDiagram();
void duplicateDiagram();
void moveDiagramUp();
+171
View File
@@ -0,0 +1,171 @@
/*
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 "crashhandler.h"
#include "logring.h"
#include "../qetversion.h"
#include <QByteArray>
#include <QSysInfo>
#include <atomic>
#include <cstring>
#ifdef Q_OS_WIN
#include <fcntl.h>
#include <io.h>
#include <share.h>
#include <sys/stat.h>
#include <windows.h>
#else
#include <cerrno>
#include <csignal>
#include <fcntl.h>
#include <unistd.h>
#endif
namespace {
// Everything the handler touches is preallocated here and filled in by
// install() (normal context, runs once at startup) -- nothing under the
// actual signal/exception path may allocate or touch QString/Qt.
const LogRing *g_ring = nullptr;
char g_dump_path[1024] = {};
char g_header[1024] = {};
int g_header_len = 0;
// Guards against two threads crashing at once, or the handler itself
// faulting while dumping: only the first crash writes a dump. See
// crashhandler.h invariant 4.
std::atomic<bool> g_already_dumped{false};
#ifndef Q_OS_WIN
// A stack-overflow SIGSEGV leaves no usable stack for a handler to run
// on at all, hence the alternate signal stack (invariant: sized well
// above any known SIGSTKSZ so this doesn't depend on
// sysconf(_SC_SIGSTKSZ), which some libc versions require at runtime
// rather than offering as a compile-time constant).
char g_altstack[65536];
const int kHandledSignals[] = {SIGSEGV, SIGABRT, SIGBUS, SIGFPE, SIGILL};
void restoreDefaultAndReraise(int sig)
{
struct sigaction sa {};
sa.sa_handler = SIG_DFL;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(sig, &sa, nullptr);
raise(sig);
}
void signalHandler(int sig)
{
if (g_already_dumped.exchange(true, std::memory_order_acq_rel)) {
// Not the first crash (concurrent fault on another thread, or
// this handler faulting while dumping): skip straight to
// restore-and-re-raise rather than risk a second, interleaved
// write to the same file.
restoreDefaultAndReraise(sig);
return;
}
// open/write/close are all on the POSIX async-signal-safe function
// list; nothing else is called here.
const int fd = ::open(g_dump_path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (fd >= 0) {
if (g_header_len > 0) {
::write(fd, g_header, static_cast<size_t>(g_header_len));
}
if (g_ring) {
g_ring->dumpToFd(fd);
}
::close(fd);
}
restoreDefaultAndReraise(sig);
}
#else // Q_OS_WIN
LONG WINAPI windowsExceptionFilter(EXCEPTION_POINTERS *)
{
bool expected = false;
if (!g_already_dumped.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
return EXCEPTION_CONTINUE_SEARCH;
}
int fd = -1;
errno_t err = _sopen_s(&fd, g_dump_path,
_O_WRONLY | _O_CREAT | _O_TRUNC | _O_BINARY,
_SH_DENYWR, _S_IREAD | _S_IWRITE);
if (err == 0 && fd >= 0) {
if (g_header_len > 0) {
_write(fd, g_header, g_header_len);
}
if (g_ring) {
g_ring->dumpToFd(fd);
}
_close(fd);
}
// Do not suppress Windows Error Reporting / an attached debugger --
// same invariant as re-raising on POSIX (see crashhandler.h,
// invariant 3).
return EXCEPTION_CONTINUE_SEARCH;
}
#endif
} // namespace
void CrashHandler::install(const LogRing *ring, const QString &dump_path)
{
g_ring = ring;
const QByteArray path_utf8 = dump_path.toUtf8();
std::strncpy(g_dump_path, path_utf8.constData(), sizeof(g_dump_path) - 1);
const QByteArray header = QByteArray("QET crash dump\n")
+ "Version: " + QetVersion::displayedVersion().toUtf8() + "\n"
+ "Git: " GIT_COMMIT_SHA "\n"
+ "OS: " + QSysInfo::prettyProductName().toUtf8() + " (" + QSysInfo::currentCpuArchitecture().toUtf8() + ")\n"
+ "Qt: " QT_VERSION_STR "\n"
+ "---\n";
g_header_len = qMin<int>(header.size(), static_cast<int>(sizeof(g_header)) - 1);
std::memcpy(g_header, header.constData(), static_cast<size_t>(g_header_len));
#ifdef Q_OS_WIN
SetUnhandledExceptionFilter(windowsExceptionFilter);
#else
stack_t ss;
ss.ss_sp = g_altstack;
ss.ss_size = sizeof(g_altstack);
ss.ss_flags = 0;
sigaltstack(&ss, nullptr);
struct sigaction sa {};
sa.sa_handler = signalHandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_ONSTACK;
for (int sig : kHandledSignals) {
sigaction(sig, &sa, nullptr);
}
#endif
}
+85
View File
@@ -0,0 +1,85 @@
/*
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 CRASHHANDLER_H
#define CRASHHANDLER_H
#include <QString>
class LogRing;
/**
@brief The CrashHandler class
Discussion #644, step 4: on a fatal crash, flush the in-memory
LogRing to a fixed file before the process dies, so the last N log
lines leading up to the crash survive it -- today they only exist in
memory and are lost with the process.
This is the highest-risk piece of the whole logging rework (the
discussion's own words: "lands last, behind its own switch"), so its
invariants are worth restating plainly:
1. The handler must never block. It takes no locks -- LogRing itself
is lock-free for exactly this reason (see logring.h). A handler
that can hang is worse than no handler: it turns a clean crash
(which at least produces a core dump) into a hung process that has
to be force-killed, producing neither a core dump nor a ring dump.
2. The handler must never allocate. Under heap corruption -- a
plausible *cause* of the very crash being handled -- malloc may
itself deadlock or fault. Every buffer this code touches at crash
time (the dump path, the header, the ring's own storage) is
preallocated by install(), which runs once at startup in normal
(non-signal) context.
3. The handler must not swallow the crash. After writing the dump it
restores the default disposition for the signal and re-raises, so
the OS still produces a core dump (POSIX) / Windows Error
Reporting still sees the exception. A handler that "fixed" the
crash by not re-raising would destroy the post-mortem evidence a
core dump provides.
4. Only the *first* crash writes a dump. An atomic test-and-set
guards against two threads faulting simultaneously (or the handler
itself faulting while dumping) producing an interleaved or
truncated file; every crash after the first goes straight to
restore-and-re-raise.
Tested in this environment: POSIX/Linux only (sigaction, sigaltstack,
SIGSEGV/SIGABRT/SIGBUS/SIGFPE/SIGILL). The Windows path
(SetUnhandledExceptionFilter) and macOS-specific behaviour (signal
handling itself is POSIX and shares the Linux code path, but sandbox
profiles can affect where the dump file may be written) are
implemented per the discussion's guidance but could not be exercised
here -- there is no Windows or macOS build available in this sandbox.
Please sanity-check both before relying on them in the field.
*/
class CrashHandler
{
public:
/// Installs the crash handler. Must be called from normal
/// (non-signal) startup code, after the LogRing it will dump
/// exists, and only once. `ring` must outlive the process (in
/// practice: the LogRing owned by QetLogger's function-local
/// static instance, which is never destroyed before exit).
/// `dump_path` is resolved and copied into a fixed-size internal
/// buffer here; nothing under the actual signal/exception path
/// touches QString.
static void install(const LogRing *ring, const QString &dump_path);
private:
CrashHandler() = delete;
};
#endif // CRASHHANDLER_H
+58
View File
@@ -0,0 +1,58 @@
/*
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 "eventloopwatchdog.h"
#include <QDebug>
#include <QProcessEnvironment>
EventLoopWatchdog::EventLoopWatchdog(QObject *parent) :
QObject(parent)
{
m_disabled = QProcessEnvironment::systemEnvironment()
.value(QStringLiteral("QET_WATCHDOG_DISABLE")) == QStringLiteral("1");
// Precise, not the default Coarse: Coarse explicitly trades timing
// accuracy for power/scheduling efficiency (platform-dependent, but
// commonly +/- a double-digit percentage), which would show up as
// noise indistinguishable from a real stall in exactly the
// measurement this class exists to make trustworthy.
m_timer.setTimerType(Qt::PreciseTimer);
connect(&m_timer, &QTimer::timeout, this, &EventLoopWatchdog::tick);
}
void EventLoopWatchdog::start()
{
if (m_disabled)
return;
m_elapsed.start();
m_timer.start(kTickIntervalMs);
}
void EventLoopWatchdog::tick()
{
// restart() returns the elapsed time and resets the clock in one
// call, so this tick's own cost is never counted against the next.
const qint64 actual_ms = m_elapsed.restart();
if (actual_ms > kStallThresholdMs) {
qWarning() << "EventLoopWatchdog: main thread stalled for"
<< actual_ms << "ms (expected a tick every"
<< kTickIntervalMs << "ms)";
}
}
+92
View File
@@ -0,0 +1,92 @@
/*
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 EVENTLOOPWATCHDOG_H
#define EVENTLOOPWATCHDOG_H
#include <QElapsedTimer>
#include <QObject>
#include <QTimer>
/**
@brief The EventLoopWatchdog class
Detects when the main (GUI) thread's event loop goes unresponsive --
QetLogger (discussion #644) can only see what an explicit qDebug()/
qInfo()/qWarning() call already decided to report, and 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.
This closes that gap the direct way: a repeating QTimer::PreciseTimer
ticks on a short, fixed interval; each tick measures the *actual*
wall-clock time elapsed since the previous one via QElapsedTimer
(monotonic -- unaffected by system clock/NTP adjustments, unlike
QDateTime). Qt does not queue up missed fires for a normal repeating
timer, so if the event loop 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 rather
than inferred from log silence.
Only fires a qWarning() (and so only touches the log at all) when a
tick is late by more than kStallThresholdMs, to stay within the
spirit of QetLogger's bounded-log design (see its class comment) --
a healthy session should produce zero output from this class. This
tells you *that* a stall happened and *how long* it was, not what
caused it; pair a reported timestamp with `docker exec`+gdb the way
the CLI hang (PR #661) was diagnosed to go from "it lagged" to a
root cause.
Escape hatch: if QET_WATCHDOG_DISABLE=1 is set in the environment at
construction time, start() does nothing.
*/
class EventLoopWatchdog : public QObject
{
Q_OBJECT
public:
/// How often the watchdog checks in. Small enough to bound the
/// measurement's own granularity, large enough that the tick
/// itself is negligible overhead on the event loop it's watching.
static constexpr int kTickIntervalMs = 50;
/// A tick arriving later than this many ms after the previous one
/// is logged as a stall. Comfortably above kTickIntervalMs so
/// ordinary OS scheduling noise doesn't produce a warning on every
/// tick, and in the range a user would actually notice as lag.
static constexpr int kStallThresholdMs = 200;
explicit EventLoopWatchdog(QObject *parent = nullptr);
/// Starts ticking. Must be called from the main thread, after the
/// event loop it watches is about to run (i.e. immediately before
/// QApplication::exec()) -- constructing this class earlier is
/// harmless, but start() before there is an event loop to tick
/// against would just measure the time until app.exec() is
/// reached. No-op if QET_WATCHDOG_DISABLE=1 was set at
/// construction time.
void start();
private slots:
void tick();
private:
QTimer m_timer;
QElapsedTimer m_elapsed;
bool m_disabled = false;
};
#endif // EVENTLOOPWATCHDOG_H
+152
View File
@@ -0,0 +1,152 @@
/*
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 "logring.h"
#include <cstring>
#ifdef Q_OS_WIN
#include <io.h>
#else
#include <cerrno>
#include <unistd.h>
#endif
static_assert(std::atomic<int>::is_always_lock_free,
"LogRing::Entry::length must be a lock-free atomic<int> -- "
"dumpToFd() reads it from a signal handler and must never block.");
namespace {
/**
@brief writeAllSignalSafe
Loops until length bytes have been written to fd or an unrecoverable
error occurs. write(2) may write fewer bytes than requested and may
return EINTR -- both are *more* likely from inside a signal handler
than in normal code, so a single write() call is not enough here.
Async-signal-safe: only calls write(2)/errno, nothing else.
*/
void writeAllSignalSafe(int fd, const char *data, int length) noexcept
{
int remaining = length;
const char *p = data;
while (remaining > 0) {
#ifdef Q_OS_WIN
const int n = _write(fd, p, static_cast<unsigned int>(remaining));
if (n <= 0) {
return;
}
#else
const ssize_t n = ::write(fd, p, static_cast<size_t>(remaining));
if (n < 0) {
if (errno == EINTR) {
continue;
}
return; // unrecoverable -- give up silently, never block/throw
}
if (n == 0) {
return;
}
#endif
p += n;
remaining -= static_cast<int>(n);
}
}
} // namespace
LogRing::LogRing() :
// The sized constructor value-initialises each Entry in place; unlike
// resize(), it doesn't require Entry to be move/copy-constructible,
// which std::atomic<int> deliberately never is. The only allocation
// this class ever does.
m_entries(kCapacityEntries)
{
}
void LogRing::append(const QByteArray &line) noexcept
{
static const char kMarker[] = "...[ring-truncated]\n";
const int marker_len = static_cast<int>(sizeof(kMarker)) - 1;
const quint64 idx = m_write_cursor.fetch_add(1, std::memory_order_relaxed);
Entry &slot = m_entries[static_cast<size_t>(idx % static_cast<quint64>(kCapacityEntries))];
// Zero the length first so a concurrent reader landing on this exact
// slot mid-copy sees "not ready" rather than the previous lap's
// (now-being-overwritten) content at a stale length.
slot.length.store(0, std::memory_order_relaxed);
int len;
if (line.size() < kEntryBytes) {
std::memcpy(slot.data, line.constData(), static_cast<size_t>(line.size()));
len = line.size();
} else {
const int keep = kEntryBytes - marker_len;
std::memcpy(slot.data, line.constData(), static_cast<size_t>(keep));
std::memcpy(slot.data + keep, kMarker, static_cast<size_t>(marker_len));
len = kEntryBytes;
}
slot.length.store(len, std::memory_order_release);
}
QVector<QByteArray> LogRing::snapshot() const
{
const quint64 cursor = m_write_cursor.load(std::memory_order_acquire);
const quint64 cap = static_cast<quint64>(kCapacityEntries);
const quint64 count = (cursor < cap) ? cursor : cap;
const quint64 start = (cursor < cap) ? 0 : (cursor - cap);
QVector<QByteArray> result;
result.reserve(static_cast<int>(count));
for (quint64 i = 0; i < count; ++i) {
const Entry &slot = m_entries[static_cast<size_t>((start + i) % cap)];
const int len = slot.length.load(std::memory_order_acquire);
if (len > 0) {
result.append(QByteArray(slot.data, len));
}
}
return result;
}
void LogRing::dumpToFd(int fd) const noexcept
{
const quint64 cursor = m_write_cursor.load(std::memory_order_acquire);
const quint64 cap = static_cast<quint64>(kCapacityEntries);
const quint64 count = (cursor < cap) ? cursor : cap;
const quint64 start = (cursor < cap) ? 0 : (cursor - cap);
for (quint64 i = 0; i < count; ++i) {
const Entry &slot = m_entries[static_cast<size_t>((start + i) % cap)];
const int len = slot.length.load(std::memory_order_acquire);
if (len > 0) {
writeAllSignalSafe(fd, slot.data, len);
}
}
}
void LogRing::clear()
{
m_write_cursor.store(0, std::memory_order_relaxed);
for (auto &entry : m_entries) {
entry.length.store(0, std::memory_order_relaxed);
}
}
+85
View File
@@ -0,0 +1,85 @@
/*
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 LOGRING_H
#define LOGRING_H
#include <QByteArray>
#include <QVector>
#include <atomic>
#include <vector>
/**
@brief The LogRing class
Fixed-capacity, always-on in-memory ring of the most recent log
lines, preallocated once at construction -- append() never
allocates.
Lock-free by construction, not just "thread-safe": step 4 (see
crashhandler.h) reads this ring from inside a POSIX signal handler,
where taking any lock is unsafe -- if the crashing thread happens to
be the one that already holds it (or any other thread does and never
gets scheduled again), the handler hangs forever, and you lose both
the ring dump *and* the core dump. So there is no mutex here at all:
append() claims a slot with a single atomic fetch-add, and
dumpToFd()/snapshot() read the preallocated entries directly.
Accepted tradeoff: if dumpToFd() runs while another thread is
mid-append into the exact slot being read (only possible in the
crash-handler case, and only for at most one slot), that one entry
may be read torn -- part old content, part new. Every other entry is
unaffected. This is deliberate: the alternative (a seqlock or similar
to detect and retry torn reads) adds real complexity for a window
that, per discussion #644, is not worth trading "the handler must
never block" against.
*/
class LogRing
{
public:
static constexpr int kCapacityEntries = 4096;
static constexpr int kEntryBytes = 512; // 4096 * 512 = 2 MiB total
LogRing();
/// Append one already-formatted, already-truncated log line.
/// Bytes beyond kEntryBytes are dropped with a truncation marker.
/// Never allocates, never blocks. Safe to call from any normal
/// (non-signal) thread concurrently.
void append(const QByteArray &line) noexcept;
/// Snapshot of the entries currently held, oldest first. Normal
/// (non-signal) context only.
QVector<QByteArray> snapshot() const;
/// Async-signal-safe: writes every entry currently held to fd via
/// write(2) only -- no allocation, no Qt, no locks. May write a
/// torn entry under the rare race described above; never blocks.
void dumpToFd(int fd) const noexcept;
void clear();
private:
struct Entry {
char data[kEntryBytes];
std::atomic<int> length{0}; // 0 = not yet written this lap
};
std::vector<Entry> m_entries; // preallocated once, capacity fixed
std::atomic<quint64> m_write_cursor{0}; // monotonically increasing
};
#endif // LOGRING_H
+431
View File
@@ -0,0 +1,431 @@
/*
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 "qetlogger.h"
#include "crashhandler.h"
#include "../qetapp.h"
#include "../qetversion.h"
#include <QDateTime>
#include <QDir>
#include <QFileInfo>
#include <QSysInfo>
#include <cstdio>
namespace {
/**
@brief legacyStderrOutput
The QET_LOG_DISABLE=1 escape hatch. Deliberately independent of
every other function in this file -- including sanitize()/
formatLine(), which are exactly the new code a problem might be in
-- so this path stays usable even if the rest of the rework
misbehaves. No ring, no file, no rotation, no mutex.
*/
void legacyStderrOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
const QByteArray local_msg = msg.toLocal8Bit();
const char *file = context.file ? context.file : "";
const char *function = context.function ? context.function : "";
const char *level = "Unknown";
switch (type) {
case QtDebugMsg: level = "Debug"; break;
case QtInfoMsg: level = "Info"; break;
case QtWarningMsg: level = "Warning"; break;
case QtCriticalMsg: level = "Critical"; break;
case QtFatalMsg: level = "Fatal"; break;
}
fprintf(stderr, "%s: %s (%s:%u, %s)\n",
level, local_msg.constData(), file, context.line, function);
}
/**
@brief ReentrancyGuard
Sets the referenced flag on construction, clears it on destruction
(including via early return / exception unwinding). Used as the
per-thread guard against the logger recursing into itself.
*/
struct ReentrancyGuard
{
bool &flag;
explicit ReentrancyGuard(bool &f) : flag(f) {flag = true;}
~ReentrancyGuard() {flag = false;}
};
} // namespace
/**
@brief QetLogger::instance
Function-local static: guaranteed constructed exactly once, in a
thread-safe way, on first use -- but the *meaningful* initialisation
(log path resolution, opening the file) happens in init(), called
explicitly from main() at a defined point, not implicitly on
whichever thread happens to log first.
*/
QetLogger &QetLogger::instance()
{
static QetLogger logger;
return logger;
}
void QetLogger::init()
{
m_disabled = (qgetenv("QET_LOG_DISABLE") == "1");
if (m_disabled) {
return;
}
m_log_dir = QETApp::dataDir();
m_base_name = QDate::currentDate().toString(QStringLiteral("yyyyMMdd"));
QMutexLocker locker(&m_file_mutex);
m_file_output_ok = ensureFileOpenLocked();
}
void QetLogger::installCrashHandler()
{
if (m_disabled) {
return;
}
CrashHandler::install(&m_ring, crashDumpPath());
}
QString QetLogger::crashDumpPath() const
{
return m_log_dir % QStringLiteral("/crash_dump.log");
}
QString QetLogger::currentLogFilePath() const
{
return m_log_dir % QStringLiteral("/") % m_base_name % QStringLiteral(".log");
}
/**
@brief QetLogger::ensureFileOpenLocked
Caller must hold m_file_mutex. Opens the current session's log file
if not already open. Refuses to follow a pre-existing symlink at
that path, and creates the file owner-read/write only.
*/
bool QetLogger::ensureFileOpenLocked()
{
if (m_file.isOpen()) {
return true;
}
QDir().mkpath(m_log_dir);
const QString path = currentLogFilePath();
const QFileInfo info(path);
if (info.exists() && info.isSymLink()) {
// Filesystem hardening: refuse a pre-planted symlink rather than
// silently appending to whatever it points at.
return false;
}
m_file.setFileName(path);
if (!m_file.open(QIODevice::WriteOnly | QIODevice::Append)) {
return false;
}
m_file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner);
m_bytes_written_current_file = m_file.size();
return true;
}
QString QetLogger::rotatedPath(int index) const
{
return m_log_dir % QStringLiteral("/") % m_base_name % QStringLiteral(".") % QString::number(index) % QStringLiteral(".log");
}
/**
@brief QetLogger::rotateLocked
Caller must hold m_file_mutex. Shifts .3.log -> .4.log (dropping the
previous .4.log), .2.log -> .3.log, .1.log -> .2.log, .log -> .1.log,
then opens a fresh, empty current file.
*/
void QetLogger::rotateLocked()
{
m_file.close();
const QString base_path = currentLogFilePath();
for (int i = kRotationKeep; i >= 1; --i) {
const QString from = (i == 1) ? base_path : rotatedPath(i - 1);
const QString to = rotatedPath(i);
if (QFile::exists(to)) {
QFile::remove(to);
}
if (QFile::exists(from)) {
QFile::rename(from, to);
}
}
m_bytes_written_current_file = 0;
m_file_output_ok = ensureFileOpenLocked();
}
void QetLogger::writeToFile(const QByteArray &line, QtMsgType type)
{
QMutexLocker locker(&m_file_mutex);
if (!m_file_output_ok) {
// Write-failure policy: once file output has failed, stop
// attempting it rather than spin-retrying every message. The
// ring keeps running regardless.
return;
}
const qint64 written = m_file.write(line);
if (written != line.size()) {
m_file_output_ok = false;
m_file.close();
return;
}
m_bytes_written_current_file += written;
if (type >= QtWarningMsg) {
m_file.flush();
}
if (m_bytes_written_current_file >= kMaxFileBytes) {
rotateLocked();
}
}
/**
@brief QetLogger::sanitize
Escapes newlines, carriage returns and other control characters.
Much of what QET logs is externally controlled (file paths, element
names, font strings read out of a .qet file); left unescaped, a
crafted string containing '\n' can forge additional log lines.
Operates on already-UTF-8-encoded bytes: this is safe because UTF-8
continuation bytes are always >= 0x80, so any byte < 0x20 found here
is a genuine ASCII control character, never part of a multi-byte
sequence.
*/
QByteArray QetLogger::sanitize(const QByteArray &input)
{
QByteArray out;
out.reserve(input.size());
for (unsigned char c : input) {
if (c == '\n') {
out += "\\n";
} else if (c == '\r') {
out += "\\r";
} else if (c == '\t') {
out += static_cast<char>(c);
} else if (c < 0x20 || c == 0x7F) {
out += "\\x";
out += QByteArray::number(c, 16).rightJustified(2, '0');
} else {
out += static_cast<char>(c);
}
}
return out;
}
/**
@brief QetLogger::truncateMessage
Caps a single message at max_bytes, appending a marker stating how
many bytes were dropped, so one pathological caller (e.g. dumping an
entire XML document to qDebug()) can't consume an unbounded amount
of the ring's or file's byte budget.
*/
QByteArray QetLogger::truncateMessage(const QByteArray &input, int max_bytes)
{
if (input.size() <= max_bytes) {
return input;
}
const int dropped = input.size() - max_bytes;
QByteArray out = input.left(max_bytes);
out += " ...[truncated ";
out += QByteArray::number(dropped);
out += " bytes]";
return out;
}
QByteArray QetLogger::formatLine(QtMsgType type, const QMessageLogContext &context, const QByteArray &sanitized_msg)
{
// Includes the date (not just the time) so that a session crossing
// midnight -- now kept in a single file -- doesn't read as ambiguous.
const QByteArray timestamp = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm:ss.zzz")).toUtf8();
const char *level = "Unknown";
switch (type) {
case QtDebugMsg: level = "Debug"; break;
case QtInfoMsg: level = "Info"; break;
case QtWarningMsg: level = "Warning"; break;
case QtCriticalMsg: level = "Critical"; break;
case QtFatalMsg: level = "Fatal"; break;
}
const char *file = context.file ? context.file : "";
const char *function = context.function ? context.function : "";
QByteArray line = timestamp;
line += ' ';
line += level;
line += ": ";
line += sanitized_msg;
if (type == QtInfoMsg) {
line += " \n";
} else {
line += " (";
line += file;
line += ":";
line += QByteArray::number(context.line ? context.line : 0);
line += ", ";
line += function;
line += ")\n";
}
return line;
}
void QetLogger::handleMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
if (m_disabled) {
legacyStderrOutput(type, context, msg);
return;
}
static thread_local bool in_handler = false;
if (in_handler) {
// The logger itself triggered a message (e.g. from inside a Qt
// call it made) -- drop it rather than recurse.
return;
}
ReentrancyGuard guard(in_handler);
const QByteArray sanitized = truncateMessage(sanitize(msg.toUtf8()), kMaxMessageBytes);
const QByteArray line = formatLine(type, context, sanitized);
fwrite(line.constData(), 1, static_cast<size_t>(line.size()), stderr);
m_ring.append(line);
writeToFile(line, type);
}
void QetLogger::pruneOldLogFiles(int days)
{
if (m_disabled) {
return;
}
const QDate today = QDate::currentDate();
const QStringList filters = {
QStringLiteral("????????.log"), // base files, e.g. 20260803.log
QStringLiteral("????????.?.log"), // rotated files, e.g. 20260803.1.log
};
const QDir dir(m_log_dir);
const auto entries = dir.entryInfoList(filters, QDir::Files);
for (const QFileInfo &file_info : entries) {
if (!file_info.isFile()) {
continue;
}
// lastModified(), not lastRead(): reading the log (opening it to
// attach to a bug report, a backup job, an indexer) must not
// reset the retention clock and keep it alive indefinitely.
if (file_info.lastModified().date().daysTo(today) > days) {
QFile::remove(file_info.absoluteFilePath());
}
}
}
// --- Step 5: getting the data back out ----------------------------------
bool QetLogger::hasPendingCrashDump() const
{
if (m_disabled) {
return false;
}
const QFileInfo info(crashDumpPath());
return info.exists() && info.isFile() && info.size() > 0;
}
QByteArray QetLogger::pendingCrashDumpContents() const
{
QFile file(crashDumpPath());
if (!file.open(QIODevice::ReadOnly)) {
return QByteArray();
}
return redact(file.readAll());
}
void QetLogger::clearPendingCrashDump()
{
QFile::remove(crashDumpPath());
}
QByteArray QetLogger::buildDiagnosticsReport() const
{
QByteArray header;
header += "QElectroTech diagnostics report\n";
header += "Generated: " % QDateTime::currentDateTime().toString(Qt::ISODate) % "\n";
header += "Version: " % QetVersion::displayedVersion() % "\n";
header += "Git: " GIT_COMMIT_SHA "\n";
header += "OS: " % QSysInfo::prettyProductName() % " (" % QSysInfo::currentCpuArchitecture() % ")\n";
header += "Qt: " QT_VERSION_STR "\n";
header += "---\n";
QByteArray body;
QFile file(currentLogFilePath());
if (file.open(QIODevice::ReadOnly)) {
body = file.readAll();
} else {
// Fall back to the in-memory ring if the file itself can't be
// read (e.g. file output already failed this session).
for (const QByteArray &line : m_ring.snapshot()) {
body += line;
}
}
return redact(header + body);
}
/**
@brief QetLogger::redact
Replaces the user's home directory with "~" wherever it appears.
Applied before a crash dump or a diagnostics report is ever shown to
the user: both are destined to be attached to a public bug tracker,
and an absolute path under the home directory leaks the account name
(discussion #644's privacy section: "/home/laurent/... leaks a
username"). This is the one redaction implemented here; the
discussion's fancier "optionally redact project filenames too" is
not attempted -- reliably telling a project path apart from
arbitrary log text is a much fuzzier problem than a literal prefix
match against a known directory.
*/
QByteArray QetLogger::redact(const QByteArray &input)
{
const QByteArray home = QDir::homePath().toUtf8();
if (home.isEmpty()) {
return input;
}
QByteArray out = input;
out.replace(home, QByteArrayLiteral("~"));
return out;
}
+159
View File
@@ -0,0 +1,159 @@
/*
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 QETLOGGER_H
#define QETLOGGER_H
#include "logring.h"
#include <QFile>
#include <QMutex>
#include <QString>
#include <QtGlobal>
/**
@brief The QetLogger class
Rework of QET's diagnostic logging (discussion #644, steps 1-3):
- Step 1: one file handle held open for the session under a mutex
instead of opening/closing per message; the log path (including
the date-stamped filename) is resolved exactly once, at init(),
instead of being recomputed on every message -- a session that
crosses midnight now stays in one file; retention now uses
lastModified() instead of lastRead(); stderr and file output both
use UTF-8 explicitly (previously stderr used the local 8-bit
codec and the file's encoding silently differed between Qt5 and
Qt6).
- Step 2: the previously-unbounded daily file is now size-capped
and rotated (kMaxFileBytes per file, kRotationKeep old files kept
beyond the current one); each message is truncated to
kMaxMessageBytes and control characters are escaped before being
written, so one pathological caller can't blow the size budget or
forge log lines; the log file is refused if it already exists as
a symlink and is created owner-read/write only.
- Step 3: every formatted line is also appended to an in-memory
LogRing (see logring.h) -- always on, fixed capacity, allocation-
free on the hot path.
- Step 4: installCrashHandler() wires the ring up to CrashHandler
(see crashhandler.h), so a SIGSEGV/SIGABRT/SIGBUS/SIGFPE/SIGILL (or,
on Windows, an unhandled structured exception) flushes the ring to
a fixed crash-dump file before the process dies.
- Step 5: hasPendingCrashDump()/pendingCrashDumpContents()/
clearPendingCrashDump() let startup code (see QETApp::checkBackupFiles())
notice and offer an unretrieved crash dump from the *previous* run;
buildDiagnosticsReport() is the equivalent for a manual "save a
report right now" action on the *current*, still-running session.
Both go through redact() before ever reaching the user, since both
are destined for a public bug tracker.
Deliberately NOT included: log categories, a full session header
beyond what the crash dump/report already carry, repeat collapsing,
rate limiting. Those are listed in discussion #644 under "best
practices worth building in", not part of the numbered steps.
Escape hatch: if QET_LOG_DISABLE=1 is set in the environment at
init() time, this class does nothing beyond a minimal, independent
stderr passthrough -- no ring, no file, no rotation -- so a problem
in this rework can be worked around without a rebuild.
*/
class QetLogger
{
public:
static constexpr qint64 kMaxFileBytes = 2 * 1024 * 1024; // 2 MiB per file
static constexpr int kRotationKeep = 4; // .1.log .. .4.log
static constexpr int kMaxMessageBytes = 4096; // per-message truncation
static QetLogger &instance();
/// Must be called exactly once, from main(), before
/// qInstallMessageHandler(). Resolves the log directory and the
/// session's log filename, and opens the file.
void init();
/// Step 4: installs the crash handler (see crashhandler.h). Must
/// be called after init() (the ring and the dump path must exist
/// first) and, like init(), only once.
void installCrashHandler();
/// The function installed via qInstallMessageHandler() forwards here.
void handleMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg);
/// Replaces the old delete_old_log_files(): same call shape, fixed
/// to use lastModified() (not lastRead()) and to also match rotated
/// file names.
void pruneOldLogFiles(int days);
/// Snapshot of the in-memory ring, oldest first.
QVector<QByteArray> ringSnapshot() const {return m_ring.snapshot();}
// --- Step 5: getting the data back out -------------------------
/// True if a previous run's crash handler left an unretrieved
/// dump behind.
bool hasPendingCrashDump() const;
/// Raw contents of the pending crash dump, or an empty array if
/// there isn't one. Does not delete it -- call
/// clearPendingCrashDump() once it has been offered to the user.
QByteArray pendingCrashDumpContents() const;
/// Deletes the pending crash dump file. Call after the user has
/// been offered it (whether they chose to save it or not) so it
/// is never offered a second time.
void clearPendingCrashDump();
/// Builds a redacted diagnostics bundle from the *current* session
/// (header + this session's log file so far) for the manual
/// "Save report" action -- as opposed to pendingCrashDumpContents(),
/// which is about a *previous*, already-terminated session.
QByteArray buildDiagnosticsReport() const;
/// Replaces occurrences of the user's home directory with "~".
/// Applied to both the crash dump and buildDiagnosticsReport()
/// before they are ever shown to the user, since both are
/// destined for a public bug tracker.
static QByteArray redact(const QByteArray &input);
private:
QetLogger() = default;
QetLogger(const QetLogger &) = delete;
bool ensureFileOpenLocked();
void rotateLocked();
void writeToFile(const QByteArray &line, QtMsgType type);
QString rotatedPath(int index) const;
QString crashDumpPath() const;
QString currentLogFilePath() const;
static QByteArray sanitize(const QByteArray &input);
static QByteArray truncateMessage(const QByteArray &input, int max_bytes);
static QByteArray formatLine(QtMsgType type, const QMessageLogContext &context, const QByteArray &sanitized_msg);
bool m_disabled = false;
QString m_log_dir;
QString m_base_name; // e.g. "20260803", resolved once in init()
QMutex m_file_mutex;
QFile m_file;
qint64 m_bytes_written_current_file = 0;
bool m_file_output_ok = false;
LogRing m_ring;
};
#endif // QETLOGGER_H
@@ -0,0 +1,91 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "diagnosticsreportdialog.h"
#include "../../qetmessagebox.h"
#include <QDialogButtonBox>
#include <QFile>
#include <QFileDialog>
#include <QFontDatabase>
#include <QLabel>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QVBoxLayout>
DiagnosticsReportDialog::DiagnosticsReportDialog(
const QString &title,
const QString &intro,
const QByteArray &content,
QWidget *parent) :
QDialog(parent)
{
setWindowTitle(title);
resize(700, 500);
auto *layout = new QVBoxLayout(this);
auto *intro_label = new QLabel(intro, this);
intro_label->setWordWrap(true);
layout->addWidget(intro_label);
auto *preview = new QPlainTextEdit(this);
preview->setReadOnly(true);
preview->setLineWrapMode(QPlainTextEdit::NoWrap);
preview->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
preview->setPlainText(QString::fromUtf8(content));
layout->addWidget(preview);
auto *buttons = new QDialogButtonBox(this);
QPushButton *save_button = buttons->addButton(tr("Enregistrer..."), QDialogButtonBox::ActionRole);
buttons->addButton(QDialogButtonBox::Close);
connect(save_button, &QPushButton::clicked, this, &DiagnosticsReportDialog::saveToFile);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(buttons->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &QDialog::accept);
layout->addWidget(buttons);
// Stash the content for saveToFile(); the preview widget already
// holds a QString copy but we save the original UTF-8 bytes to avoid
// any round-trip surprises.
setProperty("qet_report_content", content);
}
void DiagnosticsReportDialog::saveToFile()
{
const QString path = QFileDialog::getSaveFileName(
this,
tr("Enregistrer le rapport de diagnostic"),
QStringLiteral("qet-diagnostic-report.txt"),
tr("Fichiers texte (*.txt);;Tous les fichiers (*)"));
if (path.isEmpty()) {
return;
}
QFile file(path);
if (!file.open(QIODevice::WriteOnly)) {
QET::QetMessageBox::critical(
this,
tr("Erreur"),
tr("Impossible d'écrire dans le fichier « %1 ».").arg(path));
return;
}
file.write(property("qet_report_content").toByteArray());
file.close();
}
@@ -0,0 +1,51 @@
/*
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 DIAGNOSTICSREPORTDIALOG_H
#define DIAGNOSTICSREPORTDIALOG_H
#include <QDialog>
/**
@brief The DiagnosticsReportDialog class
Discussion #644, step 5: "Show what's in it before saving -- the user
is about to attach this to a public tracker." Used for both the
after-a-crash offer (QETApp::checkBackupFiles()) and the manual
"Help > Diagnostics > Save report" action -- the only difference
between the two is the intro text and where the content comes from
(QetLogger::pendingCrashDumpContents() vs. buildDiagnosticsReport()).
The content passed in is expected to already be redacted
(QetLogger::redact()) -- this dialog just displays and optionally
saves whatever it's given.
*/
class DiagnosticsReportDialog : public QDialog
{
Q_OBJECT
public:
explicit DiagnosticsReportDialog(
const QString &title,
const QString &intro,
const QByteArray &content,
QWidget *parent = nullptr);
private slots:
void saveToFile();
};
#endif // DIAGNOSTICSREPORTDIALOG_H
+29 -123
View File
@@ -16,6 +16,8 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "cli_export.h"
#include "logging/eventloopwatchdog.h"
#include "logging/qetlogger.h"
#include "machine_info.h"
#include "qet.h"
#include "qetapp.h"
@@ -62,131 +64,16 @@ class EarlyFileOpenCatcher : public QObject
#endif
/**
@brief myMessageOutput
for debugging
@param type : the messages that can be sent to a message handler
@param context : were? wat?
@param msg : Message
@brief qetLogMessageHandler
Installed via qInstallMessageHandler(); forwards to QetLogger, which
holds all the actual formatting/ring/rotation state. See
logging/qetlogger.h for the rationale (discussion #644).
*/
void myMessageOutput(QtMsgType type,
void qetLogMessageHandler(QtMsgType type,
const QMessageLogContext &context,
const QString &msg)
{
QString txt=QTime::currentTime().toString("hh:mm:ss.zzz");
QByteArray dbs =txt.toLocal8Bit();
QByteArray localMsg = msg.toLocal8Bit();
const char *file = context.file ? context.file : "";
const char *function = context.function ? context.function : "";
switch (type) {
case QtDebugMsg:
fprintf(stderr,
"%s Debug: %s (%s:%u, %s)\n",
dbs.constData(),
localMsg.constData(),
file,
context.line,
function);
txt+=" Debug: ";
break;
case QtInfoMsg:
fprintf(stderr,
"%s Info: %s \n",
dbs.constData(),
localMsg.constData());
txt+=" Info: ";
break;
case QtWarningMsg:
fprintf(stderr,
"%s Warning: %s (%s:%u, %s)\n",
dbs.constData(),
localMsg.constData(),
file, context.line,
function);
txt+=" Warning: ";
break;
case QtCriticalMsg:
fprintf(stderr,
"%s Critical: %s (%s:%u, %s)\n",
dbs.constData(),
localMsg.constData(),
file,
context.line,
function);
txt+=" Critical: ";
break;
case QtFatalMsg:
fprintf(stderr,
"%s Fatal: %s (%s:%u, %s)\n",
dbs.constData(),
localMsg.constData(),
file,
context.line,
function);
txt+=" Fatal: ";
break;
default:
fprintf(stderr,
"%s Unknown: %s (%s:%u, %s)\n",
dbs.constData(),
localMsg.constData(),
file,
context.line,
function);
txt+=" Unknown: ";
}
txt+= msg;
if(type==QtInfoMsg){
txt+=" \n";
} else {
txt+= " (";
txt+= context.file ? context.file : "";
txt+= ":";
txt+=QString::number(context.line ? context.line :0);
txt+= ", ";
txt+= context.function ? context.function : "";
txt+=")\n";
}
QFile outFile(QETApp::dataDir()
+"/"
+QDate::currentDate().toString("yyyyMMdd")
+".log");
if(outFile.open(QIODevice::WriteOnly | QIODevice::Append))
{
QTextStream ts(&outFile);
ts << txt;
}
outFile.close();
}
/**
@brief delete_old_log_files
delete old log files
@param days : max days old
*/
void delete_old_log_files(int days)
{
const QDate today = QDate::currentDate();
const QString path = QETApp::dataDir() % "/";
QString filter("%1%1%1%1%1%1%1%1.log"); // pattern
filter = filter.arg("[0123456789]"); // valid characters
Q_FOREACH (auto fileInfo,
QDir(path).entryInfoList(
QStringList(filter),
QDir::Files))
{
if (fileInfo.lastRead().date().daysTo(today) > days)
{
QString filepath = fileInfo.absoluteFilePath();
QDir deletefile;
deletefile.setPath(filepath);
deletefile.remove(filepath);
qDebug() << "File " % filepath % " is deleted!";
}
}
QetLogger::instance().handleMessage(type, context, msg);
}
/**
@@ -253,13 +140,25 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
}
}
// Resolve the logger's state (log directory, session filename, open
// file handle) explicitly here, immediately before installing the
// handler -- not implicitly on whichever thread happens to log
// first. See QetLogger::init().
//
// Install the log-file message handler BEFORE the application starts:
// QETApp's constructor does the whole startup (collections, editor,
// opening the projects given on the command line), so installing the
// handler afterwards - as was done in the startup worker below - meant
// exactly the interesting lines (collection and project load timers)
// went to stderr, which is invisible in a Windows GUI session.
qInstallMessageHandler(myMessageOutput);
QetLogger::instance().init();
qInstallMessageHandler(qetLogMessageHandler);
// Step 4 (discussion #644): flush the ring to a crash-dump file if
// the process dies from here on. Installed right after the ring
// exists (init() just constructed it) and as early as reasonably
// possible, so it also covers whatever runs between here and
// QETApp's own construction below.
QetLogger::instance().installCrashHandler();
SingleApplication app(argc, argv, true);
#ifdef Q_OS_MACOS
@@ -308,9 +207,16 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
{
qInfo("Start-up");
// delete old log files of max 7 days old.
delete_old_log_files(7);
QetLogger::instance().pruneOldLogFiles(7);
MachineInfo::instance()->send_info_to_debug();
});
// Constructed here rather than earlier: start() measures ticks against
// the event loop app.exec() is about to run, so there is no point
// (and no accurate baseline) before this line.
EventLoopWatchdog watchdog;
watchdog.start();
return app.exec();
}
+1 -1
View File
@@ -842,7 +842,7 @@ void ProjectView::initWidgets()
QHBoxLayout *TopRightCorner_Layout = new QHBoxLayout();
TopRightCorner_Layout->setContentsMargins(0,0,0,0);
// some place left to the 'next_right_view_button' button
TopRightCorner_Layout->insertSpacing(1,10);
TopRightCorner_Layout->addSpacing(10);
QHBoxLayout *TopLeftCorner_Layout = new QHBoxLayout();
TopLeftCorner_Layout->setContentsMargins(0,0,0,0);
+205
View File
@@ -267,6 +267,211 @@ QDomElement ElementData::kindInfoToXml(QDomDocument &document)
return returned_elmt;
}
/**
* @brief ElementData::plcMasterDataToXml
* Serialize PLC master data to XML (used by Element::toXml for diagram instances)
*/
QDomElement ElementData::plcMasterDataToXml(QDomDocument &document) const
{
auto xml_plc = document.createElement(QStringLiteral("plcMasterData"));
xml_plc.setAttribute(QStringLiteral("rowHeight"),
QString::number(m_plc_master_data.rowHeight, 'f', 2));
// Save break positions
{
auto xml_breaks = document.createElement(QStringLiteral("breakPositions"));
for (int bp : m_plc_master_data.breakPositions) {
auto xml_bp = document.createElement(QStringLiteral("break"));
xml_bp.appendChild(document.createTextNode(QString::number(bp)));
xml_breaks.appendChild(xml_bp);
}
xml_plc.appendChild(xml_breaks);
}
// Save column widths
auto xml_col_widths = document.createElement(QStringLiteral("columnWidths"));
for (auto it = m_plc_master_data.colWidths.constBegin();
it != m_plc_master_data.colWidths.constEnd(); ++it) {
auto xml_col = document.createElement(QStringLiteral("column"));
xml_col.setAttribute(QStringLiteral("index"), it.key());
xml_col.setAttribute(QStringLiteral("width"), QString::number(it.value(), 'f', 2));
xml_col_widths.appendChild(xml_col);
}
xml_plc.appendChild(xml_col_widths);
// Save column visibility
auto xml_col_vis = document.createElement(QStringLiteral("columnVisibility"));
for (auto it = m_plc_master_data.colVisible.constBegin();
it != m_plc_master_data.colVisible.constEnd(); ++it) {
auto xml_col = document.createElement(QStringLiteral("column"));
xml_col.setAttribute(QStringLiteral("index"), it.key());
xml_col.setAttribute(QStringLiteral("visible"), it.value() ? "true" : "false");
xml_col_vis.appendChild(xml_col);
}
xml_plc.appendChild(xml_col_vis);
// Save fonts
if (!m_plc_master_data.headerFont.family().isEmpty()) {
auto xml_hfont = document.createElement(QStringLiteral("headerFont"));
xml_hfont.setAttribute(QStringLiteral("family"), m_plc_master_data.headerFont.family());
xml_hfont.setAttribute(QStringLiteral("size"), m_plc_master_data.headerFont.pointSize());
xml_hfont.setAttribute(QStringLiteral("bold"), m_plc_master_data.headerFont.bold() ? "true" : "false");
xml_plc.appendChild(xml_hfont);
}
if (!m_plc_master_data.cellFont.family().isEmpty()) {
auto xml_cfont = document.createElement(QStringLiteral("cellFont"));
xml_cfont.setAttribute(QStringLiteral("family"), m_plc_master_data.cellFont.family());
xml_cfont.setAttribute(QStringLiteral("size"), m_plc_master_data.cellFont.pointSize());
xml_cfont.setAttribute(QStringLiteral("bold"), m_plc_master_data.cellFont.bold() ? "true" : "false");
xml_plc.appendChild(xml_cfont);
}
// Save custom column names
if (!m_plc_master_data.columnNames.isEmpty()) {
auto xml_names = document.createElement(QStringLiteral("columnNames"));
for (int i = 0; i < m_plc_master_data.columnNames.size(); ++i) {
auto xml_name = document.createElement(QStringLiteral("column"));
xml_name.setAttribute(QStringLiteral("index"), i);
xml_name.appendChild(document.createTextNode(m_plc_master_data.columnNames.at(i)));
xml_names.appendChild(xml_name);
}
xml_plc.appendChild(xml_names);
}
// Save column order
if (!m_plc_master_data.columnOrder.isEmpty()) {
auto xml_order = document.createElement(QStringLiteral("columnOrder"));
QString order_str;
for (int i = 0; i < m_plc_master_data.columnOrder.size(); ++i) {
if (i > 0) order_str += QStringLiteral(",");
order_str += QString::number(m_plc_master_data.columnOrder.at(i));
}
xml_order.appendChild(document.createTextNode(order_str));
xml_plc.appendChild(xml_order);
}
// Save showHeaders
{
auto xml_sh = document.createElement(QStringLiteral("showHeaders"));
xml_sh.appendChild(document.createTextNode(
m_plc_master_data.showHeaders ? QStringLiteral("1") : QStringLiteral("0")));
xml_plc.appendChild(xml_sh);
}
// Save IO entries
auto xml_ios = document.createElement(QStringLiteral("plcIOs"));
for (const auto &io : m_plc_master_data.ios) {
auto xml_io = document.createElement(QStringLiteral("plcIO"));
xml_io.setAttribute(QStringLiteral("type"), plcIOTypeToString(io.type));
xml_io.setAttribute(QStringLiteral("address"), io.address);
xml_io.setAttribute(QStringLiteral("functionText"), io.functionText);
xml_io.setAttribute(QStringLiteral("comment"), io.comment);
xml_io.setAttribute(QStringLiteral("crossRef"), io.crossRef);
xml_io.setAttribute(QStringLiteral("terminalCount"), io.terminalCount);
for (const auto &t : io.terminals) {
auto xml_term = document.createElement(QStringLiteral("terminal"));
xml_term.appendChild(document.createTextNode(t));
xml_io.appendChild(xml_term);
}
xml_ios.appendChild(xml_io);
}
xml_plc.appendChild(xml_ios);
return xml_plc;
}
/**
* @brief ElementData::plcMasterDataFromXml
* Deserialize PLC master data from XML
*/
void ElementData::plcMasterDataFromXml(const QDomElement &xml_plc)
{
if (xml_plc.isNull())
return;
// Reset PLC data before loading to avoid appending to existing data
m_plc_master_data = PlcMasterData();
m_plc_master_data.rowHeight = xml_plc.attribute(
QStringLiteral("rowHeight"), QStringLiteral("8.0")).toDouble();
// Load break positions
auto xml_breaks = xml_plc.firstChildElement(QStringLiteral("breakPositions"));
for (const auto &xml_bp : QETXML::findInDomElement(xml_breaks, QStringLiteral("break"))) {
m_plc_master_data.breakPositions.append(xml_bp.text().toInt());
}
// Load column widths
auto xml_col_widths = xml_plc.firstChildElement(QStringLiteral("columnWidths"));
for (const auto &xml_col : QETXML::findInDomElement(xml_col_widths, QStringLiteral("column"))) {
int idx = xml_col.attribute(QStringLiteral("index")).toInt();
qreal w = xml_col.attribute(QStringLiteral("width")).toDouble();
m_plc_master_data.colWidths.insert(idx, w);
}
// Load column visibility
auto xml_col_vis = xml_plc.firstChildElement(QStringLiteral("columnVisibility"));
for (const auto &xml_col : QETXML::findInDomElement(xml_col_vis, QStringLiteral("column"))) {
int idx = xml_col.attribute(QStringLiteral("index")).toInt();
bool vis = xml_col.attribute(QStringLiteral("visible")) == QLatin1String("true");
m_plc_master_data.colVisible.insert(idx, vis);
}
// Load fonts
auto xml_hfont = xml_plc.firstChildElement(QStringLiteral("headerFont"));
if (!xml_hfont.isNull()) {
m_plc_master_data.headerFont.setFamily(xml_hfont.attribute(QStringLiteral("family")));
m_plc_master_data.headerFont.setPointSize(xml_hfont.attribute(QStringLiteral("size")).toInt());
m_plc_master_data.headerFont.setBold(xml_hfont.attribute(QStringLiteral("bold")) == QLatin1String("true"));
}
auto xml_cfont = xml_plc.firstChildElement(QStringLiteral("cellFont"));
if (!xml_cfont.isNull()) {
m_plc_master_data.cellFont.setFamily(xml_cfont.attribute(QStringLiteral("family")));
m_plc_master_data.cellFont.setPointSize(xml_cfont.attribute(QStringLiteral("size")).toInt());
m_plc_master_data.cellFont.setBold(xml_cfont.attribute(QStringLiteral("bold")) == QLatin1String("true"));
}
// Load custom column names
auto xml_names = xml_plc.firstChildElement(QStringLiteral("columnNames"));
for (const auto &xml_col : QETXML::findInDomElement(xml_names, QStringLiteral("column"))) {
int idx = xml_col.attribute(QStringLiteral("index")).toInt();
while (m_plc_master_data.columnNames.size() <= idx)
m_plc_master_data.columnNames.append(QString());
m_plc_master_data.columnNames.replace(idx, xml_col.text());
}
// Load column order
auto xml_order = xml_plc.firstChildElement(QStringLiteral("columnOrder"));
if (!xml_order.isNull()) {
QStringList order_str_list = xml_order.text().split(',');
for (const auto &s : order_str_list) {
m_plc_master_data.columnOrder.append(s.trimmed().toInt());
}
}
// Load showHeaders
auto xml_sh = xml_plc.firstChildElement(QStringLiteral("showHeaders"));
if (!xml_sh.isNull()) {
m_plc_master_data.showHeaders = xml_sh.text() == QLatin1String("1");
}
// Load IO entries
auto xml_ios = xml_plc.firstChildElement(QStringLiteral("plcIOs"));
for (const auto &xml_io : QETXML::findInDomElement(xml_ios, QStringLiteral("plcIO"))) {
PlcIO io;
io.type = plcIOTypeFromString(xml_io.attribute(QStringLiteral("type")));
io.address = xml_io.attribute(QStringLiteral("address"));
io.functionText = xml_io.attribute(QStringLiteral("functionText"));
io.comment = xml_io.attribute(QStringLiteral("comment"));
io.crossRef = xml_io.attribute(QStringLiteral("crossRef"));
io.terminalCount = xml_io.attribute(QStringLiteral("terminalCount")).toInt();
for (const auto &xml_term : QETXML::findInDomElement(xml_io, QStringLiteral("terminal"))) {
io.terminals.append(xml_term.text());
}
m_plc_master_data.ios.append(io);
}
}
/**
* @brief ElementData::setTerminalType
* Override the terminal type by \p t_type
+7
View File
@@ -130,6 +130,11 @@ class ElementData : public PropertiesInterface
QList<int> columnOrder; ///< Column display order (logical indices)
bool showHeaders = true; ///< Show column headers on sheet
PlcMasterData() {
headerFont.setFamily(QString());
cellFont.setFamily(QString());
}
bool operator==(const PlcMasterData &other) const {
return ios == other.ios
&& breakPositions == other.breakPositions
@@ -201,6 +206,8 @@ class ElementData : public PropertiesInterface
QDomElement toXml(QDomDocument &xml_element) const override;
bool fromXml(const QDomElement &xml_element) override;
QDomElement kindInfoToXml(QDomDocument &document);
QDomElement plcMasterDataToXml(QDomDocument &document) const;
void plcMasterDataFromXml(const QDomElement &xml_plc);
void setTerminalType(ElementData::TerminalType t_type);
ElementData::TerminalType terminalType() const;
+2
View File
@@ -17,6 +17,7 @@
*/
#include "terminaldata.h"
#include "../qetapp.h"
#include "../utils/qetutils.h"
#include <QGraphicsObject>
@@ -38,6 +39,7 @@ TerminalData::TerminalData(QGraphicsObject *parent):
void TerminalData::init()
{
m_label_font = QETApp::diagramTextsFont();
}
TerminalData::~TerminalData()
+53
View File
@@ -40,6 +40,8 @@
#include "machine_info.h"
#include "TerminalStrip/ui/terminalstripeditorwindow.h"
#include "qetversion.h"
#include "logging/qetlogger.h"
#include "logging/ui/diagnosticsreportdialog.h"
#include <cstdlib>
#include <iostream>
@@ -2575,6 +2577,10 @@ void QETApp::checkBackupFiles()
}
if (stale_files.isEmpty()) {
// Only offer an unretrieved crash dump when there's no project
// to recover this run -- discussion #644 step 5 is explicit
// that the two prompts must never both show at once.
checkCrashDump();
return;
}
@@ -2628,6 +2634,53 @@ void QETApp::checkBackupFiles()
}
}
/**
@brief QETApp::checkCrashDump
Discussion #644, step 5: if the crash handler (step 4) left an
unretrieved dump from a previous run, offer it to the user. Only
called from checkBackupFiles() when there was no stale project file
to recover this run, so the two prompts never both show at once.
*/
void QETApp::checkCrashDump()
{
QetLogger &logger = QetLogger::instance();
if (!logger.hasPendingCrashDump()) {
return;
}
const QByteArray content = logger.pendingCrashDumpContents();
DiagnosticsReportDialog dialog(
tr("Rapport de plantage"),
tr("QElectroTech ne s'est pas fermé correctement lors de sa dernière exécution.\n"
"Voici les derniers messages enregistrés avant l'arrêt -- vous pouvez les "
"enregistrer pour les joindre à un rapport de bug."),
content);
dialog.exec();
// Offered once, then marked retrieved -- regardless of whether the
// user chose to save it -- so it is never offered a second time.
logger.clearPendingCrashDump();
}
/**
@brief QETApp::showDiagnosticsReport
Discussion #644, step 5: the manual "Help > Diagnostics > Save
report" action. Unlike checkCrashDump(), this is about the *current*,
still-running session, not a previous one.
*/
void QETApp::showDiagnosticsReport()
{
const QByteArray content = QetLogger::instance().buildDiagnosticsReport();
DiagnosticsReportDialog dialog(
tr("Rapport de diagnostic"),
tr("Ceci contient les derniers messages de journalisation de cette session. "
"Vérifiez le contenu avant de le joindre à un rapport de bug public."),
content);
dialog.exec();
}
/**
@brief QETApp::fetchWindowStats
Updates the booleans concerning the state of the windows
+2
View File
@@ -271,6 +271,7 @@ class QETApp : public QObject
void openTitleBlockTemplateFiles(const QStringList &);
void configureQET();
void aboutQET();
void showDiagnosticsReport();
void receiveMessage(int instanceId, QByteArray message);
private:
@@ -287,6 +288,7 @@ class QETApp : public QObject
void initSystemTray();
void buildSystemTrayMenu();
void checkBackupFiles();
void checkCrashDump();
void fetchWindowStats(
const QList<QETDiagramEditor *> &,
const QList<QETElementEditor *> &,
+23
View File
@@ -185,6 +185,7 @@ void QETDiagramEditor::setUpElementsPanel()
connect(pa, SIGNAL(requestForProjectClosing (QETProject *)), this, SLOT(closeProject(QETProject *)));
connect(pa, SIGNAL(requestForProjectPropertiesEdition (QETProject *)), this, SLOT(editProjectProperties(QETProject *)));
connect(pa, SIGNAL(requestForNewDiagram (QETProject *)), this, SLOT(addDiagramToProject(QETProject *)));
connect(pa, SIGNAL(requestForNewDiagramAt (QETProject *, int)), this, SLOT(addDiagramToProjectAt(QETProject *, int)));
connect(pa, SIGNAL(requestForDiagramPropertiesEdition (Diagram *)), this, SLOT(editDiagramProperties(Diagram *)));
connect(pa, SIGNAL(requestForDiagramsDeletion (const QList<Diagram *> &)), this, SLOT(removeDiagrams(const QList<Diagram *> &)));
connect(pa, SIGNAL(requestForDiagramMoveUp (const QList<Diagram *> &)), this, SLOT(moveDiagramUp(const QList<Diagram *>&)));
@@ -881,6 +882,8 @@ void QETDiagramEditor::setUpMenu()
// menu Projet
menu_project -> addAction(m_project_edit_properties);
menu_project -> addAction(m_auto_conductor);
menu_project -> addSeparator();
menu_project -> addAction(m_project_add_diagram);
menu_project -> addAction(m_remove_diagram_from_project);
menu_project -> addAction(m_clean_project);
@@ -916,6 +919,7 @@ void QETDiagramEditor::setUpMenu()
menu_affichage -> addAction(m_mode_visualise);
menu_affichage -> addSeparator();
menu_affichage -> addAction(m_draw_grid);
menu_affichage -> addAction(m_draw_guides);
menu_affichage -> addAction(m_grey_background);
menu_affichage -> addSeparator();
menu_affichage -> addActions(m_zoom_actions_group.actions());
@@ -2274,6 +2278,25 @@ void QETDiagramEditor::addDiagramToProject(QETProject *project)
project_view->project()->addNewDiagram();
}
}
/**
@brief QETDiagramEditor::addDiagramToProjectAt
Add a diagram to project, inserted at a specific position.
@param project
@param pos
*/
void QETDiagramEditor::addDiagramToProjectAt(QETProject *project, int pos)
{
if (!project) {
return;
}
if (ProjectView *project_view = findProject(project))
{
activateProject(project);
project_view->project()->addNewDiagram(pos);
}
}
/**
* @brief QETDiagramEditor::removeDiagram
* Wrapper für einzelne Diagramme, um Abwärtskompatibilität zu erhalten.
+1 -2
View File
@@ -137,6 +137,7 @@ class QETDiagramEditor : public QETMainWindow
void editDiagramProperties(DiagramView *);
void editDiagramProperties(Diagram *);
void addDiagramToProject(QETProject *);
void addDiagramToProjectAt(QETProject *, int);
void removeDiagram(Diagram *);
void removeDiagrams(const QList<Diagram *> &diagrams);
void removeDiagramFromProject();
@@ -191,7 +192,6 @@ class QETDiagramEditor : public QETMainWindow
*redo, ///< Redo the latest cancelled operation
*m_paste, ///< Paste clipboard content on the current diagram
*m_auto_conductor, ///< Enable/Disable the use of auto conductor
*conductor_default, ///< Show a dialog to edit default conductor properties
*m_grey_background, ///< Switch the background color in white or grey
*m_draw_grid, ///< Switch the background grid display or not
*m_draw_guides = nullptr, ///< Switch the custom guides display or not
@@ -199,7 +199,6 @@ class QETDiagramEditor : public QETMainWindow
*m_project_add_diagram, ///< Add a diagram to the current project.
*m_remove_diagram_from_project, ///< Delete a diagram from the current project
*m_clean_project, ///< Clean the content of the current project by removing useless items
*m_project_folio_list, ///< Sommaire des schemas
*m_csv_export, ///< generate nomenclature
*m_add_nomenclature, ///< Add nomenclature graphics item;
*m_add_summary, ///<Add summary graphics item
+38 -5
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "element.h"
#include "../qetapp.h"
#include "../qetproject.h"
#include "../PropertiesEditor/propertieseditordialog.h"
#include "../autoNum/assignvariables.h"
@@ -33,6 +34,7 @@
#include "../qetgraphicsitem/terminal.h"
#include "../ui/elementpropertieswidget.h"
#include "../undocommand/changeelementinformationcommand.h"
#include "../undocommand/setautonumcontextcommand.h"
#include "dynamicelementtextitem.h"
#include "elementtextitemgroup.h"
#include "iostream"
@@ -860,6 +862,15 @@ bool Element::fromXml(QDomElement &e,
}
}
//Load PLC master data override from diagram XML
if (m_data.m_type == ElementData::Master &&
m_data.m_master_type == ElementData::PLC)
{
auto xml_plc = e.firstChildElement(QStringLiteral("plcMasterData"));
if (!xml_plc.isNull())
m_data.plcMasterDataFromXml(xml_plc);
}
//We must block the update of the alignment when loading the information
//otherwise the pos of the text will not be the same as it was at save time.
for(DynamicElementTextItem *deti : m_dynamic_text_list)
@@ -993,6 +1004,15 @@ QDomElement Element::toXml(
element.appendChild(properties);
}
//Save PLC master data override for elements on diagram
if (m_data.m_type == ElementData::Master &&
m_data.m_master_type == ElementData::PLC)
{
auto xml_plc = m_data.plcMasterDataToXml(document);
if (!xml_plc.isNull())
element.appendChild(xml_plc);
}
//Dynamic texts
QDomElement dyn_text = document.createElement(QStringLiteral("dynamic_texts"));
for (DynamicElementTextItem *deti : m_dynamic_text_list)
@@ -1626,7 +1646,7 @@ void Element::hoverLeaveEvent(QGraphicsSceneHoverEvent *e)
(ex K for coil) with condition :
formula is empty, text tagged "label" is emptty or "_";
*/
void Element::setUpFormula(bool code_letter)
void Element::setUpFormula(bool code_letter, QUndoCommand *parent_undo)
{
Q_UNUSED(code_letter)
@@ -1655,8 +1675,21 @@ void Element::setUpFormula(bool code_letter)
nc,
diagram(),
element_currentAutoNum);
diagram()->project()->addElementAutoNum(element_currentAutoNum,
ncc.next());
NumerotationContext new_context = ncc.next();
QETProject *project = diagram()->project();
auto setter = [project](const QString &k, const NumerotationContext &c) {project->addElementAutoNum(k, c);};
if (parent_undo)
{
new SetAutoNumContextCommand(setter, element_currentAutoNum, nc, new_context, parent_undo);
}
else
{
auto *undo = new SetAutoNumContextCommand(setter, element_currentAutoNum, nc, new_context);
undo->setText(tr("Numéroter automatiquement un élément", "undo caption"));
diagram()->undoStack().push(undo);
}
if(!m_freeze_label && !formula.isEmpty())
{
@@ -1876,12 +1909,12 @@ void Element::drawPlcTable(QPainter *painter)
// Fonts
QFont header_font = plc_data.headerFont;
if (header_font.family().isEmpty()) {
header_font = painter->font();
header_font = QETApp::diagramTextsFont();
header_font.setBold(true);
}
QFont cell_font = plc_data.cellFont;
if (cell_font.family().isEmpty()) {
cell_font = painter->font();
cell_font = QETApp::diagramTextsFont();
}
for (const QPointF &pos : positions) {
+2 -1
View File
@@ -35,6 +35,7 @@ class Terminal;
class Conductor;
class DynamicElementTextItem;
class ElementTextItemGroup;
class QUndoCommand;
/**
This is the base class for electrical elements.
@@ -142,7 +143,7 @@ class Element : public QetGraphicsItem
{return m_autoNum_seq;}
autonum::sequentialNumbers& rSequenceStruct()
{return m_autoNum_seq;}
void setUpFormula(bool code_letter = true);
void setUpFormula(bool code_letter = true, QUndoCommand *parent_undo = nullptr);
void setPrefix(QString);
QString getPrefix() const;
void freezeLabel(bool freeze);
+8
View File
@@ -136,6 +136,12 @@ void QETMainWindow::initCommonActions()
about_qt_ = new QAction(QET::Icons::QtLogo, tr("À propos de &Qt"), this);
about_qt_ -> setStatusTip(tr("Affiche des informations sur la bibliothèque Qt", "status bar tip"));
connect(about_qt_, SIGNAL(triggered()), qApp, SLOT(aboutQt()));
diagnostics_action_ = new QAction(QET::Icons::DialogInformation, tr("Enregistrer un rapport de diagnostic..."), this);
diagnostics_action_ -> setStatusTip(tr("Génère un rapport avec les derniers messages de journalisation, pour l'inclure dans un rapport de bug", "status bar tip"));
connect(diagnostics_action_, &QAction::triggered, this, []() {
QETApp::instance()->showDiagnosticsReport();
});
}
/**
@@ -158,6 +164,8 @@ void QETMainWindow::initCommonMenus()
help_menu_ -> addAction(donate_);
help_menu_ -> addAction(about_qt_);
help_menu_ -> addAction(about_qet_);
help_menu_ -> addSeparator();
help_menu_ -> addAction(diagnostics_action_);
#ifdef Q_OS_WIN32
upgrade_ -> setVisible(true);
+2 -1
View File
@@ -60,8 +60,9 @@ class QETMainWindow : public QMainWindow {
QAction *youtube_; ///< Launch browser on QElectroTech Youtube channel
QAction *upgrade_; ///< Launch browser on QElectroTech Windows Nightly builds
QAction *upgrade_M; ///< Launch browser on QElectroTech MAC_OS_X builds
QAction *donate_; ///< Launch browser to donate link
QAction *donate_; ///< Launch browser to donate link
QAction *about_qt_; ///< launch the "About Qt" dialog
QAction *diagnostics_action_; ///< Open the diagnostics report dialog (discussion #644, step 5)
QMenu *settings_menu_; ///< Settings menu
QMenu *help_menu_; ///< Help menu
QMenu *display_toolbars_; ///< Show/hide toolbars/docks
-117
View File
@@ -1,117 +0,0 @@
/********************************************************************************
** Form generated from reading UI file 'addlinkdialog.ui'
**
** Created: Thu 4. Apr 17:13:59 2013
** by: Qt User Interface Compiler version 4.8.4
**
** WARNING! All changes made in this file will be lost when recompiling UI file!
********************************************************************************/
#ifndef UI_ADDLINKDIALOG_H
#define UI_ADDLINKDIALOG_H
#include <QtCore/QVariant>
#include <QAction>
#include <QApplication>
#include <QButtonGroup>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QFrame>
#include <QHeaderView>
#include <QLabel>
#include <QLineEdit>
#include <QSpacerItem>
#include <QVBoxLayout>
QT_BEGIN_NAMESPACE
class Ui_AddLinkDialog
{
public:
QVBoxLayout *verticalLayout;
QFormLayout *formLayout;
QLabel *label;
QLineEdit *titleInput;
QLabel *label_2;
QLineEdit *urlInput;
QSpacerItem *verticalSpacer;
QFrame *line;
QDialogButtonBox *buttonBox;
void setupUi(QDialog *AddLinkDialog)
{
if (AddLinkDialog->objectName().isEmpty())
AddLinkDialog->setObjectName(QString::fromUtf8("AddLinkDialog"));
AddLinkDialog->setSizeGripEnabled(false);
AddLinkDialog->setModal(true);
verticalLayout = new QVBoxLayout(AddLinkDialog);
verticalLayout->setObjectName(QString::fromUtf8("verticalLayout"));
formLayout = new QFormLayout();
formLayout->setObjectName(QString::fromUtf8("formLayout"));
label = new QLabel(AddLinkDialog);
label->setObjectName(QString::fromUtf8("label"));
formLayout->setWidget(0, QFormLayout::LabelRole, label);
titleInput = new QLineEdit(AddLinkDialog);
titleInput->setObjectName(QString::fromUtf8("titleInput"));
titleInput->setMinimumSize(QSize(337, 0));
formLayout->setWidget(0, QFormLayout::FieldRole, titleInput);
label_2 = new QLabel(AddLinkDialog);
label_2->setObjectName(QString::fromUtf8("label_2"));
formLayout->setWidget(1, QFormLayout::LabelRole, label_2);
urlInput = new QLineEdit(AddLinkDialog);
urlInput->setObjectName(QString::fromUtf8("urlInput"));
formLayout->setWidget(1, QFormLayout::FieldRole, urlInput);
verticalLayout->addLayout(formLayout);
verticalSpacer = new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding);
verticalLayout->addItem(verticalSpacer);
line = new QFrame(AddLinkDialog);
line->setObjectName(QString::fromUtf8("line"));
line->setFrameShape(QFrame::HLine);
line->setFrameShadow(QFrame::Sunken);
verticalLayout->addWidget(line);
buttonBox = new QDialogButtonBox(AddLinkDialog);
buttonBox->setObjectName(QString::fromUtf8("buttonBox"));
buttonBox->setOrientation(Qt::Horizontal);
buttonBox->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Ok);
verticalLayout->addWidget(buttonBox);
retranslateUi(AddLinkDialog);
QObject::connect(buttonBox, SIGNAL(accepted()), AddLinkDialog, SLOT(accept()));
QObject::connect(buttonBox, SIGNAL(rejected()), AddLinkDialog, SLOT(reject()));
QMetaObject::connectSlotsByName(AddLinkDialog);
} // setupUi
void retranslateUi(QDialog *AddLinkDialog)
{
AddLinkDialog->setWindowTitle(QApplication::translate("AddLinkDialog", "Insert Link", nullptr));
label->setText(QApplication::translate("AddLinkDialog", "Title:", nullptr));
label_2->setText(QApplication::translate("AddLinkDialog", "URL:", nullptr));
} // retranslateUi
};
namespace Ui {
class AddLinkDialog: public Ui_AddLinkDialog {};
} // namespace Ui
QT_END_NAMESPACE
#endif // UI_ADDLINKDIALOG_H
+5 -1
View File
@@ -214,7 +214,11 @@ void NewDiagramPage::applyConf()
rpw->toSettings(settings, "diagrameditor/defaultreport");
// default xref properties
QHash <QString, XRefProperties> hash_xrp = xrefpw -> properties();
const QHash<QString, XRefProperties> hash_xrp = xrefpw->properties();
for (auto it = hash_xrp.constBegin() ; it != hash_xrp.constEnd() ; ++it) {
it.value().toSettings(settings,
QStringLiteral("diagrameditor/defaultxref") % it.key());
}
// Global in QSettings speichern
QList<Diagram::Guide> current_guides = m_gpw->guides();
+113
View File
@@ -0,0 +1,113 @@
/*
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 "customelementinfopartwidget.h"
#include "../diagramcontext.h"
#include "../qeticons.h"
#include <QGridLayout>
#include <QLineEdit>
#include <QToolButton>
/**
@brief CustomElementInfoPartWidget::CustomElementInfoPartWidget
Constructor
@param key initial key name (empty for a freshly added row)
@param value initial value
@param parent parent widget
*/
CustomElementInfoPartWidget::CustomElementInfoPartWidget(
const QString &key,
const QString &value,
QWidget *parent) :
QWidget(parent),
m_key_edit(new QLineEdit(key, this)),
m_value_edit(new QLineEdit(value, this)),
m_remove_button(new QToolButton(this))
{
m_key_edit->setPlaceholderText(tr("nom_de_la_propriete"));
m_key_edit->setToolTip(tr("Lettres minuscules, chiffres, tiret et underscore uniquement"));
m_value_edit->setClearButtonEnabled(true);
m_remove_button->setIcon(QET::Icons::Remove);
m_remove_button->setToolTip(tr("Supprimer cette propriété"));
m_remove_button->setAutoRaise(true);
auto *layout = new QGridLayout(this);
layout->setContentsMargins(0, 2, 0, 2);
layout->setVerticalSpacing(2);
layout->setHorizontalSpacing(0);
layout->addWidget(m_key_edit, 0, 0);
layout->addWidget(m_value_edit, 1, 0);
layout->addWidget(m_remove_button, 0, 1, 2, 1);
connect(m_key_edit, &QLineEdit::textChanged, this, &CustomElementInfoPartWidget::validateKey);
connect(m_key_edit, &QLineEdit::textChanged, this, &CustomElementInfoPartWidget::changed);
connect(m_value_edit, &QLineEdit::textChanged, this, &CustomElementInfoPartWidget::changed);
connect(m_remove_button, &QToolButton::clicked, this, [this]() {
emit removeRequested(this);
});
setFocusProxy(m_key_edit);
validateKey();
}
CustomElementInfoPartWidget::~CustomElementInfoPartWidget()
{
}
/**
@return the key name currently typed in this row
*/
QString CustomElementInfoPartWidget::key() const
{
return m_key_edit->text().trimmed();
}
/**
@return the value currently typed in this row
*/
QString CustomElementInfoPartWidget::value() const
{
return m_value_edit->text();
}
/**
@return true if the typed key is non-empty and matches
DiagramContext::isKeyAcceptable()
*/
bool CustomElementInfoPartWidget::hasValidKey() const
{
const QString k = key();
return !k.isEmpty() && DiagramContext::isKeyAcceptable(k);
}
/**
@brief CustomElementInfoPartWidget::validateKey
Flag the key field when it doesn't match the accepted format,
instead of silently dropping it later.
*/
void CustomElementInfoPartWidget::validateKey()
{
const QString k = key();
if (k.isEmpty() || DiagramContext::isKeyAcceptable(k)) {
m_key_edit->setStyleSheet(QString());
} else {
m_key_edit->setStyleSheet(QStringLiteral("border: 1px solid red;"));
}
}
+61
View File
@@ -0,0 +1,61 @@
/*
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 CUSTOMELEMENTINFOPARTWIDGET_H
#define CUSTOMELEMENTINFOPARTWIDGET_H
#include <QWidget>
class QLineEdit;
class QToolButton;
/**
@brief The CustomElementInfoPartWidget class
A single row letting the user define their own element information
key/value pair, unlike ElementInfoPartWidget which is bound to one
predefined key. The key is validated against
DiagramContext::isKeyAcceptable() as the user types.
*/
class CustomElementInfoPartWidget : public QWidget
{
Q_OBJECT
public:
explicit CustomElementInfoPartWidget(
const QString &key = QString(),
const QString &value = QString(),
QWidget *parent = nullptr);
~CustomElementInfoPartWidget() override;
QString key() const;
QString value() const;
bool hasValidKey() const;
signals:
void changed();
void removeRequested(CustomElementInfoPartWidget *self);
private slots:
void validateKey();
private:
QLineEdit *m_key_edit;
QLineEdit *m_value_edit;
QToolButton *m_remove_button;
};
#endif // CUSTOMELEMENTINFOPARTWIDGET_H
+95
View File
@@ -17,12 +17,14 @@
*/
#include "elementinfowidget.h"
#include <QCheckBox>
#include <QPushButton>
#include "../diagram.h"
#include "../qetapp.h"
#include "../qetgraphicsitem/element.h"
#include "../qetinformation.h"
#include "../ui_elementinfowidget.h"
#include "../undocommand/changeelementinformationcommand.h"
#include "customelementinfopartwidget.h"
#include "elementinfopartwidget.h"
/**
@@ -47,6 +49,7 @@ ElementInfoWidget::ElementInfoWidget(Element *elmt, QWidget *parent) :
ElementInfoWidget::~ElementInfoWidget()
{
qDeleteAll(m_eipw_list);
qDeleteAll(m_custom_eipw_list);
delete ui;
}
@@ -207,6 +210,11 @@ void ElementInfoWidget::buildInterface()
ui->scroll_vlayout->addWidget(eipw);
m_eipw_list << eipw;
}
m_add_custom_property_btn = new QPushButton(tr("Ajouter une propriété personnalisée"), this);
connect(m_add_custom_property_btn, &QPushButton::clicked, this, [this]() { addCustomProperty(); });
ui->scroll_vlayout->addWidget(m_add_custom_property_btn);
ui->scroll_vlayout->addStretch();
// Existing potential isolating checkbox
@@ -235,6 +243,67 @@ void ElementInfoWidget::buildInterface()
m_potential_isolating_cb->setVisible(false);
}
}
/**
@brief ElementInfoWidget::predefinedKeys
@return every key this widget already exposes a dedicated row for,
whether through ElementInfoPartWidget (the ~40 ELMT_* keys) or one
of the standalone checkboxes. Anything present in the element's
informations but absent from this list is a user-defined custom
property.
*/
QStringList ElementInfoWidget::predefinedKeys() const
{
QStringList keys = (m_element.data()->elementData().m_type == ElementData::Terminal)
? QETInformation::terminalElementInfoKeys()
: QETInformation::elementInfoKeys();
keys << QStringLiteral("auto_num_locked")
<< QStringLiteral("potential_isolating")
<< QStringLiteral("exclude_from_bom");
return keys;
}
/**
@brief ElementInfoWidget::addCustomProperty
Append a new user-defined key/value row to the widget.
@param key initial key, left empty for a freshly added row
@param value initial value
*/
void ElementInfoWidget::addCustomProperty(const QString &key, const QString &value)
{
auto *widget = new CustomElementInfoPartWidget(key, value, this);
const int insert_index = ui->scroll_vlayout->indexOf(m_add_custom_property_btn);
ui->scroll_vlayout->insertWidget(insert_index >= 0 ? insert_index : ui->scroll_vlayout->count(), widget);
m_custom_eipw_list << widget;
connect(widget, &CustomElementInfoPartWidget::removeRequested, this, &ElementInfoWidget::removeCustomProperty);
connect(widget, &CustomElementInfoPartWidget::changed, this, [this]() {
if (m_live_edit) apply();
});
if (key.isEmpty()) {
widget->setFocus();
}
}
/**
@brief ElementInfoWidget::removeCustomProperty
Remove a user-defined key/value row.
@param widget the row to remove
*/
void ElementInfoWidget::removeCustomProperty(CustomElementInfoPartWidget *widget)
{
if (!m_custom_eipw_list.removeOne(widget))
return;
ui->scroll_vlayout->removeWidget(widget);
widget->deleteLater();
if (m_live_edit) apply();
}
/**
@brief ElementInfoWidget::infoPartWidgetForKey
@param key
@@ -271,6 +340,21 @@ void ElementInfoWidget::updateUi()
for (ElementInfoPartWidget *eipw : m_eipw_list) {
eipw -> setText (element_info[eipw->key()].toString());
}
// Rebuild the custom-property rows to match whatever
// user-defined keys this element currently carries.
while (!m_custom_eipw_list.isEmpty()) {
CustomElementInfoPartWidget *w = m_custom_eipw_list.takeLast();
ui->scroll_vlayout->removeWidget(w);
delete w;
}
const auto known_keys = predefinedKeys();
for (const QString &key : element_info.keys()) {
if (!known_keys.contains(key)) {
addCustomProperty(key, element_info[key].toString());
}
}
// Load the lock status for auto numbering
if (m_element->elementData().m_type == ElementData::Terminal) {
QString lock_value = element_info.value(QStringLiteral("auto_num_locked")).toString();
@@ -314,6 +398,17 @@ DiagramContext ElementInfoWidget::currentInfo() const
}
}
for (const auto &custom : std::as_const(m_custom_eipw_list))
{
if (custom->hasValidKey() && !custom->value().isEmpty())
{
QString txt{custom->value()};
txt.remove(QStringLiteral("\r"));
txt.remove(QStringLiteral("\n"));
info_.addValue(custom->key(), txt);
}
}
// Save the auto numbering lock status
if (m_element->elementData().m_type == ElementData::Terminal) {
info_.addValue(QStringLiteral("auto_num_locked"), ui->m_auto_num_locked_cb->isChecked() ? QStringLiteral("true") : QStringLiteral("false"));
+7
View File
@@ -26,8 +26,10 @@
class Element;
class QUndoCommand;
class ElementInfoPartWidget;
class CustomElementInfoPartWidget;
class ChangeElementInformationCommand;
class QCheckBox;
class QPushButton;
namespace Ui {
class ElementInfoWidget;
@@ -63,15 +65,20 @@ class ElementInfoWidget : public AbstractElementPropertiesEditorWidget
private:
void buildInterface();
ElementInfoPartWidget *infoPartWidgetForKey(const QString &key) const;
QStringList predefinedKeys() const;
private slots:
void firstActivated();
void elementInfoChange();
void addCustomProperty(const QString &key = QString(), const QString &value = QString());
void removeCustomProperty(CustomElementInfoPartWidget *widget);
//ATTRIBUTES
private:
Ui::ElementInfoWidget *ui;
QList <ElementInfoPartWidget *> m_eipw_list;
QList <CustomElementInfoPartWidget *> m_custom_eipw_list;
QPushButton *m_add_custom_property_btn = nullptr;
QCheckBox *m_potential_isolating_cb = nullptr;
QCheckBox *m_exclude_from_bom_cb = nullptr;
bool m_first_activation;
+1 -1
View File
@@ -535,7 +535,7 @@ void MasterPropertiesWidget::updateUi()
tr("Commentaire"), tr("Réf. croisée")
});
m_plc_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectRows);
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectItems);
m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_plc_table->setMinimumHeight(200);
@@ -0,0 +1,52 @@
/*
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 "setautonumcontextcommand.h"
#include <utility>
/**
@brief SetAutoNumContextCommand::SetAutoNumContextCommand
@param setter the QETProject setter to call on undo/redo
(addConductorAutoNum/addElementAutoNum/addFolioAutoNum, bound to a project)
@param key the numerotation context's name/key
@param old_context the context's value before this placement
@param new_context the context's value after this placement
@param parent parent undo command
*/
SetAutoNumContextCommand::SetAutoNumContextCommand(
Setter setter,
const QString &key,
const NumerotationContext &old_context,
const NumerotationContext &new_context,
QUndoCommand *parent) :
QUndoCommand(parent),
m_setter(std::move(setter)),
m_key(key),
m_old_context(old_context),
m_new_context(new_context)
{}
void SetAutoNumContextCommand::redo()
{
m_setter(m_key, m_new_context);
}
void SetAutoNumContextCommand::undo()
{
m_setter(m_key, m_old_context);
}
@@ -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 SETAUTONUMCONTEXTCOMMAND_H
#define SETAUTONUMCONTEXTCOMMAND_H
#include "../autoNum/numerotationcontext.h"
#include <QUndoCommand>
#include <functional>
/**
@brief The SetAutoNumContextCommand class
Undo/redo wrapper around one of QETProject's add*AutoNum() setters
(conductor/element/folio numerotation counters). Placing an
auto-numbered item advances one of these counters as a side effect;
without this command the counter change sits outside the undo stack
entirely, so undoing the placement removes the visible number but
leaves the counter advanced, silently burning it.
*/
class SetAutoNumContextCommand : public QUndoCommand
{
public:
using Setter = std::function<void(const QString &, const NumerotationContext &)>;
SetAutoNumContextCommand(
Setter setter,
const QString &key,
const NumerotationContext &old_context,
const NumerotationContext &new_context,
QUndoCommand *parent = nullptr);
void undo() override;
void redo() override;
private:
Setter m_setter;
QString m_key;
NumerotationContext m_old_context;
NumerotationContext m_new_context;
};
#endif // SETAUTONUMCONTEXTCOMMAND_H