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.
"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.
@plc-user asked (review on #633) for a way to see a broken folder
directly in the tree instead of only on tooltip hover, originally
suggesting a "FixMe: " text prefix on the displayed name. That name is
reused verbatim in dialog titles and status-bar messages elsewhere
(elementscollectionwidget.cpp), so baking a prefix into it would leak
into those too. An icon badge gets the same visibility without
touching the name value.
setUpIcon() overlays a small warning glyph on the folder icon when
m_qet_directory_unreadable is set. Also drop the "already has an icon,
skip" guard for directories specifically: that flag is only known once
the async setUpData()/localName() job completes (QtConcurrent::map), so
without this a directory painted before that finished would have its
plain folder icon cached forever and never pick up the badge.
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.
Suggests the element's filename (without its .elmt extension) as the
default save name when exporting to SVG, instead of only defaulting to
the customElementsDir with no filename. Addresses plc-user's review
suggestion on PR #637.
Implements discussion #605. The diagram editor can already export a folio
to SVG; the element editor, where a single .elmt symbol is drawn, had no
export capability at all -- confirmed by grepping its header for "export"
before starting: nothing.
## Renders the live scene, not ElementPictureFactory's cache
The discussion proposed sourcing this from ElementPictureFactory's cached
per-element QPicture (m_pictures_H), the one used for the elements-panel
preview icons. Checked that cache's actual invalidation before building on
it: nothing in the editor ever tells it to drop an entry on edit, and it is
keyed by the element's on-disk uuid. So for any element already previewed
once in the panel, exporting from the cache would silently produce stale
content after any edit; for a brand-new, never-saved element, no entry
would exist at all. Neither is acceptable for a File > Export action a
user expects to reflect what's on screen right now.
Renders ElementScene directly instead, the same way
ExportDialog::generateSvg() already renders the live Diagram for the
diagram editor's own SVG export: no new drawing logic, only a new playback
target (QSvgGenerator instead of the screen), sized to the element's own
content bounds via the existing elementSceneGeometricRect() helper.
## Hotspot cross excluded from the export
ElementScene::drawForeground() draws the red origin/hotspot cross on every
render() call, unconditionally -- it's an editing aid, not part of the
element being drawn, and diagram editor's SVG export has no equivalent
problem since Diagram doesn't draw one. Added a settable
hotspotVisible flag, defaulting to true (the existing editing view is
completely unaffected) and turned off only for the duration of the export
render() call.
## Verified end-to-end via a real Xvfb session, not just a build
Opened a real shipped element (en_60617_05_06_04.elmt, "Phototransistor"),
exported it, and rendered the resulting SVG back to a bitmap with a small
QSvgRenderer-based harness -- pixel-identical in shape to the element as
shown in the editor. Confirmed the file is valid XML and contains no
red/#ff0000 stroke (the hotspot cross did not leak in).
Then the case the whole "render live, not cached" decision was about:
opened the same element, drew a new line with the line tool, and exported
again *without saving*. The new line is present in the exported SVG.
git status on the source .elmt file after both exports shows it completely
untouched -- the export is read-only and reflects live, unsaved editor
state, exactly the property a cache-based implementation would have gotten
wrong.
Built clean, no new warnings.
Reported by @scorpio810 on #626: "The field does not update automatically;
you need to list the other rules for it to update."
Two reasons, both mine:
The refresh was wired to the combo boxes' activated() signal, which Qt
emits only for user interaction. Nothing that changed a context
programmatically -- which is to say, numbering an element -- ever reached
it. Re-picking a rule from the combo was not a workaround so much as the
only code path that refreshed at all.
And there was no signal to hang it on for two of the three categories:
addElementAutoNum() emitted elementAutoNumAdded(), but addConductorAutoNum()
and addFolioAutoNum() emitted nothing, so even a listener would not have
heard a conductor counter advance.
Add QETProject::autoNumContextUpdated(), emitted by all three setters, and
have the dock re-read its three fields on it. Kept deliberately separate
from the existing *AutoNumAdded/*Removed signals: those make listeners
rebuild their rule lists, which is both heavier than needed here and would
disturb the user's current selection every time an element is numbered.
This one only says "re-read me".
The automatic refresh skips a field that has keyboard focus, so numbering
an element cannot overwrite a value half-typed under the cursor. Explicit
refreshes after a reset or an edit still write unconditionally, so the
field always ends up showing the canonical stored value.
Measured, advancing a counter the way numbering advances it and without
touching the combo box:
field before advance "5"
context after advance 6
field after advance "6" (was still "5")
A cyclic part could only ever be rendered at its natural width, which is
fine for one of @scorpio810's two real layouts and wrong for the other:
April 5000/2000, 32-point cards %IX0.0 .. %IX0.31, then %IX1.0
Schneider M340, 64-point cards I1.00 .. I1.63, then I2.00
The first wants no padding, the second wants two digits. Since the two
conflict, the width cannot be derived from the modulus or from the part
type -- it has to be the user's to set.
Add a format field holding a run of zeros, the same convention a
spreadsheet uses for integer padding: "00" renders 7 as 07, "000" as 007.
The field's length is the minimum number of digits. It applies to every
numeric part type, not only cyclic ones, so "Chiffre 01" can be widened
past two digits without inventing another type for it.
An empty mask means the part type's own natural width, so it reproduces
exactly what every existing context does today -- Chiffre 1 stays 7,
Chiffre 01 stays 07, Chiffre 001 stays 007. That is what makes this safe
for existing projects: absent is the default, and absent changes nothing.
Stored as a sixth field on the context part and as an XML attribute
written only when set, following how modulus was added: readers guard on
size() and treat a short item as "no format". All seven places that
rebuild a part while incrementing it now carry the format through --
missing one would have silently dropped the padding on the second element
numbered.
The editor field is restricted to zeros by a validator, and is enabled
only for types that render as a number.
Measured:
April, mask empty %IX0.29 %IX0.30 %IX0.31 %IX1.0 %IX1.1
M340, mask "00" I1.00 I1.01 ... I1.62 I1.63 I2.00 I2.01
no mask unit 7,8,9 ten 07,08,09 hundred 007,008,009
ten with mask "0000" 0007 0008 0009
Reported by @scorpio810 on #632 with a screenshot: a "Chiffre 1" followed by
a "Cyclique (modulo) 8" numbers elements 0..7 and then jumps straight to 9,
never showing 8, and never producing the 0-7 / 10-17 / 20-27 pattern the
feature exists for.
The cause is that the wrap-and-carry feature shipped without its rendering
half. Commit 68c2603 added the arithmetic and the editor UI across seven
files, none of them assignvariables.*, so there is no %seqw_ variable, no
wrap list in sequentialNumbers, no branch in setSequential(), and no branch
in numerotationContextToFormula(). A cyclic part therefore contributes
nothing to the generated formula and cannot be referenced from one -- it is
invisible.
Invisible but not inert: it still advances and still carries. So the digit
in front of it receives +1 from the carry on top of its own increment, and
the only digit the label does show jumps by two. That is the missing 8.
Add the missing half:
- sequentialNumbers gains a wrap list, handled in the copy constructor,
assignment, comparison, clear(), toXml() and fromXml();
- setSequential() collects wrap parts when the label uses %seqw_;
- assignSequence() substitutes %seqw_N and counts wrap in its bound, so a
context whose only counter is cyclic still resolves;
- numerotationContextToFormula() emits %seqw_N, so adding a Cyclique part
in the editor now puts a token in the formula instead of nothing.
Old projects are unaffected: <wrap> is simply absent from files written
before this, which fromXml() reads as an empty list, and such files have no
cyclic parts to reference in the first place. An older QET reading a newer
file ignores the unknown child.
Measured on the exact configuration from the report, unit + wrap(mod 8):
formula generated %sequ_1%seqw_1 (was %sequ_1 -- wrap contributed none)
carry digit increment 1 00 11 22 33 44 55 66 77 90 101 112 ...
carry digit increment 0 00 01 02 03 04 05 06 07 10 11 ... 17 20 21
The second line is the requested pattern. The first shows what is left once
the rendering is fixed but the carry digit still increments itself as well
as receiving the carry -- worth a UI decision, noted on the PR.
Follows @scorpio810's review on PR #626 and three defects found by finally
running the thing rather than only building it.
Replace the "?" button with an editable value field, as asked for. It shows
the current value of the context's counter -- the last part that actually
progresses, i.e. the least significant digit -- and typing a new value and
committing it writes that value back. This is strictly more useful than the
button it replaces: "?" is still reachable by typing it, and any other value
is now reachable too, which was the point of the request.
It also removes a destructive edge the button had: "reset to ?" rewrote
*every* part, so a scheme built as "K" + counter became "?????" and the
configured prefix was gone for good. There is no undo command for
numbering contexts.
Two bugs fixed in the reset path itself:
- The project was never marked modified. addConductorAutoNum() and friends
are a plain insert into a QMap; they emit nothing and set no dirty flag,
and the properties dialog that this code was modelled on calls
setModified(true) separately afterwards. Without it the user resets a
counter, closes the project, is not asked to save, and the reset is lost.
Verified before the fix: projectWasModified() stayed false across a click.
- A wrap part was reset to "1". A modulo part cycles over [0, modulus) --
the PLC addressing that motivated the feature runs %IX0.0..%IX0.31 -- so
its starting value is 0, not 1.
An empty value field is treated as "no change" rather than as an empty
value, so clearing the box by accident cannot wipe a counter, and the field
is refreshed from the context after every write and whenever the selected
context changes.
Fixes https://qelectrotech.org/bugtracker/view.php?id=332
localName() set a non-root folder's label only inside the success path of
loading its qet_directory file. If that load failed -- file missing,
malformed, or unopenable because of the Windows path-encoding problem with
accented characters that plc-user diagnosed on the tracker -- nothing was
set at all, and since a fresh item's text() is null the folder rendered
with a completely blank label. That is the reported symptom.
Resolve the name into a local and always fall back to the folder's own
directory name, so the label is never empty whatever went wrong.
The fallback is applied *after* NamesList::name() rather than passed into
it. This matters: name() returns a caller-supplied fallback before it
reaches its "first available translation" step, so passing m_path in would
replace a perfectly good name in some other language with the raw
directory name. A folder named only in French, viewed under an English
locale, previously showed "Accentué" and must keep doing so.
Falling back on its own would then hide the broken file -- the user sees a
plausible name and never learns there is anything to repair. So a folder
whose qet_directory could not be read now says so in its tooltip, naming
the file, above the collection path that tooltip already carried.
Suggested by plc-user on PR #622. The flag is recorded in localName() and
consumed in setUpData(), because setUpData() assigns the tooltip after
localName() runs and would otherwise discard it.
Only a file-level failure is flagged. A readable qet-directory with no
entry for the current language is not an error; NamesList::name() resolves
that itself and no warning is shown.
Verified on a fixture collection of four folders -- valid, malformed,
missing, and one named only in French:
master this patch
fr-only Accentué Accentué (no warning)
malformed <blank> malformed (warning)
no qet_directory <blank> no_file (warning)
valid Valid Folder Valid Folder (no warning)
Turning the default "Chiffre 1" part into a "Cyclique (modulo)" one left
the modulus spin box at 0, and a modulus of 0 means "no cycle" -- so the
part counted upward forever instead of wrapping, which is the whole point
of the type. Reported on #593 against a modulus-7 test and, more usefully,
against a real April 5000 PLC layout addressed %IX0.0..%IX0.31 per card.
setType() defaulted the modulus to 8 inside the block that installs numeric
behaviour, and that block runs only when the *previous* type was
non-numeric. Switching from one numeric type to another skips it. Since a
fresh part starts out as "Chiffre 1", the ordinary way to reach this
feature -- change the type of the part in front of you -- was exactly the
path that skipped the default. Going the long way round, via "Texte", set
the modulus to 8 and worked, which is why the feature tests fine when you
build the context some other way.
Moved the default out of that block so it applies whatever the part was
before, and made it fire only when the current modulus is unusable, so a
value the user picked on purpose survives switching type away and back.
The wrap/carry arithmetic itself was already correct: with a carry target
in front of it, a modulus-32 part yields %IX0.0..%IX0.31, %IX1.0 as asked.
Saved configurations are untouched -- a stored modulus, including a 0 left
behind by this bug, still loads and round-trips exactly as it was.
Resetting an active numbering counter back to a starting value, or
marking it as needing manual numbering, currently requires the full
round trip through the project properties dialog: open it from the
dock's Configure button, locate the right numbering context, select
the specific part row, clear and retype the value, confirm.
Add two small buttons next to each of the three combo boxes
(Conductor/Element/Folio) on AutoNumberingDockWidget itself:
- Reset to start: calls NumerotationContext::replaceValue() on every
part that represents a progressing counter, using a sensible
per-type value -- the part's own stored initialvalue for
folio-anchored types (unitfolio/tenfolio/hundredfolio), "1" for
plain numeric types and wrap, "a" for alpha. Non-incrementing types
(string, plant, locmach, idfolio, folio, elementline,
elementcolumn, elementprefix) are left untouched, since there's no
meaningful "start" distinct from whatever the user configured for a
fixed/contextual value.
- Reset to "?": sets every part's value to the literal placeholder
"?" unconditionally, for marking a context as needing manual
numbering.
Both write the updated context back via the same
addConductorAutoNum/addElementAutoNum/addFolioAutoNum calls the
project properties dialog itself already uses, so the dock's existing
refresh signals fire exactly as they do today.
Verified with a full build (Qt6) after the change -- clean compile
and link, including the .ui-generated Ui class correctly picking up
the six new button object names. Wasn't able to get a reliable live
GUI run in this environment to click-test the buttons themselves (ran
into unrelated session/display instability before any interaction
with the new buttons occurred), so this is verified by code review
and successful build rather than a runtime screenshot.
Implements https://github.com/qelectrotech/qelectrotech-source-mirror/discussions/597
ElementsPanel and ElementsTreeView already force a fixed light palette
(white base, black text) on themselves, specifically because element
icons are rendered with colors read directly from each .elmt file --
almost always black linework, matching printed-schematic convention --
onto a transparent background. That only stays legible if the row
background is reliably light, regardless of the OS/desktop theme.
But QAbstractItemView paints row backgrounds using its viewport's
palette, not the view widget's own palette. setPalette() on the view
itself doesn't propagate to viewport() in the general case, so under
styles that actually respect the viewport's (unset, therefore
theme-inherited) palette -- e.g. KDE Plasma's Breeze Dark -- the row
background falls through to the app's dark palette while the element
linework is still literal black, making library icons and terminal
symbols invisible.
Apply the same QPalette to viewport() right after setPalette() in both
constructors, so the fix these two classes already clearly intended
actually takes effect under every style.
Fixes https://qelectrotech.org/bugtracker/view.php?id=335
Adds a real base-26 incrementing part type to the autonumbering engine,
alongside the 14 existing NumStrategy leaves. Unlike StringNum (a fixed,
non-incrementing text segment), AlphaNum::next()/previous() carry/borrow
entirely within the part's own value -- the composition loop in
NumerotationContextCommands doesn't need to change, since (unlike #578's
wrap-and-carry) nothing here needs to signal an adjacent part.
- incrementAlpha()/decrementAlpha() implement the spreadsheet-column-name
algorithm: increment carries right-to-left on 'z'/'Z' overflow,
prepending a new leading letter if the whole value overflows (z -> aa,
az -> ba). decrement is the exact inverse, including the symmetric
shrink case (aa -> z) once every position has borrowed. A single letter
already at "a"/"A" has no representable predecessor and is clamped
rather than turned into "z" -- caught via manual testing, since the
initial implementation mutated the string in the borrow loop before
checking whether to clamp, silently discarding the original value.
- Registered in NumerotationContext::validRegExpNum() but deliberately
not in validRegExpNumber(), so addValue() doesn't force alphabetic
values through int conversion.
- New "Cyclique"-adjacent "Alphabétique" entry in numparteditorw's type
dropdown, with its own letters-only QRegularExpressionValidator; the
increase spinbox is disabled since the step is always exactly one
letter, not a configurable amount.
Also wires the new part type through to actual element/conductor labels,
which turned out to be required for the feature to do anything visible
beyond folio numbering (which applies a NumerotationContext's
represented string directly). Element and conductor numbering instead
go through a separate formula-substitution layer
(autonum::sequentialNumbers + %sequ_/%seqt_/%seqh_-style placeholders in
AssignVariables::assignSequence()) that numerotationContextToFormula()
auto-populates. Without a matching placeholder, an "alpha" part would
silently vanish from the generated formula and never reach the label,
even though the underlying counter was advancing correctly:
- sequentialNumbers gained an `alpha` QStringList member (copy ctor,
operator=, operator==, toXml/fromXml, clear()).
- numerotationContextToFormula() emits a new %seqa_N placeholder for
alpha parts, the same way %sequ_N is emitted for unit parts.
- setSequential()/setSequentialToList() populate seqStruct.alpha,
passing the raw string through as-is rather than the .toInt()-based
formatting used for the numeric part types.
- AssignVariables::assignSequence() substitutes %seqa_N from
seqStruct.alpha, mirroring the existing %sequ_N/%seqt_N/%seqh_N
substitutions.
No "alphafolio" variant was added, matching the discussion's scope (only
unit/ten/hundred have folio-anchored variants).
Verified against production code via the numbering config dialog's own
Suivant/Précédent buttons: from "a", 25 clicks reached "z"; one more
produced "aa"; 25 more reached "az"; one more produced "ba" (carry).
Reversed: "ba"->"az"->(25 clicks)->"aa"->"z" (shrink)->(25 clicks)->"a".
One more "previous" at "a" correctly stayed at "a" after the clamp fix.
Also confirmed the Formule field auto-updates to "%seqa_1" the instant
the type is switched to "Alphabétique", confirming the formula-generation
wiring works live in the UI, not just at the engine level.
DialogWaiting pumps the event loop while the folios of a project are
built, so a second openAndAddProject() can run to completion nested
inside the first one (drop on another editor window, queued open) and
the plain reset/read counters would then report the wrong numbers.
Replace them with a RAII counting window (FontRestorationScope): the
constructor keeps the enclosing counts aside, the destructor restores
them. The nesting is strictly LIFO - the nested load completes inside
the pump of the outer one - so each load reports exactly its own
numbers, and the early-return paths of openAndAddProject() restore the
outer window automatically.
Suggested by ispyisail in the review of the reporting change.
Adds a configurable wrap-at-N counter type to the autonumbering engine
(NumerotationContext + NumerotationContextCommands), covering PLC/rack-style
addressing conventions like "e0.0...e0.7, e1.0...e1.7" (8 channels per
card) generally, rather than hardcoding octal specifically.
- New "wrap" part type (WrapNum, alongside the existing UnitNum/TenNum/
HundredNum strategies) stores a modulus in addition to the existing
value/increase/initialvalue fields. Its own next()/previous() only wraps
its own value modulo the configured modulus -- carrying into (or
borrowing from) the adjacent part requires visibility across parts,
which only the composition loop has.
- NumerotationContextCommands::next()/previous() gained carry()/borrow()
helpers: when a wrap part's own next() would reach/exceed its modulus
(or go below 0 on previous()), the nearest preceding numeric part is
bumped by exactly one unit, skipping non-numeric parts (e.g. a "."
string separator). Wrap parts chain correctly if adjacent (e.g. seconds
wrapping into minutes wrapping into hours).
- For the leading part of a wrap-and-carry pair to stay fixed except when
carried into (i.e. actually produce "e0.0...e0.7, e1.0..." rather than
advancing on every step under its own strategy), its own increase must
be 0. The increase spinbox's minimum was 1, which made this
configuration impossible through the UI -- lowered to 0 and documented
with a tooltip, since this wasn't obvious from the UI alone.
- NumerotationContext gained a 5th pipe-separated field (modulus) in its
serialized string form, defaulting to 0 (non-wrapping) for every
existing part type; toXml()/fromXml() persist it as a "modulus" XML
attribute the same way "initialvalue" is already persisted for
unitfolio/tenfolio/hundredfolio.
- New "Cyclique (modulo)" entry in the part-type dropdown (numparteditorw),
available for element, conductor, and folio autonumbering alike, since
all three already go through NumerotationContextCommands.
Verified in the running app via the numbering config dialog's own
Suivant/Précédent buttons (which call the production
NumerotationContextCommands::next()/previous() directly): a two-part
context (unit, increase=0 + wrap mod 8) produced exactly
e0.0→...→e0.7→e1.0→...→e1.7 on repeated "next", and the exact reverse
(with correct borrowing) on repeated "previous".
Until now a font description that could not be parsed only produced
console warnings most users never see, so nobody learned that their
texts silently lost their formatting (see the reports in issue #553).
Count in QETUtils::fontFromString() how many descriptions were salvaged
from a foreign or corrupt format and how many stayed unreadable, and
show a message box after opening a project when either happened:
salvaged descriptions are rewritten in the stable format on the next
save, unreadable ones fall back to the default font. Projects without
font issues open exactly as before, and non-interactive opens only log
the counters.
Verified with a Qt 5.15 build on a project carrying 52 19-field and
one 21-field description: the dialog reports 53 restored descriptions;
the same file on a Qt 6.11 build (which parses those formats natively)
shows no dialog.
See issue #553.
Three new QUndoCommand subclasses (AddDiagramCommand, RemoveDiagramCommand,
MoveDiagramCommand) pushed onto the project's existing (already
project-scoped) undo stack, so folio structure edits are undoable
alongside every item-level edit already on that stack.
- QETProject::addDiagram()/detachDiagram() are the shared attach/detach
primitives: they mutate the diagram list, connect/disconnect the two
per-diagram signals set up at add time, and emit diagramAdded/
diagramRemoved. AddDiagramCommand and RemoveDiagramCommand call these
(via friend access) for both redo and undo, so a removed diagram is
parked rather than destroyed -- it's only actually deleted if the
command itself falls out of undo history while still detached.
- ProjectView reacts to diagramRemoved the same way it already reacted to
diagramAdded (tearing down/rebuilding the tab), so both directions of
both commands go through the same reactive path every other diagram
listener (project database, cross-references, generic panel) already
relies on.
- MoveDiagramCommand wraps a new ProjectView::setDiagramPosition(), which
performs the tab move and the project's diagramOrderChanged() list
reorder synchronously in one step, instead of relying on the queued
tabMoved connection (needed for interactive drag-and-drop) to catch up
later -- avoiding a second, redundant reorder from that queued call.
- Multi-folio delete and multi-folio move (QETDiagramEditor::removeDiagrams()
and the moveDiagram*(QList<Diagram*>) batch slots) wrap their per-diagram
loop in QUndoStack::beginMacro()/endMacro(), so a multi-select action is
one undo step, matching current UX.
- Softened the delete confirmation's "this change is irreversible" wording
now that it no longer is.
Verified headlessly (Xvfb + xdotool + scrot): add/undo/redo, delete/undo/
redo (single and multi-select, single undo step for the batch), and
move/undo/redo all behave correctly against a 7-folio project.
Displays the cursor's scene position (same grid units as the parts'
X/Y property spinboxes) in a permanent status bar label, updated on
every mouse move. Addresses the overlapping-node mis-click case from
the originating forum report: with a live readout, precise pointing
no longer requires guessing against nearby z-ordered points.
ElementScene::mouseMoveEvent already computed the (optionally
grid-snapped) scene position on every move; it now also emits it via
a new mouseMoved(QPointF) signal, which QETElementEditor's status bar
label subscribes to.
Implements the first pillar of #574: a "Shortcuts" preferences page letting
users rebind, search and reset every keyboard shortcut in the app.
What it does
- New ShortcutManager singleton: every one of the ~95 setShortcut()/
setShortcuts() call sites across qet.cpp, qetmainwindow.cpp,
elementspanelwidget.cpp, autonumberingdockwidget.cpp, richtexteditor.cpp,
qetdiagrameditor.cpp, qettemplateeditor.cpp and qetelementeditor.cpp now
calls registerAction(target, id, category, default_sequence) instead,
which applies the user's saved override (or the default) and remembers
the target for later editing.
- New ShortcutsConfigPage, added to the existing "Configurer QElectroTech"
dialog: a filterable table of every registered shortcut, grouped by
category, each with a QKeySequenceEdit and a per-row reset button, plus a
"reset all" button. Bindings are only persisted (via
ShortcutManager::setSequence()) when the dialog is accepted.
- Conflict detection: rows whose currently-edited sequence collides with
another row are highlighted with a tooltip naming the conflicting action.
- Overrides are stored under a "shortcuts/" QSettings group, one key per
id, keyed to match the id (not persisted at all when equal to the
hardcoded default), so a future QET version can safely raise a default
for anyone who never customized it.
Design notes
- Targets are handled generically via QObject rather than QAction, since one
call site (autonumberingdockwidget's "Configurer" button) is a
QPushButton, not a QAction. Both declare an identical "shortcut"
QKeySequence Q_PROPERTY, so registerAction() reads/writes it through the
property system instead of needing a separate code path.
- Several live targets can share one id at once -- QET allows multiple
windows of the same kind (diagram editor, element editor...) open
simultaneously, each constructing its own QAction with the same id.
setSequence() updates every live target for that id in one call, so a
rebind takes effect in all open windows immediately, without restart.
- A shortcut's description is captured from its target's text() the first
time that id is registered, then cached -- so the config page stays
correct even after the owning window is closed. One consequence: a
shortcut belonging to an on-demand window (element editor, title block
editor, rich text editor) only appears in the list once that window has
been opened at least once in the current session, since nothing has
registered its id yet otherwise.
Testing
Full CMake build (qmake CONFIG+=no_kf5, Qt 5.15) compiles clean with zero
errors and zero new warnings. Verified end-to-end in a real running session
(Xvfb + xdotool):
- The Shortcuts page appears in Configure QElectroTech with the right icon,
lists every always-registered shortcut with correct category/action name/
current binding.
- The filter box correctly narrows the list, and correctly returns nothing
for an action whose owning window hasn't been constructed yet this
session (confirming the on-demand-registration behavior above is working
as designed, not silently broken).
- Conflict detection correctly flagged a real pre-existing same-key overlap
between "Supprimer" (delete selection, Del) and "Supprimer ce folio"
(delete diagram from panel, Del) -- both highlighted with explanatory
tooltips.
- Rebound "Manuel en ligne" to Ctrl+Shift+M, clicked OK: persisted under
[shortcuts] in QElectroTech.conf, and the Aide menu's entry showed the new
binding immediately, no restart needed.
- Reopened the dialog: the rebind was still shown. Clicked its per-row
reset button, then OK: the settings key was removed entirely (not stored
as "F1"), correctly falling back to the hardcoded default.
Retrofitting the Tab/Shift+Tab, select-all (#585) and Ctrl+G jump-to-element
(#586) shortcuts through this registry is left for a follow-up once those
PRs land, to avoid re-merging still-open branches into this one.
Developed with assistance from Claude (Anthropic).