setTransformOriginPoint() was only applied inside
parentElementRotationChanged(), so loading an already-rotated element,
or enabling keep_visual_rotation while rotation_point_center was
already true, left the origin at (0, 0) until the next parent rotation.
Apply the origin directly in both setters so it's always in sync.
Added .ts files (de and en) to the commit
Bug #331: "Il serait intéressant de pouvoir directement dans la fenêtre
'Sélection numérotation auto' modifier la valeur d'incrément et visualiser
la prochaine numérotation qui sera appliquée. Ceci sans être obligé
d'ouvrir la page de configuration."
The dock (AutoNumberingDockWidget) already let you see and edit a rule's
*current* value inline (added in 52c8ef6b4/031710b5f/ee4ba82d2). The
increment itself, and any preview of where the numbering is headed, was
reachable only through Configurer -> the full project-properties dialog.
Two new widgets per row (conductor/element/folio):
- An increment spin box, read from and written to the same NumerotationContext
field NumPartEditorW's increase_spinBox already edits in the full dialog --
same data, second place to reach it.
- A read-only next-value field, computed via
NumerotationContextCommands::next() -- the identical engine the "Suivant"
button in the full dialog already uses to step a whole context. Reusing it
rather than reimplementing the arithmetic means wrap-and-carry between parts
comes out identical to what actually happens when the number is next
consumed, and zero-padding matches real rendering
(NumerotationContext::formatValue(), mirroring
autonum::setSequentialToList()'s padding rule by hand since that function is
local to assignvariables.cpp).
NumerotationContext gains replaceIncrease(index, increase), a sibling to the
existing replaceValue() that touches only the increment field.
Every refresh call site in the file (13 of them) previously refreshed just the
value field; they now go through a new refreshRow(category), which refreshes
value + increment + next-value-preview together via a small per-row widget
bundle (rowFor()). This also let resetAutoNum()'s three-way switch collapse to
one line, and refreshValueFields()'s three near-identical blocks collapse to a
loop -- both existing before this change, not new here.
Verified live under Xvfb: created an element numbering rule "K" (Chiffre 1,
value 1, increment 1) via the full dialog, confirmed the dock showed
Valeur=1/Incrément=1/Suivant=2. Changed the dock's own Incrément to 3 --
Suivant updated live to 4, no dialog needed. Changed Valeur to 10 -- Suivant
became 13. Reopened the full configuration dialog and confirmed it read back
the same value_field=10/increase_spinBox=3, i.e. the round trip through
replaceIncrease()/storeContext() does not disturb type, initial value, modulus
or format.
Builds clean, CMake/Ninja Release, Qt 5.15, 820/820, no new warnings.
Fixes: https://qelectrotech.org/bugtracker/view.php?id=331
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bugtracker #308: the "current date" preset for a project's default
title block doesn't persist. A later comment on the report pinpointed
it exactly: the setting falls back to "No date" unless the folio tab
remains active when saving settings, and the same happens in Project
Properties.
TitleBlockPropertiesWidget::properties() (and its near-duplicate
sibling propertiesAutoNum(), copy-pasted with the same bug) reads the
date radio buttons like this:
else if (ui->m_current_date_rb->isVisible() && ui->m_current_date_rb->isChecked()) {
prop.useDate = TitleBlockProperties::CurrentDate;
...
Both the New Project settings page and Project Properties embed this
widget as one page of a QTabWidget (NewDiagramPage, in
configpage/configpages.cpp). QWidget::isVisible() depends on the
whole ancestor chain being visible, not just the widget's own state --
switch to any other tab before clicking OK/Apply and this radio
button's isVisible() goes false even though it's still checked
underneath, silently falling through all three branches. The function
returns a default-constructed TitleBlockProperties for the date
fields (useDate = UseDateValue, date = QDate(), i.e. "no date"),
matching exactly what was reported.
Fix: use isHidden() instead, which reflects only this widget's own
explicit state and mirrors the read side's own check in
setProperties()/initDialog() just above it in the same file -- that
side already uses isHidden(), not isVisible(), for the identical
"is the current-date option even offered here" question.
Verified directly: a standalone Qt program constructing the real
NewDiagramPage, checking "current date", switching the tab widget
away from Folio to Conducteur (reproducing the report's exact
trigger), then calling applyConf() and reading back the QSettings
value. Against the original code this saves date="null"; with the
fix, date="now" -- the same scenario, same tab switch, only the one
line differs. Also confirmed a full Release build (504/504, CMake/
Ninja, Qt 5.15.18) with no new warnings.
columns summed to exactly 100% (e.g. the shipped A4_1.titleblock), this produced qRound(NaN), which fatally aborted under Qt6's stricter qCheckedFPConversionToInteger assertion -- reached via
double-clicking a title block template to edit it.
Introduce TitleBlockTemplate::classifyWidthConstraint(), shared by minimumWidth() and maximumWidth(), returning std::optional WidthConstraintCase> to distinguish three non-finite outcomes:
Unconstrained (RTT columns == 100%, no absolute columns -- an ordinary, valid template), RelativeWidthExceeds100Percent (RTT alone exceeds 100%), and AbsoluteColumnsExceedRemainingWidth (RTT == 100%
with at least one absolute column also present) -- the latter two meaning the template's columns cannot be laid out at any width.
maximumWidth() previously only checked "are all columns absolute", which incorrectly reported "no upper bound" for the two unsatisfiable cases above; it now shares the same classification, so both functions
agree.
Update TitleBlockTemplateView::updateDisplayedMinMaxWidth() to show distinct, accurate tooltip text for all four cases instead of printing the old std::numeric_limits<int>::max()
sentinel or a misleading "no constraint" message for an unsatisfiable template.
Manually verified all four cases: a normal template (finite width), A4_1.titleblock (Unconstrained), an over-100% RTT template
(RelativeWidthExceeds100Percent), and RTT==100% with an absolute column present (AbsoluteColumnsExceedRemainingWidth).
Translations still partially missing.
Bugtracker #335: element library icons are black and nearly invisible
under a dark desktop theme (reported on KDE Plasma / Fedora 43).
The main elements panel (ElementsCollectionWidget) already forces a
fixed light palette on its tree views via ElementsTreeView, added in
a8e2a7acf and completed in bb61dde81 -- element icons are rendered
with colors read from each .elmt file (almost always black linework,
matching printed-schematic convention) onto a transparent
background, so any view showing them needs to stay light regardless
of the OS theme. ElementsTreeView's own class doc already says
"This class must be used when the tree view have an
ElementsCollectionModel as model" -- but two other dialogs showing
the exact same model were still using a plain QTreeView and missed
that fix: the Open/Save Element/Category/Template dialog
(ElementDialog) and the New Element Wizard's parent-category picker
(NewElementWizard). Same underlying ElementsCollectionModel, same
black-on-transparent icons, same invisibility on a dark theme.
Fix: use ElementsTreeView in both, matching the main panel and the
class's own documented contract. No other behavior changes --
ElementsTreeView only additionally overrides startDrag() to use a
nicer drag pixmap, which is inert unless drag-out is enabled.
Verified with a full Release build (504/504, no new warnings) and a
standalone Qt program that shows the real ElementDialog under a
forced dark QPalette (simulating a dark OS theme, since neither this
build environment nor QET itself forces the palette one way or the
other): screenshots down through nested collection categories
(Electric > IEC 60617 > Conductors and connecting devices) confirm
the tree view keeps a white background against the dark dialog
chrome around it.
Now a new checkbox in the dynamictextfieldeditor is available (by default set to false for legacy) to make it possible to turn dynamic text fields around its own center.
This resolves an undesirable behaviour that occurs when the text alignment is retained
Including translation to english and german
Raised by plc-user in discussion #618: the diagram editor allows moving
elements by as little as 1px, and asked that rotation not undershoot
that. Checking the actual constraint (Settings -> DiagramEditor_xGrid_sb
/ _yGrid_sb, both independently configurable, minimum 1, maximum 30)
turned up a real, verified gap this PR's existing fractional-pivot fix
doesn't cover: an ASYMMETRIC grid (xGrid != yGrid).
Swapping X/Y deltas for a 90-degree turn -- the exact-arithmetic path
already in this file -- only stays on the configured grid if
xGrid == yGrid. With an asymmetric grid, a delta that was a clean
multiple of xGrid lands on the Y axis after the swap, where the grid
unit is yGrid, and one is not generally a multiple of the other.
Verified on a real build (not just derived): two elements at (100,210)
and (150,420), both on-grid under xGrid=10/yGrid=7, selected and
group-rotated 90 degrees via a temporary local CLI harness.
before this change: (242,292) and (32,342) -- off-grid on both axes
after this change: (240,294) and ( 30,343) -- exactly on-grid
Confirmed the same drift is present without this change too (i.e. not
something introduced elsewhere) and that xGrid==yGrid, the common case,
is unaffected: snapping an already-on-grid point is a no-op.
Fix: re-snap the final computed position to Diagram::snapToGrid(), not
just the shared pivot, for the exact-90-degree path. Left the
arbitrary-angle trig fallback alone -- it has no caller today (the
diagram editor only ever passes multiples of 90) and "on-grid" doesn't
have a clean meaning for an arbitrary angle regardless of grid shape.
Does not attempt to fix a separate, pre-existing property surfaced
while testing this: four consecutive 90-degree turns do not reliably
return a selection to its exact starting position, even on a symmetric
grid, because each RotateSelectionCommand recomputes the pivot fresh
from the selection's current sceneBoundingRect(), and an item whose
bounding box isn't rotationally symmetric reports a different box (and
therefore a different centre) at 0 and 90 degrees. Verified this drift
is identical with and without this change, so it is not a regression --
just a different, harder guarantee this change does not attempt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Following up on the earlier division-by-zero fix: return -1 from the bad denominator branch of minimumWidth(), matching the "no
constraint" convention maximumWidth() already uses, instead of std::numeric_limits<int>::max() or 0. Update
TitleBlockTemplateView::updateDisplayedMinMaxWidth() to skip the "Longueur minimale" line when minimumWidth() reports -1, mirroring
its existing handling of maximumWidth() == -1.
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>
Rotating a selection around the raw bounding-box centre moved
grid-aligned elements off the grid permanently. sceneBoundingRect()
comes from font metrics and pen widths, so the centre is almost never
a round number: with a pivot of (132.67, 101.11), an element sitting
at x=100 landed at x=133.78, and no further rotation brought it back.
Positions are written with QString::number() (%.6g), which hides the
floating-point noise but keeps the offset, so the diagram ends up
subtly misaligned with no way to repair it from the UI.
Snap the pivot with Diagram::snapToGrid(), which also follows the
user's configured X/Y grid rather than assuming the 10 px default.
Also compute the rotated offset exactly for multiples of 90 degrees
instead of going through qCos()/qSin(). The rotate actions only ever
pass right angles, and qCos(90 deg) is 6.12e-17 rather than 0, so the
trig path added error for no benefit -- four 90 degree steps did not
return a point to where it started. A quadrant is an axis swap, which
is exact; trig is kept as the fallback for any other angle.
With both, four 90 degree rotations of a grid-aligned element return
it exactly to its original position and every intermediate step stays
on the grid.
Reported by plc-user, who hit the same problem rotating graphical
primitives in the Element Editor -- discussion #618.
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.
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.
Never leave a collection folder without a name (replaces #622)
Mark elements-folder with an exclamation-mark, when "qet_directory" is missing or faulty.
The unconditional early return was narrowed to non-directories only, so
that the just-added warning badge could be picked up once setUpData()
resolved m_qet_directory_unreadable asynchronously. But every directory
then called setIcon() on every single data(Qt::DecorationRole) query --
not just once -- and QStandardItem::setIcon() -> setData() emits
dataChanged() unconditionally (QIcon has no equality check to suppress
it). QTreeView handles dataChanged() by recomputing the row's size hint,
which re-enters data() for the same index, calling setIcon() again:
unbounded mutual recursion, confirmed by an isolated reproduction to
overflow the stack in a single frame (100k+ frames) well before the
first paint completes. Matches plc-user's report of a segfault right as
the elements tree begins drawing.
The race the guard was widened for doesn't actually occur:
ElementsCollectionModel only attaches itself to the tree view (the only
way data() becomes reachable) from loadingFinished(), which fires after
the QtConcurrent::map over every item -- this one included -- has
already finished. m_qet_directory_unreadable is therefore always final
before setUpIcon() can run for the first time, so the plain, always-only-
once guard is sufficient and the badge still works correctly.
RotateSelectionCommand's existing "Pivoter" action (Space) only ever
bumps each selected item's own rotation property -- QGraphicsItem's
setRotation() spins an item around its own local origin and never
touches pos(). Select three elements arranged in a row and rotate:
each spins 90 degrees individually, but the row stays a row. That's
"rotate each item," not "rotate the group."
Add a rotate_as_group parameter to RotateSelectionCommand (default
false, so the existing action and its one call site are unchanged).
When set, it computes a shared pivot once -- the bounding-box center
of the whole selection -- and queues a second, parallel "pos"
QPropertyUndoCommand alongside the existing "rotation" one, rotating
each item's position around that pivot by the same angle.
Scoped the position change to Element/IndependentTextItem/
DiagramImageItem only: these are the only selectable types with
scene-space pos(). ConductorTextItem, DynamicElementTextItem and
ElementTextItemGroup are all parent-relative children (confirmed by
reading their constructors), so when their owning Element is also
selected and gets its own pos() rotated, they're carried along for
free by Qt's normal parent/child transform propagation -- exactly
what the existing "skip rotation if parent is also selected" guard
already assumes for those three cases.
Exposed as a new, separate action ("Pivoter le groupe", Shift+Space)
next to the existing one rather than changing Space's behavior, since
some workflows may rely on the current per-item rotation.
"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.
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).