The inverted painting, the rubber band replay and the changed()
receiver lived in DiagramView, which the unit tests cannot link, so the
update-flag regression was only covered through a stand-in view. They
now live in PaletteGraphicsView, a QGraphicsView subclass with no
other dependency, and DiagramView derives from it. The view tells a
subclass through paintingInverted(bool) when it renders for an
inverted display; DiagramView forwards that to the diagram. The grid
dot rule moves out of Diagram::drawBackground into
QET::Palette::gridDotColor().
tst_qetpalette now links the real class: gridDotColorSoftensInvertedDots,
paletteViewFollowsThePalette (light sheet, dark sheet at text contrast
with a red box still red and the paintingInverted calls in order, back
to light), paletteViewKeepsSceneUpdatesFlowing (three whole-scene
updates and a selection each repaint, scene set after construction),
paletteViewDrawsTheRubberBand.
Hovering a tool button changed only its frame, and on the dark palette
Fusion's hover frame is too faint to notice, so nothing told the user
which button was under the pointer (GitHub #870, PR 3 of the plan).
QETStyle wraps the running style. For QIcon::Active it returns line-art
icons tinted in the palette's highlight color, lightened until the tint
reads at 3:1 on the Light role, which is the top of Fusion's hover
gradient. Colored icons keep their colors, using the same line-art rule
as misc/make_icon_themes.py. Every other mode goes to the base style.
Fusion also asks for QIcon::Active for the icon of a highlighted menu
item and paints it on the highlight bar, where the tint would vanish.
The generated pixmap is cached per icon and cannot tell a menu from a
toolbar, so drawControl(CE_MenuItem) hands the base style an icon whose
Active pixmap is in the HighlightedText color instead.
QETApp::initStyle() installs the proxy on every platform, keeping the
base style's object name so the Fusion checks still match.
tests/qttest/tst_qeticons: hovering changes the icon ink to the tint and
stays at 3:1 on both palettes, on a raised button and on a checked one
drawn sunken, where the frame gives no hover cue at all; a colored icon is unchanged on hover; a
highlighted menu item's icon reads at 3:1 on the highlight bar.
inkcontrast.h gains background() and ink() helpers for those checks.
Both panels forced a light palette on themselves (white rows, black
text, their own selection blue) so that element previews, which are
black line art drawn for the white sheet, would stay visible on a dark
desktop (bugtracker 335). On a dark palette the two docks were the
only white windows left.
The forced palettes are gone. Element previews are now kept as drawn,
on a transparent background, and adapted where they are shown:
ElementPreviewDelegate, installed on the collection tree, hands the
view a copy with its lightness inverted when the palette is dark
(QET::Palette::forPalette), so black ink becomes the palette's light
gray while colored icons such as folders stay as they are; the drag
pixmap is adapted the same way. A light palette shows the previews
untouched. This fixes bugtracker 335 on every dark desktop rather than
masking it with a white panel.
The preview cache stored the old white-sheet pictures; it records the
format now and drops a cache written before this change once. The
amber "show this directory" highlight sets black text so it reads on
both palettes. The Projects panel only shows icons from the icon
theme, which has a dark variant, so nothing else changes there.
tests/qttest/tst_qetpalette: the line-art rule tells ink from color;
inversion keeps hue and alpha; a preview reads at 3:1 on the Base
color of both palettes; in a tree on the dark palette the delegate
inverts a line-art icon and leaves a colored one alone.
Fixes#945.
An electrician on the forum draws 400 V and 24 V circuits in the same
folio and wants the colour to be one click away
(qelectrotech.org/forum, viewtopic pid=23296). Today the quickest route
is F2, which opens a colour dialog and refuses to act unless exactly one
conductor is selected, so colouring a run means one conductor, one
dialog, at a time.
This adds a swatch button beside the auto-conductor actions. Picking a
colour does two things:
- recolours every conductor currently selected, as ONE undo step;
- becomes the colour of the next conductor drawn, through the
LastUsedStyle mechanism #888 already added and Conductor's
constructor already reads.
Either half is useful alone: with nothing selected it just sets the pen
for what comes next.
The menu lists the colours the trade names -- the three phases, neutral,
earth, and the ones used for control and extra-low-voltage circuits --
then any custom colours picked this session, then the full colour
dialog. A colour already in the standard list is not repeated under
"recently used".
Nothing is written to the project or to QSettings. That is deliberate:
it is the same session-scoped "what did I just use" idea as
LastUsedStyle, so it adds no persisted state and no file-format change.
Named presets stored per project -- what #461 actually asks for -- are a
larger feature that needs a maintainer decision first; the question is
still open on that issue since 21 June.
Verified on a virtual display against examples/Habitat-Schemas_developpes.qet,
reading colours back from the saved project rather than the screen:
select all on folio 1, pick Rouge
21 conductors {none:1, #ff5500:2, #ff0000:6, #00aa00:5, #0000ff:7}
-> all 21 #ff0000
one Ctrl+Z
-> back to the original five-colour mix, exactly
pick Marron with nothing selected, then draw a conductor
-> the new conductor is #7b3f00
ctest 12/12, Qt 6.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CaKympWT3owLotCpEN2CFj
main.cpp has forced the Fusion style on macOS since 2019, but the palette
still came from Qt's macOS platform theme, which is built for the native
style. It hands Fusion a Window, Button and Base that are the same color,
a Dark lighter than Light, and in dark mode an Inactive ButtonText of
black. Fusion derives its frames, gradients and indicators from those
roles, so fields had no edges, the radio buttons in the text alignment
dialog vanished, and the "Handles" combo box in the diagram editor
toolbar drew its text black on a dark combo the moment the window lost
focus. Light mode had the same flatness, with Window, Button and Base
all white.
Add QET::Palette (sources/qetpalette.{h,cpp}) with a light and a dark
palette laid out the way Fusion expects, and install one from
QETApp::initStyle() on macOS when the running style is Fusion, choosing
by the platform palette's lightness and keeping the platform's accent
color when it reads at 4.5:1. On macOS the base palette is now applied
whether or not "use system colors" is checked, since the system palette
cannot be drawn by Fusion; that setting only decides whether style.css
is layered on top (#467). On Qt 6.5+ the app follows the OS light/dark
switch through QStyleHints::colorSchemeChanged.
Other platforms are untouched: Fusion is Qt's default style on Linux
desktops without a platform theme, and the palette there carries the
user's desktop colors. Making Fusion and this palette the default
everywhere is discussed in #870.
tests/qttest/tst_qetpalette checks every text role pair at WCAG 4.5:1
(3:1 disabled), that Inactive equals Active, and paints a Fusion combo
box, radio buttons, buttons and a line edit on the offscreen platform to
measure the ink against its background. Set QET_TEST_DUMP_DIR to keep
the rendered images.
Following up on my own comments there: a deliberately small, mostly
read-only scripting surface, exposed to scripts as a single global
`qet` object (QetScriptApi) built on QJSEngine rather than an embedded
Python interpreter -- no new toolchain to package (QJSEngine ships in
every Qt SDK QET already targets, via the Qml module), no GIL, no
version pinning, automatic reflection of the QObject-derived core
classes' own methods with no hand-written binding layer.
## What a script can do
- Read the model: project title, file path, folio count/titles,
element/conductor counts per folio.
- Trigger the same operations the --export-* CLI flags already do
(pdf/png/svg/cables/wires/bom/wiring/nets/links/info), plus
set-titleblock and save -- thin wrappers around CLIExport::run(),
reusing its already-tested logic rather than duplicating it.
Deliberately NOT in this version: creating or editing diagram
geometry, undo integration, driving the GUI. All explicitly out of
scope per the discussion on #162.
## Two entry points, both built and tested
- `qelectrotech --run script.js project.qet` -- headless/CI.
- Projet > "Exécuter un script..." -- an interactive macro against the
currently open project. Export/save calls act on the project's file
on disk (see QetScriptApi's class comment for why), so unsaved GUI
edits aren't visible to the script; save first if that matters.
## Optional dependency, not a hard requirement
Qt::Qml is probed the same way QtPdf already is in this codebase:
QUIET, non-fatal, behind a QET_HAS_SCRIPTING compile definition. A
build without it compiles and links identically; the CLI flag and
menu action are simply absent (main.cpp) or compile to a clear
"not available" stderr message rather than silently disappearing
(qetscripting.cpp), matching the existing QtPdf pattern rather than
introducing a new one.
One real bug caught building this, not assumed away: my first pass
conditionally excluded the new source files from QET_SRC_FILES behind
`if(QET_HAS_SCRIPTING)` inside qet_compilation_vars.cmake -- but that
file is included before QET_HAS_SCRIPTING is set in the top-level
CMakeLists.txt, so the variable didn't exist yet at that point and the
files were silently never compiled, only caught by an undefined-symbol
link error. Fixed by following the QtPdf file's own precedent:
compile the files unconditionally, guard their Qt::Qml-dependent
content internally instead.
## Verified
Qt6, build clean, ctest 6/6.
- Headless: a script reading project/folio/element/conductor counts,
calling exportInfo() and exportPdf() against a real project --
correct JSON, a real single-page PDF confirmed with `file`.
Error paths: a thrown script exception reports file:line:message and
exit 1; missing script/project arguments exit 2 (matching
CLIExport's own usage-error convention); a missing project file is
reported and does not hang.
- Corpus: the same read-model script run against all 24 shipped
example projects, 0 failures.
- GUI: "Exécuter un script..." opens a real file dialog filtered to
*.js, running the picked script against the live open project
produced the exact expected JSON export file, and the application
was still fully responsive afterward.
Refs #162.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ctrl+V pasted in place, which put the copy exactly on top of the original.
Nothing appeared to happen: the only clue was a doubled outline, and the
copy had to be dragged off the original to be seen at all. The cursor was
ignored entirely.
Ctrl+V now starts a placement. The items appear under the cursor and follow
it until a left click or Return drops them; Escape or a right click takes
them away again. That is the same interaction as placing a new element, so
paste behaves like every other way of putting something on a folio, and the
copy lands where the user is looking.
Implemented as a DiagramEventInterface beside the existing add-element and
add-macro tools. The pasted items are the real ones from the start rather
than a preview: Diagram::fromXml creates them exactly as before, this class
moves them, and PasteDiagramCommand is pushed only once they are dropped.
PasteDiagramCommand's first redo() deliberately does not add items to the
scene -- it assumes fromXml already did -- so pushing it on commit adopts
them rather than duplicating them. One copy of the paste logic, and a
cancelled paste leaves nothing on the undo stack.
Conductors are not moved directly; they are drawn from their terminals and
follow the elements they attach to. On cancel they are removed before the
elements, so none is left in the scene holding a pointer to a freed
terminal.
Verified by counting elements in the saved file rather than by eye:
56 to start, 56 after paste-then-Escape, 57 after paste-then-drop, and 56
again after undo. Save determinism run against this build: pass, no
regressions against baseline. Tests 5/5 on Qt 6.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Shapes and images can now be resized, rotated, and skewed directly
on the canvas, not just moved. Both share one small transform
struct (rotation, then skew, then scale, anchored on a movable
pivot) and one handle widget, so a corner drag, an edge skew, or
grabbing the rotate handle behaves the same way and runs through
the same matrix math everywhere, instead of every item type
reinventing its own.
Shapes also gained a proper pen tool (bezier paths, corner/smooth/
symmetric nodes), arc support, and mirroring. Images gained
non-destructive cropping and colour-keyed transparency, both
remember their own settings, so reopening the dialog picks up
where you left off instead of starting over.
Properties dialogs for both were extended to match (position,
size, angle, skew), with undo/redo wired through for every handle
drag.
Old XML files can read easily as the transformation is only added
if needed and the old syntax is still used and understood if it is
not needed.
QPdfDocument::pagePointSize() (used for PDF page import) requires
Qt >= 6.4, and the QtPdf module itself is missing entirely on some
Qt6 distributions (e.g. the Flatpak org.kde.Platform runtime), since
it ships from the qtwebengine source tree rather than Qt6 core.
CMake: probe Pdf with find_package(... QUIET) instead of REQUIRED,
mirroring the existing GuiPrivate pattern. Define QET_HAS_QTPDF
only when the module is found AND Qt >= 6.4.
Replace the ad-hoc QT_VERSION_CHECK(6, 0, 0) / (6, 4, 0) guards in
qetdiagrameditor.cpp, diagrameventaddpdf.{h,cpp} and
pdfpagesdialog.{h,cpp} with #ifdef QET_HAS_QTPDF, so version and
module-availability checks live in one place.
Fixes the Flatpak build (missing Qt6Pdf) and the Windows/Debian CI
failures (QPdfDocument::pagePointSize undeclared on Qt < 6.4). The
PDF import toolbar action is now silently unavailable wherever
QtPdf isn't usable, instead of breaking the whole build.
Compiled into the binary but never instantiated -- searching the tree
for any reference outside its own three files finds nothing, and this
still holds on current master. Contains a latent bug that would be
user-visible if the widget were ever reachable
(on_tableDiagram_customContextMenuRequested compares QMenu::exec()'s
return value against one action but falls through to "select all" on
both the other action and on a plain dismiss, since exec() returns
nullptr on Escape/click-away and that's not equal to either QAction*),
which supports genuine disuse rather than temporary disconnection.
Removed the three files and their three explicit entries in
cmake/qet_compilation_vars.cmake (qelectrotech.pro globs sources/ui/*
so needs no change). Builds clean; no other file references
diagramselection.
Fixes#756.
- Add DPI selection (150/300/600) to the page selection dialog
- Add live page preview in the selection dialog
- Conditionally compile PDF import only for Qt6 (#if QT_VERSION)
- Add custom pdf-import icon (PDF document with + symbol)
- Register new icon in qelectrotech.qrc
- Qt5 builds: PDF import action is hidden, everything else works as before
Slice 4 of discussion #503, on top of slice 3 (#629): the smallest
surface that makes wiring_list_view visible, plus the diagnostic the
view needs to be honest about what it is missing.
Projet > "Liste de câblage (base de données)" opens a read-only table of
wiring_list_view, headed by a line stating how many conductors are
listed and, when non-zero, how many were excluded and why.
Deliberately not another exporter. QET already ships a wiring-list CSV
export (Projet > Exporter le plan de câblage, and --export-cables) which
walks the project XML; measured on the same projects it produces a row
per conductor and resolves labels correctly when the project has them.
Adding a second, competing CSV would be worse, not better -- the
database path's value is what it unlocks (terminal plans, BOM joins),
not replacing that export.
projectDataBase::excludedConductorCount() counts, from the live scene,
the conductors deliberately absent from the conductor table because a
terminal has no uuid. Counted from the scene precisely because the
database is where those conductors are not. Verified: 671 on
examples/industrial.qet (which has 1794 terminals and no terminal uuids
at all, so its list is empty and now says so), 0 on a project whose
elements do carry terminal uuids.
KNOWN GAP, not fixed here and the reason this is opened for discussion
rather than merge: after a save/reload the component columns are blank
for slave elements. populateElementTable()/populateElementInfoTable()
only insert Simple|Terminal|Master|Thumbnail, so slave elements -- relay
contacts, i.e. a large share of real wire endpoints -- have no row in
element_info for the view to read a label from. Measured on a two-slave-
contact project after reload: element rows 0, element_info rows 0,
terminal rows 2, conductor rows 1; the wire is listed (slice 3's LEFT
JOIN keeps it) but both component names are empty, where the existing
CSV export shows K1 -> K2 for the same file.
Closing that gap means widening a filter shared with the nomenclature
and summary views, which would change what those existing, shipped
features contain. That is a maintainer decision, not one to take
unilaterally inside an additive slice.
QET_MIME_PACKAGE_PATH was "../share/mime/packages/", a path relative
to CMAKE_INSTALL_PREFIX. This only worked by accident with the old
default prefix (/usr/local -> ../share resolves to /usr/share/mime,
the conventional system location regardless of app prefix).
With -DCMAKE_INSTALL_PREFIX=/usr (as used by Debian/Ubuntu packaging),
the same "../share" escapes /usr entirely, landing at /share/mime
instead of /usr/share/mime, which breaks dh_install (file not found
under usr/) and would silently install the mime package definition
outside any path desktop environments actually scan.
Drop the "../" so the mime package path stays under the install
prefix, matching standard practice (/usr/share/mime/packages or
/usr/local/share/mime/packages).
FetchContent_Declare unconditionally tries to clone SingleApplication
from GitHub, which breaks offline builds (e.g. Debian/Ubuntu pbuilder
with FETCHCONTENT_FULLY_DISCONNECTED=ON, Launchpad PPA builds).
If the SingleApplication submodule is already checked out in the
source tree, point FETCHCONTENT_SOURCE_DIR_SINGLEAPPLICATION at it so
FetchContent skips the network step entirely and reuses the local
copy. Falls back to the existing git clone behavior otherwise, so
this is a no-op for setups that don't vendor the submodule.
Follow-up to #740, which fixed the slave-side "(n-Xn)" cross-reference
label. The master-side item - the small table/cross drawn next to a
report or master element, listing where each of its slaves is used -
was still missing from DXF export. Measured against examples/
industrial.qet with the PDF export as an oracle (renders the whole
scene, so it shows what should be there):
before after PDF
slave xrefs "(n-Xn)" 41 41 41 (already fixed, #740)
folio/position strings 358 403 403
DXF now matches the PDF exactly.
## Why this needed a different approach than #740
The slave label is a plain QGraphicsTextItem - one string, trivial to
walk and re-emit as a single DXF TEXT entity, which is what #740 did.
The master-side item (CrossRefItem) is not: it paints itself with
~600 lines of hand-written QPainter calls across three modes
(drawAsCross/drawAsContacts/drawAsPlcTable), including a header
table, contact symbols, and rules. Hand-porting that logic to emit
DXF primitives directly would mean maintaining two divergent
implementations of the same drawing that have to be kept in sync by
hand forever.
## Approach: a QPaintEngine that intercepts CrossRefItem's own paint()
DxfPaintEngine/DxfPaintDevice (sources/dxfpaintdevice.{h,cpp}) is a
QPaintEngine/QPaintDevice pair - the same mechanism QPrinter and
QSvgGenerator use to redirect QPainter output elsewhere. Constructing
a QPainter on a DxfPaintDevice and calling item->paint() on it produces
DXF entities instead of pixels, using the exact same drawing code that
already renders correctly on screen. CrossRefItem::paint() is
unmodified.
Scope is deliberately narrow - only the QPainter calls CrossRefItem's
paint() is observed to make: drawLines -> LINE, drawRects/drawPath's
fill case -> outline-only LWPOLYLINE (no HATCH support in v1 - DXF's
fill primitive is a separate, more involved entity type; documented as
a known limitation rather than attempted here), drawEllipse -> CIRCLE
or a flattened polygon for rotated ellipses, drawPath's arc case (from
drawArc/drawPie) -> chord-flattened LINE segments, drawPolygon ->
LWPOLYLINE, drawTextItem -> TEXT. drawPixmap is intentionally
unimplemented (qWarning + skip) since CrossRefItem never calls it -
this is not a general-purpose DXF paint engine, and isn't meant to be
in this PR.
CrossRefItem::paint() is protected, per the normal QGraphicsItem
contract - added a small paintForExport() wrapper rather than making
paint() itself public, or reaching around access control.
## Explicitly out of scope
QetShapeItem::toDXF() and QetGraphicsTableItem::toDXF() (both already
implemented and working) are untouched. Rewriting working exporters
onto this engine to prove an architectural point would be a large,
unrelated diff with no user-visible benefit - if that consolidation is
wanted later, it's a separate proposal once this engine has shipped
and proven out on the one item that currently has no DXF export at
all.
## Testing
Built clean on Qt5/Linux. Verified via the GUI export dialog
(Fichier > Exporter > DXF) against examples/industrial.qet, 50 folios:
export completes without error or crash, all 50 .dxf files are
structurally well-formed (balanced SECTION/ENDSEC, single EOF each),
and grepping the folio-position pattern gives the before/after/PDF
numbers above. Spot-checked several real label strings (e.g. "18-B18",
"20-A2") present as TEXT entity values in the output, not just an
artifact of the count matching.
a) using a system provided KF6
b) downloading and compiling KF6
c) using the vendored-in re-creation of the functionality
The behaviour for both Qt5 and Qt6 is steered with the same two variables which were renamed to become version agnostic:
a) BUILD_WITH_KF=ON BUILD_KF=OFF
b) BUILD_WITH_KF=ON BUILD_KF=ON
c) BUILD_WITH_KF=OFF
The version is automatically derived from the chosen Qt major version.
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>
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.
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.
Drawing tools on the diagram canvas always started new shapes and free
text from a fixed hardcoded default (Qt's plain QPen()/QBrush(), and
the static Preferences font) -- changing a shape's color or a text's
font had zero effect on what the next new item of that type got, even
within the same editing session.
Add LastUsedStyle: a small in-memory, session-scoped static helper
(no QSettings, no persistence across restarts -- this is a live "what
did I just use" value, not a new app-wide default). Write side hooks
capture the value right where the properties editors already apply a
change (ShapeGraphicsItemPropertiesWidget::associatedUndo(), both the
live-edit dock path and the modal editProperty() dialog path; and
IndiTextPropertiesWidget::on_m_font_pb_clicked() right after the font
dialog returns). Read side hooks apply the stored value, if any, to a
newly created item: DiagramEventAddShape::mousePressEvent for shapes,
IndependentTextItem's constructor for free text (falling back to the
existing QETApp::indiTextsItemFont() Preferences default otherwise).
Doesn't touch the element/symbol editor's own drawing tools (a
separate subsystem) or add last-used text color (no color control
exists in the text properties UI to originate it from yet).
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.
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.
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.