A project can now keep the texts drawn in its symbols, and the names of
their terminals, horizontal when a symbol is turned: Project properties >
General, "Garder horizontaux les textes des symboles pivotés". The box of
each text turns with the symbol; the text does not, and reads as it does
in the symbol itself.
It is a project setting, saved as <symbol_texts upright="true"/> and
only when on. A new project starts with it on; a project saved without
it (every existing one) reads with it off, so it looks and saves exactly
as before, and looks the same on every computer. The MCP server's new
projects start with it on too.
It builds on the mirror of #1354, which already redraws the texts of a
mirrored symbol readable: what the symbol does to its texts is now its
mirrors and, with the setting on, its turn (Element::symbolTextsTransform()).
ElementPictureFactory caches one drawing per such transform, terminal
names undo it the same way, and the DXF export places the texts alike.
The fields of a symbol (label, comment...) already keep their angle with
"Garder la rotation visuelle" and are left as they are.
Known limit: two texts stacked in a symbol end up side by side when it is
turned, and can overlap when kept horizontal; the setting can be turned
off for such a project.
Test: tst_uprightsymboltexts turns the symbols of a folio and checks the
angle of each motor's "M" in the DXF, with the setting off and on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found by review of #1354:
- Terminal names (shown on the folio) read backwards on a mirrored
symbol: Terminal::paint() now mirrors them back about the centre of
their box, in the frame they are drawn in.
- "Reload the drawings of the symbols" left mirrored symbols with the old
drawing: reloadPicture() refreshes the mirrored pictures too.
- Turning a group of texts on a mirrored symbol left the group's
readable mirror about the centre of its old box until the next layout:
ElementTextItemGroup::setRotation() reapplies it, as texts already do.
- The mirrored build of a symbol's drawing leaked the texts of its
primitives, which only the drawing without mirror keeps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit > "Miroir horizontal" (M) and "Miroir vertical" (F), also in the
folio's right-click menu, mirror the selected symbols in place. The keys
are the ones the element editor uses for the same two actions.
An element keeps two mirrors about its own axes, applied before its
rotation, and saves them as mirror="horizontal|vertical|both" on its
<element> (written only when set, so other projects save byte for byte
as before). On a symbol turned by 90 or 270 degrees, a mirror of the
folio is the other mirror of the symbol itself, so the rotation never
changes: a label kept upright does not swing round, and "Pivoter" still
turns a mirrored symbol clockwise.
- Terminals face the mirrored way (Terminal::orientation()), so wires
follow.
- The symbol stays where it was: its centre is kept, on the grid, since
the hotspot is often a corner.
- Texts read normally. The element's texts, text groups and cross
reference are mirrored a second time about the centre of their own box
(Element::keepReadable()), and ElementPictureFactory draws the texts
of the symbol itself the same way, in a cached picture per mirror.
Groups and cross references held at the bottom of the folio stay
centred under their element.
- DXF export mirrors the symbol's lines, arcs and texts.
- Scripting: qet.mirrorElement(folio, uuid, vertical) and
qet.elementMirror(folio, uuid); live mode may run both menu commands;
the MCP server gets a mirror_element op and qet_diff reports mirrors.
Not done: the parts of a PLC table drawn at run time
(Element::drawPlcTable()) are not kept readable on a mirrored PLC; the
project database has no column for it, as it has none for the rotation.
Test: tst_scriptmirror mirrors a symbol through --run, checks every
terminal's side and facing, the round trip, undo, a save and reload,
and a symbol turned by 90 degrees.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MSVC stopped with C2280 at liveserver.cpp:861 (issue #1333): it copies
the result of the ?: expression, and QPrinter cannot be copied. GCC and
Clang build the object in place under C++17, so Linux and CI built.
QPrinter(info) gives the same printer in both cases: info is only set
when printing to a printer, and QPrinter() itself constructs with an
empty QPrinterInfo (qprinter.cpp, QPrinter::QPrinter(PrinterMode)).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the terminal strip manager, after a free terminal was moved into a
strip, clicking another free terminal showed nothing on the right. It
only came back after selecting a strip first.
The tree remembers whether a free terminal is selected and only tells
the window to update when that, or the selected strip, changes. Moving
a terminal rebuilds the tree, which clears the selection and shows the
empty page, but the "free terminal selected" flag stayed true. The next
click on a free terminal then looked like no change and was ignored.
The flag is now reset when the selection is cleared, and updated on
every change it takes part in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
layoutLines() trimmed the trailing spaces of every line and kept
non-breaking spaces, which the DXF export used to write as plain spaces
(toPlainText()). A DXF R10 file has no code page, so a reader assuming
Windows-1252 could show them as 'Â '. Now a paragraph that does not
wrap gives exactly its line of toPlainText().split('\n') again
(trailing spaces and tabs kept, non-breaking spaces as plain spaces,
<br> as a line break). In a paragraph that wraps, every line loses its
trailing spaces, so spaces that wrap to a line of their own give an
empty line: not written, but it keeps the spacing of the next lines.
Test: tst_textlines compares with toPlainText() for trailing spaces,
tabs, non-breaking spaces, <br> and paragraphs, and checks wrapped
lines (fails before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- setTextWidth() returns without a signal when the width does not
change, which is also the case on every load of a text without one.
- Say why IndependentTextItem::textWidth()/setTextWidth() hide the
non-virtual QGraphicsTextItem functions, and what that means for code
holding a base pointer.
- The fixture no longer carries a /tmp path from the machine it was
made on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The independent_text table had the text and rotation of a free text but
not its width, and a width change did not update the row at all -- not
even its width and height, which follow the wrapped text. Add a
text_width column (NULL for the automatic width) and update the row on
textWidthChanged.
Test: tst_scriptfreetextwidth reads the table with qet.query() (fails
before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A scene drops the event filters of an item that leaves it. The handles
installed theirs once, in the constructor, and only if the text was in
a scene: after the text was removed and added back (undoing a delete
does that), or when the handles were made before the text was in a
scene, they no longer reacted, and a drag moved the text instead.
TextResizeHandles now installs the filters again whenever it enters a
scene.
Tests: dragAfterLeavingTheScene, dragWhenCreatedOutsideAScene (both
fail before).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the element editor, changing the width of a right-aligned or centred
dynamic text field (with the width box) moved its aligned edge: the
field always grew or shrank from its left edge, so a right-aligned
field's right edge moved. PartDynamicTextField::setTextWidth() now
keeps the point chosen by the alignment in place, with the
prepareAlignment()/finishAlignment() pair setPlainText() and setFont()
already use. Not while the field is read from the .elmt, whose saved
position is the one to keep.
No file format change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
updateForm() filled the box with setValue(textWidth()), and setValue(int)
truncates: a field 61.7 px wide showed 61 while editingFinished compared
with qRound() = 62, so leaving the box still set the width to 61 and
pushed an undo step. Round in both places.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Shortcuts page now shows:
- each command's icon, and a Menu column saying where it is
("Projet › Scripts"); the text filter matches that too
- a category filter beside the existing Bound/Unbound/Conflicts one
- "Rechercher par touche": press a key combination and only the
commands using it are listed, however the key is spelled in the
user's language. A key also finds the two-key shortcuts it starts.
- "Copier la liste": the shown rows go on the clipboard as
tab-separated text with a header, so they paste into a spreadsheet
one cell per column
ShortcutManager::ShortcutInfo carries the icon and the first live
QAction so the page needs no second registry.
New test tst_shortcutspage covers the menu path, the columns, both
filters and the copied text.
Second stage of discussion #1319.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A symbol text with a width (text_width) is drawn wrapped on the folio,
but the DXF export split texts on '\n' only, so it was written as one
long line, running past its box and over the drawing. The export now
writes the lines of folio texts as they are laid out
(TextLines::layoutLines(), new header-only textlines.h). Texts without
a width give the same lines as before.
Tests: tst_textlines, tst_dxfwrappedtext (fails without this change).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The width box of a dynamic text field in the element editor showed -1
for the automatic width and stopped at 500 px. It now shows "Auto" and
"px", and goes up to 10000 px, so a wider field is not cut to 500 just
by being shown.
Leaving the box without changing it no longer pushes a change either:
the box holds whole pixels, while a width fitted to the text (or set
elsewhere) can have decimals, and the comparison made the field's
width round itself on every focus loss.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TextResizeHandles puts four handles at the corners of a selected text
(any QGraphicsTextItem with a qreal "textWidth" property, -1 = automatic
width). Dragging one changes the width: the opposite corner stays in
place in scene coordinates, for any rotation and rotation point, the
text wraps and the height follows it. Vertical mouse movements are
ignored, the width never goes below the longest word and is rounded to
whole pixels, and a click without a move changes nothing.
Double-clicking a handle goes back to the automatic width.
The handles are a child of the text, so they follow it when it moves,
rotates or is zoomed (as for the symbol text handles, #1002). The width
is changed live; the owner makes it undoable with TextResizeCommand,
which sets the width then the position on both undo and redo -- a text
may move itself when its width changes, so two property commands would
restore them in the wrong order.
The geometry is header-only (textresize.h). Nothing uses the handles
yet: the follow-up changes add them to free texts and to text fields in
the element editor.
Test: tst_textresizehandles, on a plain QGraphicsTextItem.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A free text (IndependentTextItem) gets a textWidth property: -1 is the
automatic width (one line, or lines broken by hand, as today); a
positive width makes the text wrap between words. Its top-left corner
stays in place. The property is what a spin box and resize handles will
set in follow-up changes.
File format: the width is saved as text_width on <input>, only when one
is set, so a project without widths is saved byte-identical
(tst_resaveunchanged). An older QElectroTech ignores the attribute and
shows the text on one line, with no load error.
DiagramTextItem::setHtml() no longer widens a centred or right-aligned
rich text to idealWidth() + 40 when the text has a width of its own;
with the automatic width it still does.
Test: tst_freetextwidth (--resave keeps the width only where set,
--export-svg draws the text wrapped).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- replaceImage(), mirror() and setTransparentColor() no longer add a
dummy QUndoCommand child: the imageSource child already keeps
QPropertyUndoCommand::mergeWith() from merging two of them.
- The explanation of the position and pivot maths moves from crop() to
applyCrop(), where that code now lives; the stale older doc block of
crop() goes.
- applyCrop() returns false when nothing was cropped, and
qet.cropImage() passes that on. tst_imagecropundo checks it for the
current crop, an empty rectangle and one outside the picture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
44 commands in the folio editor's menus and toolbars had no
ShortcutManager id, so they could not be given a key, found by the
command search or put on the shortcut bar: Export to PDF, the grid and
guide toggles, Selection/View mode, most of the Project menu, Tile and
Cascade, the Help menu items, and others. Register each with no default
key, as the add-item tools already are.
What's This keeps the Shift+F1 that Qt gives it. No other command
had a key before, so nothing changes for anyone who has not set one.
Left out: the option lists inside submenus (text grid step, the
Show menu's item kinds, background colours). They are choices, not
commands.
First stage of discussion #1319.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since #849 the parts list has a row for every contact block (slave) and
terminal-type element. Users who draw the dots and bends where wires
branch as terminal-type symbols (114_connections) get one empty row per
junction: 109 of 151 rows on one reported cabinet.
Two options, both off by default so existing exports are unchanged:
- leave out the contact blocks: --no-slaves, "no_slaves" in qet_export,
or uncheck the new "Contacts esclaves" element type in the dialog;
- leave out the junctions: terminal-type elements with no label,
designation, manufacturer or manufacturer reference. --no-junctions,
"no_junctions", or "Laisser de côté les jonctions" in the dialog.
A terminal block with a label or a part number stays.
The element type filter had no box for slaves, so every query it built
left them out: the export dialog never listed contact blocks, although
the command line has since #849. The dialog now checks the new box by
default and gives the same rows as --export-bom. The box is unchecked by
default elsewhere, so nomenclature tables keep their rows.
docs/smart-device-bom.md still said slaves and terminals were excluded;
corrected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rewriting the links copied the old trailer up to %%EOF, so the old
startxref stayed in the file before the new one. Its offset is the size
the file had before the rewrite, and the links carry the output path,
so the same project exported to two folders gave different bytes, and
a different document id. Both rewrites now copy the trailer dictionary
only, through one helper; the print window's rewrite no longer leaves a
stray %%EOF either.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qet.setHouseStyle() only saved the setting, so an assistant reading
qet-assistant.json saw the old text until QElectroTech restarted.
AssistantInfo::refresh() writes the file again, but only in a
QElectroTech that writes it at all: a headless --run would otherwise
replace the open QElectroTech's live channel with its own empty one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
QetSettings::houseStyle()/setHouseStyle() (assistant/house_style),
qet.houseStyle()/qet.setHouseStyle() in the script API, a house_style
field in qet-assistant.json, and qet_about in the qet MCP server
showing it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At the printer's own resolution (600-1200 dpi) symbols and texts were
replayed many times too big next to the wires and grid. Seen on a test
print to PDF; the same page now matches qet_export's PDF.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
QElectroTech (LiveServer):
- print: folios to the default (or a named) printer with no print dialog,
always after asking the user (paper cannot be undone); or to a new PDF
file. Rendered as the PDF export draws a folio (no grid, no terminals).
- save_project (own file, or save as a new file, never over one) and
close_project (only without unsaved changes: never discards work).
- changes: the undo history, each step marked assistant or user, with
"since" to ask what changed after a point.
- snapshot: a copy of the open project as on screen, for a check to read,
without saving it or changing its file.
- qet.elementTextGeometry(): where a symbol's text is drawn on the folio.
qet MCP server:
- qet_live_print, qet_live_save_project, qet_live_close_project,
qet_live_changes, qet_live_layout_check (snapshot + qet_layout_check, no
"run this script?" window); write paths never overwrite.
- Layout rules from the house style, for file and live checks alike:
label_on_wire, four_way_junction, misaligned_branch.
- The element index is saved under ~/.cache/qet-mcp: a new server process
loads 8839 symbols in 0.4 s instead of parsing them for 6.9 s; the
change check is a third faster.
- qet_about lists the installation's standard symbols
(standard-symbols.json in QElectroTech's data folder) when there are any.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
openAndAddProject() asked "Créer une copie de sauvegarde ?" even when
called non-interactively, so open_project waited for an answer from the
user in front of the window. Only live mode opens projects that way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live mode asked at every start (the "Mode direct" warning) and forgot
"always" for scripts when QElectroTech closed, so a user who works
with an assistant every day re-enabled it every day. Both are now
opt-in and remembered: the warning has a "Ne plus demander au
démarrage" checkbox (scripting/live_skip_warning), and the ask-first
choice is saved (scripting/live_ask_first; the Assistant panel's
checkbox and "Toujours" in the script window). The status bar still
shows live mode whenever it is open. Switching live mode off in the
settings clears "don't ask again", so turning it back on warns again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
open_project opens a .qet and makes it current, as File > Open does but
with no file dialog and no error windows: a missing, unreadable or
foreign file is refused in the answer. A project already open is only
made current. switch_project makes another open project current, by its
index or its file. status now lists every open project (index, title,
file, folios, unsaved changes, current), so the assistant sees what it
can switch to. MCP: qet_live_open_project, qet_live_switch_project; both
paths go through the workspace policy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An assistant in live mode could only work on the project already open.
new_project makes one as File > New does -- this installation's
new-folio defaults, optional title and number of folios -- and makes it
the current project, so the next request works on it. An optional
absolute path saves it at once, never over an existing file: QElectroTech
refuses, and the MCP server refuses before asking (a new never-overwrite
class in its path policy, so an "overwrite" flag cannot get round it).
Pages and links across pages already worked through scripts
(qet.addFolio, setFolioTitle, linkElements across folios); the tool's
description now says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A request with "timing": true gets queue, confirm and exec times in its
answer; with QET_LIVE_PERF_LOG set, QElectroTech also appends one JSON
line per request, with the time until the folio is next painted. The
qet MCP server logs its side of each live call to the same file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stacking added in #1287 tested the crosses' bounding rects, so
crosses whose frames or empty margins touched were raised although no
text was hidden (the list display case found in review). The texts each
cross really draws are now recorded while its geometry is built, and a
cross is raised only when one of its texts would be drawn over a text of
another one.
A new "Empiler les références dont les textes se chevauchent" checkbox
in the cross reference properties, per type, turns the stacking off;
the crosses of that type are then only centred, as before #1287. It is
saved in the project (xref attribute "stackoverlapping") and in the
settings, and is on when absent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Crop a picture as the crop tool does, in one undo step, and read its
crop rectangle back. tst_imagecropundo uses them on the real binary:
a crop that was undone is not saved, and redoing it saves it again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>