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.
- Remove dead #if QT_VERSION conditionals (both branches were identical)
- Add settings.remove() guard on restoreState() failure consistently
across all three editors (now safe since all run after show())
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.
m_jump_to_element was the only action in the tree that set its
QKeySequence directly instead of going through
ShortcutManager::registerAction() -- of 98 actions carrying a runtime
shortcut, 95 matched a registerAction() call, 2 were Qt built-ins, and
this was the sole exception (verified by dumping every QAction from a
running instance and cross-checking against a static scan of the
source; the only other setShortcut() call in the tree clears a
shortcut rather than setting one).
Bypassing the registry meant the binding didn't appear on the
Shortcuts preferences page (so it couldn't be discovered or rebound),
and checkConflicts() couldn't see it either, so assigning Ctrl+G to
another action there would silently collide at runtime instead of
being flagged.
Fixes#758.
- 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.
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.
QETDiagramEditor::openBackupFiles() deleted the just-constructed
QETProject when it failed to reach ProjectState::Ok, but had no
continue/else after the delete - so addProject(project) ran
unconditionally on the now-dangling pointer, and addProject()
immediately dereferences it (new ProjectView(project), etc.).
This matches the report exactly: clicking Cancel on the restore-files
dialog (which just deletes the stale markers directly, never calling
openBackupFiles()) works fine, while clicking OK crashes whenever any
listed backup fails to open cleanly. Because the crash happens mid-
loop, cleanup for that file (and any later ones in the same batch)
never completes, which also explains the reporter's second complaint
that the restore list kept growing across sessions.
Fix: add the missing `continue` so a failed project is skipped
instead of being passed use-after-free to addProject().
Verified: clean rebuild, only the intended object file recompiled
and linked successfully. I attempted a live repro by crafting a
malformed stale-file marker to force ProjectState != Ok and clicking
OK under Xvfb, but this local build links against real KDE Frameworks
(BUILD_WITH_KF5=ON, confirmed via CMakeCache.txt) rather than the
in-tree nokde/kautosavefile.cpp reimplementation I initially targeted,
which uses a different marker directory/naming scheme
(~/.local/share/stalefiles/<app>/ via real KF5::KAutoSaveFile) that
I wasn't able to reverse-engineer well enough in the time available
to produce a matching malformed marker. Confidence in the fix instead
rests on the code being an unambiguous, textbook use-after-free (this
exact object is deleted on the line immediately above the missing
continue) with a single-line, side-effect-free fix.
etc.), these three are ambiguous on the *slot* side:
activateProject(QETProject*)/activateProject(ProjectView*),
closeProject(ProjectView*)/closeProject(QETProject*), and
showError(const QETResult&)/showError(const QString&) each have two declarations on QETDiagramEditor. &QETDiagramEditor::activateProject
etc. alone won't compile with two candidates present; qOverload<T>() picks the one matching the actual signal's argument type, same as
the old SIGNAL()/SLOT() macro text did implicitly.
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>
removed in Qt6, replaced by mappedInt/mappedWidget/mappedString.
What was broken:
* the logo-conflict rename dialog
* the system tray show/hide toggle
* the Window menu
* export dialog's per-diagram preview controls
Switched to the modern mappedInt/mappedWidget signals with pointer-to-member connect(), guarded for Qt < 5.15 until Qt5 can be dropped.
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.
On macOS in particular there's currently no way to tell from the window
chrome alone whether the active project has unsaved changes. The main
window's title is set once in the constructor to a static string and
never updated afterward, and QET never sets Qt's windowModified
property anywhere -- so the native "document modified" indicator
(the dot in the close button on macOS; an asterisk in the title on
platforms that render it as text) never appears.
Add QETDiagramEditor::updateWindowModifiedState(), which sets the
window title to "<project>[*] - QElectroTech" (the "[*]" is Qt's own
placeholder convention for this) and calls setWindowModified() with
the active project's own modified flag. Call it from two places:
- subWindowActivated(), the single existing choke point already used
whenever the visible MDI tab changes, so switching projects
immediately reflects the newly active one's own state.
- A new per-project connection to QETProject::projectModified, added
in addProject() alongside the existing undo-stack registration,
filtered to only act when the modified project is the currently
active one.
With no project open, the title and modified flag both revert to the
original static, unmodified state.
Implements https://github.com/qelectrotech/qelectrotech-source-mirror/discussions/596
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.
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.
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).
Implements the third pillar of #574: a lightweight quick-open popup
for jumping straight to an element on the current diagram, rather
than scrolling/scanning visually.
New JumpToElementDialog (sources/ui/): a small QDialog with a filter
QLineEdit and a live-filtered QListWidget beneath it. Built from
every Element on the diagram, searchable against its label
(elementInformations().value("label")), type name (Element::name()),
and every other element information value, joined into one
lowercased search string per candidate. Up/Down move through the
filtered list, Enter selects the highlighted element on the diagram
(clearing the rest of the selection) and scrolls it into view via
ensureVisible(), Escape cancels without changing the current
selection. All three are handled via an event filter on the line
edit, so the user never has to leave the text field to navigate or
confirm.
Triggered by a new Ctrl+G action in QETDiagramEditor, added next to
the existing Ctrl+F "search and replace" action and to the Edit
menu. Confirmed free: not used anywhere in qetdiagrameditor.cpp or
qetmainwindow.cpp today.
Explicitly not a duplicate of the existing SearchAndReplace module
(also on this menu, via Ctrl+F): that's a bulk property search/replace
tool across whole diagrams; this is a single-item navigational
popup with no editing capability.
Verified end-to-end in a real running session (Xvfb + xdotool)
against a multi-transistor schematic: Ctrl+G opens the popup listing
every element; typing "Q16" live-filters down to the one match;
arrow keys move the highlighted row through the filtered list;
Enter selects the highlighted element (confirmed via the properties
panel showing its label) and closes the popup; Escape closes it
without changing the selection.
See discussion #574.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a ProjectUsageTracker (sources/project/) that accumulates how
long a project has been the active tab, using QElapsedTimer so the
value is computed on demand rather than via polling. It's hosted on
ProjectPropertiesHandler per that class's own stated design intent
("all new properties should be managed by this class").
The accumulated time is persisted as a new <usage time_spent="N"
enabled="true|false"/> element, a sibling of <properties> in the
project XML, written/read by new QETProject::writeUsageXml()/
readUsageXml(). It rides along on the existing autosave path for
free, since writeBackup() already serializes the full project via
toXml().
QETDiagramEditor::subWindowActivated() now pauses every open
project's tracker except the one whose tab just became current, so
switching between several open projects keeps each project's tracked
time isolated.
Surfaced in the existing Project Properties "Général" page: a
"Temps passé sur ce projet" display, a "Réinitialiser" button, and
an opt-out checkbox ("uniquement enregistré localement dans ce
fichier" - this is local-only, never transmitted anywhere).
Verified beyond compiling: full CMake build, then an actual runtime
session confirming the saved XML's time_spent value, that closing
and reopening the project round-trips and resumes timing, that the
reset button works, and - the key correctness check - that with two
projects open, the inactive one's time_spent stays frozen while the
active one accumulates real elapsed time, over the same interval.
See discussion #576.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>