DiagramImageItem::toXml() PNG-encoded every picture on every save,
autosave and copy, whether or not it had changed. That is most of the
cost of pictures in a project: resaving one holding 60 of them took
6.2 s and now takes 3.8 s.
The PNG bytes are now kept and reused while QPixmap::cacheKey() still
matches, so any edit (replace, crop, mirror, transparency) re-encodes
without each of those functions having to invalidate anything. On load
the cache is filled with the file's own bytes, so the first save
encodes nothing either.
Output is byte-identical to before on the example projects. A picture
whose PNG came from another encoder now keeps its original bytes
instead of being re-encoded; the pixels are identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
A picture can now carry a caption, set from its properties panel
("Libellé"). It is drawn centred under the picture at the folio's
normal text size whatever the picture's scale, turns with it, and
moves, copies and prints with it because the picture itself paints it.
Clicking the caption selects the picture.
Saved as a "label" attribute on <image>, written only when non-empty:
a project without labels saves byte-for-byte as before, and older
versions open a labelled project and simply ignore the caption.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Since #1046, QETSql::execReadOnly() runs a query with PRAGMA query_only
set and switches it off before returning. Switching it off aborts a
statement SQLite is still stepping through ("abort due to ROLLBACK"), and
QSQLITE has already stepped to the first row by then. A query that
produces its rows as it goes -- a UNION ALL without ORDER BY -- therefore
came back with its first row only and no error. A sorted query was not
affected, because SQLite has read every row before returning the first.
Every query from the SQL box of a table, a saved <graphics_table> query
and the scripting qet.query() goes through here.
The checked run is now finished before query_only is switched off, and a
query that passed is run again for the caller. SQLite refuses a write at
its first step, so passing that step is what proves a statement reads
only; the second run is of a statement already shown to be read-only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Ajouter une ligne, un rectangle, une ellipse... had no ShortcutManager
id, so the command search could not list them. Give each one an id with
no default key; they can also be bound in the Shortcuts page now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ctrl+Shift+M (Édition → "Rechercher une commande…") opens a small
search box at the cursor listing every command of the diagram editor
window, as SolidWorks' "Search Commands" and the command palette of
many editors do. Typing narrows it, best match first: name starting
with the text, then a word starting with it, then containing it.
Matching ignores case, accents and mnemonic "&", so "editer" finds
"Éditer l'item sélectionné". Each row shows the command's key when it
has one, which also teaches the keys. Disabled commands are listed,
greyed, and cannot be run. Enter runs the highlighted one after
closing the box; Esc closes.
The list is ShortcutManager's registry, restricted to the actions this
window owns (ShortcutManager::action(id, owner)), so a second editor
window's commands never appear and nothing has to be listed by hand.
Ctrl+Shift+P, the usual key for this, is already the autonumbering
dock's.
tst_commandsearch covers the folding, the ranking, that another
window's commands are left out and that a disabled command does not
run; both behaviours were checked to fail the test when broken.
Discussion #1033.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
BorderTitleBlock::draw() leaves the row header's room before the first
column even when the row header is hidden; insideBorderRect() does not,
so the top ruler was one header width out of line on such a folio. Take
the first cell's position from the header sizes, as draw() does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Affichage › "Afficher les limites des cases" draws a faint dashed line
at every column and row limit of the folio border, so that zoomed into
the middle of a folio you can see where a cell ends, not only which one
the headers name. Off by default, remembered once set
(diagrameditor/cell_lines).
Drawn by DiagramView::drawBackground(), under every item and only in
the view: printing and export render the Diagram, never the view, so
they cannot pick the lines up. PaletteGraphicsView gains scenePainter()
so a subclass can draw there on the inverted (dark palette) path too.
The lines follow the border's own cell positions, and each direction is
hidden when the folio hides that header.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Windows 11 style gives QPalette::Button a translucent colour
(#FFFFFFB3). The rulers filled their background with it, and since they
paint with WA_OpaquePaintEvent nothing clears them first: every zoom
step blended the new labels over the old ones, leaving a fading trail.
Fill with the button colour composed over the window colour instead,
which is always opaque.
Each ruler is now also hidden while the folio's own column (or row)
header is wholly in sight, so zoomed out only the folio's headers show,
not both. When a ruler comes or goes the drawing keeps its place on
screen: the ruler covers or uncovers the edge of the viewport.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since #983, projectDataBase::newQuery() checked a query with
sqlite3_prepare_v2() and sqlite3_stmt_readonly() on the handle of the
QSQLITE driver. Those calls go to the libsqlite3 QElectroTech links. The
QSQLITE plugin of the Qt online installer does not use that library: it
carries its own copy of SQLite, so the handle belongs to another library
and the call crashes. #1021 then put newQuery() on every element
selection, which is where #1045 hits it.
The check now runs the query with PRAGMA query_only set, through the
driver. SQLite refuses a write itself, before touching a row, so the CTE
prefix #983 closed ("WITH x AS (SELECT 1) DELETE FROM element") stays
closed. A refused or failed query comes back empty, because several
callers call exec() again on what newQuery() returns, after query_only
is off.
QElectroTech no longer calls the SQLite C API anywhere.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Info.plist template shipped LSMinimumSystemVersion=12.3.0, but
the CMake build embedded the host SDK's deployment target (minos
26.0 on recent build machines) into the binary's LC_BUILD_VERSION.
Launch Services relies on the binary's minos, not the plist, so it
refused to launch the app on macOS < 26 even though it ran fine
when invoked directly.
- Set CMAKE_OSX_DEPLOYMENT_TARGET=14.0 explicitly in the cmake
configure step, matching the real minimum imposed by the
Homebrew Qt6 toolchain used for this build.
- Sync misc/Info.plist's LSMinimumSystemVersion to 14.0.0, and have
MacQetDeploy_arm64_cmake.sh set it via PlistBuddy at bundle
install time so it can't drift from the build target again.
Reported-by: guillaume.ruivo (forum)
DiagramEventAddElement is already a good placement mode: the element
follows the cursor on the grid, a left click drops it, Space rotates it,
and it stays loaded for a run of the same symbol. Its only caller was
DiagramView::handleElementDrop(), so it could be reached only by
finishing a drag. Double-clicking a symbol in the Collections dock
opened the element editor instead.
- DiagramView::startElementPlacement() is split out of
handleElementDrop(). defaultPlacementPos() uses the cursor when it is
over the view and the centre of the visible area otherwise.
- ElementsCollectionWidget emits insertElementRequested() on double
click, or Enter on the highlighted item. The host decides which view
receives it, so the widget can later be reused outside the editor.
- When there is nowhere to place it (no folio open, read-only project),
the element editor opens, as a double click did before.
- "Insérer le dernier élément" (Édition menu, default key A) places the
last element again. DiagramView reports every placement it starts, so
an element dropped by drag counts too. Macros are not remembered.
A rather than Space: Space rotates the pending element inside placement
mode and is bound three more times in this editor. The key is a
ShortcutManager default and can be changed in the Shortcuts page.
Double click placing is a behaviour change, so it has a preference,
"elementscollection/double-click-inserts" (default true), shown in
Configuration as an opt-out: "Double-cliquer dans la collection ouvre
l'éditeur d'élément au lieu de l'insérer".
Discussions #676 and #1033.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Right-clicking an empty folio offered Paste here, Folio properties and
four row/column actions. Four of six entries change the folio's layout,
which is rarely wanted and easy to hit by mistake, while nothing in the
menu helps draw.
The empty-folio menu now holds the "Ajouter" submenu (text, image,
shapes, terminal strip plan -- the same actions as the Edition menu and
toolbar), then Folio properties, then the row and column actions in a
"Lignes et colonnes" submenu. The selection menu is unchanged.
A submenu whose actions are all disabled is dropped, the same way
disabled actions already are.
Discussion #1033.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
ElementTextsMover::endMovement() returned early when the text had not
moved without clearing m_movement_running, so every later
beginMovement() on that folio was refused. The dragged text still
moves, because it positions itself, but the mover no longer tracks the
drag and builds no undo step for it.
To reproduce: Shift+drag one element text a couple of pixels so it
snaps back where it was, then Shift+drag another text of the element
and press Ctrl+Z. The second text stays where it was dropped; with this
change it goes back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
ElementTextsMover::endMovement() returned early when the text had not
moved without clearing m_movement_running, so every later
beginMovement() on that folio was refused. The dragged text still
moves, because it positions itself, but the mover no longer tracks the
drag and builds no undo step for it.
To reproduce: Shift+drag one element text a couple of pixels so it
snaps back where it was, then Shift+drag another text of the element
and press Ctrl+Z. The second text stays where it was dropped; with this
change it goes back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Drop 1:2.5: on a grid of 10 it steps by 4, which misses 10, so texts on
two elements 10 apart could never line up. Every remaining divisor is a
whole number, which tst_textgrid checks.
The divisor list and the snapping arithmetic move to the header-only
textgrid.h so the toolbar menu, the preferences page and the test share
them. The preferences page gets the same choice under Grille + Clavier;
QETApp::textGridChanged keeps every editor's toolbar button in step.
While element texts are dragged, the status bar names the text grid and
says to release Shift and hold Ctrl for free placement -- Ctrl+Shift
together is the pan shortcut.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Texts snapped to the folio grid, so the first drag of an off-grid label
pulled it sideways by up to half a grid step (discussion #1020). Texts
now snap to a fraction of the folio grid, chosen from a "Textes 1:N"
button in the View toolbar and a menu under Affichage: Off, 1:1, 1:2,
1:2.5, 1:5, 1:10. Because the step divides the folio grid, texts on
different elements still line up. Ctrl still places a text freely.
1:1 is the default and matches the previous behaviour. The setting is
stored in QSettings; nothing changes in saved projects.
All five text movers switch together: element texts, text groups,
texts moved with a selection, conductor texts and independent texts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
The popup now also takes a folio position before the cell, the way
cross references and folio reports write it by default (%f-%l%c): 3-B13
offers "Folio 3 (<title>), case B13", and Enter shows that folio and
zooms onto the cell. Case and a leading "p" are ignored, and the
separator is optional: 3-b13, 3b13, P3B13 and p3-b13 all work. A plain
B13 still means the current folio, and P3 alone is still row P,
column 3: a folio always needs a cell after it. A folio position past
the last folio, or a cell outside that folio's border, offers nothing.
The zoom on another folio is queued, so it runs once the tab switch
has been handled. Checked on the ATS example: jumping to 1-C5 from
folio 3 gives the same view, pixel for pixel, as C5 typed on folio 1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Affichage > "Garder les en-têtes visibles" adds a bar along the top and
the left of the diagram view that repeats the folio's column numbers and
row letters, aligned with the cells at any zoom, so they stay in sight
when the folio's own headers are scrolled away. Off by default; the
choice is stored as diagrameditor/cell_rulers.
The bars are CellRuler widgets in the view's margins
(setViewportMargins), not scene items, so printing and PDF/PNG/DXF
export never see them, and they paint with the application palette
outside of the dark-palette inversion. They keep a constant thickness;
when cells get narrower than their labels, only every 2nd, 5th, 10th...
label is written. A bar is hidden when the folio hides that header.
Showing or hiding them keeps the centre of the view where it was.
The labels come from BorderCellLabels, now also used by
BorderTitleBlock::draw(), so the bars and the border cannot disagree.
PNG export of all 133 folios of the examples is pixel-identical to
master, with border-columns_0 true and false.
Known limits: changing border-columns_0 repaints the bars at the next
scroll or zoom; the menu toggle updates the views of its own editor
window only, like the grid toggle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Typing a cell reference into the "Atteindre un élément" popup now offers
"Case B13"; Enter zooms the view onto that cell with one cell of margin.
The cell is read the way the border labels it: row letter(s) then column
number, honouring the "columns start at 0" setting and multi-letter rows
(AA, AB...). Cells outside the folio are not offered.
BorderTitleBlock::cellRect() is the reverse of convertPosition(); a
round trip over every cell of a 30x23 folio, under both column-numbering
settings, returned the same cell for all 1380.
When an element is labelled exactly like the cell (K1), the element stays
first so Enter keeps its old meaning; the cell is listed after it.
DiagramView::zoomToRect() re-centres from a queued call: zooming in makes
the scroll bars appear, and the viewport resize that follows is anchored
under the mouse (setResizeAnchor(AnchorUnderMouse)), which otherwise
scrolls the view away from the cell straight after the zoom.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
Windows reads the device through hid.dll and SetupAPI with ctypes, as
hidapi's Windows backend does: shared, so 3DxWare can keep running, and
dropping the 0 report ID Windows adds, as hidapi does, so the bytes are
the ones QET decodes. Windows does not give out the report descriptor,
so a Windows recording has none; tst_spacemousehid then uses its
fallback layout.
The docstring, printed in full by --help, now has step-by-step
instructions for Linux, macOS and Windows, download included.
Tested: Windows Python 3.12 (embeddable) under Wine 10, against the
virtual uhid 3D mouse: --list finds it, a push sent during `right`
lands in `right`, and a button press and release land in `buttons`,
with the same bytes the Linux path records for the same push. Linux
and the mocked macOS path still pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The recorder only read /dev/hidraw, so it could not run on a Mac. It now
also reads through IOKit via ctypes, using the calls hidapi's mac backend
makes: a shared open (as #1028 does) and the input report callback.
hidapi passes those bytes through unchanged, so a recording is exactly
what QET decodes on macOS. Standard library only, and no sudo.
If 3DxWare holds the device, or Input Monitoring is not granted, it says
which instead of recording nothing. The JSON adds "backend" and
"other_readers" (any 3DxWare or spacenavd process that was running).
Reports that queue up while it waits for Enter are now dropped, so each
step holds only its own movement. The Linux path is otherwise unchanged.
Tested: the Linux path against the virtual uhid device
(tools/hid-capture/fake-spacemouse.py in the docker harness). A stale
report was dropped and the step's own push was kept. The macOS path has
only been run against a mocked IOKit, not on a Mac.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SpacePilot PRO recording on #599 has a weak `right` step: pushed
too lightly to read above the cap's normal cross-axis noise. A longer
window makes it easier to hold a firm push and gives the decoder more
samples to average over.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>