Compare commits

..

224 Commits

Author SHA1 Message Date
Laurent Trinques 1331ed6677 Merge pull request #1079 from ispyisail/feat/1071-dxf-layers
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m1s
Put the DXF export on named layers
2026-09-27 20:25:43 +02:00
ispyisail ae400d4da6 Merge master into feat/1071-dxf-layers 2026-09-28 07:22:42 +13:00
Laurent Trinques 7fdd4417a4 Merge pull request #1074 from ispyisail/feat/1070-item-groups
Add Group and Ungroup for items on a folio
2026-09-27 20:15:37 +02:00
Laurent Trinques 6637127910 Merge branch 'master' into feat/1070-item-groups 2026-09-27 20:15:27 +02:00
Laurent Trinques 38d7ecd67a Merge pull request #1084 from ispyisail/fix/1082-text-rotation-dialog
Fix #1082: text angle dialog opens at 0, unclear shortcut-bar tooltip
2026-09-27 20:10:13 +02:00
Laurent Trinques 27b60ec30e Merge pull request #1077 from ispyisail/fix/nomenclature-old-type-names
Fix old parts lists losing rows and tables when a project opens
2026-09-27 19:55:55 +02:00
Laurent Trinques 4bd5a6e5ac Merge pull request #1078 from ispyisail/feat/1072-export-dxf-cli
Add DXF export from the command line (--export-dxf)
2026-09-27 19:53:23 +02:00
Laurent Trinques 58cf62c782 Merge pull request #1083 from ispyisail/feature/search-tree-option
Add a setting to show collection search results as a tree
2026-09-27 19:47:54 +02:00
ispyisail 83bb7e14e0 Add the status tip to the S shortcut bar's tooltips too
The previous commit changed only the bar that appears beside the
cursor after a click. The S shortcut bar builds its own tooltips, so
its "Orienter les textes" button still said only that. Its tooltips
now carry the status tip on a second line as well, after the keyboard
shortcut.

Checked in the GUI: the S bar's second button shows "Orienter les
textes (Ctrl+Space)" over "Pivote les textes sélectionnés à un angle
précis".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 06:23:33 +13:00
ispyisail 239065af7d Open the text orientation dialog at the texts' current angle
The "Orienter les textes" dialog always opened at 0, so turning a text
at 90 degrees by a little meant typing the angle in again. It now opens
at the angle every selected text and text group shares, and at 0 as
before when they differ.

The shortcut bar showed only a command's short name as its tooltip,
which for this one ("Choose texts orientation") does not say what it
does. It now adds the command's status tip on a second line: "Rotate
selected texts to a specific angle".

Checked in the GUI on a folio holding one text at 90 degrees: master
opens the dialog at 0.00, this at 90.00.

Fixes #1082

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 06:16:17 +13:00
ispyisail adcf11270c Upgrade old element type names in saved query presets too
A query saved to nomenclature.json (Load/Save in the query editor, since
2020) before June 2022 still says element_type = 'Simple'. Loading it
ticked none of the type boxes and kept the old query, so the new table
came out empty. Pass it through LegacyElementTypes::upgradeQuery() as
ProjectDBModel::fromXml() now does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 05:55:20 +13:00
ispyisail 539708da3c Merge remote-tracking branch 'upstream/master' into fix/nomenclature-old-type-names
# Conflicts:
#	tests/qttest/CMakeLists.txt
2026-09-28 05:52:31 +13:00
ispyisail f62a44827b Add a setting to show collection search results as a tree
#1051 replaced the filtered tree with a ranked flat list. A new checkbox
in Settings > General, "Afficher les résultats de recherche sous forme de
liste triée" (elementscollection/search-flat-list, default on), keeps the
ranked list; unticked restores the pre-#1051 filtered tree search.

The Insert picker and shortcut bar call rankedSearch() directly and are
unaffected. Down/Enter from the search field only apply to the flat list.

Requested by scorpio810 on #1051.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-28 05:43:11 +13:00
ispyisail b350a2dbaa Merge branch 'master' into feat/1072-export-dxf-cli
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 05:42:44 +13:00
ispyisail e1152ba570 Merge master to resolve conflicts with snap-to-grid (#1073)
Both branches added an undo command, an include and a test target at
the same spots; kept both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 05:40:05 +13:00
ispyisail 8184ee1a18 Merge branch 'master' into feat/1071-dxf-layers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 05:38:31 +13:00
Laurent Trinques d56e99702c Update EN translations files 2026-09-27 16:51:51 +02:00
Laurent Trinques 08325061fd Flatpak add Hidapi support for 3DMouse 2026-09-27 13:17:37 +02:00
Laurent Trinques e9610d3f96 Update translations files 2026-09-27 12:47:18 +02:00
Laurent Trinques d5027eadbb Merge pull request #1076 from ispyisail/fix/dxf-titleblock-linebreak
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m25s
Fix title blocks with a line break making the DXF unreadable
2026-09-27 12:03:02 +02:00
Laurent Trinques da09206826 Merge pull request #1075 from ispyisail/fix/dxf-entity-order
Fix DXF export writing entities in a different order every time
2026-09-27 12:00:33 +02:00
Laurent Trinques 9812166071 Merge pull request #1073 from ispyisail/feat/1069-snap-to-grid
Add "Snap to grid" for selected symbols, pictures and texts
2026-09-27 11:55:49 +02:00
ispyisail a399923575 DXF export: keep the wires' colours
Every wire was written in the default colour, whatever its colour on the
folio: 723 of the 3,190 wires in the example projects (23 %, in 10
projects) lost theirs. The wire outline now takes the wire's colour, and
its number the wire's text colour, mapped to the nearest DXF colour as
the free texts already are. Black still comes out as BYLAYER.

A two-colour wire gets its main colour, and dashed wires (46 in the
examples) stay continuous: DXF line types are not written yet.

Checked on the 24 example projects, 133 folios: exactly 723 wire
outlines now carry a colour of their own, the only change from the
previous commit; ezdxf reads all files with no audit errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-27 21:54:44 +13:00
ispyisail 1d980b164f DXF export: colour BYLAYER where QElectroTech sets no colour (#1071)
Most entities were written with colour 0, BYBLOCK. Outside a block that
falls back to the default colour, so it shows black on white or white on
black, and no layer colour could change it: recolouring QET_WIRES in a
CAD program left the wires as they were. Black itself maps to 0 too
(RGBcodeTable[0]).

Colour 0 is now written as 256, BYLAYER. The layers are colour 7, so a
file looks the same on opening, and recolouring a layer now recolours
its contents. Real colours (free texts, terminal markers) are kept.

Checked on the 24 example projects, 133 folios: the only change from
the previous commit is 110,283 colour codes 0 -> 256, no entity is left
on 0, and ezdxf reads all of them with no audit errors. Rendered with
QET_WIRES set to red and QET_SYMBOLS to blue: before, 0 red and 0 blue
pixels; after, the wires and symbols take the layer colours.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-27 21:54:44 +13:00
ispyisail 20a3e4f636 Put the DXF export on named layers (#1071)
Everything in an exported DXF was on layer 0, and the file had no LAYER
table: in a CAD program the border, title block, symbols, wires and texts
could not be hidden, printed or recoloured separately. Each kind of
content now has its own layer, declared in a LAYER table (white/black,
continuous, so the file looks the same on opening):

  QET_BORDER, QET_TITLEBLOCK, QET_SYMBOLS, QET_SYMBOL_TEXTS,
  QET_TERMINALS, QET_WIRES, QET_WIRE_NUMBERS, QET_JUNCTIONS, QET_TEXTS,
  QET_XREFS, QET_SHAPES, QET_TABLES, QET_IMAGES

Fixed, untranslated names, so layer filters and scripts work in any
language. Createdxf gets a current layer beside its xScale/yScale;
DxfExport sets it before each kind of content, and BorderTitleBlock
switches to the title block layer itself. No DXF version change: layers
exist in R10.

Checked on the 24 example projects, 133 folios: with the layer values
set back to 0 and the LAYER table removed, every file is byte-identical
to before; no entity is on an undeclared layer or left on 0; ezdxf reads
all of them with no audit errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-27 21:54:44 +13:00
ispyisail 4278ef864e Add DXF export from the command line (--export-dxf)
qelectrotech --export-dxf <project.qet> <output_dir> [--show-terminals]
writes one DXF per folio, named <NN>_<title>.dxf like the PNG and SVG
exports. Discussion #1072.

The DXF code moves out of ExportDialog into DxfExport, unchanged except
that its options come from an ExportProperties argument instead of the
dialog. The dialog and the command line both call it, and the command
line uses the dialog's default options (the preferences' export
settings), so both write the same entities.

- Createdxf answers a file it cannot open with a message box and
  exit(0); the command line checks the file first and fails with a
  message and exit code 1 instead.
- A note is printed when pictures become outline boxes, as the dialog
  warns.
- Scripting: qet.exportDxf(outDir, showTerminals). MCP: format "dxf".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-27 21:54:36 +13:00
ispyisail f98d5d00e9 Merge branch 'fix/dxf-titleblock-linebreak' (#1076) into feat/1072-export-dxf-cli 2026-09-27 21:54:36 +13:00
ispyisail 166ee34f52 Fix parts lists saved before June 2022 losing rows and tables on open
A nomenclature table saved by an older version filters on the element
type names the project database used then: element_type = 'Simple',
'Terminale', 'Master'. Commit 2e70d2e59 (June 2022) changed the database
to "simple", "terminal", "master", and SQLite compares text
case-sensitively, so such a table silently lost every row of that type
on open. Its continuation tables were then empty, and
removeUselessNextTable() deleted them: opening and saving the example
industrial.qet removed seven of its ten parts-list tables (folios 44-50),
and the remaining three listed 76 of 258 parts.

ProjectDBModel::fromXml() now rewrites old names in element_type = '...'
comparisons to the current ones (LegacyElementTypes::upgradeQuery()),
which also lets the query editor tick the right boxes again. A query
saved by a current version is unchanged, and so is any other text that
happens to contain "Simple".

Checked in the GUI on industrial.qet, open then save: master keeps
tables on 3 of folios 41-50, this keeps all 10, with 258 rows (the last
table 24 of 26) and the query saved as 'simple'. tst_legacyelementtypes
fails when a name maps wrongly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-27 21:46:30 +13:00
ispyisail 4efe28ce03 Fix title blocks with a line break writing an unreadable DXF
A title block cell holding a line break (tableau_domestique.qet's
"DESSINE" + CR LF + "%{author}") was written into the DXF as one TEXT
value with the break inside it. A DXF value ends at the line break, so
every group code after it was off by one line: ezdxf refuses all five
folios of that example with DXFStructureError, and other readers take
values for codes.

- TitleBlockTemplate::renderTextCellDxf() writes each line of a cell as
  its own TEXT, stacked by the cell's vertical alignment as the screen
  shows them, 1.6 text heights apart as the diagram texts' DXF export
  does. A single-line cell is written exactly as before.
- Createdxf's text writers turn any line break still in a value into a
  space, so no other caller can break the file either.

Checked in the Export dialog: tableau_domestique.qet's five folios, which
ezdxf refused on master, now read with no audit errors; grafcet.qet, which
has no line break, keeps exactly the same entities. (Byte comparison waits
on the DXF entity-order fix: until then every export reorders its entities.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-27 21:33:00 +13:00
ispyisail 7c4355338b Fix DXF export writing entities in a different order every time
Exporting the same folio to DXF twice gave files with the same entities
in a different order, so two exports never compared equal and a DXF could
not be diffed to spot a change. The export walked diagram->items(), whose
order follows memory addresses; saving had the same fault until
bugtracker #343.

Items are now taken in stacking order (z, then insertion order, which a
load makes the file's order), from the same rect query Diagram::toXml()
uses. Anything the query misses keeps its items() place after the rest.

Checked in the Export dialog on grafcet.qet, three folios, two exports
per build: master's differ every time, these are byte-identical, and both
contain exactly the same entities, only reordered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-27 21:28:36 +13:00
ispyisail f7591e0c9d Add grouping of folio items: Group and Ungroup (#1070)
Symbols, free texts, shapes and pictures can be grouped from the Edit
menu, the selection's context menu or the command search. Clicking one
member selects the group, so moving, copying and deleting act on all of
it; Ctrl+click on a member deselects the group; a rubber band touching
part of a group selects all of it when released. No default shortcut:
Ctrl+G is "jump to element".

A group is not an object in the scene. Each member keeps its place and
carries the group's uuid (QGraphicsItem::data()), saved as a "group"
attribute written only when set: a project without groups saves exactly
as before, and older versions open a grouped one and ignore the groups.
Re-parenting under a QGraphicsItemGroup would have made every member's
position group-relative; ElementTextItemGroup already needs nine special
cases for that.

- Selection is completed on clicks and at the end of a rubber band, not
  on every selectionChanged(): export, search and Tab select items
  themselves and must not have groups pulled back in.
- Project database: group_uuid on element, shape, independent_text and
  image, kept in step by projectDataBase::itemGroupChanged().
- Paste and folio duplication give each source group one new uuid.
- Undo of Ungroup restores each item's exact group.

Builds on #1065 (uuids and database rows for texts, shapes and images).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-27 21:14:47 +13:00
ispyisail 4c3e86c07e Add "Snap to grid" for the selected symbols, pictures and texts
Edit > Aligner > Aligner sur la grille, also in the selection's
context menu and the command search, puts each selected item back where
dragging it would have left it: symbols and pictures on the folio grid,
free texts on the text grid. One undo step; wires follow their symbols.

Symbols leave the grid through the fine nudge (Alt+arrow, 1 px), and
nothing put them back: 508 of the 3,678 symbols in the example projects
are off the 10 px grid. First stage of discussion #1069.

The status bar says how many items moved, or that the selection was
already on the grid, and how many locked items were left in place.
Shapes are left out: they are made of several points and no single one
is the obvious one to snap.

The snap never reads the keyboard, unlike Diagram::snapToGrid(), so a
user who binds the action to a shortcut with Ctrl gets the grid, not
pixel rounding. The geometry is in alignment.h, tested by tst_alignment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-27 21:00:18 +13:00
Laurent Trinques 44cabbcfe4 Merge pull request #1065 from ispyisail/feat/uuid-shapes-texts-images
Add a permanent id to shapes, texts, pictures and symbol parts
2026-09-27 09:55:25 +02:00
ispyisail a2a30ef7c4 Merge branch 'master' into feat/uuid-shapes-texts-images
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:51:23 +13:00
Laurent Trinques 0b4f480dfe Merge pull request #502 from ispyisail/proto-conductor-selection-properties
Add wire properties to the Selection properties panel, as an option (#500)
2026-09-27 09:11:22 +02:00
Laurent Trinques 05260de87b Merge pull request #1060 from ispyisail/feature/shortcut-bar-symbols
Add pinned elements to the shortcut bar, with a width you can set
2026-09-27 09:09:44 +02:00
Laurent Trinques db154a8d06 Merge pull request #1059 from ispyisail/fix/ctrl-shift-pan-stuck
Fix the folio staying in pan mode after Ctrl+Shift+M
2026-09-27 09:08:02 +02:00
Laurent Trinques 7dc1d345b0 Merge pull request #1057 from ispyisail/feature/mouse-gestures
Add right-drag mouse gestures to run commands
2026-09-27 09:02:44 +02:00
Laurent Trinques ba1811a0cf Merge pull request #1056 from ispyisail/feature/repeat-command
Add repeating the last drawing or placing command with Enter
2026-09-27 09:02:02 +02:00
Laurent Trinques a3dc8d3ad4 Merge pull request #1055 from ispyisail/feature/context-toolbar
Add a command bar beside the selection after a click
2026-09-27 09:00:29 +02:00
Laurent Trinques b1fe1ef7a7 Merge pull request #1054 from ispyisail/feature/shortcut-bar-customise
Add customising the shortcut bar in place
2026-09-27 08:59:55 +02:00
Laurent Trinques 9584fc4b7c Merge pull request #1053 from ispyisail/feature/shortcut-bar
Add a shortcut bar that opens at the cursor with S
2026-09-27 08:59:32 +02:00
Laurent Trinques 25baf48153 Merge pull request #1052 from ispyisail/feature/element-picker
Add an element picker that opens at the cursor with Insert
2026-09-27 08:58:50 +02:00
Laurent Trinques 7f51a3e10a Merge pull request #1051 from ispyisail/feature/ranked-search
Show collection search results as a ranked list
2026-09-27 08:47:30 +02:00
Laurent Trinques 503823bfce Merge pull request #1050 from ispyisail/feature/context-menu-placement
Add placing the last element and folio references to the folio menu
2026-09-27 08:47:00 +02:00
Laurent Trinques 30c97612a2 Merge pull request #1042 from ispyisail/feature/place-without-drag
Add double-click placement from the collection, and A to repeat it
2026-09-27 08:40:42 +02:00
Laurent Trinques 8cdf51405a Merge pull request #1066 from ispyisail/fix/readonly-query-all-rows
Fix table and script queries returning only their first row
2026-09-27 08:06:08 +02:00
Laurent Trinques 4a5755e552 Merge pull request #1068 from ispyisail/feat/349-image-label
Add a label to pictures (#349)
2026-09-27 08:04:44 +02:00
Laurent Trinques ef8b3866ba Merge pull request #1067 from ispyisail/perf/image-save-png-cache
Speed up saving projects that contain pictures
2026-09-27 08:04:29 +02:00
ispyisail f6f53e3f52 Stop re-encoding unchanged pictures on every save
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
2026-09-27 16:43:11 +13:00
ispyisail 5120feb417 Add an optional label to pictures (#349)
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
2026-09-27 16:12:51 +13:00
ispyisail cd38f6f2a2 qet-mcp: test the uuids given to drawn items in older files
A file written before drawn items carried a uuid gets them from the folio
uuid, the kind of item and its order in the file. Three integration tests
pin what that promises: opening the same file twice gives the same uuids,
the first save writes exactly those and a reopen keeps them, and a uuid
repeated in a hand-edited file ends up naming one item only.

Each fails when the derived uuid is replaced by a random one. They read
drawing_item_view unsorted, which is what found the first-row-only bug
in QETSql::execReadOnly() fixed by the previous commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-27 14:00:16 +13:00
ispyisail 29fe731728 Fix read-only queries stopping after the first row
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
2026-09-27 14:00:16 +13:00
ispyisail b740508b7f Fix read-only queries stopping after the first row
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
2026-09-27 14:00:07 +13:00
ispyisail 76eea09192 qet-mcp: address texts, shapes and images by uuid; part uuids in symbols
- qet_edit: every op taking a text, shape or image "index" also takes its
  uuid, resolved at run time with textIndex()/shapeIndex()/imageIndex().
  Those are only required when a uuid is used.
- qet_element_build writes a uuid on every part (the caller's, or a new
  one) and returns them in part_uuids; qet_element_info lists part and
  terminal uuids.
- README and tests: drawing_item_view, uuid addressing, part uuids.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 11:16:37 +13:00
ispyisail 30e83d8c52 Element editor: give every primitive a uuid
Lines, rectangles, ellipses, arcs, polygons, PLC tables and static texts
now save a uuid, as terminals and dynamic texts already did, so a tool can
name one part of a symbol. The graphic parts keep it next to their styles,
since all of them save through stylesToXml(); a part read from a
definition that predates this gets one on the next save.

Paste renews them, as it already did for terminals -- and now for dynamic
texts too, which were pasted with their source's uuid. A placed element
gives its dynamic texts fresh uuids anyway, so only the definition's own
identity changes. PartTerminal::uuid() returns the terminal's saved uuid
rather than inheriting the graphic-part one it never writes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 11:16:37 +13:00
ispyisail 3a50cd9b0e Project database: tables for shapes, independent texts and images
Now that they carry a uuid, the folio's drawing furniture gets rows of its
own: shape, independent_text and image, plus drawing_item_view, which finds
any of them by uuid without knowing its kind first and says which folio it
is on.

Rows follow edits, not just loads. The script API's query() and every
other reader go through newQuery() without a rebuild, so an item's row is
queued on each change (moves, restyles, text edits, uuid renewal) and the
queue is flushed by newQuery() and updateDB() -- a queued write is a set
insertion, which matters for a drag that moves hundreds of items per mouse
step.

A pasted copy joins its folio still carrying its source's uuid and is only
renewed afterwards, so a flush in between must not let the copy overwrite
its source's row. Each row remembers the item that wrote it; another item
with the same uuid waits until uuidChanged() says it has its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 11:16:37 +13:00
ispyisail acb770909e DiagramPosition: compile the letter filter once
The constructor built QRegularExpression("[^A-Z]") on every call, and
compiling it cost more than the rest of Diagram::convertPosition() put
together -- about 95k instructions a call under callgrind. Every element
row of the project database pays it on each rebuild, as does anything
else that turns a scene point into a folio cell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 11:16:37 +13:00
ispyisail a9ad6e2f34 Give shapes, independent texts and images a persisted uuid
Elements, conductors, terminals and tables already carry a uuid. The
drawing furniture beside them did not, so a script or the MCP server could
only name a line, a box or a note by its index in a position-sorted list,
which shifts whenever one is added or removed.

- QetShapeItem, IndependentTextItem and DiagramImageItem get uuid(),
  newUuid() and setUuid(), read from and written to a "uuid" attribute.
- A folio loaded from a file written before this (or carrying a duplicate
  uuid) derives one from the folio uuid, the item kind and its order in the
  file, so the same file gives the same uuids on every load and a re-save
  is stable -- the #754 lesson for conductors.
- Paste and folio duplication renew them, as they already do for elements
  and conductors.
- Scripting: texts(), shapes() and images() end each line with the uuid;
  textIndex(), shapeIndex() and imageIndex() turn one back into an index.
- misc/qet-mcp: qet_diff keys texts, shapes and images on uuid when both
  sides have one, so a move or edit reads as a change to that item.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:54:54 +13:00
ispyisail ff795258c4 Conductor dock: turn it on in the preferences, off by default (#500)
Showing a selected conductor's properties in the selection-properties
dock is now a preference, on the General page under Appearance:
"Afficher les propriétés d'un conducteur sélectionné dans le panneau
Propriétés de la sélection". It is off by default, so selecting a
conductor does what it did before unless the user turns it on.

This replaces the View-menu toggle, which defaulted to on; that entry
is dropped in the merge with master. Same setting key
(diagrameditor/conductor_properties_panel).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:13:55 +13:00
ispyisail 0cb424bd06 Merge remote-tracking branch 'upstream/master' into proto-conductor-selection-properties
# Conflicts:
#	cmake/qet_compilation_vars.cmake
#	lang/qet_en.ts
#	sources/qetdiagrameditor.cpp
2026-09-27 09:07:41 +13:00
ispyisail 47e5a8072b Shortcut bar: reopen at the cursor, wrap to a width the user sets
After customising, the bar now opens at the cursor like every other
time, instead of where the customising window was.

A size grip in the bar's corner sets its width. The tiles are laid out
in rows that wrap at that width, so a bar with many commands and pinned
elements becomes a block rather than one long strip; the height follows
the rows, and the width is saved. Until the user sets one, the tiles
stay on one row, as before. The grip is driven by hand: QSizeGrip asks
the window manager to resize, and on X11 a popup is not managed, so
nothing would happen. The hint line wraps rather than being cut off at
narrow widths.

The customising window remembers the size it was left at.

Discussion #1033.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 03:16:56 +13:00
ispyisail 5bd55801e9 Pin elements to the shortcut bar
The bar's customising window gets an element search next to the command
list. Type part of a name, then drag a hit onto the bar, double-click
it, or press Enter to pin the best one. A pinned element shows as its
icon among the commands, and clicking it places the element, as the
picker does.

Pinned elements are saved in the same list as the commands, by
collection path (common://, custom://, company://), so the row keeps
the user's order. Elements embedded in a project are not offered: their
path names the project as loaded now. Elements are offered for the
empty-folio bar only; with something selected the bar is for acting on
it. Once any element is pinned there, the palette folder grid under the
bar is hidden, and comes back while typing a search.

Dragging a pinned element off the bar onto the commands, or a double
click, removes it. The Preferences page shows pinned elements by name
and icon, and removing one there drops it instead of listing it as a
command. The customising window is now kept on screen, since it is
taller than before.

Discussion #1033.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 03:16:56 +13:00
ispyisail 5ccb089cdd Merge repeat-command into mouse-gestures (search results drag fix)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-27 03:00:26 +13:00
ispyisail 416ababc41 Merge context-toolbar into repeat-command (search results drag fix)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-27 01:59:59 +12:00
ispyisail 63771a41b5 Merge shortcut-bar-customise into context-toolbar (search results drag fix)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-27 01:59:59 +12:00
ispyisail 57905a5789 Merge shortcut-bar into shortcut-bar-customise (search results drag fix)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-27 01:59:59 +12:00
ispyisail 451cccd615 Merge element-picker into shortcut-bar (search results drag fix)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-27 01:59:59 +12:00
ispyisail af28a49b72 Merge ranked-search into element-picker (search results drag fix)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-27 01:59:59 +12:00
ispyisail 926bbcf75f Let search results be dragged onto a folio like the tree
While a collection search is active the tree is replaced by the flat
ranked list, and that list had no drag support: dragging a result only
moved the selection. Anyone used to searching and then dragging lost the
drag as soon as they typed.

The tree's drag is moved into a static ElementsTreeView::execElementDrag()
taking the source widget, and the results list uses it from the path each
row already carries, so the drag content and pixmap are the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-27 01:56:46 +12:00
ispyisail 6bcdb87524 Fix the folio staying in pan mode after a Ctrl+Shift shortcut
Holding Ctrl+Shift over a folio pans it, and only a key release seen by
the view ends that. A Ctrl+Shift shortcut that opens a window -- the
command search, Ctrl+Shift+M -- takes the keyboard before Ctrl and Shift
are released, so the view never sees the release. On Windows, where the
Shift press itself already reports Ctrl+Shift, the folio then stayed in
pan mode after a command was chosen: a hand cursor, and clicks ignored,
so a drawing tool picked from the search did nothing. On Linux the Enter
key's release happened to reach the view and end it.

The view now remembers that it pans because of Ctrl+Shift (not because
the hand tool was chosen) and stops when it loses the focus, or at a
click made without Ctrl+Shift, instead of waiting for a release it may
never get.

Reproduced on Linux by holding back Enter's release, as Windows does:
the line drawn after "Ctrl+Shift+M, une ligne, Enter" is not saved on
master and is with this change. Ctrl+Shift+drag and the hand tool still
pan (a dragged step does not move), as on master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 01:35:56 +12:00
ispyisail f850d0bee8 Keep mouse gestures working while a tool is running
Most commands on the empty-folio ring start a drawing tool, and the view
left the right button alone while a tool ran, so the gesture after one
that started a tool only cancelled the tool: every other gesture opened
the context menu instead of the ring.

Now a right drag always shows the ring. A running tool is ended when the
drag starts, as picking another tool from the toolbar would. A plain
right click still goes to the tool, which cancels or finishes it as
before.

Discussion #1033.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-27 01:34:55 +12:00
Laurent Trinques d7036ad807 Merge pull request #1046 from ispyisail/fix/1045-sqlite-handle
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m15s
Fix #1045: crash on clicking an element with online-installer Qt
2026-09-26 14:20:43 +02:00
Laurent Trinques 09fb81b99a Merge branch 'master' into fix/1045-sqlite-handle 2026-09-26 14:07:32 +02:00
ispyisail 501379def2 Merge repeat-command (with master) into mouse-gestures
# Conflicts:
#	cmake/qet_compilation_vars.cmake
2026-09-26 22:44:21 +12:00
ispyisail 52a831e1fa Merge context-toolbar (with master) into repeat-command
# Conflicts:
#	sources/qetdiagrameditor.cpp
2026-09-26 22:44:11 +12:00
ispyisail c1bb3b33f4 Merge shortcut-bar-customise (with master) into context-toolbar
# Conflicts:
#	cmake/qet_compilation_vars.cmake
2026-09-26 22:44:01 +12:00
ispyisail f91ec74dde Merge shortcut-bar (with master) into shortcut-bar-customise 2026-09-26 22:43:50 +12:00
ispyisail b08d0692b7 Merge element-picker (with master) into shortcut-bar
# Conflicts:
#	cmake/qet_compilation_vars.cmake
#	sources/qetdiagrameditor.cpp
2026-09-26 22:43:40 +12:00
ispyisail 627b818a30 Merge ranked-search (with master) into element-picker
# Conflicts:
#	sources/qetdiagrameditor.cpp
#	sources/qetdiagrameditor.h
2026-09-26 22:43:27 +12:00
ispyisail 0d2befbc00 Merge place-without-drag (with master) into context-menu-placement 2026-09-26 22:43:06 +12:00
ispyisail db387525e2 Merge place-without-drag (with master) into ranked-search 2026-09-26 22:43:06 +12:00
ispyisail 6763103d57 Merge master into place-without-drag (#1042)
# Conflicts:
#	sources/diagramview.h
#	sources/qetdiagrameditor.cpp
2026-09-26 22:39:52 +12:00
Laurent Trinques 852c32fc07 Merge pull request #1048 from ispyisail/feature/cell-lines
Add an option to show the folio cell limits across the drawing
2026-09-26 12:26:58 +02:00
Laurent Trinques e82548bee9 Merge pull request #1047 from ispyisail/fix/cell-ruler-ghosting
Fix the header bars: ghost labels on Windows, shown twice zoomed out
2026-09-26 12:21:24 +02:00
Laurent Trinques 60ab39b0f6 Merge pull request #1049 from ispyisail/feature/command-search-standalone
Add a command search: type part of a command's name, press Enter
2026-09-26 12:19:20 +02:00
ispyisail b57c37ca62 Run commands with right-drag mouse gestures
Hold the right button on a folio and drag: a ring appears around the
point where the button went down, showing up to eight commands, and the
one in the mouse's direction is highlighted with its name underneath.
Releasing runs it; releasing near the centre cancels. These are
SolidWorks' mouse gestures.

The commands are the shortcut bar's for the selection, placed clockwise
from the top, so customising the bar customises the ring. As with the
context menu, a right press selects what is under the mouse first.

A plain right click still opens the context menu, now on release on
every platform. The view tracks the right button and opens the menu
itself; the platform's own right-click event (sent on press on X11, on
release on Windows) is ignored while gestures are on, so the menu is
never opened twice. The keyboard menu is unchanged.

The view keeps out of the way while a tool or a placement is running,
since a right click cancels or finishes those, and while a text is
being edited. The new General option "Gestes de la souris avec le
bouton droit" (diagrameditor/mouse_gestures, on by default) turns it
off and restores the previous right button exactly.

Discussion #1033.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
(cherry picked from commit a625142f36e619bb4d4f3551b6086f2dd8eabc86)
2026-09-26 21:40:46 +12:00
ispyisail 329831d986 Repeat the last drawing or placing command with Enter
Enter on a folio runs the last drawing or placing command again, as it
does in SolidWorks: the last tool from the add-item family (text, line,
rectangle, terminal strip plan...) or the last element placed, however
it was placed -- dragged, double-clicked, picked, or from the shortcut
bar.

Enter is handled in DiagramView::keyPressEvent, not bound as a shortcut,
so it keeps working everywhere else: search fields, the collection
tree, dialogs. On the folio it is left alone while a tool is running or
an item has the focus (a text being edited), and with any modifier held.

The same command is in Édition, named after what it will do --
"Répéter : Ajouter une ligne", "Répéter : insérer « Diode »" -- and
registered with ShortcutManager (diagrameditor.repeat_last_command, no
default key), so it can be bound or put on the shortcut bar.

Discussion #1033.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
(cherry picked from commit 1e06ce97961ee18734ddaaa9b72b0303a169b9b2)
2026-09-26 21:40:32 +12:00
ispyisail b80d6e7cd3 Show the selection's commands beside the cursor after a click
After a click that selects something on a folio, a small row of commands
appears just above and to the right of the cursor, as the SolidWorks
context toolbar does. It fades as the mouse moves away and is gone past
200 pixels; a click elsewhere, the wheel, a key press or an emptied
selection hide it too. Clicking a command leaves it up, so rotate can be
clicked again.

The commands are the shortcut bar's for that selection -- elements or
conductors -- at most eight, so customising the bar customises this as
well. It is a child of the view's viewport, never a window, and never
takes the focus.

It is not shown after a drag (moving items, a rubber band), while
placing or drawing (Diagram::eventInterfaceIsRunning()), on
a read-only folio, or when switched off with the new General option
"Afficher les commandes près de la sélection" (diagrameditor/
context_toolbar, on by default).

ShortcutBarSettings::contextFor() now decides the context for both the
bar and this toolbar.

Discussion #1033.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
(cherry picked from commit 78f3c1a2bc5c3cf1f7beae4bd60aefec20847d6d)
2026-09-26 21:40:32 +12:00
ispyisail 6d42556d9f Customise the shortcut bar in place
Right-click the shortcut bar, or click the "…" button at its end, and it
turns into a small window holding two lists: the bar's commands, left
to right, and every other command. Drag a command onto the bar, off
it, or to another place on it; a double click moves it to the other
list. Terminé saves and shows the bar again where it was, with the
result; Annuler, Esc or closing the window leaves it as it was.
"Valeurs par défaut" puts back the defaults for this context.

A Qt::Popup closes on a press outside it and holds the mouse grab, so
the bar is re-shown as a Qt::Tool window for the time of the edit.
Only QSettings is written, through ShortcutBarSettings, never the
project's undo stack.

The popup now looks the commands up itself (popUpShortcutBar(pos,
context)) instead of being handed actions, since it has to rebuild
them after an edit. ShortcutBarSettings::availableIds() lists what can
go on the bar, shared with the configuration page. An empty bar still
shows its "…" button, so it can be filled again.

Discussion #1033.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
(cherry picked from commit 4145d4bec1d403126b2a6587105aea47092db8a2)
2026-09-26 21:40:19 +12:00
ispyisail 2da101ca90 Add a shortcut bar that opens at the cursor with S
Pressing S on a folio opens the element picker at the cursor with a row
of commands above it, chosen by what is selected, like the SolidWorks
shortcut bar:

  nothing selected     insert last element, element picker, text, line,
                       rectangle, terminal strip plan, paste, folio
                       properties
  elements selected    rotate, rotate texts, edit, copy, cut, delete
  only conductors      reset path, edit, delete

Each row is a list of ShortcutManager ids, so any registered command can
go on it and the bar carries no command list of its own. The lists are
in QSettings (diagrameditor/shortcut_bar/<context>); a context the user
has not changed follows the defaults. A disabled command keeps its
place, greyed, so a row looks the same each time. Clicking a button
closes the bar and triggers the action.

A new configuration page, "Barre de raccourcis", edits the three lists:
add, remove and reorder any diagram editor command.

To make that possible:
- ShortcutManager::action(id, owner) returns the action a given window
  registered under an id, since each editor window registers its own.
- The add-item actions (text, image, shapes, terminal strip plan) are
  registered as diagrameditor.add_<kind>, with no default key. They also
  appear in the Shortcuts page and can now be bound.

Discussion #1033.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
(cherry picked from commit 53c1e213282b9f7226ca1c09e81703113236bb88)
2026-09-26 21:40:19 +12:00
ispyisail 07501d604e Add an element picker that opens at the cursor
Insert opens a small picker where the mouse is, with its search field
focused. Type to search the whole collection, Enter to place the best
hit (it is preselected), Up/Down to choose another, Esc to close. The
chosen element goes into the usual placement mode, so it can be placed
several times and repeated with A.

With the field empty the picker shows a palette: the elements of a
folder, as an icon grid. The palette is a folder rather than a setting
or a file format. Subfolders are read in, the 01_/02_ filename prefixes
the shipped collection already uses give the order, and sharing it is
putting it in the company collection. Only the path is stored,
"elementscollection/palette-path", defaulting to the user collection.
It is read each time the picker opens, capped at 60 entries and three
folder levels.

The picker builds no second collection model. It asks the Collections
panel's rankedSearch(), so both give the same results in the same order
and startup is unchanged.

"Insérer un élément…" is in the Édition menu, registered with
ShortcutManager on Insert, which nothing else uses, and disabled with no
folio open or on a read-only project.

Discussion #676.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
(cherry picked from commit 01e1e0303b96380e710da8d4bb2300b21d01e938)
2026-09-26 21:40:19 +12:00
ispyisail 73ad43bd8f Show collection search results as a ranked list
The Collections search hid non-matching rows, so the hits stayed spread
through the expanded folders. Searching "diode" against the shipped
collection left five levels of tree open, and the first visible hit was
"Avalanche diode bidirectional".

The same matches are now shown as a flat list that takes the tree's
place while a search is active, best match first:

  exact name            1000
  name starts with       800
  name contains          600, less the match position
  info field only        300
  less the name length, so "Diode" precedes "Diode Zener bidirectional"

"diode" now gives Diode, diode-tube, Diode bridge, Diodes Module Pilot
Wire, Photodiode. No new index: this ranks what match() already returns
against Qt::UserRole+1, the string built at startup from the name and
every element-info field. An element matching several "+" terms is
listed once, and each row's tooltip is its folder, so similar names can
be told apart.

Double click or Enter on a result places it, as in the tree. Down from
the search field moves into the results, so searching and placing needs
no mouse. Emptying the field brings the tree back.

The query and ranking live in rankedSearch(), which returns hits without
model indexes, so another list can reuse them.

Discussion #676.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
(cherry picked from commit 3d6a280f9608758527ef0ca2de7223a19dfbfa45)
2026-09-26 21:40:19 +12:00
ispyisail c1d9e60656 Place the last element and folio references from the folio's context menu
Right-clicking an empty folio now also offers:

- "Insérer le dernier élément": the same action as Édition and the A
  key, shown once something has been placed.
- "Renvoi de folio": the folio report elements the project already
  uses, read from its embedded collection, with dated copies of the same
  element listed once. A project that has none yet gets the coming and
  going arrows of the common collection. An entry places its element
  where the menu was opened.

The submenu is rebuilt each time the menu opens, from the embedded
collection's XML, not from the folios' scenes. It is left empty, and so
hidden, on a read-only folio.

Discussion #1033.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
(cherry picked from commit 94e3f61bf93689066b3964f2316e3c8f1ee7b673)
2026-09-26 21:40:18 +12:00
ispyisail 92cbc8fd2a Merge master into stage 2 (#1042) as the base of the stage PRs 2026-09-26 21:40:18 +12:00
ispyisail db56262924 Register the drawing tools so the command search finds them
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>
2026-09-26 21:35:47 +12:00
ispyisail 86cb7e430e Add a command search: type part of a command's name, press Enter
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
2026-09-26 21:34:02 +12:00
ispyisail 3d260edcdb Cell rulers: line the top ruler up with the columns when the row header is hidden
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>
2026-09-26 21:18:35 +12:00
ispyisail 53706a80d8 Add an option to draw the folio cell limits across the drawing
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>
2026-09-26 21:17:19 +12:00
ispyisail 1e74bcd9de Cell rulers: no ghost labels on Windows, hidden while the folio's own header shows
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>
2026-09-26 21:06:13 +12:00
ispyisail 45aec7735b Fix #1045: crash on selecting an element with the online-installer Qt
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>
2026-09-26 20:06:07 +12:00
Laurent Trinques 923367d2a8 Merge pull request #1039 from ispyisail/fix/text-mover-null-drag
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m22s
Fix undo lost after a text drag that snaps back to its start
2026-09-26 08:44:04 +02:00
Laurent Trinques 6dd260a7b2 Merge pull request #1038 from ispyisail/feature/cell-rulers
Add an option to keep the folio row and column headers visible (#1034)
2026-09-26 08:43:42 +02:00
Laurent Trinques 132c564aef Merge pull request #1040 from ispyisail/feature/text-grid
Add a finer snap grid for dragged texts
2026-09-26 08:21:48 +02:00
Laurent Trinques e9c4969af7 Merge pull request #1044 from qelectrotech/revert-1037-german-translation
Revert "german translation qm file"
2026-09-26 08:06:56 +02:00
Laurent Trinques 040c4b5e29 Merge pull request #1041 from ispyisail/feature/context-menu-drawing
Add the drawing tools to the folio's right-click menu
2026-09-26 08:06:25 +02:00
Laurent Trinques 71d708e2da Merge pull request #1036 from ispyisail/feature/goto-cell
Add jumping to a folio cell such as B13 from Ctrl+G (#1034)
2026-09-26 08:04:17 +02:00
Laurent Trinques b490720652 Merge pull request #1035 from ispyisail/fix/spacemouse-capture-seconds-macos
Add macOS, Windows and --seconds to the 3D mouse recording script
2026-09-26 08:00:53 +02:00
Laurent Trinques 519f2245f6 macOS: fix arm64 bundle rejection (-10825) on macOS Sequoia
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)
2026-09-26 07:51:31 +02:00
ispyisail b61193f66f Revert "german translation qm file" 2026-09-26 17:50:02 +12:00
ispyisail e6291eb8e8 Place an element from the collection without dragging it
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
2026-09-26 11:53:34 +12:00
ispyisail 0ca4b9f6c3 Put drawing first in the folio's context menu, rows and columns one level down
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
2026-09-26 11:40:25 +12:00
ispyisail a08cdf7272 Reset the element text mover after a drag that ends where it started
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
2026-09-26 10:37:21 +12:00
ispyisail 2fdb225936 Reset the element text mover after a drag that ends where it started
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
2026-09-26 10:36:40 +12:00
ispyisail 95f3ea2f6d Share the text grid choices; add a preference, a status hint and a unit test
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
2026-09-26 10:22:50 +12:00
ispyisail 0e02190eb2 Add a finer snap grid for dragged texts
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
2026-09-26 09:15:35 +12:00
ispyisail 2807771200 Jump to a cell on another folio from Ctrl+G, typed as 3-B13
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
2026-09-26 08:36:53 +12:00
ispyisail 4cffdf93b6 Merge pull request #1037 from Kellermorph/german-translation
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m15s
german translation qm file
2026-09-26 08:28:04 +12:00
ispyisail 9738345b45 Add an option to keep the folio row and column headers visible (#1034)
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
2026-09-26 08:22:12 +12:00
Kellermorph 286002fe25 german translation qm file 2026-09-25 21:48:49 +02:00
ispyisail ce0e7e6867 Add jumping to a folio cell such as B13 from Ctrl+G (#1034)
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
2026-09-26 07:37:38 +12:00
ispyisail 5f0b015186 Merge pull request #1031 from ispyisail/fix/bugtracker-240-backup-crash
Fix bugtracker #240: crash when a recovery file cannot be opened
2026-09-26 07:13:40 +12:00
ispyisail e5738c5b7d Merge pull request #1024 from arummler/fix-picture-insert
Fix picture and graph primitives issues
2026-09-26 07:13:20 +12:00
ispyisail c3c264cd78 Merge pull request #1010 from Kellermorph/full-contact-comb
Full contact comb
2026-09-26 07:12:57 +12:00
ispyisail d392ab4b8f Let misc/spacemouse-capture.py record on Windows, and say how on each system
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>
2026-09-26 06:50:33 +12:00
ispyisail ebd4a57563 Let misc/spacemouse-capture.py record on macOS
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>
2026-09-26 06:44:08 +12:00
ispyisail 23d83ec153 Add --seconds to spacemouse-capture.py to record longer per step
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>
2026-09-26 06:43:58 +12:00
Laurent Trinques d43c535cc9 Merge pull request #1030 from ispyisail/fix/bugtracker-343-save-order
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m2s
Fix bugtracker #343: saving reorders texts and shapes after an edit
2026-09-25 15:55:44 +02:00
Andre Rummler 71782aec20 Fix wheel scale hardening 2026-09-25 15:09:21 +02:00
Laurent Trinques 59dc59cf2f Merge pull request #1029 from ispyisail/fix/bugtracker-340-pdfxid
Fix bugtracker #340: small PDF text drawn too bold in Adobe Acrobat
2026-09-25 14:35:43 +02:00
Laurent Trinques 469c8cbc70 Merge pull request #1032 from ispyisail/feat/spacemouse-3dxware-macos
Fix 3D mouse doing nothing on macOS when 3DxWare is installed
2026-09-25 14:33:59 +02:00
Andre Rummler 1330dbb435 Fix janking behaviour during resize and Ctrl action. 2026-09-25 13:37:49 +02:00
ispyisail e7f19de2da Mention ConnexionBackend in SpaceMouseBackend's class comment
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 23:25:27 +12:00
ispyisail 4c9a307cb9 Say what realLibrary() can and cannot check
Measured on a macOS 15 runner with 3DxWare 10.8.13: an ad-hoc signed
test binary with the hardened runtime loads 3DconnexionClient with or
without the entitlement, so ad-hoc signing does not enforce library
validation and CI cannot prove the entitlement is needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 23:07:34 +12:00
ispyisail 4381c6caa3 Read the 3D mouse through 3DxWare on macOS when it is installed
On macOS, 3DxWare installs a driver extension that takes the 3D mouse
over. With it installed, HidBackend opens the device but receives
nothing, so the mouse did nothing in QElectroTech until 3DxWare was
uninstalled (discussion #599, PR #1028). Most Mac owners of a 3D mouse
have 3DxWare installed.

ConnexionBackend asks 3DxWare for the motion instead, through
3DconnexionClient.framework, as Blender does. SpaceMouseListener tries
it first. When 3DxWare is not installed, or is installed but its driver
is not running, it falls back to HidBackend, so the device works in
both setups. Take-over mode stops 3DxWare's own actions in QET, so the
view does not move twice.

The library is loaded at run time from where 3DxWare installs it:
nothing is linked or bundled, and the build needs no SDK. The few
declarations are written here, from Blender's
GHOST_NDOFManagerCocoa.mm, because 3Dconnexion's SDK headers may not be
redistributed. 3DxWare's axes are y up and z away from the user; they
are mapped to QET's raw USB convention by comparing Blender's 3DxWare
and spacenavd code paths.

The release script signs with the hardened runtime, which refuses a
library another team signed. misc/qelectrotech.entitlements adds
com.apple.security.cs.disable-library-validation (Blender's notarized
build carries the same one), and MacQetDeploy_arm64_cmake.sh now passes
it to all four signings of the app, including the re-sign inside the
DMG.

Tested: tst_spacemouseconnexion runs the backend on every platform
against fakeconnexion, a stand-in library that answers from its own
thread as 3DxWare does: registration, the axis mapping, buttons, other
clients' messages, 3DxWare not installed or not running, deletion
with a message in flight. Flipping an axis sign or dropping the client
check turns it red. realLibrary() loads the real framework when
3DxWare is installed. Linux Qt 6 build with the 3D mouse enabled: all
18 tests pass.

Not tested: on a Mac with a real device. The axis signs and whether
buttons arrive as a bitmask with current 3DxWare are unverified.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 22:54:07 +12:00
ispyisail 07d7aedc26 Fix bugtracker #240: crash when a recovery file cannot be opened
After a crash, QElectroTech offers to reopen its recovery files. If one
cannot be read, answering OK crashed the program instead of showing the
"could not open" warning: QETProject(KAutoSaveFile *) takes ownership of
the file and deletes it on failure, and openBackupFiles() then read the
file name from the deleted object to build the warning. Read the name
first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-25 21:45:07 +12:00
Kellermorph 5622308ba2 Derive empty-slot labels from the group's declared terminals
Both issues from the review, preview of empty contact comb slots only:

- Slice the flat label list over the declared poles in drawAsContacts
  (terminals per pole from labels.size()/contactCount) instead of a
  fixed stride of 2 (3 for a switch) inside drawContact: terminalCount
  and contactCount are edited independently, so with 3 poles and the
  default terminal count of 2 the fixed stride starved every pole but
  the first. Available labels are now distributed across the poles.
- Map changeover labels of a slot to their own position always
  (common=stored[0] right, NC=stored[1] bottom-left, NO=stored[2]
  top-left, missing entries stay empty) instead of falling back to the
  raw stored order, which put the numbers on the wrong contact halves
  when the terminal count was below three.
- Element editor: changing the contact count now keeps the terminal
  count in step (same terminals per contact, type default 2/3 for
  inconsistent data, minimum 3 for a switch), so the mismatch cannot
  be created anymore; legacy mismatched data is handled by the new
  slicing.
- Drop the now redundant elmt check before is_power_ctc (it already
  includes it) - the dead null check from the review.
2026-09-25 11:24:35 +02:00
ispyisail 149ae961cc Fix bugtracker #343: saving reorders texts and shapes after an edit
Diagram::toXml() writes texts, images, shapes and tables in items()
order. The diagram scene uses NoIndex, and Qt's linear index sorts its
item list by pointer address the first time any item is removed from
the scene, which the editor does constantly (selection handles, for
one). From then on the order in the saved file follows memory
addresses, so moving one element reshuffles unrelated blocks and a
version-control diff of the project becomes unreadable.

Write those blocks in stacking order instead, read from a rect query,
which Qt sorts by z and insertion order even with NoIndex. Reloading a
file rebuilds exactly that order, so a resave is stable and the drawing
does not change. Elements and conductors were already sorted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-25 21:16:34 +12:00
Laurent Trinques ac40d110fb Merge pull request #1028 from ispyisail/fix/spacemouse-macos-shared-open
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m51s
Fix 3D mouse not detected on macOS while 3DxWare is running
2026-09-25 11:13:33 +02:00
ispyisail 4f309770f0 Fix bugtracker #340: small PDF text drawn too bold in Adobe Acrobat
Qt 6 declares the PDF/X namespace (pdfxid) in the XMP metadata of every
PDF it writes, even when the file is not PDF/X. Adobe Acrobat then draws
small text too bold at some zoom levels. Qt only needs the declaration
for PDF/X-4 output, which QElectroTech never asks for.

Blank the declaration out after export, in place with spaces so no
offsets move, in both the export window and the --export-pdf path.
Diagnosis and the Acrobat testing by Alf, bugtracker #340.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-25 20:44:02 +12:00
ispyisail 72cf86ece0 Open the 3D mouse non-exclusively on macOS
hidapi's macOS backend opens every device with
kIOHIDOptionsTypeSeizeDevice unless told otherwise (hid_init() calls
hid_darwin_set_open_exclusive(1) for backward compatibility). When
3DxWare is running it already holds the SpacePilot/SpaceMouse, so the
seize fails, hid_open_path() returns NULL and HidBackend::scan() keeps
retrying every 3 s without ever finding the device.

Reported in discussion #599: a SpacePilot Pro (046d:c629) is listed by
hid_enumerate() and works in 3DxWare, but has no effect in QET's macOS
build. Enumeration never opens a device, so it did not exercise this.

Guarded on HID_API_VERSION >= 0.12, where the setter first appeared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 20:27:37 +12:00
Laurent Trinques 8aefdcc481 Merge pull request #1027 from ispyisail/fix/bugtracker-95-collection-project-name
Fix bugtracker #95: Collections panel shows "Projet sans titre" for untitled projects
2026-09-25 09:05:25 +02:00
ispyisail 3a45625d69 Fix bugtracker #95: collections pane shows "Untitled project" for a titleless project
The root of a project's embedded collection showed "Projet sans titre"
whenever the project had no title, while the project panel shows the
file name. Fall back to the file name the same way, and only use
"Projet sans titre" for a project that has neither.

The name was also computed once, so changing the project title or saving
it under a new name left the pane stale until the collections were
reloaded. Update it on projectTitleChanged and projectFilePathChanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 17:24:17 +12:00
Laurent Trinques 988357057f macOS add SPACEMOUSE_BACKEND=hid 2026-09-25 05:15:06 +02:00
Laurent Trinques e9676ef87e Merge pull request #1026 from ispyisail/feature/spacemouse-windows
Build the Windows package with 3D mouse support
2026-09-25 04:30:40 +02:00
Laurent Trinques ca507dc0f3 Merge pull request #1025 from ispyisail/feature/spacemouse-hid
Add a 3D mouse backend that reads the device over USB (hidapi)
2026-09-25 04:12:35 +02:00
Laurent Trinques 1932150ea6 Merge pull request #1023 from ispyisail/feature/spacemouse-settings
Add 3D mouse speed, direction and twist-to-zoom settings
2026-09-25 03:53:15 +02:00
Laurent Trinques 23241fba69 Merge pull request #1022 from ispyisail/fix/spacemouse-element-editor
Fix 3D mouse doing nothing in the element editor
2026-09-25 03:52:23 +02:00
ispyisail f2f1051914 Build the Windows package with 3D mouse support (hidapi)
Installs MSYS2's hidapi and turns on QET_ENABLE_SPACEMOUSE with the
hidapi backend, so the Windows package reads a 3Dconnexion SpaceMouse
directly over USB, with no 3Dconnexion driver (discussion #599).

The existing transitive DLL scan copies libhidapi-0.dll into the package,
and both installers take everything in bin/. Two checks make a silent
loss fail the build instead: CMake only warns when hidapi is missing, so
the exe must link against it, and the DLL must be deployed, or
QElectroTech.exe would not start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:30:48 +12:00
ispyisail 313aafc95b Add misc/spacemouse-capture.py to record a 3D mouse's raw USB reports
For device owners on Linux: guided movements, with every raw report and
the device's report descriptor saved to one JSON file. Dropped into
tests/qttest/fixtures/spacemouse/, a recording is checked by
tst_spacemousehid against what the user was asked to do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:00:59 +12:00
ispyisail aaeaff55cc Add a hidapi 3D mouse backend that needs no 3Dconnexion driver
The 3D mouse only worked on Linux, through spacenavd. This adds a second
backend that reads the device directly over USB through hidapi, with no
3Dconnexion driver or SDK: the route to Windows and macOS (discussion
#599), and usable on Linux without spacenavd.

SpaceMouseHid decodes the raw reports from the device's own report
descriptor -- where each axis and button sits, its range, absolute or
relative -- so no per-model table is needed, with the classic report
1/2/3 layout as a fallback when the descriptor cannot be read and the
0x1c button list newer devices send. Absolute axes are rescaled to
+-500 exactly as spacenavd does, so both backends give QET the same
values. HidBackend polls from the main thread (fast while moving, slow
when still), emits one sample per poll, and looks for a device every 3 s
so plugging one in or back in needs no restart.

QET_SPACEMOUSE_BACKEND (auto, spnav, hid) picks the backend; auto keeps
libspnav on Linux when it is found and uses hidapi otherwise. hidapi is
found through pkg-config as hidapi-hidraw (Linux) or hidapi (MSYS2,
Homebrew).

A sample arriving in the same millisecond as the previous one now counts
for no time instead of a full period, so a burst of queued samples no
longer moves the view further than the time it covers.

Tested without a device: tst_spacemousehid (descriptor parsing, broken
and hostile descriptors, every report form, recordings from real devices
once they are added to fixtures/spacemouse), and end to end on Linux
through a virtual USB device created with /dev/uhid: the same moves give
byte-identical screenshots through the hidapi and libspnav backends, an
absolute axis is rescaled as spacenavd does, buttons trigger their
bound action, and unplugging and replugging while QET runs (including
with a dialog open that a device button opened) reconnects cleanly.
Not tested on Windows, macOS or real hardware.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:55:30 +12:00
Andre Rummler bb037ee3ff Fix: when pasting the picture or graphics object is nowunder the top left corner instead far away. 2026-09-25 00:24:15 +02:00
ispyisail 17ebbffbca Add 3D mouse speed, direction, dead zone and twist-to-zoom settings
The 3D mouse's pan and zoom speeds were fixed guesses, and each sample
was applied as it came, so the speed on screen depended on how often
the driver sends samples -- different for every platform and device.

Motion now goes through SpaceMouseMotion::map(), which scales each
sample by the time since the previous one, and applies the user's
settings from a new "Mouvement" section of Configuration > Souris 3D:
pan and zoom speed, a dead zone, inverting each axis, and zooming by
push/pull (as before) or by twisting the cap. The defaults keep the
previous behaviour. Zoom is now exponential in the deflection, so the
factor stays positive however hard the cap is pulled (1 + z/1000 went
negative past z = -1000) and an equal push and pull cancel out. Sub-
pixel pan is carried over between samples instead of being rounded
away. The backend now reports all six axes.

tst_spacemousemotion covers the mapping without a device and is built
whether or not QET_ENABLE_SPACEMOUSE is on. The new behaviour was also
checked end to end with tools/spnav-shim (qelectrotech-docker): twist
with a dead zone of 10 ignores push/pull and small drift, and a twist of
60 gives the same frame as a push of 50 with the defaults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 09:56:04 +12:00
ispyisail 04e2279a7f Make the 3D mouse pan and zoom the element editor too
The 3D mouse only acted on the diagram editor: SpaceMouseListener
ignored every other window, so the element editor did not move at
all (reported by scorpio810 in PR #635 with a SpacePilot Pro).

applyMotion() now also handles a QETElementEditor, driving its
ElementView through the same scrollbar pan and a new
ElementView::zoom(factor), which keeps the wheel zoom's clamping.
The element editor's scene rect only covers what is on screen, so it
is grown before each pan sample, as its middle-button pan does;
without that the scrollbars have no range and the pan does nothing.

Verified under Xvfb with an LD_PRELOAD stand-in for libspnav feeding
recorded motion samples: element editor zooms 1.63x for ten z=50
samples and pans; diagram editor screenshots are byte-identical to
master's for the same input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 09:32:10 +12:00
Laurent Trinques 9987f05835 Merge pull request #1021 from ispyisail/feat/217-element-info-completer
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m2s
Fix bugtracker #217: suggest element information already used in the project
2026-09-24 23:19:48 +02:00
ispyisail a94f30b256 Fix bugtracker #217: suggest element information already used in the project
Each information field of the element properties now offers, as a
drop-down while typing, the values the other elements of the project
already carry for it: supplier, manufacturer, reference...

The values come from the project database's element_info table, not
from a walk over the folios. Spellings that differ only by case are
offered once, as most elements spell them, and the edited element's
own value is left out. The field key is checked against
elementInfoKeys() before it becomes part of the SQL text.

Browsing the list with live edit on applies each highlighted value,
as typing applies each keystroke; ChangeElementInformationCommand
merges them, so this still leaves one undo entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:28:41 +12:00
Laurent Trinques 73075a5154 Snap enable DQET_ENABLE_SPACEMOUSE=ON
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m31s
2026-09-24 17:20:38 +02:00
Laurent Trinques 9875de85d7 Merge pull request #635 from ispyisail/feature-3dmouse-spnav
3D mouse (SpaceMouse/SpacePilot) pan/zoom via libspnav — Linux phase (discussion #599)
2026-09-24 16:49:00 +02:00
Laurent Trinques 48883b320e Merge branch 'master' into feature-3dmouse-spnav 2026-09-24 15:45:33 +02:00
Andre Rummler d1256872c2 Fix transformation order. 2026-09-24 15:21:42 +02:00
Laurent Trinques b93d8d8baa Merge pull request #704 from arummler/master-feature-renum-elements-rebased
Autonumbering: renumbering of elements
2026-09-24 15:17:38 +02:00
Laurent Trinques ae41dd7686 Merge branch 'master' into master-feature-renum-elements-rebased 2026-09-24 15:17:17 +02:00
Kellermorph 800189debb Address review: restore hover entries for empty position text
- Call updateLabel() explicitly when the xref is created in itemChange for a master that must show its configured contact groups without slaves (same pattern as the PLC branch above).
- Register the hover/click hit rect of a contact independently of its position text again, as before this feature: only the drawing stays guarded by !str.isEmpty(), and the map insert is now keyed on elmt so free slots (nullptr) never enter the map.
- Revert is_power_ctc to the original element-type test (with a null guard): the Power-flag term was redundant for every caller that passes an element, so no linked contact changes classification.
- Clarify the label-order comment: single pole NO/NC are swapped, changeover labels are rotated per pole inside drawContact() (multi pole included), multi pole NO/NC groups keep the master order.
2026-09-24 15:11:31 +02:00
Andre Rummler 522e008c26 Fix rotation of inserted picture. 2026-09-24 13:37:06 +02:00
Laurent Trinques 7fbbb71aac Merge pull request #1018 from ispyisail/revive/785-info-flags-case-insensitive
Fix element flags written as True or 1 showing unticked
2026-09-24 13:08:28 +02:00
Laurent Trinques 232724bf82 Merge pull request #1017 from ispyisail/revive/752-rotate-texts-no-dialog-in-ctor
Move the rotate-texts dialog out of its undo command
2026-09-24 13:06:23 +02:00
Laurent Trinques 12f826e969 Merge pull request #1016 from ispyisail/revive/729-backup-prompt-drops-files
Fix projects opened during the start-up restore prompt being lost
2026-09-24 13:04:19 +02:00
Laurent Trinques 245aafb0d1 Merge pull request #1015 from ispyisail/revive/526-free-terminal-move
Fix the terminal strip's move button getting stuck disabled (#409)
2026-09-24 13:02:43 +02:00
Laurent Trinques 0d231ef548 Merge pull request #1014 from arummler/fix-explicit-file-open
Fix warning about reading XML without explicit file opening.
2026-09-24 13:00:47 +02:00
ispyisail 3513f692fd Read the remaining element flags through QET::infoFlagIsTrue()
5b0785fcc routed most reads of auto_num_locked, potential_isolating and
exclude_from_bom through QET::infoFlagIsTrue(), which accepts the same
spellings as the parts-list query (true/1/yes/on, trimmed, any case).
Four checkbox reads still compared against a literal lowercase "true":
the "exclude from parts list" box in the folio properties panel, and all
three flags in the element editor.

A value such as "True" or "1" from a hand-written .elmt/.qet was
therefore left out of the parts list, but shown unticked in both panels,
and pressing Apply there wrote back "false" and flipped the flag.

Revives the unconverted part of PR #785.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 22:34:42 +12:00
Laurent Trinques 5a27f3cf80 Merge pull request #1013 from ispyisail/fix/273-skip-open-backup
Fix bugtracker #273: skip crash-recovery backups of an unchanged project
2026-09-24 12:33:18 +02:00
Andre Rummler d3a9adbe44 Fix warning about reading XML without explicit file opening. This was implicit up to now and will go away in future Qt versions. 2026-09-24 12:32:47 +02:00
ispyisail 8ab4e24b61 Take the modal dialog out of RotateTextsCommand's constructor
RotateTextsCommand called QDialog::exec() from inside its constructor, so
the command could not be built without a human answering a dialog. That
made it untestable headlessly, undrivable from any script or test harness,
and it is why bugtracker #312 (PR #707) shipped with its save/reload
round-trip unverified -- the symptom could not be reproduced without a GUI.

The command now takes the angle as a parameter and does no asking. Two
statics carry the interactive half:

  hasSelectedTexts(diagram)  -- is there anything to rotate
  askRotation(rotation)      -- open the dialog, false if cancelled

The single call site in QETDiagramEditor asks first, then builds the
command, so the user-visible behaviour is unchanged: same dialog, same
title, same no-dialog-on-empty-selection. Keeping askRotation() in this
class also keeps the QObject tr() context, so existing translations of
"Orienter les textes sélectionnés" are not invalidated.

Also guards undo()/redo() against a null m_anim_group. When nothing is
selected the constructor calls setObsolete(true) without ever creating the
animation group, and QUndoStack::push() calls redo() before discarding an
obsolete command -- a latent null dereference on that path.

Verified headlessly, which was the point: driving the command through a
scratch --test-ops op on examples/741.qet (67 conductors), rotation
attributes written on save go 0 -> 67 with the #707 fix present and stay
at 0 with it reverted, while the reverted build instead writes userx on
all 67. That is bugtracker #312 reproduced and fixed under test for the
first time.
2026-09-24 22:30:20 +12:00
Laurent Trinques 2d2bf5ca2b Merge pull request #1012 from ispyisail/fix/340-ms-shell-dlg-pdf-font
Fix bugtracker #340: Windows texts in 'MS Shell Dlg 2' render as Arial
2026-09-24 12:27:50 +02:00
ispyisail 2f6ab8f63b Don't drop files handed to a starting instance while the backup prompts are up
QETApp's constructor ends with checkBackupFiles(), which opens modal
dialogs -- the "restore these files?" prompt, and the crash report offer.
Those dialogs run their own event loop, so the constructor does not return
until the user answers them.

main() still has work to do at that point. In particular this, a few lines
later:

    QObject::connect(&app, &SingleApplication::receivedMessage,
                     &qetapp, &QETApp::receiveMessage);

While the prompts are up that connection does not exist yet. A second
instance launched during the window -- double-clicking a project, or
xdg-open, while the first copy is still asking about restore files --
hands its file names over, SingleApplication accepts and delivers them,
and nothing is listening. The message is discarded and the second process
has already exited, so the file is simply lost with no error.

Deferring checkBackupFiles() to the event loop lets the constructor return
promptly. main() finishes wiring up, and the prompts appear immediately
afterwards exactly as before.

Verified with a stale restore file present, sending a project to the
running instance while the restore prompt is displayed: before, the file
was dropped and never appeared, even after answering the prompt; after, it
opens. The restore and backup prompts still appear and still work.

Note this is only observable together with the fix for bugtracker #248 --
before that, no file passed to a running instance was opened under any
circumstances.
2026-09-24 22:26:38 +12:00
ispyisail 2266497b0b Write the move button tooltips in French, the tr() source language
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 22:20:43 +12:00
Shane Ringrose dcb79b6553 fix(terminal-strip): free-terminal move button disabled and unresponsive
Three bugs stacked to produce the symptom in issue #409 (move button does
nothing):

1. selectionChanged() was declared in freeterminaleditor.h but had no body
   and was never connected to the selection model, so the move button had no
   awareness of whether a terminal was selected. The button could appear
   enabled with nothing selected, then silently return early in
   on_m_move_pb_clicked() at the real_t_vector.isEmpty() guard.

2. The dataChanged→setDisabledMove(true) connection was a one-way trap: any
   cell edit (including accidentally opening and closing a type/function
   combo, or toggling LED back to its current value) permanently disabled the
   move button until reload() was called. There was no tooltip explaining why
   the button was greyed out, so the user had no way to recover.

3. FreeTerminalModel::setData() for LED_CELL had no change guard, unlike
   LABEL_CELL which checks label != value. Clicking the LED combo while it
   was already at the same value still emitted dataChanged and triggered the
   disable.

Fix:
- Implement selectionChanged() to enable the move controls only when at
  least one row is selected AND there are no pending (yellow) edits.
- Connect it to both selectionModel::selectionChanged and model::dataChanged
  so the button state is always consistent with actual UI state.
- Route reload()'s re-enable through selectionChanged() instead of calling
  setDisabledMove(false) directly, so the selection state is respected
  immediately after a move.
- Add a tooltip to m_move_pb explaining the disabled state.
- Add the missing change guard to LED_CELL in setData().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-24 22:20:43 +12:00
ispyisail 745b4a936c Fix bugtracker #273: skip crash-recovery backups of an unchanged project
writeBackup() rebuilds the whole project's XML with toXml() on the GUI
thread before handing the file write to a worker thread. On a big project
that freezes the interface for seconds (bugtracker #273, and #329, where
the interval was raised from 2 to 20 minutes to make it rarer). It ran
right after every project was opened, and then every 20 minutes whether
or not anything had changed.

Only write a backup when something changed since the last one: the undo
stack moved, setModified(true) was called, or the embedded element or
title block collections changed. A project just opened from a file starts
clean, since the file is already what a crash would restore. New projects
and projects restored from a backup are backed up at once, as before.

Measured on examples/industrial.qet (50 folios), each backup blocks the
GUI thread for about 0.28 s on a fast machine; with a 2 s test interval,
the old code backed up on every tick, the new code only after a change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 21:57:42 +12:00
ispyisail 7ac3719c37 Fix bugtracker #340: map "MS Shell Dlg" aliases on Qt 6/Windows
"MS Shell Dlg 2" is a Windows font alias, not a font, and projects and
settings saved on Windows carry it. Qt 5's GDI backend let Windows resolve
it to Tahoma; Qt 6's DirectWrite backend does not know the alias and falls
back to Arial, so those texts render heavier on screen and in exported PDFs.

Register the substitutions Windows itself uses, before any application
object exists so the headless export and scripting paths get them too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 21:41:00 +12:00
Laurent Trinques ddfa234943 Merge pull request #1011 from Kellermorph/save-sheet-colour
Remember the sheet background colour between runs
2026-09-24 10:50:34 +02:00
Kellermorph 9af562fcbd Remember the sheet background colour between runs
Picking a sheet (folio) background colour in the diagram editor was lost
on every restart. Diagram::background_color is a static initialised to
white and PaletteGraphicsView's s_custom_bg a static bool, and neither
was ever written anywhere -- Diagram::toXml() carries no colour attribute
either -- so closing and reopening a project always came back on the
default and the choice had to be made again.

Store it in QSettings under diagrameditor/sheet_background_* as a pair of
values rather than one: the colour, and whether it was picked explicitly.
Both halves are needed. "#ffffff, follow the system" and "#ffffff, always
white" are the same colour and two behaviours -- the first is what the
views invert on a dark palette -- so keeping only the colour would
silently turn one into the other on the next start, which is the reported
problem one step removed.

The colour is written as HexRgb on purpose. The SVG export gives
Diagram::background_color an alpha of 0 to render a transparent
background and never puts it back, and that transient value must not be
persisted as a permanently transparent sheet.

Applied from main() after the headless export and scripting branch -- those
return before reaching it and must keep rendering on plain white, the rule
ProjectPrintWindow already enforces for printing -- and before QETApp is
constructed, since that constructor already loads the projects given on
the command line. The GUI export dialog is left alone: it renders through
drawBackground(), so what you see is what you export, as it already was
within a session.

Saved at the moment the colour is applied rather than at shutdown, so
neither the print window's temporary white nor the SVG export's alpha can
reach it. The button's constructor now mirrors the stored state instead of
always claiming "system colour", and the "recently used" list is stored
alongside it.

Covered by tst_sheetbackgroundsetting, which pins the custom flag and the
dropped alpha -- the two rules a single stored colour would lose.
2026-09-24 10:30:01 +02:00
Laurent Trinques c85276f61b Merge pull request #1009 from ispyisail/fix/1002-resize-handle-artefacts
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 6m31s
Fix green fragments left behind when moving or zooming an element (#1002)
2026-09-24 10:17:53 +02:00
Kellermorph c1cc9a5b98 Show all master-defined slaves in the contact comb behind a new option
Add a new cross reference setting per xref type (coil, protection,
commutator, PLC), labelled "Afficher tous les esclaves definis par le
maitre" and persisted as showallconfiguredslaves. When it is enabled,
the contacts display is selected and the master declares contact
groups, the contact comb draws every contact group of the master in the
master's own order, even when no slave is linked to it yet. Masters
without declared contact groups and the option turned off keep the
previous behaviour exactly: linked slaves only, sorted by position.

- XRefProperties: new property stored in the settings and in the
  project XML (attribute showallconfiguredslaves, absent means false so
  old files are unaffected), included in operator==.
- XRefPropertiesWidget: new checkbox placed after the terminal names
  one, enabled only while "Afficher en contacts" is selected; its
  enabled state is now also set explicitly when a type is loaded (a
  radio button that does not change emits no toggled()).
- For the PLC type the contacts/cross radios, the two display
  checkboxes and the cross options group are hidden: a PLC master is
  always drawn as its IO table, those settings have no effect there.
  Positioning and label settings, which the table really uses, stay.
- CrossRefItem: free slots draw the symbol of the group plus the
  terminal names the master defines (pairs swapped for a single pole
  NO/NC contact, labels of a changeover contact rotated one step
  counter-clockwise), without position text and without hover/click.
  Linked slaves keep drawing from their own data at their assigned
  group position; links without a group are appended at the end in
  position order.
- Xref lifecycle: the item is created and kept without linked slaves
  for snap-to-bottom (MasterElement::mustShowXrefWithoutSlave) and for
  snap-to-label (DynamicElementTextItem::updateXref and
  ElementTextItemGroup::updateXref, which now also run when the element
  lands on the scene and re-establish their project connection), so a
  freshly placed master shows its comb immediately instead of only
  after the next settings change. updateLabel() resets its geometry
  when the option is turned off again, so no stale ghost stays.
2026-09-24 09:59:21 +02:00
ispyisail f3a31c291f Fix green fragments left by text resize handles when moving an element
The two width-resize handles added in #591 were free scene items, moved
with setPos() from inside DynamicElementTextItem::paint(). Moving an item
while the view is painting is outside what QGraphicsView's partial
repaint tracks: the handle was drawn only where the current repaint band
overlapped it, and its old position was not reliably cleared. Moving or
zooming a selected element left green slivers on the folio (#1002).
Since 29d16c333 the handles show on every text of a selected element,
so any ordinary element move triggered it.

Make the handles children of the text instead. Qt then moves and
repaints them together with the element, in local coordinates, and
their position only needs updating when the text's size changes, which
documentSizeChanged reports (text, font, width, undo of a resize).
This also takes paint() out of the handle logic entirely.

Reproduced on Linux (Xvfb), so the issue is not Windows-specific.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:46:26 +12:00
Laurent Trinques a06220e8dc Merge pull request #1008 from ispyisail/fix/1004-element-preview-cache
Fix #1004: element preview stays stale after editing until the collection reloads
2026-09-24 09:28:33 +02:00
Laurent Trinques 79990e40ee git submodule update --remote elements 2026-09-24 04:26:56 +02:00
Laurent Trinques e1e7ea4888 Merge pull request #1006 from ispyisail/i18n/ui-phrasebooks-29-languages
Add translation suggestions for the interface to 29 phrase books
2026-09-24 04:24:08 +02:00
Laurent Trinques 54b8fe5df4 Merge pull request #1007 from elevatormind/restore-sqlite-dependencies
Restore last sqlite dependencies
2026-09-24 04:02:28 +02:00
Laurent Trinques be0d214b75 Merge pull request #1005 from ispyisail/fix/1002-conductor-color-equipotential
Fix #1002: conductor colour toolbar swatch only recolours one wire segment
2026-09-24 04:01:13 +02:00
ispyisail 2f988e6e2c Refresh cached element preview after saving from the editor
Editing an element already in the user collection, saving, and closing
the editor left the old thumbnail in the elements panel until the
whole collection was reloaded. ElementsLocation::icon() serves the
preview from two caches keyed by path+uuid -- ElementPictureFactory's
in-memory picture cache and ElementsCollectionCache's on-disk SQLite
cache -- and neither was ever told the file changed.

QETElementEditor::toLocation() writes the new XML and returns;
ElementsCollectionWidget::locationWasSaved() then refreshes the panel
item, but it reads the icon through the same two stale caches, so the
refresh was a no-op. slot_reloadElementDrawings() already shows the
correct invalidation call for ElementPictureFactory; this wires the
same pattern, plus a matching refresh of ElementsCollectionCache's
row, into the save path itself.

Fixes the preview half of #1004. The paste-cursor-jump half of that
report is a live design disagreement between two recent commits from
a different contributor and is written up separately rather than
fixed here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 12:09:01 +12:00
Magnus Hellströmer 9786006ca4 fix(database): align sqlite3 forward declarations 2026-09-23 21:45:16 +02:00
ispyisail 91e8956734 Add translation suggestions for the interface to 29 phrase books
Fills lang/phrase/qet_fr_xx.qph for every language QET ships, following
on from discussion #873. A phrase book is never compiled into what
ships -- a translator opens it in Qt Linguist and sees a suggestion
already filled in, to accept or correct.

Only ever appends. phrasebook.write() skips any source string already
in the file (case-insensitive), so no existing entry -- human or
machine -- is touched. 24 languages had no phrase book at all before
this; the other 5 (da, de, nl, ru, sv) keep every line they had.

Built and run with tools/qet-i18n in the qelectrotech-docker harness
(DeepSeek Flash). Companion to qelectrotech-elements#82, which does the
same for the element collection.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 07:34:47 +12:00
ispyisail 6fad3390df Fix issue #1002: conductor-colour toolbar swatch skips the rest of the potential
ConductorColorToolButton::applyColor() only recoloured the selected
Conductor objects. A wire drawn across a junction is several separate
Conductor segments sharing one electrical potential, so selecting one
segment and picking a colour left the rest of the wire its old colour
-- visible only by falling back to F2/double-click, which already
expands to relatedPotentialConductors() for exactly this reason
(ConductorPropertiesDialog, and QetScriptApi::setConductorProperty).

Expand each selected conductor to its potential before building the
undo command, matching that existing pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 07:08:31 +12:00
Magnus Hellströmer 231ab990c3 Revert "refactor: use VACUUM INTO for database export"
This reverts commit 27dea3ffab.
2026-09-23 20:18:25 +02:00
Laurent Trinques f10275cb22 Merge pull request #997 from ispyisail/revive/659-preserve-links
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m15s
Preserve master/slave links when pasting or duplicating a folio
2026-09-23 18:40:34 +02:00
Laurent Trinques 3d0e7a27a3 Merge pull request #998 from ispyisail/revive/785-case-insensitive-flags
Make auto_num_locked/potential_isolating case-insensitive
2026-09-23 18:38:38 +02:00
Laurent Trinques f14278324c Merge pull request #995 from ispyisail/revive/713-collection-cancel-race
Cancel the collection-loading map before waiting on it in ~ElementsCollectionModel
2026-09-23 18:36:17 +02:00
Laurent Trinques d5318eb7b4 Merge pull request #996 from ispyisail/revive/682-nonfinite-coordinates
Reject non-finite values in QET::attributeIsAReal()
2026-09-23 18:34:57 +02:00
Laurent Trinques f10ef3ded7 Merge pull request #994 from ispyisail/revive/664-element-info-orphan
Fix element_info orphan row causing UNIQUE constraint errors on undo
2026-09-23 18:33:05 +02:00
Laurent Trinques 6f7ea63589 Merge pull request #1000 from ispyisail/fix/933-german-source-strings
Fix bugtracker #933: German source string and comments in templates code
2026-09-23 18:26:45 +02:00
Laurent Trinques 9a6029a2bb Merge pull request #1001 from Kellermorph/plc-table-size
Plc table size
2026-09-23 18:24:15 +02:00
Laurent Trinques bc0ca56c7e Merge pull request #999 from ispyisail/fix/ci-missing-libsqlite3-dev
CI: install libsqlite3-dev, required by PR #983's find_package(SQLite3)
2026-09-23 18:22:47 +02:00
Laurent Trinques 1d4c0326d4 Merge pull request #1003 from qelectrotech/revert-986-no-sqlite
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m55s
Revert "fix: harden project database export"
2026-09-23 13:51:07 +02:00
Kellermorph 3457a88e81 Open the PLC master properties dialog at three times its width
Element::editProperty() gave PropertiesEditorDialog no size of its own,
so the dialog fell back to its sizeHint, which is sized for the compact
general-purpose editors it usually hosts. A PLC master instead shows a
six-column IO table that grows horizontally, and the dialog came out
too narrow to read those columns in.

For a master whose type is PLC, resize the dialog to three times its
natural width before exec(), keeping the natural height. The width is
clamped to the available screen so it cannot run off the display, and
every other element type opens exactly as before.
2026-09-23 10:28:00 +02:00
Kellermorph 4e361f93fe Let the PLC master IO table columns be resized, and remember the layout
The IO table of MasterPropertiesWidget -- the panel that opens when a
PLC master placed on a schematic is edited -- set every section to
QHeaderView::Stretch. Stretch spreads the sections evenly over the
widget and disables section dragging altogether, so the column
boundaries were permanently fixed: they could be neither widened nor
narrowed, and only the width of the whole panel had any effect.

Use QHeaderView::Interactive instead, with movable sections and a set
of default widths, so the columns can be dragged as they are everywhere
else in the application. The layout reached this way is written to
QSettings under masterpropertieswidget/plc-table-header-state on every
sectionResized and sectionMoved, and restored the next time the table
is built -- the same header-state trick the free and linked element
trees of this widget already use, except saved automatically rather
than only from the context menu.
2026-09-23 10:27:50 +02:00
ispyisail 6a8838e719 Fix bugtracker #933: German source string and comments in templates code
sources/ElementsCollection/fileelementcollectionitem.cpp had a German
tr() source string ("Makros") in a project whose source language is
French/English elsewhere. Renamed to "Macros" (identical in both
languages, so no translation catalog change is needed). Translated
three German-language comments in elementscollectionmodel.cpp and
diagramview.cpp to English.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 17:31:38 +12:00
ispyisail 774f803626 CI: install libsqlite3-dev for find_package(SQLite3 REQUIRED)
PR #983 added a hard SQLite3 system dependency to CMakeLists.txt, but
the workflow only installed libqt6sql6-sqlite (Qt's runtime driver
plugin), not the C headers/library find_package(SQLite3) needs. Every
PR based on current master has been failing CI with "Could NOT find
SQLite3" since #983 merged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 17:02:36 +12:00
ispyisail 5b0785fcce Make auto_num_locked/potential_isolating case-insensitive
Reviving the still-relevant part of #785, closed 2026-09-10 purely to
clear a review backlog, not on merit. Investigated fresh against
current master -- one of the original PR's three targets turned out
to already be fixed independently: element_nomenclature_view's SQL
predicate for exclude_from_bom already does
"COALESCE(LOWER(TRIM(ei.exclude_from_bom)), '') NOT IN ('true', '1',
'yes', 'on')" (projectDataBase::createElementNomenclatureView()).

auto_num_locked and potential_isolating had no equivalent: five call
sites across terminal.cpp, terminalnumberingdialog.cpp and
elementinfowidget.cpp compared the raw stored string against the
literal "true" with QString::operator==, silently treating "True",
"TRUE", a trailing space, or any value written by something other
than this app's own checkbox as off -- with no error and no visible
difference from the checkbox being genuinely unticked.

Added QET::infoFlagIsTrue(), matching the same accepted spellings
("true"/"1"/"yes"/"on", case-insensitive, trimmed) the SQL predicate
already uses, and switched all five call sites to it.

Verified the exact comparison logic in isolation, outside any QET
build: 15 cases including "True", "TRUE", padded whitespace, "1",
"yes", "on", and their false counterparts -- all correctly
discriminated. Qt 6.10.2, ctest 13/13.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 16:47:07 +12:00
ispyisail de9b3eae06 Preserve master/slave links when pasting or duplicating a folio
Reviving #659, closed 2026-09-10 purely to clear a review backlog
(#630), not on merit. Rebuilt fresh against current master rather than
merged from the old branch (elementspanelwidget.cpp had drifted enough
that a textual merge risked silently losing content, as it did earlier
in this same session for a different revival). Builds discussion #607.

Cutting/copying a linked group of elements -- a relay coil with its
contacts, a PLC master with its slave I/O elements -- dropped the
master/slave link entirely. Traced end to end: Element::toXml() writes
each partner's uuid into <link_uuid>, Element::fromXml() reads it back
into a deferred, unresolved buffer (tmp_uuids_link), and the only code
that ever resolves that buffer is initLink(QETProject *) -- called
only from Diagram::refreshContents(), itself only called from full
project load and macro-block insertion. Neither DiagramView::paste()
nor ElementsPanelWidget::duplicateDiagram() ever call it, so
tmp_uuids_link is populated correctly and never resolved: the link is
silently dropped. duplicateDiagram() already knew this and worked
around it by calling clearPendingLinks() -- correct to not link back
to a stale source, but it meant folio duplication never preserved a
link either.

Added Element::initLink(const QList<Element *> &candidates) --
resolves against a caller-supplied list instead of a project-wide
search. The scoping is the subtle part: right after the XML round-trip
and before uuids are renewed, a pasted/duplicated element's
tmp_uuids_link still holds its source's original partner uuid, which
at that exact moment still equals the not-yet-renewed uuid of that
partner's own copy, if it was carried along in the same batch.
Resolving only within the batch is what stops a linked pair pasted
together from matching an original element left elsewhere that
happens to still carry that same soon-to-be-replaced uuid. If only one
half of a linked group is in the batch, its entry finds no match and
is dropped -- the same "leave it unlinked" outcome as before.

Wired into PasteDiagramCommand::redo(), before the existing newUuid()
loop and gated by the same first_redo flag. Wired into
duplicateDiagram() the same way, replacing its clearPendingLinks()
call (initLink() clears tmp_uuids_link internally, matched or not).

Verified live -- the original PR's own test plan left both of these
unchecked, so this closes that gap rather than repeating it. Built a
project with a linked PLC master/slave pair (qet-mcp's link_elements),
then drove the real interaction under Xvfb:

  Ctrl+A, Ctrl+C, Ctrl+V:
    originals   95ad58fc <-> e728632c   (unchanged)
    pasted      513e6bf8 <-> 29aa60b4   (linked to each other)

  Right-click folio > "Copier et coller":
    originals   95ad58fc <-> e728632c   (unchanged)
    duplicated  0a33ccb4 <-> 3264fe66   (linked to each other)

Neither copy links back to an original or comes in unlinked. Qt 6.10.2,
ctest 13/13.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 16:37:45 +12:00
ispyisail 032f2249a3 Reject non-finite values in QET::attributeIsAReal()
Reviving the still-relevant third of #682 (closed 2026-09-18 purely to
clear a review backlog, not on merit). Investigated fresh against
current master rather than merged wholesale -- two of the original
PR's three findings turned out to already be resolved independently:

- Element::valideXml() and Terminal::valideXml() already reject a
  non-finite x/y (qIsFinite checks, with comments citing this exact
  class of bug) -- added by someone else since #682 was written.
  Verified live: a project with x="nan" on an element loads and
  exports cleanly on current master, 3.1s, no hang.
- The illegal-XML-control-byte segfault in QDomDocument::setContent()
  does not reproduce either. Tested both bytes from the original
  report (0x00, 0x0E) against a real Qt6 build: both are now refused
  cleanly (XmlParsingFailed, exit 0), no crash. Qt6's QDom parses
  differently to the Qt5 one #682 was written and tested against.

What's still genuinely open: QET::attributeIsAReal() itself --
QString::toDouble()'s output parameter reports success for "nan"/
"inf"/"-inf", and this shared helper (26+ call sites across the
codebase, per #682's own count) had no finiteness check independent of
element.cpp/terminal.cpp's own since-added ones. Confirmed several
call sites are not behind either of those two gates -- notably
elementpicturefactory.cpp's line/rect/ellipse/circle/arc parsing for a
symbol's own drawing (a corrupted .elmt, not just a corrupted project
file), which was and remains reachable through this helper alone.

Qt 6.10.2, ctest 13/13.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 16:25:39 +12:00
ispyisail 1890cee801 Cancel the collection-loading map before waiting on it in ~ElementsCollectionModel
Reviving #713, closed 2026-09-21 purely to clear a maintainer review
backlog (#630), not on merit; not superseded. The crash #713 was
originally named after (bugtracker #291) was already fixed separately
by 39ac5716c, merged 14 Aug -- confirmed still on master. What's left,
and what this revives, is the one-line follow-up #713 itself narrowed
to after that: m_future.cancel() before the wait.

Without it, ~ElementsCollectionModel()'s wait runs the whole queued
QtConcurrent::map() to completion, so cancelling the open-element
dialog blocks until every remaining item has been processed -- a
visible hang on the button pressed precisely to stop the work.
cancel() drops the not-yet-started items so the wait is short, while
still waiting for whatever item is already in flight (needed so it
can't dereference this object after it's gone).

Qt 6.10.2, ctest 13/13. The responsiveness gain itself is reasoned
from QFuture's documented cancel()/waitForFinished() semantics rather
than timed -- same as the original PR's own stated verification.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 16:20:38 +12:00
ispyisail 3eea3059bf Fix element_info orphan row causing UNIQUE constraint errors on undo
Reviving #664, closed 2026-09-18 purely to clear a maintainer review
backlog (#630), not on merit; not superseded. Rewritten fresh against
current master rather than merged from the old branch -- that branch
predates the Qt6-only switch and much of dataBase/projectdatabase.cpp's
later rewrite, and the two had diverged too far for a textual merge to
be trustworthy.

projectDataBase::removeElement() only ran DELETE FROM element WHERE
uuid=:uuid. It never touched element_info, even though every element
also has a row there (element_uuid is its PRIMARY KEY, with a FOREIGN
KEY back to element.uuid that isn't enforced by this connection -- no
ON DELETE CASCADE in effect). So deleting an element left its
element_info row orphaned.

Re-adding an element with that same uuid later -- undo of that same
deletion, or a redo replaying it -- goes through addElement(), which
INSERTs into both tables. The element insert succeeds (that row really
was removed). The element_info insert hits the orphaned row's primary
key and fails, silently: the error is logged and swallowed, so the
element re-enters the scene with no element_info row at all, and
nothing later re-syncs it.

removeDiagram() already cascades this cleanup when a whole folio is
removed (a later, unrelated addition) -- confirmed on current master --
but that path never runs for a single element removed on its own,
which is the case this fixes.

Verified on the built binary, not just read: placed an element, deleted
it (Ctrl+A, Delete), undid the deletion (Ctrl+Z). Reverting just this
fix and repeating the identical sequence reproduces the exact reported
error:

  Debug: projectDataBase::addElement insert element info error :
  QSqlError("1555", "Unable to fetch row",
  "UNIQUE constraint failed: element_info.element_uuid")

With the fix, the same sequence produces nothing. Qt 6.10.2, ctest
13/13.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 16:13:04 +12:00
ispyisail bf7f49f334 Merge remote-tracking branch 'upstream/master' into review-635-fix
# Conflicts:
#	CMakeLists.txt
#	cmake/developer_options.cmake
#	cmake/qet_compilation_vars.cmake
#	sources/qetapp.cpp
2026-09-22 23:10:34 +12:00
Andre Rummler 2ec413a578 Adding generated headers to gitignore. 2026-08-09 23:13:09 +02:00
Andre Rummler 61ce75e380 First draft of elements renumber function. 2026-08-09 23:13:09 +02:00
ispyisail 83b9f32bd0 Add device-button-to-action bindings, for any 3D mouse backend
Discussion #599's own scope explicitly deferred this ("Related, not
proposed here... a natural follow-up once basic pan/zoom motion works").
Basic pan/zoom now works (previous commits on this branch), so this adds
it -- generically, for whichever SpaceMouseBackend is in use, not tied to
libspnav specifically, matching the seam the previous commit built.

## Reuses ShortcutManager instead of inventing a second action registry

ShortcutManager is already an app-wide registry of every named, rebindable
action -- undo, redo, rotate selection, cut/copy/paste, autonum configure,
and dozens more -- each carried by a live QAction or QAbstractButton. A
device button binding to one of *those* ids, rather than to a bespoke
QET-3D-mouse-only action list, means the discussion's own examples
(rotate/mirror/undo) are available for free, and any action added to the
app in the future is automatically bindable too.

Added ShortcutManager::trigger(id): find the first still-alive target for
an id and call QAction::trigger() or QAbstractButton::click(), whichever
it is. Deliberately not disambiguated by which window is currently active,
unlike SpaceMouseListener's own pan/zoom dispatch -- a target's owning
top-level window isn't reliably discoverable from a bare QAction. Correct
in the overwhelming common case of one open editor window; documented in
the header as a known simplification, not silently assumed correct.

## The binding itself: SpaceMouseButtonMap

A thin QSettings-backed button-number -> action-id map, unbound by default
for every button on every device -- nothing happens on any button press
until the user opens Configuration > Souris 3D and binds something,
matching this whole feature's "silent until asked for" default.

## Backend side: SpaceMouseBackend::buttonPressed(int)

Added to the platform interface alongside the existing motion() signal.
SpnavBackend now handles SPNAV_EVENT_BUTTON (previously explicitly
ignored) and emits on press only -- release is not reported, since nothing
downstream has a use for it. A future non-spnav backend implements the
same signal and gets button support for free through
SpaceMouseListener::applyButton(), without that logic being duplicated or
re-verified per backend -- the same reasoning the previous commit's seam
was built around.

## Configuration UI: SpaceMouseConfigPage

Modelled directly on the existing ShortcutsConfigPage -- same QTableWidget
shape, same "persist on applyConf(), not live" contract -- one row per
binding: button number (spin box, unbounded, since button count and
numbering genuinely vary from 2 to 30+ across real devices and this could
not be checked against hardware) and action (combo box populated from
ShortcutManager::instance().allShortcuts(), the exact same live registry
the Shortcuts page itself lists). Only added to the Configuration dialog
when QET_SPACEMOUSE_SUPPORT is compiled in.

## Verified, including the one thing that doesn't need hardware to prove

Rebuilt from scratch both ways: option off adds zero new object code
(confirmed via a forced rebuild of the one unconditionally-changed file,
shortcutmanager.cpp, which alone picked up new warning-free code); option
on compiles all four new/changed files warning-free and links clean.

The backend's button *detection* (SPNAV_EVENT_BUTTON -> buttonPressed
signal) still cannot be verified without a real device or daemon -- same
limitation as the motion path from the previous commits, stated plainly
rather than glossed over.

What *is* fully verified, because none of it needs hardware:
 - SpaceMouseButtonMap: unbound by default, set/read-back, clearing via an
   empty id, enumeration -- all confirmed via a standalone harness linked
   against the real compiled objects.
 - ShortcutManager::trigger(): registered a real QAction, confirmed
   trigger() fires it exactly once and returns true; confirmed it returns
   false (not a crash) for an unknown id.
 - SpnavBackend: constructs safely with no daemon present (isAvailable()
   false, as it must be), and both its motion and buttonPressed signals
   are correctly wired per Qt's own metaobject data (QSignalSpy).
 - The configuration page end-to-end, via a real Xvfb session: opened
   Configuration > Souris 3D, confirmed the action combo box lists the
   live, real ShortcutManager registry (undo, rotate, cut/copy/paste,
   dozens more -- not a mock), added rows, edited the button number,
   removed rows, selected "Éditeur de schémas — Pivoter" (Rotate -- the
   discussion's own example) for button 3, clicked OK, and confirmed via
   the actual settings file that it persisted exactly as
   "buttons\3=diagrameditor.rotate_selection". Reopened the dialog and
   confirmed it read back correctly. This is a full, real round trip
   through the UI, not a claim.
2026-08-02 22:11:30 +12:00
ispyisail e35cab33f0 Extract a SpaceMouseBackend seam ahead of a future Windows/macOS backend
The user asked for phase 2 (Windows/macOS via 3Dconnexion's proprietary
3DxWare SDK) on top of #635. This sandbox has no 3DxWare SDK, no Windows
toolchain, and no macOS toolchain -- nothing to compile, link, or run a
single line of platform code against, unlike the Linux/libspnav path,
which was built and actually tested here for real. Writing 3DxWare
integration code that has never even built would be a materially weaker,
unverifiable thing sitting in this PR, so it is not in this commit.

What is: the structural seam that makes adding it later a contained,
reviewable change instead of a rewrite of code that already works.

## Before

SpaceMouseListener did three unrelated things in one class: own the
libspnav connection, read spnav events, and apply motion to the active
DiagramView. A Windows/macOS backend would have had to either duplicate
all of the DiagramView-facing logic (the pan/zoom calls, the Z-to-zoom-
factor mapping, the "which view is active" lookup -- all already verified)
or bolt onto the same class with a maze of #ifdefs. Either way, touching
that file again would put the already-tested Linux path back in scope for
review.

## After

- SpaceMouseBackend: a tiny interface (isAvailable(), a motion(dx,dy,dz)
  signal). A backend's only job is owning one platform's connection to the
  driver/daemon and translating its native event into this one signal.
- SpnavBackend: the libspnav code from the previous commit, moved behind
  that interface with no behaviour change -- still spnav_open() in the
  constructor, still a QSocketNotifier on spnav_fd(), still silent when no
  daemon/device is present.
- SpaceMouseListener: now backend-agnostic. Owns whichever backend the
  platform provides, applies its motion to the active DiagramView exactly
  as before. The DiagramView-facing code (pan/zoom calls,
  zoomFactorForZAxis) did not need to change at all -- only its input
  changed from a spnav_event_motion struct to three plain ints.

A future 3DxWare backend implements SpaceMouseBackend, is selected in
SpaceMouseListener's constructor behind its own
QET_SPACEMOUSE_BACKEND_3DXWARE guard (see the comment marking exactly
where), and never has to touch SpnavBackend or SpaceMouseListener's
DiagramView-facing half.

## CMake: one user option, one define per backend

QET_ENABLE_SPACEMOUSE is unchanged as the single option a user sets.
Internally, find_spacemouse.cmake now decides *which* backend (if any)
that resolves to: on Linux with libspnav found, QET_SPACEMOUSE_BACKEND_SPNAV
plus the umbrella QET_SPACEMOUSE_SUPPORT. Turning the option on anywhere
else today downgrades cleanly with a warning naming discussion #599,
instead of trying (and failing) to find libspnav on a platform that
doesn't ship it. Adding 3DxWare later means adding one more branch here,
not restructuring this file.

## Verified this is a pure refactor, not just "still compiles"

Reconfigured and rebuilt both ways from scratch:
 - option off: unchanged from before -- no new source files compiled, zero
   new object code.
 - option on: both new files compile with zero warnings, binary still
   links against libspnav.so.0 (confirmed via ldd), and run to completion
   in this environment (which has no spacenavd) with zero crashes and zero
   spnav-related output -- identical to before the refactor.
 - zoomFactorForZAxis re-linked and re-run in isolation: identical output
   to the pre-refactor commit (z=0 -> exactly 1.0, z=+-350 -> 1.35/0.65),
   confirming the math moved unchanged rather than being reimplemented.
2026-08-02 21:22:42 +12:00
ispyisail 2e4bb486bb Add 3D mouse (SpaceMouse/SpacePilot) pan/zoom support via libspnav
Implements discussion #599's phase 1 (Linux, libspnav): a 3Dconnexion
6-DOF device pans and zooms the active diagram view, via spacenavd.

## Off by default, zero cost when off

QET_ENABLE_SPACEMOUSE (cmake/developer_options.cmake) is OFF. Verified in
two separate build directories from a clean configure: with it off, the
new cmake/find_spacemouse.cmake step runs and does nothing (no library
lookup, no definition, no new source files compiled), and qetapp.cpp/.h
produce zero new object code -- both are entirely #ifdef'd out. The
default build is byte-for-byte the same shape as before this commit.

With it on, libspnav is located via its pkg-config file (spnav.pc, shipped
by libspnav-dev on Debian/Ubuntu and equivalent packages elsewhere). If the
option is on but the library isn't found, this does not hard-fail
configure: it downgrades back to off with a warning, so an opt-in feature
never blocks a developer who doesn't have the library installed.

## No new navigation logic -- a new input source for the existing one

DiagramView::wheelEvent() already turns a physical wheel's delta into
horizontalScrollBar()/verticalScrollBar() calls for pan and a
zoom(1 + value/1000) call for zoom -- see diagramview.cpp:661-687.
SpaceMouseListener calls exactly those same primitives from spnav motion
events instead of wheel events. It does not reimplement panning or
zooming.

## Safe by construction even when compiled in

The overwhelming majority of users of a build with the option on still
won't have spacenavd running or a device attached -- that must never
surface as an error dialog or a startup warning. SpaceMouseListener::
isAvailable() reflects this: spnav_open() failing is treated as the
ordinary case, not an error, and the object then does nothing at all.
Verified for real in this environment, which genuinely has no spacenavd
running: built with the option on, ran the resulting binary to completion,
and confirmed zero crashes and zero spnav-related output of any kind --
the silence is the point.

Motion is read via a QSocketNotifier on spnav_fd() (event-driven, no
polling loop, no idle cost) and applied to whichever DiagramView is
currently active, found via qApp->activeWindow() -> QETDiagramEditor ->
currentProjectView() -> currentDiagram(): a 6-DOF device is one ambient
input source for the whole application, not something tied to a
particular window, so there is exactly one listener, owned by QETApp.

## What could not be verified without hardware

The Z-axis-to-zoom-factor mapping (SpaceMouseListener::zoomFactorForZAxis)
is a pure function specifically so it could be tested without a live
device: confirmed a centered device (z=0) yields exactly 1.0 (an exact
no-op, not an epsilon-off value that could trip DiagramView::zoom()'s
>=1 branch), and that push/pull produce symmetric zoom-in/out factors.

What genuinely cannot be checked in this environment: which physical axis
is "left/right" vs "up/down" vs "push/forward", their sign, and whether
the ZOOM_DIVISOR/PAN_SCALE constants feel right on real hardware. Both are
named constants specifically so recalibrating them is a one-line change
once someone with a device tries it -- flagged plainly in the PR rather
than presented as verified.

## Not in this commit

Windows/macOS (proprietary 3DxWare SDK, materially bigger lift) and
device button mapping are both explicitly out of scope for this phase per
the discussion.
2026-08-02 21:04:57 +12:00
ispyisail 4e754cfac1 Conductor dock: sync to external property changes (#500)
The dock editor snapshotted the conductor once and never refreshed, so a
change made via the modal Edit-conductor dialog left the dock holding stale
values; on deselection apply() wrote that stale snapshot back, overwriting
the dialog's change. Subscribe to Conductor::propertiesChange and refresh via
updateUi() so the dock mirrors external edits and apply() becomes a no-op
when nothing changed in the dock.
2026-06-19 07:43:27 +12:00
Shane Ringrose 151b16ba76 Conductor dock: English translations + align checkbox wording (#500)
Add English translations (qet_en.ts) for the new conductor-panel strings
so non-French users don't see untranslated source text: the panel title,
the two undo captions, the apply-to-all checkbox, and the View-menu
toggle + status tip.

The apply-to-all checkbox now reuses the modal dialog's exact wording
("Appliquer les propriétés a l'ensemble des conducteurs de ce potentiel")
for consistency and so it shares the existing translation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:38:47 +12:00
Shane Ringrose 0902725a59 Conductor dock: apply-to-all-potential checkbox (#500)
Port the modal dialog's "apply to all conductors" option into the dock
panel, resolving the open propagation question. A persisted checkbox,
pinned above the tabs, makes each edit propagate to every conductor on
the same potential in one undo step - identical semantics to
ConductorPropertiesDialog, but the choice is remembered across edits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:35:49 +12:00
Shane Ringrose 280051745a Conductor dock: apply edits live, edit via label, fill height (#500)
Addresses the testing feedback on the conductor selection-properties
prototype:

1. Edits were never applied. The dock drives every editor through
   setLiveEdit(true), which each editor overrides to connect its field
   changes to apply(); ConductorPropertiesEditorWidget didn't override it,
   so the call was a no-op. Implement setLiveEdit() to connect the hosted
   ConductorPropertiesWidget's controls (commit-style signals) to apply().
   A m_updating guard suppresses the signals emitted while the widget is
   loaded programmatically, so a partial mid-load state is never committed.

2. Clicking a conductor's text label showed nothing, because the label is
   a ConductorTextItem, not a Conductor. Map it to its parentConductor() in
   the editor factory, mirroring the double-click-the-label dialog behaviour.

3. The panel sat at a small size hint with empty space below it. Give the
   editor an Expanding vertical size policy (and a minimum height) so it
   fills the dock like the other editors.

Refs #500

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:35:49 +12:00
Shane Ringrose e3ede85efd conductor dock: scrollable/narrowable + View-menu enable toggle (#500) 2026-06-18 10:35:49 +12:00
Shane Ringrose 95c2f6cf6d proto: conductor properties in selection-properties dock (#500) 2026-06-18 10:35:48 +12:00
261 changed files with 235241 additions and 53029 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates build-essential cmake ninja-build git pkg-config \ ca-certificates build-essential cmake ninja-build git pkg-config \
qt6-base-dev qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools \ qt6-base-dev qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools \
libqt6svg6-dev libqt6sql6-sqlite libcups2-dev \ libqt6svg6-dev libqt6sql6-sqlite libcups2-dev libsqlite3-dev \
libxkbcommon-x11-0 \ libxkbcommon-x11-0 \
xvfb openbox xdotool x11-utils xvfb openbox xdotool x11-utils
# extra-cmake-modules and the KF6 libraries are installed rather than # extra-cmake-modules and the KF6 libraries are installed rather than
+20 -1
View File
@@ -49,6 +49,7 @@ jobs:
mingw-w64-ucrt-x86_64-qt6-tools mingw-w64-ucrt-x86_64-qt6-tools
mingw-w64-ucrt-x86_64-qt6-translations mingw-w64-ucrt-x86_64-qt6-translations
mingw-w64-ucrt-x86_64-qt6-pdf mingw-w64-ucrt-x86_64-qt6-pdf
mingw-w64-ucrt-x86_64-sqlite3
mingw-w64-ucrt-x86_64-pkg-config mingw-w64-ucrt-x86_64-pkg-config
mingw-w64-ucrt-x86_64-kwidgetsaddons mingw-w64-ucrt-x86_64-kwidgetsaddons
mingw-w64-ucrt-x86_64-kcoreaddons mingw-w64-ucrt-x86_64-kcoreaddons
@@ -56,6 +57,7 @@ jobs:
mingw-w64-ucrt-x86_64-nsis mingw-w64-ucrt-x86_64-nsis
mingw-w64-ucrt-x86_64-angleproject mingw-w64-ucrt-x86_64-angleproject
mingw-w64-ucrt-x86_64-qt6-declarative mingw-w64-ucrt-x86_64-qt6-declarative
mingw-w64-ucrt-x86_64-hidapi
- name: Cache ccache - name: Cache ccache
uses: actions/cache@v5 uses: actions/cache@v5
@@ -119,9 +121,12 @@ jobs:
-DCMAKE_POLICY_DEFAULT_CMP0077=NEW \ -DCMAKE_POLICY_DEFAULT_CMP0077=NEW \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DQET_EXPORT_PROJECT_DB=ON \ -DQET_EXPORT_PROJECT_DB=ON \
-DQET_ENABLE_SPACEMOUSE=ON \
-DQET_SPACEMOUSE_BACKEND=hid \
-DCMAKE_C_COMPILER_LAUNCHER=/ucrt64/bin/ccache \ -DCMAKE_C_COMPILER_LAUNCHER=/ucrt64/bin/ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=/ucrt64/bin/ccache \ -DCMAKE_CXX_COMPILER_LAUNCHER=/ucrt64/bin/ccache \
-DSQLite3_INCLUDE_DIR=/ucrt64/include \
-DSQLite3_LIBRARY=/ucrt64/lib/libsqlite3.dll.a \
.. ..
ninja -j"$NPROC" ninja -j"$NPROC"
@@ -144,6 +149,11 @@ jobs:
SIZE=$(stat -c%s "$EXE") SIZE=$(stat -c%s "$EXE")
echo "Exe found: $EXE ($SIZE bytes)" echo "Exe found: $EXE ($SIZE bytes)"
[ "$SIZE" -gt 100000 ] || { echo "ERROR: exe too small"; exit 1; } [ "$SIZE" -gt 100000 ] || { echo "ERROR: exe too small"; exit 1; }
# 3D mouse support (discussion #599): CMake only warns when hidapi
# is missing, which would ship a package without it -- fail here.
ldd "$EXE" | grep -qi 'libhidapi' \
|| { echo "ERROR: exe not linked against hidapi -- 3D mouse support missing"; exit 1; }
echo "3D mouse support: linked against $(ldd "$EXE" | grep -io 'libhidapi[^ ]*' | head -1)"
- name: Deploy — copy exe + windeployqt + DLLs - name: Deploy — copy exe + windeployqt + DLLs
shell: msys2 {0} shell: msys2 {0}
@@ -189,11 +199,20 @@ jobs:
echo "=== $DLL_COUNT DLLs present after scan ===" echo "=== $DLL_COUNT DLLs present after scan ==="
ls -lh "$BIN/QElectroTech.exe" || { echo "ERROR: exe missing from bin/"; exit 1; } ls -lh "$BIN/QElectroTech.exe" || { echo "ERROR: exe missing from bin/"; exit 1; }
[ "$DLL_COUNT" -gt 5 ] || { echo "ERROR: too few DLLs"; exit 1; } [ "$DLL_COUNT" -gt 5 ] || { echo "ERROR: too few DLLs"; exit 1; }
# Without it QElectroTech.exe would not start at all.
[ -f "$BIN/libhidapi-0.dll" ] || { echo "ERROR: libhidapi-0.dll not deployed"; exit 1; }
cd "$GITHUB_WORKSPACE" cd "$GITHUB_WORKSPACE"
cp /ucrt64/bin/libgcc_s_seh-1.dll "$BIN/" cp /ucrt64/bin/libgcc_s_seh-1.dll "$BIN/"
cp /ucrt64/bin/libstdc++-6.dll "$BIN/" cp /ucrt64/bin/libstdc++-6.dll "$BIN/"
cp /ucrt64/bin/libwinpthread-1.dll "$BIN/" cp /ucrt64/bin/libwinpthread-1.dll "$BIN/"
SQLITE=$(find /ucrt64/bin -name "libsqlite3*.dll" | head -1)
if [ -n "$SQLITE" ]; then
cp "$SQLITE" "$BIN/"
echo "SQLite3 copied: $(basename $SQLITE)"
else
echo "WARNING: libsqlite3 not found in /ucrt64/bin/"
fi
cp "$GITHUB_WORKSPACE/build-aux/windows/QET64.nsi" "$NSIS_ROOT/" cp "$GITHUB_WORKSPACE/build-aux/windows/QET64.nsi" "$NSIS_ROOT/"
cp "$GITHUB_WORKSPACE/build-aux/windows/lang_extra.nsh" "$NSIS_ROOT/" cp "$GITHUB_WORKSPACE/build-aux/windows/lang_extra.nsh" "$NSIS_ROOT/"
+1 -9
View File
@@ -9,12 +9,4 @@ doc/*
!doc/QElectroTech.qch !doc/QElectroTech.qch
QElectroTech.tag QElectroTech.tag
!doc/doc-utils !doc/doc-utils
lang/*.qm ui_*.h
# -- IDE settings/intermediate files --
# zed
.zed
.cache
# VS Code
.vscode
+9
View File
@@ -36,6 +36,7 @@ include(cmake/git_update_submodules.cmake)
include(cmake/git_last_commit_sha.cmake) include(cmake/git_last_commit_sha.cmake)
include(cmake/fetch_singleapplication.cmake) include(cmake/fetch_singleapplication.cmake)
include(cmake/fetch_pugixml.cmake) include(cmake/fetch_pugixml.cmake)
include(cmake/find_spacemouse.cmake)
include(cmake/qet_compilation_vars.cmake) include(cmake/qet_compilation_vars.cmake)
set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_INCLUDE_CURRENT_DIR ON)
@@ -169,6 +170,13 @@ else()
) )
endif() endif()
if(QET_SPACEMOUSE_BACKEND_SPNAV_ENABLED)
list(APPEND QET_SPACEMOUSE_LIBRARIES PkgConfig::SPNAV)
endif()
if(QET_SPACEMOUSE_BACKEND_HID_ENABLED)
list(APPEND QET_SPACEMOUSE_LIBRARIES PkgConfig::HIDAPI)
endif()
if(APPLE) if(APPLE)
# CFBundleIdentifier must not be empty. CMake's default Info.plist # CFBundleIdentifier must not be empty. CMake's default Info.plist
# template fills it from MACOSX_BUNDLE_GUI_IDENTIFIER; with that unset # template fills it from MACOSX_BUNDLE_GUI_IDENTIFIER; with that unset
@@ -287,6 +295,7 @@ target_link_libraries(
SQLite3::SQLite3 SQLite3::SQLite3
${KF_PRIVATE_LIBRARIES} ${KF_PRIVATE_LIBRARIES}
${QET_PRIVATE_LIBRARIES} ${QET_PRIVATE_LIBRARIES}
${QET_SPACEMOUSE_LIBRARIES}
) )
target_include_directories( target_include_directories(
+21 -7
View File
@@ -27,12 +27,18 @@ git submodule update --init --recursive
| C++17 compiler | required | GCC or Clang on Unix-like platforms; MSVC or MinGW-w64 g++ on Windows — see [Choosing a compiler](#3-choosing-a-compiler-unix) / [Building on Windows](#6-building-on-windows-msvc--mingw) | | C++17 compiler | required | GCC or Clang on Unix-like platforms; MSVC or MinGW-w64 g++ on Windows — see [Choosing a compiler](#3-choosing-a-compiler-unix) / [Building on Windows](#6-building-on-windows-msvc--mingw) |
| Qt6 base + widgets | required | | | Qt6 base + widgets | required | |
| Qt6 **GuiPrivate** headers | required | needed for clickable PDF hyperlinks; **hard build failure** at CMake generate time if missing, see below | | Qt6 **GuiPrivate** headers | required | needed for clickable PDF hyperlinks; **hard build failure** at CMake generate time if missing, see below |
| SQLite3 | required | used by the nomenclature/summary database |
| Qt Linguist tools (`lrelease`) | required | compiles the tracked `.ts` files into `.qm` as part of every normal build | | Qt Linguist tools (`lrelease`) | required | compiles the tracked `.ts` files into `.qm` as part of every normal build |
| pugixml | handled automatically | fetched and built via CMake FetchContent if not already present on the system — see [pugixml](#8-pugixml) below | | pugixml | handled automatically | fetched and built via CMake FetchContent if not already present on the system — see [pugixml](#8-pugixml) below |
| Qt Test module | required if building tests | `PACKAGE_TESTS` is `ON` by default; QtTest ships as part of the base Qt6 dev packages listed below on every platform, no extra package needed | | Qt Test module | required if building tests | `PACKAGE_TESTS` is `ON` by default; QtTest ships as part of the base Qt6 dev packages listed below on every platform, no extra package needed |
| KDE Frameworks (KF6) | optional | see [Building without KDE Frameworks](#9-building-without-kde-frameworks) | | KDE Frameworks (KF6) | optional | see [Building without KDE Frameworks](#9-building-without-kde-frameworks) |
| QtPdf module | optional | see [PDF page import](#7-pdf-page-import-qtpdf) | | QtPdf module | optional | see [PDF page import](#7-pdf-page-import-qtpdf) |
A note on CMake versions: the project declares a minimum of 3.5 but is
routinely built with much newer releases; if your CMake is older than 4.3 it
simply won't have the newer `SQLite3::SQLite3` target name, which the build
script compensates for automatically. There is nothing you need to do either
way.
## 3. Building (out-of-source build) ## 3. Building (out-of-source build)
@@ -100,7 +106,7 @@ the closest match.
sudo apt install \ sudo apt install \
build-essential cmake ninja-build git \ build-essential cmake ninja-build git \
qt6-base-dev qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools \ qt6-base-dev qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools \
libsqlite3-dev \
libkf6coreaddons-dev libkf6widgetsaddons-dev libkf6coreaddons-dev libkf6widgetsaddons-dev
``` ```
@@ -132,6 +138,7 @@ sudo apt install libpugixml-dev
sudo dnf install \ sudo dnf install \
cmake gcc-c++ git \ cmake gcc-c++ git \
qt6-qtbase-devel qt6-qtbase-private-devel qt6-qttools-devel \ qt6-qtbase-devel qt6-qtbase-private-devel qt6-qttools-devel \
sqlite-devel \
kf6-kcoreaddons-devel kf6-kwidgetsaddons-devel kf6-kcoreaddons-devel kf6-kwidgetsaddons-devel
``` ```
@@ -148,10 +155,11 @@ Optional, for a system pugixml: `sudo dnf install pugixml-devel`.
pkg install \ pkg install \
cmake git \ cmake git \
qt6-base qt6-tools \ qt6-base qt6-tools \
sqlite3 \
kf6-kcoreaddons kf6-kwidgetsaddons kf6-kcoreaddons kf6-kwidgetsaddons
``` ```
(from ports: `devel/qt6-base`, `devel/qt6-tools`, (from ports: `devel/qt6-base`, `devel/qt6-tools`, `databases/sqlite3`,
`devel/kf6-kcoreaddons`, `x11-toolkits/kf6-kwidgetsaddons`.) Qt6's `devel/kf6-kcoreaddons`, `x11-toolkits/kf6-kwidgetsaddons`.) Qt6's
`GuiPrivate` headers ship as part of `qt6-base` on FreeBSD, no separate `GuiPrivate` headers ship as part of `qt6-base` on FreeBSD, no separate
package is needed. `QtPdf` is not packaged on FreeBSD at the time of writing package is needed. `QtPdf` is not packaged on FreeBSD at the time of writing
@@ -167,7 +175,7 @@ Optional, for a system pugixml: `pkg install pugixml` (`devel/pugixml`).
Using [Homebrew](https://brew.sh): Using [Homebrew](https://brew.sh):
```sh ```sh
brew install cmake qt ninja brew install cmake qt sqlite ninja
``` ```
Homebrew's `qt` formula is Qt6 and includes the private headers, so no Homebrew's `qt` formula is Qt6 and includes the private headers, so no
@@ -185,7 +193,7 @@ Optional, for a system pugixml: `brew install pugixml`.
See [Building on Windows](#6-building-on-windows-msvc--mingw) below — the See [Building on Windows](#6-building-on-windows-msvc--mingw) below — the
package sources differ enough from the Unix-like platforms above (no system package sources differ enough from the Unix-like platforms above (no system
package manager and Qt aren't provided the same way) that it gets package manager, SQLite3 and Qt aren't provided the same way) that it gets
its own section. its own section.
## 5. pugixml ## 5. pugixml
@@ -207,7 +215,7 @@ on Debian/Ubuntu, `pugixml-devel` on Fedora).
Both toolchains QET's CMake build targets on Windows are covered here: Both toolchains QET's CMake build targets on Windows are covered here:
**MSVC** (Visual Studio 2019/2022) and **MinGW-w64** (gcc). Unlike the **MSVC** (Visual Studio 2019/2022) and **MinGW-w64** (gcc). Unlike the
Unix-like platforms above, there's no single system package manager, so Unix-like platforms above, there's no single system package manager, so
Qt and (optionally) KDE Frameworks each need to be sourced Qt, SQLite3 and (optionally) KDE Frameworks each need to be sourced
separately per toolchain. separately per toolchain.
One piece of good news either way: unlike Debian/Fedora, the official Qt One piece of good news either way: unlike Debian/Fedora, the official Qt
@@ -228,7 +236,11 @@ normally use `-DBUILD_WITH_KF=OFF` (see the
1. Install Visual Studio with the "Desktop development with C++" workload, 1. Install Visual Studio with the "Desktop development with C++" workload,
and install Qt6 for MSVC (e.g. the `msvc2019_64` or `msvc2022_64` kit) and install Qt6 for MSVC (e.g. the `msvc2019_64` or `msvc2022_64` kit)
via the [Qt Online Installer](https://www.qt.io/download-qt-installer). via the [Qt Online Installer](https://www.qt.io/download-qt-installer).
2. Configure and build from an "x64 Native Tools Command Prompt for VS": 2. Get SQLite3 — the simplest route is [vcpkg](https://vcpkg.io):
```bat
vcpkg install sqlite3:x64-windows
```
3. Configure and build from an "x64 Native Tools Command Prompt for VS":
```bat ```bat
mkdir build && cd build mkdir build && cd build
cmake .. -G "Visual Studio 17 2022" -A x64 ^ cmake .. -G "Visual Studio 17 2022" -A x64 ^
@@ -281,7 +293,9 @@ Using the Qt Online Installer's bundled MinGW kit instead: point
`CMAKE_PREFIX_PATH` at that kit (e.g. `C:\Qt\6.x.x\mingw_64`) and make sure `CMAKE_PREFIX_PATH` at that kit (e.g. `C:\Qt\6.x.x\mingw_64`) and make sure
its bundled `g++.exe` comes first on `PATH`, or pass its bundled `g++.exe` comes first on `PATH`, or pass
`-DCMAKE_C_COMPILER`/`-DCMAKE_CXX_COMPILER` explicitly so CMake doesn't pick `-DCMAKE_C_COMPILER`/`-DCMAKE_CXX_COMPILER` explicitly so CMake doesn't pick
up a different MinGW installation. up a different MinGW installation. SQLite3 still has to come from elsewhere
in this path — vcpkg with a `mingw`-flavoured triplet, or MSYS2's package as
above.
## 7. Qt6 private headers (mandatory) ## 7. Qt6 private headers (mandatory)
@@ -13,7 +13,7 @@
"finish-args": [ "finish-args": [
"--socket=wayland", "--socket=wayland",
"--socket=fallback-x11", "--socket=fallback-x11",
"--device=dri", "--device=all",
"--share=ipc", "--share=ipc",
"--share=network", "--share=network",
"--socket=cups", "--socket=cups",
@@ -33,6 +33,23 @@
"modules": [ "modules": [
"tkinter.json", "tkinter.json",
"pypi-dependencies.json", "pypi-dependencies.json",
{
"name": "hidapi",
"buildsystem": "cmake-ninja",
"config-opts": [
"-DCMAKE_INSTALL_PREFIX=/app",
"-DCMAKE_BUILD_TYPE=Release",
"-DHIDAPI_BUILD_HIDTEST=OFF",
"-DHIDAPI_WITH_TESTS=OFF"
],
"sources": [
{
"type": "git",
"url": "https://github.com/libusb/hidapi.git",
"tag": "hidapi-0.15.0"
}
]
},
{ {
"name": "qelectrotech", "name": "qelectrotech",
"buildsystem": "cmake", "buildsystem": "cmake",
@@ -45,7 +62,9 @@
"-DBUILD_PUGIXML=ON", "-DBUILD_PUGIXML=ON",
"-DFETCHCONTENT_FULLY_DISCONNECTED=ON", "-DFETCHCONTENT_FULLY_DISCONNECTED=ON",
"-DFETCHCONTENT_SOURCE_DIR_PUGIXML=/run/build/qelectrotech/pugixml", "-DFETCHCONTENT_SOURCE_DIR_PUGIXML=/run/build/qelectrotech/pugixml",
"-DQET_EXPORT_PROJECT_DB=ON" "-DQET_EXPORT_PROJECT_DB=ON",
"-DQET_ENABLE_SPACEMOUSE=ON",
"-DQET_SPACEMOUSE_BACKEND=hid"
], ],
"post-install": [ "post-install": [
"mv ${FLATPAK_DEST}/share/mime/packages/qelectrotech.xml ${FLATPAK_DEST}/share/mime/packages/org.qelectrotech.QElectroTech.xml" "mv ${FLATPAK_DEST}/share/mime/packages/qelectrotech.xml ${FLATPAK_DEST}/share/mime/packages/org.qelectrotech.QElectroTech.xml"
+6 -2
View File
@@ -50,6 +50,7 @@ parts:
- python3-lxml - python3-lxml
- python3-tk - python3-tk
- libtk8.6 - libtk8.6
- libspnav0
kde-sdk-setup: kde-sdk-setup:
plugin: nil plugin: nil
@@ -71,6 +72,7 @@ parts:
source: . source: .
stage-packages: stage-packages:
- git - git
- sqlite3
- xdg-user-dirs - xdg-user-dirs
- libqt6qml6 - libqt6qml6
# libxcb-cursor0 workaround was needed against the Qt5/KF5 core22 content # libxcb-cursor0 workaround was needed against the Qt5/KF5 core22 content
@@ -81,7 +83,7 @@ parts:
- git - git
- cmake - cmake
- ninja-build - ninja-build
- libsqlite3-dev
- qt6-tools-dev - qt6-tools-dev
- qt6-base-private-dev - qt6-base-private-dev
- qt6-declarative-dev - qt6-declarative-dev
@@ -91,6 +93,7 @@ parts:
# runs FindCups at configure time and fails without the CUPS headers. # runs FindCups at configure time and fails without the CUPS headers.
# Build-time only: nothing from it is staged into the snap. # Build-time only: nothing from it is staged into the snap.
- libcups2-dev - libcups2-dev
- libspnav-dev
override-build: | override-build: |
displayed_version=$(cat sources/qetversion.cpp | grep "return QVersionNumber{"| head -n 1| awk -F "{" '{ print $2 }' | awk -F "}" '{ print $1 }' | sed -e 's/,/./g' -e 's/ //g') displayed_version=$(cat sources/qetversion.cpp | grep "return QVersionNumber{"| head -n 1| awk -F "{" '{ print $2 }' | awk -F "}" '{ print $1 }' | sed -e 's/,/./g' -e 's/ //g')
snap_version="${displayed_version}-g$(git rev-parse --short=8 HEAD)" snap_version="${displayed_version}-g$(git rev-parse --short=8 HEAD)"
@@ -105,7 +108,8 @@ parts:
-DBUILD_KF=OFF \ -DBUILD_KF=OFF \
-DPACKAGE_TESTS=OFF \ -DPACKAGE_TESTS=OFF \
-DBUILD_PUGIXML=ON \ -DBUILD_PUGIXML=ON \
-DQET_EXPORT_PROJECT_DB=ON -DQET_EXPORT_PROJECT_DB=ON \
-DQET_ENABLE_SPACEMOUSE=ON
ninja -C build -j${CRAFT_PARALLEL_BUILD_COUNT} ninja -C build -j${CRAFT_PARALLEL_BUILD_COUNT}
DESTDIR="$CRAFT_PART_INSTALL" ninja -C build install DESTDIR="$CRAFT_PART_INSTALL" ninja -C build install
override-stage: | override-stage: |
+6 -1
View File
@@ -31,7 +31,7 @@ add_definitions(-DQT_MESSAGELOGCONTEXT)
# In order to do so, uncomment the following line. # In order to do so, uncomment the following line.
#add_definitions(-DTODO_LIST) #add_definitions(-DTODO_LIST)
# Build with KDE Frameworks. # Build with KDE Frameworks.
option(BUILD_WITH_KF "Build with KDE Frameworks" ON) option(BUILD_WITH_KF "Build with KDE Frameworks" ON)
# Precompiled headers for the Qt umbrella headers. # Precompiled headers for the Qt umbrella headers.
@@ -47,3 +47,8 @@ option(BUILD_WITH_KF "Build with KDE Frameworks" ON)
# compile for everyone else. Leaving it off keeps CI and contributors on the # compile for everyone else. Leaving it off keeps CI and contributors on the
# strict behaviour, and only developers who opt in trade that for the speed. # strict behaviour, and only developers who opt in trade that for the speed.
option(QET_ENABLE_PCH "Use precompiled headers (developer build speed; may mask missing #includes)" OFF) option(QET_ENABLE_PCH "Use precompiled headers (developer build speed; may mask missing #includes)" OFF)
# Discussion #599: 3Dconnexion SpaceMouse/SpacePilot pan/zoom support. Off by
# default -- see cmake/find_spacemouse.cmake for the backends and what happens
# when it is on but no library is found.
option(QET_ENABLE_SPACEMOUSE "Build with 3D mouse (3Dconnexion SpaceMouse) support for pan/zoom; needs libspnav or hidapi, see QET_SPACEMOUSE_BACKEND" OFF)
+95
View File
@@ -0,0 +1,95 @@
# Copyright 2006 The QElectroTech Team
# This file is part of QElectroTech.
#
# QElectroTech is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# QElectroTech is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
message(" - find_spacemouse")
# QET_ENABLE_SPACEMOUSE (cmake/developer_options.cmake) is off by default,
# so none of this runs, and the default build is entirely unaffected: no new
# dependency, no new source files, no new symbols.
#
# When it is on, QET_SPACEMOUSE_BACKEND picks how the device is read -- see
# sources/spacemouse/spacemousebackend.h for what a backend is:
# spnav Linux, through the spacenavd daemon (libspnav)
# hid any platform, directly over USB (hidapi), no 3Dconnexion driver
# auto spnav on Linux when libspnav is found, hid otherwise
# On macOS the 3DxWare backend is added to whichever of these is chosen.
# A backend whose library is not found downgrades the option to off with a
# warning, rather than failing configure for an opt-in feature.
set(QET_SPACEMOUSE_BACKEND "auto" CACHE STRING "3D mouse backend: auto, spnav or hid")
set_property(CACHE QET_SPACEMOUSE_BACKEND PROPERTY STRINGS auto spnav hid)
set(QET_SPACEMOUSE_ENABLED FALSE)
set(QET_SPACEMOUSE_BACKEND_SPNAV_ENABLED FALSE)
set(QET_SPACEMOUSE_BACKEND_HID_ENABLED FALSE)
set(QET_SPACEMOUSE_BACKEND_CONNEXION_ENABLED FALSE)
if(QET_ENABLE_SPACEMOUSE)
find_package(PkgConfig)
set(_qet_spacemouse_try_spnav FALSE)
set(_qet_spacemouse_try_hid FALSE)
if(QET_SPACEMOUSE_BACKEND STREQUAL "spnav")
set(_qet_spacemouse_try_spnav TRUE)
elseif(QET_SPACEMOUSE_BACKEND STREQUAL "hid")
set(_qet_spacemouse_try_hid TRUE)
else()
if(UNIX AND NOT APPLE)
set(_qet_spacemouse_try_spnav TRUE)
endif()
set(_qet_spacemouse_try_hid TRUE)
endif()
# libspnav: Debian/Ubuntu's libspnav-dev ships spnav.pc.
if(_qet_spacemouse_try_spnav AND PkgConfig_FOUND)
pkg_check_modules(SPNAV IMPORTED_TARGET spnav)
if(SPNAV_FOUND)
set(QET_SPACEMOUSE_ENABLED TRUE)
set(QET_SPACEMOUSE_BACKEND_SPNAV_ENABLED TRUE)
add_definitions(-DQET_SPACEMOUSE_BACKEND_SPNAV)
message("QET_ENABLE_SPACEMOUSE ON (backend: libspnav ${SPNAV_VERSION})")
endif()
endif()
# hidapi: hidapi-hidraw.pc on Linux (libhidapi-dev), hidapi.pc from
# MSYS2 (mingw-w64-ucrt-x86_64-hidapi) and Homebrew (hidapi).
if(NOT QET_SPACEMOUSE_ENABLED AND _qet_spacemouse_try_hid AND PkgConfig_FOUND)
pkg_search_module(HIDAPI IMPORTED_TARGET hidapi-hidraw hidapi)
if(HIDAPI_FOUND)
set(QET_SPACEMOUSE_ENABLED TRUE)
set(QET_SPACEMOUSE_BACKEND_HID_ENABLED TRUE)
add_definitions(-DQET_SPACEMOUSE_BACKEND_HID)
message("QET_ENABLE_SPACEMOUSE ON (backend: hidapi ${HIDAPI_VERSION})")
endif()
endif()
# macOS: with 3DxWare installed the device can only be read through
# 3DxWare, so its backend comes too, whichever backend was asked for.
# It loads 3DxWare's library at run time and needs nothing to build.
if(APPLE)
set(QET_SPACEMOUSE_ENABLED TRUE)
set(QET_SPACEMOUSE_BACKEND_CONNEXION_ENABLED TRUE)
add_definitions(-DQET_SPACEMOUSE_BACKEND_CONNEXION)
message("QET_ENABLE_SPACEMOUSE ON (backend: 3DxWare when installed)")
endif()
if(QET_SPACEMOUSE_ENABLED)
add_definitions(-DQET_SPACEMOUSE_SUPPORT)
else()
message(WARNING "QET_ENABLE_SPACEMOUSE is ON but no library was found for the "
"'${QET_SPACEMOUSE_BACKEND}' backend (libspnav-dev for spnav, "
"hidapi for hid, via pkg-config) -- building WITHOUT 3D mouse support.")
endif()
endif()
+67
View File
@@ -168,6 +168,9 @@ set(QET_SRC_FILES
${QET_DIR}/sources/borderproperties.h ${QET_DIR}/sources/borderproperties.h
${QET_DIR}/sources/bordertitleblock.cpp ${QET_DIR}/sources/bordertitleblock.cpp
${QET_DIR}/sources/bordertitleblock.h ${QET_DIR}/sources/bordertitleblock.h
${QET_DIR}/sources/bordercelllabels.h
${QET_DIR}/sources/cellruler.cpp
${QET_DIR}/sources/cellruler.h
${QET_DIR}/sources/conductorautonumerotation.cpp ${QET_DIR}/sources/conductorautonumerotation.cpp
${QET_DIR}/sources/conductorautonumerotation.h ${QET_DIR}/sources/conductorautonumerotation.h
${QET_DIR}/sources/conductornumexport.cpp ${QET_DIR}/sources/conductornumexport.cpp
@@ -189,6 +192,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/configdialog.h ${QET_DIR}/sources/configdialog.h
${QET_DIR}/sources/createdxf.cpp ${QET_DIR}/sources/createdxf.cpp
${QET_DIR}/sources/createdxf.h ${QET_DIR}/sources/createdxf.h
${QET_DIR}/sources/dxfexport.cpp
${QET_DIR}/sources/dxfexport.h
${QET_DIR}/sources/dxfpaintdevice.cpp ${QET_DIR}/sources/dxfpaintdevice.cpp
${QET_DIR}/sources/dxfpaintdevice.h ${QET_DIR}/sources/dxfpaintdevice.h
${QET_DIR}/sources/diagramcommands.cpp ${QET_DIR}/sources/diagramcommands.cpp
@@ -229,6 +234,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/exportpropertieswidget.h ${QET_DIR}/sources/exportpropertieswidget.h
${QET_DIR}/sources/genericpanel.cpp ${QET_DIR}/sources/genericpanel.cpp
${QET_DIR}/sources/genericpanel.h ${QET_DIR}/sources/genericpanel.h
${QET_DIR}/sources/itemgroups.cpp
${QET_DIR}/sources/itemgroups.h
${QET_DIR}/sources/lastusedstyle.cpp ${QET_DIR}/sources/lastusedstyle.cpp
${QET_DIR}/sources/lastusedstyle.h ${QET_DIR}/sources/lastusedstyle.h
${QET_DIR}/sources/machine_info.cpp ${QET_DIR}/sources/machine_info.cpp
@@ -282,6 +289,14 @@ set(QET_SRC_FILES
${QET_DIR}/sources/recentfiles.h ${QET_DIR}/sources/recentfiles.h
${QET_DIR}/sources/shortcutmanager.cpp ${QET_DIR}/sources/shortcutmanager.cpp
${QET_DIR}/sources/shortcutmanager.h ${QET_DIR}/sources/shortcutmanager.h
${QET_DIR}/sources/shortcutbarsettings.cpp
${QET_DIR}/sources/shortcutbarsettings.h
${QET_DIR}/sources/diagramcontexttoolbar.cpp
${QET_DIR}/sources/diagramcontexttoolbar.h
${QET_DIR}/sources/diagramgestureoverlay.cpp
${QET_DIR}/sources/diagramgestureoverlay.h
${QET_DIR}/sources/commandsearchpopup.cpp
${QET_DIR}/sources/commandsearchpopup.h
${QET_DIR}/sources/titleblockcell.cpp ${QET_DIR}/sources/titleblockcell.cpp
${QET_DIR}/sources/titleblockcell.h ${QET_DIR}/sources/titleblockcell.h
${QET_DIR}/sources/titleblockproperties.cpp ${QET_DIR}/sources/titleblockproperties.cpp
@@ -297,6 +312,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/autoNum/numerotationcontextcommands.h ${QET_DIR}/sources/autoNum/numerotationcontextcommands.h
${QET_DIR}/sources/autoNum/numerotationcontext.cpp ${QET_DIR}/sources/autoNum/numerotationcontext.cpp
${QET_DIR}/sources/autoNum/numerotationcontext.h ${QET_DIR}/sources/autoNum/numerotationcontext.h
${QET_DIR}/sources/autoNum/renumberelementscommand.cpp
${QET_DIR}/sources/autoNum/renumberelementscommand.h
${QET_DIR}/sources/autoNum/ui/autonumberingdockwidget.cpp ${QET_DIR}/sources/autoNum/ui/autonumberingdockwidget.cpp
${QET_DIR}/sources/autoNum/ui/autonumberingdockwidget.h ${QET_DIR}/sources/autoNum/ui/autonumberingdockwidget.h
${QET_DIR}/sources/autoNum/ui/autonumberingmanagementw.cpp ${QET_DIR}/sources/autoNum/ui/autonumberingmanagementw.cpp
@@ -307,9 +324,12 @@ set(QET_SRC_FILES
${QET_DIR}/sources/autoNum/ui/formulaautonumberingw.h ${QET_DIR}/sources/autoNum/ui/formulaautonumberingw.h
${QET_DIR}/sources/autoNum/ui/numparteditorw.cpp ${QET_DIR}/sources/autoNum/ui/numparteditorw.cpp
${QET_DIR}/sources/autoNum/ui/numparteditorw.h ${QET_DIR}/sources/autoNum/ui/numparteditorw.h
${QET_DIR}/sources/autoNum/ui/renumberelementsdialog.cpp
${QET_DIR}/sources/autoNum/ui/renumberelementsdialog.h
${QET_DIR}/sources/autoNum/ui/selectautonumw.cpp ${QET_DIR}/sources/autoNum/ui/selectautonumw.cpp
${QET_DIR}/sources/autoNum/ui/selectautonumw.h ${QET_DIR}/sources/autoNum/ui/selectautonumw.h
${QET_DIR}/sources/dataBase/legacyelementtypes.h
${QET_DIR}/sources/dataBase/projectdatabase.cpp ${QET_DIR}/sources/dataBase/projectdatabase.cpp
${QET_DIR}/sources/dataBase/projectdatabase.h ${QET_DIR}/sources/dataBase/projectdatabase.h
${QET_DIR}/sources/dataBase/sqlreadonly.cpp ${QET_DIR}/sources/dataBase/sqlreadonly.cpp
@@ -444,6 +464,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/ElementsCollection/elementpreviewdelegate.h ${QET_DIR}/sources/ElementsCollection/elementpreviewdelegate.h
${QET_DIR}/sources/ElementsCollection/elementscollectionmodel.cpp ${QET_DIR}/sources/ElementsCollection/elementscollectionmodel.cpp
${QET_DIR}/sources/ElementsCollection/elementscollectionmodel.h ${QET_DIR}/sources/ElementsCollection/elementscollectionmodel.h
${QET_DIR}/sources/ElementsCollection/elementpickerpopup.cpp
${QET_DIR}/sources/ElementsCollection/elementpickerpopup.h
${QET_DIR}/sources/ElementsCollection/elementscollectionwidget.cpp ${QET_DIR}/sources/ElementsCollection/elementscollectionwidget.cpp
${QET_DIR}/sources/ElementsCollection/elementscollectionwidget.h ${QET_DIR}/sources/ElementsCollection/elementscollectionwidget.h
${QET_DIR}/sources/ElementsCollection/elementslocation.cpp ${QET_DIR}/sources/ElementsCollection/elementslocation.cpp
@@ -699,6 +721,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/ui/borderpropertieswidget.h ${QET_DIR}/sources/ui/borderpropertieswidget.h
${QET_DIR}/sources/ui/compositetexteditdialog.cpp ${QET_DIR}/sources/ui/compositetexteditdialog.cpp
${QET_DIR}/sources/ui/compositetexteditdialog.h ${QET_DIR}/sources/ui/compositetexteditdialog.h
${QET_DIR}/sources/ui/conductorpropertieseditorwidget.cpp
${QET_DIR}/sources/ui/conductorpropertieseditorwidget.h
${QET_DIR}/sources/ui/contactgroupselectiondialog.cpp ${QET_DIR}/sources/ui/contactgroupselectiondialog.cpp
${QET_DIR}/sources/ui/contactgroupselectiondialog.h ${QET_DIR}/sources/ui/contactgroupselectiondialog.h
${QET_DIR}/sources/ui/conductorpropertiesdialog.cpp ${QET_DIR}/sources/ui/conductorpropertiesdialog.cpp
@@ -790,6 +814,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/ui/configpage/guidespropertieswidget.h ${QET_DIR}/sources/ui/configpage/guidespropertieswidget.h
${QET_DIR}/sources/ui/configpage/shortcutsconfigpage.cpp ${QET_DIR}/sources/ui/configpage/shortcutsconfigpage.cpp
${QET_DIR}/sources/ui/configpage/shortcutsconfigpage.h ${QET_DIR}/sources/ui/configpage/shortcutsconfigpage.h
${QET_DIR}/sources/ui/configpage/shortcutbarconfigpage.cpp
${QET_DIR}/sources/ui/configpage/shortcutbarconfigpage.h
${QET_DIR}/sources/undocommand/addelementtextcommand.cpp ${QET_DIR}/sources/undocommand/addelementtextcommand.cpp
${QET_DIR}/sources/undocommand/addelementtextcommand.h ${QET_DIR}/sources/undocommand/addelementtextcommand.h
@@ -815,6 +841,10 @@ set(QET_SRC_FILES
${QET_DIR}/sources/undocommand/removediagramcommand.h ${QET_DIR}/sources/undocommand/removediagramcommand.h
${QET_DIR}/sources/undocommand/setautonumcontextcommand.cpp ${QET_DIR}/sources/undocommand/setautonumcontextcommand.cpp
${QET_DIR}/sources/undocommand/setautonumcontextcommand.h ${QET_DIR}/sources/undocommand/setautonumcontextcommand.h
${QET_DIR}/sources/undocommand/groupitemscommand.cpp
${QET_DIR}/sources/undocommand/groupitemscommand.h
${QET_DIR}/sources/undocommand/alignselectioncommand.cpp
${QET_DIR}/sources/undocommand/alignselectioncommand.h
${QET_DIR}/sources/undocommand/rotateselectioncommand.cpp ${QET_DIR}/sources/undocommand/rotateselectioncommand.cpp
${QET_DIR}/sources/undocommand/rotateselectioncommand.h ${QET_DIR}/sources/undocommand/rotateselectioncommand.h
${QET_DIR}/sources/undocommand/promoteshapecommand.cpp ${QET_DIR}/sources/undocommand/promoteshapecommand.cpp
@@ -871,6 +901,43 @@ list(APPEND QET_SRC_FILES
${QET_DIR}/sources/scripting/qetscripting.h ${QET_DIR}/sources/scripting/qetscripting.h
) )
if(QET_SPACEMOUSE_ENABLED)
list(APPEND QET_SRC_FILES
${QET_DIR}/sources/spacemouse/spacemousebackend.h
${QET_DIR}/sources/spacemouse/spacemousebuttonmap.cpp
${QET_DIR}/sources/spacemouse/spacemousebuttonmap.h
${QET_DIR}/sources/spacemouse/spacemouselistener.cpp
${QET_DIR}/sources/spacemouse/spacemouselistener.h
${QET_DIR}/sources/spacemouse/spacemousemotion.cpp
${QET_DIR}/sources/spacemouse/spacemousemotion.h
${QET_DIR}/sources/ui/configpage/spacemouseconfigpage.cpp
${QET_DIR}/sources/ui/configpage/spacemouseconfigpage.h
)
endif()
if(QET_SPACEMOUSE_BACKEND_SPNAV_ENABLED)
list(APPEND QET_SRC_FILES
${QET_DIR}/sources/spacemouse/spnavbackend.cpp
${QET_DIR}/sources/spacemouse/spnavbackend.h
)
endif()
if(QET_SPACEMOUSE_BACKEND_HID_ENABLED)
list(APPEND QET_SRC_FILES
${QET_DIR}/sources/spacemouse/hidbackend.cpp
${QET_DIR}/sources/spacemouse/hidbackend.h
${QET_DIR}/sources/spacemouse/spacemousehid.cpp
${QET_DIR}/sources/spacemouse/spacemousehid.h
)
endif()
if(QET_SPACEMOUSE_BACKEND_CONNEXION_ENABLED)
list(APPEND QET_SRC_FILES
${QET_DIR}/sources/spacemouse/connexionbackend.cpp
${QET_DIR}/sources/spacemouse/connexionbackend.h
)
endif()
set(TS_FILES set(TS_FILES
${QET_DIR}/lang/qet_ar.ts ${QET_DIR}/lang/qet_ar.ts
${QET_DIR}/lang/qet_ca.ts ${QET_DIR}/lang/qet_ca.ts
File diff suppressed because it is too large Load Diff
+222
View File
@@ -0,0 +1,222 @@
<!DOCTYPE QPH>
<QPH sourcelanguage="fr_FR" language="ca">
<phrase>
<source>Réf. croisée</source>
<target>Ref. creuada</target>
</phrase>
<phrase>
<source>armoire</source>
<target>armari</target>
<definition>cabinet / enclosure</definition>
</phrase>
<phrase>
<source>bobine</source>
<target>bobina</target>
<definition>coil</definition>
</phrase>
<phrase>
<source>borne</source>
<target>born</target>
<definition>terminal - a connection point on a symbol</definition>
</phrase>
<phrase>
<source>bornier</source>
<target>bornes</target>
<definition>terminal strip</definition>
</phrase>
<phrase>
<source>cable</source>
<target>cable</target>
<definition>cable</definition>
</phrase>
<phrase>
<source>calibre</source>
<target>calibre</target>
<definition>rating - the current rating of a device</definition>
</phrase>
<phrase>
<source>cartouche</source>
<target>cartutx</target>
<definition>title block - the framed information panel</definition>
</phrase>
<phrase>
<source>champ de texte</source>
<target>camp de text</target>
<definition>text field</definition>
</phrase>
<phrase>
<source>collection</source>
<target>col·lecció</target>
<definition>collection - the symbol library</definition>
</phrase>
<phrase>
<source>conducteur</source>
<target>conductor</target>
<definition>conductor - the drawn wire between terminals</definition>
</phrase>
<phrase>
<source>contact</source>
<target>contacte</target>
<definition>contact</definition>
</phrase>
<phrase>
<source>contacteur</source>
<target>contactor</target>
<definition>contactor</definition>
</phrase>
<phrase>
<source>courant</source>
<target>corrent</target>
<definition>current</definition>
</phrase>
<phrase>
<source>disjoncteur</source>
<target>disjuntor</target>
<definition>circuit breaker</definition>
</phrase>
<phrase>
<source>dossier</source>
<target>carpeta</target>
<definition>folder</definition>
</phrase>
<phrase>
<source>element</source>
<target>element</target>
<definition>element - a symbol placed on a sheet</definition>
</phrase>
<phrase>
<source>esclave</source>
<target>esclau</target>
<definition>slave - the contact block belonging to a master</definition>
</phrase>
<phrase>
<source>fil</source>
<target>fil</target>
<definition>wire</definition>
</phrase>
<phrase>
<source>folio</source>
<target>plec</target>
<definition>sheet - one drawing of a set</definition>
</phrase>
<phrase>
<source>fusible</source>
<target>fusible</target>
<definition>fuse</definition>
</phrase>
<phrase>
<source>grille</source>
<target>graella</target>
<definition>grid</definition>
</phrase>
<phrase>
<source>interrupteur</source>
<target>interruptor</target>
<definition>switch</definition>
</phrase>
<phrase>
<source>maitre</source>
<target>mestre</target>
<definition>master - the element a cross-reference points from</definition>
</phrase>
<phrase>
<source>modele</source>
<target>plantilla</target>
<definition>template</definition>
</phrase>
<phrase>
<source>moteur</source>
<target>motor</target>
<definition>motor</definition>
</phrase>
<phrase>
<source>neutre</source>
<target>neutre</target>
<definition>neutral</definition>
</phrase>
<phrase>
<source>nomenclature</source>
<target>nomenclatura</target>
<definition>parts list / bill of materials</definition>
</phrase>
<phrase>
<source>numerotation</source>
<target>numeració</target>
<definition>numbering - automatic wire and part numbering</definition>
</phrase>
<phrase>
<source>phase</source>
<target>fase</target>
<definition>phase</definition>
</phrase>
<phrase>
<source>potentiel</source>
<target>potencial</target>
<definition>potential - conductors electrically joined as one node</definition>
</phrase>
<phrase>
<source>projet</source>
<target>projecte</target>
<definition>project - the .qet file holding the sheets</definition>
</phrase>
<phrase>
<source>puissance</source>
<target>potència</target>
<definition>power</definition>
</phrase>
<phrase>
<source>relais</source>
<target>relé</target>
<definition>relay</definition>
</phrase>
<phrase>
<source>renvoi de folio</source>
<target>referència de plec</target>
<definition>sheet cross-reference</definition>
</phrase>
<phrase>
<source>repere</source>
<target>referència</target>
<definition>designation - the identifying mark on a part</definition>
</phrase>
<phrase>
<source>report de folio</source>
<target>remissió de plec</target>
<definition>sheet cross-reference (the arrow symbol)</definition>
</phrase>
<phrase>
<source>schema</source>
<target>esquema</target>
<definition>diagram</definition>
</phrase>
<phrase>
<source>section</source>
<target>secció</target>
<definition>cross-section - conductor size in mm2</definition>
</phrase>
<phrase>
<source>sommaire</source>
<target>sumari</target>
<definition>summary / table of contents</definition>
</phrase>
<phrase>
<source>tension</source>
<target>tensió</target>
<definition>voltage</definition>
</phrase>
<phrase>
<source>terre</source>
<target>terra</target>
<definition>earth / ground</definition>
</phrase>
<phrase>
<source>texte</source>
<target>text</target>
<definition>text</definition>
</phrase>
<phrase>
<source>transformateur</source>
<target>transformador</target>
<definition>transformer</definition>
</phrase>
</QPH>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4
View File
@@ -100,4 +100,8 @@
<source>traduction</source> <source>traduction</source>
<target>Übersetzung</target> <target>Übersetzung</target>
</phrase> </phrase>
<phrase>
<source>Modifier le nom de la borne</source>
<target>Klemmenname ändern</target>
</phrase>
</QPH> </QPH>
File diff suppressed because it is too large Load Diff
+918
View File
@@ -0,0 +1,918 @@
<!DOCTYPE QPH>
<QPH sourcelanguage="fr_FR" language="en">
<phrase>
<source>Blue : Blue</source>
<target>Blue : Blue</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Blue : CornflowerBlue</source>
<target>Blue : CornflowerBlue</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Blue : DarkBlue</source>
<target>Blue : DarkBlue</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Blue : DeepSkyBlue</source>
<target>Blue : DeepSkyBlue</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Blue : DodgerBlue</source>
<target>Blue : DodgerBlue</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Blue : LightBlue</source>
<target>Blue : LightBlue</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Blue : LightSkyBlue</source>
<target>Blue : LightSkyBlue</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Blue : LightSteelBlue</source>
<target>Blue : LightSteelBlue</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Blue : MediumBlue</source>
<target>Blue : MediumBlue</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Blue : MidnightBlue</source>
<target>Blue : MidnightBlue</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Blue : Navy</source>
<target>Blue : Navy</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Blue : PowderBlue</source>
<target>Blue : PowderBlue</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Blue : RoyalBlue</source>
<target>Blue : RoyalBlue</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Blue : SkyBlue</source>
<target>Blue : SkyBlue</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Blue : SteelBlue</source>
<target>Blue : SteelBlue</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Brown : Bisque</source>
<target>Brown : Bisque</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Brown : BlanchedAlmond</source>
<target>Brown : BlanchedAlmond</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Brown : Brown</source>
<target>Brown : Brown</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Brown : Burlywood</source>
<target>Brown : Burlywood</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Brown : Chocolate</source>
<target>Brown : Chocolate</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Brown : Cornsilk</source>
<target>Brown : Cornsilk</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Brown : DarkGoldenrod</source>
<target>Brown : DarkGoldenrod</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Brown : Goldenrod</source>
<target>Brown : Goldenrod</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Brown : Maroon</source>
<target>Brown : Maroon</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Brown : NavajoWhite</source>
<target>Brown : NavajoWhite</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Brown : Peru</source>
<target>Brown : Peru</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Brown : RosyBrown</source>
<target>Brown : RosyBrown</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Brown : SaddleBrown</source>
<target>Brown : SaddleBrown</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Brown : SandyBrown</source>
<target>Brown : SandyBrown</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Brown : Sienna</source>
<target>Brown : Sienna</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Brown : Tan</source>
<target>Brown : Tan</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Brown : Wheat</source>
<target>Brown : Wheat</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Cyan : Aqua</source>
<target>Cyan : Aqua</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Cyan : Aquamarine</source>
<target>Cyan : Aquamarine</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Cyan : CadetBlue</source>
<target>Cyan : CadetBlue</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Cyan : Cyan</source>
<target>Cyan : Cyan</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Cyan : DarkCyan</source>
<target>Cyan : DarkCyan</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Cyan : DarkTurquoise</source>
<target>Cyan : DarkTurquoise</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Cyan : LightCyan</source>
<target>Cyan : LightCyan</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Cyan : LightSeaGreen</source>
<target>Cyan : LightSeaGreen</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Cyan : MediumTurquoise</source>
<target>Cyan : MediumTurquoise</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Cyan : PaleTurquoise</source>
<target>Cyan : PaleTurquoise</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Cyan : Teal</source>
<target>Cyan : Teal</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Cyan : Turquoise</source>
<target>Cyan : Turquoise</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Gray : Black</source>
<target>Gray : Black</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Gray : DarkGray</source>
<target>Gray : DarkGray</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Gray : DarkSlateGray</source>
<target>Gray : DarkSlateGray</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Gray : DimGray</source>
<target>Gray : DimGray</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Gray : Gainsboro</source>
<target>Gray : Gainsboro</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Gray : Gray</source>
<target>Gray : Gray</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Gray : LightGray</source>
<target>Gray : LightGray</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Gray : LightSlateGray</source>
<target>Gray : LightSlateGray</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Gray : Silver</source>
<target>Gray : Silver</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Gray : SlateGray</source>
<target>Gray : SlateGray</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Green : Chartreuse</source>
<target>Green : Chartreuse</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Green : DarkGreen</source>
<target>Green : DarkGreen</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Green : DarkOliveGreen</source>
<target>Green : DarkOliveGreen</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Green : DarkSeaGreen</source>
<target>Green : DarkSeaGreen</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Green : ForestGreen</source>
<target>Green : ForestGreen</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Green : Green</source>
<target>Green : Green</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Green : GreenYellow</source>
<target>Green : GreenYellow</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Green : LawnGreen</source>
<target>Green : LawnGreen</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Green : LightGreen</source>
<target>Green : LightGreen</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Green : Lime</source>
<target>Green : Lime</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Green : LimeGreen</source>
<target>Green : LimeGreen</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Green : MediumAquamarine</source>
<target>Green : MediumAquamarine</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Green : MediumSeaGreen</source>
<target>Green : MediumSeaGreen</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Green : MediumSpringGreen</source>
<target>Green : MediumSpringGreen</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Green : Olive</source>
<target>Green : Olive</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Green : OliveDrab</source>
<target>Green : OliveDrab</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Green : PaleGreen</source>
<target>Green : PaleGreen</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Green : SeaGreen</source>
<target>Green : SeaGreen</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Green : SpringGreen</source>
<target>Green : SpringGreen</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Green : YellowGreen</source>
<target>Green : YellowGreen</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Orange : Coral</source>
<target>Orange : Coral</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Orange : DarkOrange</source>
<target>Orange : DarkOrange</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Orange : Orange</source>
<target>Orange : Orange</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Orange : OrangeRed</source>
<target>Orange : OrangeRed</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Orange : Tomato</source>
<target>Orange : Tomato</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Pink : DeepPink</source>
<target>Pink: DeepPink</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Pink : HotPink</source>
<target>Pink: HotPink</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Pink : LightPink</source>
<target>Pink: LightPink</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Pink : MediumVioletRed</source>
<target>Pink: MediumVioletRed</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Pink : PaleVioletRed</source>
<target>Pink: PaleVioletRed</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Pink : Pink</source>
<target>Pink: Pink</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Purple : BlueViolet</source>
<target>Purple : BlueViolet</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Purple : DarkMagenta</source>
<target>Purple : DarkMagenta</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Purple : DarkOrchid</source>
<target>Purple : DarkOrchid</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Purple : DarkSlateBlue</source>
<target>Purple : DarkSlateBlue</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Purple : DarkViolet</source>
<target>Purple : DarkViolet</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Purple : Fuchsia</source>
<target>Purple : Fuchsia</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Purple : Indigo</source>
<target>Purple : Indigo</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Purple : Lavender</source>
<target>Purple : Lavender</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Purple : Magenta</source>
<target>Purple : Magenta</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Purple : MediumOrchid</source>
<target>Purple : MediumOrchid</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Purple : MediumPurple</source>
<target>Purple : MediumPurple</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Purple : MediumSlateBlue</source>
<target>Purple : MediumSlateBlue</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Purple : Orchid</source>
<target>Purple : Orchid</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Purple : Plum</source>
<target>Purple : Plum</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Purple : Purple</source>
<target>Purple : Purple</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Purple : SlateBlue</source>
<target>Purple : SlateBlue</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Purple : Thistle</source>
<target>Purple : Thistle</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Purple : Violet</source>
<target>Purple : Violet</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Red : Crimson</source>
<target>Red : Crimson</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Red : DarkRed</source>
<target>Red : DarkRed</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Red : DarkSalmon</source>
<target>Red: DarkSalmon</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Red : Firebrick</source>
<target>Red : Firebrick</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Red : IndianRed</source>
<target>Red: IndianRed</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Red : LightCoral</source>
<target>Red: LightCoral</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Red : LightSalmon</source>
<target>Red: LightSalmon</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Red : Red</source>
<target>Red : Red</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Red : Salmon</source>
<target>Red: Salmon</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>White : AliceBlue</source>
<target>White : AliceBlue</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>White : AntiqueWhite</source>
<target>White : AntiqueWhite</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>White : Azure</source>
<target>White : Azure</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>White : Beige</source>
<target>White : Beige</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>White : FloralWhite</source>
<target>White : FloralWhite</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>White : GhostWhite</source>
<target>White : GhostWhite</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>White : Honeydew</source>
<target>White : Honeydew</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>White : Ivory</source>
<target>White : Ivory</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>White : LavenderBlush</source>
<target>White : LavenderBlush</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>White : Linen</source>
<target>White : Linen</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>White : MintCream</source>
<target>White : MintCream</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>White : MistyRose</source>
<target>White : MistyRose</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>White : OldLace</source>
<target>White : OldLace</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>White : Seashell</source>
<target>White : Seashell</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>White : Snow</source>
<target>White : Snow</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>White : White</source>
<target>White : White</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>White : WhiteSmoke</source>
<target>White : WhiteSmoke</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Yellow : DarkKhaki</source>
<target>Yellow : DarkKhaki</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Yellow : Gold</source>
<target>Yellow : Gold</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Yellow : Khaki</source>
<target>Yellow : Khaki</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Yellow : LemonChiffon</source>
<target>Yellow : LemonChiffon</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Yellow : LightGoldenrodYellow</source>
<target>Yellow : LightGoldenrodYellow</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Yellow : LightYellow</source>
<target>Yellow : LightYellow</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Yellow : Moccasin</source>
<target>Yellow : Moccasin</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Yellow : PaleGoldenrod</source>
<target>Yellow : PaleGoldenrod</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Yellow : PapayaWhip</source>
<target>Yellow : PapayaWhip</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Yellow : PeachPuff</source>
<target>Yellow : PeachPuff</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>Yellow : Yellow</source>
<target>Yellow : Yellow</target>
<definition>element part filling</definition>
</phrase>
<phrase>
<source>armoire</source>
<target>cabinet</target>
<definition>cabinet / enclosure</definition>
</phrase>
<phrase>
<source>bobine</source>
<target>coil</target>
<definition>coil</definition>
</phrase>
<phrase>
<source>borne</source>
<target>terminal</target>
<definition>terminal - a connection point on a symbol</definition>
</phrase>
<phrase>
<source>bornier</source>
<target>terminal strip</target>
<definition>terminal strip</definition>
</phrase>
<phrase>
<source>cable</source>
<target>cable</target>
<definition>cable</definition>
</phrase>
<phrase>
<source>calibre</source>
<target>rating</target>
<definition>rating - the current rating of a device</definition>
</phrase>
<phrase>
<source>cartouche</source>
<target>title block</target>
<definition>title block - the framed information panel</definition>
</phrase>
<phrase>
<source>champ de texte</source>
<target>text field</target>
<definition>text field</definition>
</phrase>
<phrase>
<source>collection</source>
<target>collection</target>
<definition>collection - the symbol library</definition>
</phrase>
<phrase>
<source>conducteur</source>
<target>conductor</target>
<definition>conductor - the drawn wire between terminals</definition>
</phrase>
<phrase>
<source>contact</source>
<target>contact</target>
<definition>contact</definition>
</phrase>
<phrase>
<source>contacteur</source>
<target>contactor</target>
<definition>contactor</definition>
</phrase>
<phrase>
<source>courant</source>
<target>current</target>
<definition>current</definition>
</phrase>
<phrase>
<source>disjoncteur</source>
<target>circuit breaker</target>
<definition>circuit breaker</definition>
</phrase>
<phrase>
<source>dossier</source>
<target>folder</target>
<definition>folder</definition>
</phrase>
<phrase>
<source>element</source>
<target>element</target>
<definition>element - a symbol placed on a sheet</definition>
</phrase>
<phrase>
<source>esclave</source>
<target>slave</target>
<definition>slave - the contact block belonging to a master</definition>
</phrase>
<phrase>
<source>fil</source>
<target>wire</target>
<definition>wire</definition>
</phrase>
<phrase>
<source>folio</source>
<target>sheet</target>
<definition>sheet - one drawing of a set</definition>
</phrase>
<phrase>
<source>fusible</source>
<target>fuse</target>
<definition>fuse</definition>
</phrase>
<phrase>
<source>grille</source>
<target>grid</target>
<definition>grid</definition>
</phrase>
<phrase>
<source>interrupteur</source>
<target>switch</target>
<definition>switch</definition>
</phrase>
<phrase>
<source>maitre</source>
<target>master</target>
<definition>master - the element a cross-reference points from</definition>
</phrase>
<phrase>
<source>modele</source>
<target>template</target>
<definition>template</definition>
</phrase>
<phrase>
<source>moteur</source>
<target>motor</target>
<definition>motor</definition>
</phrase>
<phrase>
<source>neutre</source>
<target>neutral</target>
<definition>neutral</definition>
</phrase>
<phrase>
<source>nomenclature</source>
<target>parts list</target>
<definition>parts list / bill of materials</definition>
</phrase>
<phrase>
<source>numerotation</source>
<target>numbering</target>
<definition>numbering - automatic wire and part numbering</definition>
</phrase>
<phrase>
<source>phase</source>
<target>phase</target>
<definition>phase</definition>
</phrase>
<phrase>
<source>potentiel</source>
<target>potential</target>
<definition>potential - conductors electrically joined as one node</definition>
</phrase>
<phrase>
<source>projet</source>
<target>project</target>
<definition>project - the .qet file holding the sheets</definition>
</phrase>
<phrase>
<source>puissance</source>
<target>power</target>
<definition>power</definition>
</phrase>
<phrase>
<source>relais</source>
<target>relay</target>
<definition>relay</definition>
</phrase>
<phrase>
<source>renvoi de folio</source>
<target>sheet cross-reference</target>
<definition>sheet cross-reference</definition>
</phrase>
<phrase>
<source>repere</source>
<target>designation</target>
<definition>designation - the identifying mark on a part</definition>
</phrase>
<phrase>
<source>report de folio</source>
<target>sheet cross-reference</target>
<definition>sheet cross-reference (the arrow symbol)</definition>
</phrase>
<phrase>
<source>schema</source>
<target>diagram</target>
<definition>diagram</definition>
</phrase>
<phrase>
<source>section</source>
<target>cross-section</target>
<definition>cross-section - conductor size in mm2</definition>
</phrase>
<phrase>
<source>sommaire</source>
<target>summary</target>
<definition>summary / table of contents</definition>
</phrase>
<phrase>
<source>tension</source>
<target>voltage</target>
<definition>voltage</definition>
</phrase>
<phrase>
<source>terre</source>
<target>earth</target>
<definition>earth / ground</definition>
</phrase>
<phrase>
<source>texte</source>
<target>text</target>
<definition>text</definition>
</phrase>
<phrase>
<source>transformateur</source>
<target>transformer</target>
<definition>transformer</definition>
</phrase>
</QPH>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+941
View File
@@ -0,0 +1,941 @@
<!DOCTYPE QPH>
<QPH sourcelanguage="fr_FR" language="nl_BE">
<phrase>
<source> Contacts : NO : %1, NC : %2, inverseurs : %3, autres : %4
</source>
<target>Contacten: NO: %1, NC: %2, wisselcontacten: %3, andere: %4
</target>
</phrase>
<phrase>
<source> Contacts : NO : %1/%2, NC : %3/%4, inverseurs : %5/%6, autres : %7/%8
</source>
<target>Contacten: NO: %1/%2, NC: %3/%4, wisselcontacten: %5/%6, andere: %7/%8
</target>
</phrase>
<phrase>
<source> %</source>
<target>%</target>
</phrase>
<phrase>
<source> (Ctrl pendant le glissement = position libre, sans accrochage à la grille)</source>
<target>(Ctrl tijdens het slepen = vrije positie, zonder vastklikken aan het raster)</target>
</phrase>
<phrase>
<source> -- %1 : mode %2</source>
<target>-- %1 : modus %2</target>
</phrase>
<phrase>
<source> ; point rouge : glisser pour repositionner le centre de rotation</source>
<target>; rood punt: slepen om het draaipunt te verplaatsen</target>
</phrase>
<phrase>
<source> ; point turquoise : arc</source>
<target>; turquoise punt: boog</target>
</phrase>
<phrase>
<source> ; un bord : inclinaison</source>
<target>; een rand: kanteling</target>
</phrase>
<phrase>
<source> °</source>
<target>°</target>
</phrase>
<phrase>
<source> — Cliquer : mode %1</source>
<target>— Klikken: modus %1</target>
</phrase>
<phrase>
<source>, Alt = créer des poignées</source>
<target>, Alt = handgrepen creëren</target>
</phrase>
<phrase>
<source>, Alt = détacher en polyligne</source>
<target>, Alt = losmaken als polylijn</target>
</phrase>
<phrase>
<source>Ajoute une courbe de Bézier sur le folio actuel</source>
<target>Voegt een Béziercurve toe op het huidige blad</target>
</phrase>
<phrase>
<source>Ajouter la borne %1</source>
<target>Klem %1 toevoegen</target>
</phrase>
<phrase>
<source>Ajouter un point à une courbe</source>
<target>Een punt aan een curve toevoegen</target>
</phrase>
<phrase>
<source>Ajouter une courbe</source>
<target>Een curve toevoegen</target>
</phrase>
<phrase>
<source>Angle</source>
<target>Hoek</target>
</phrase>
<phrase>
<source>Anguleux</source>
<target>Hoekig</target>
</phrase>
<phrase>
<source>Aperçu</source>
<target>Voorbeeld</target>
</phrase>
<phrase>
<source>Arrondir les coins d&apos;%1</source>
<target>Hoeken van %1 afronden</target>
</phrase>
<phrase>
<source>Borne 1</source>
<target>Klem 1</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Borne 2</source>
<target>Klem 2</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Catégorie</source>
<target>Categorie</target>
</phrase>
<phrase>
<source>Ce format ne prend pas en charge la transparence : l&apos;image sera enregistrée telle qu&apos;elle était avant l&apos;application de la couleur transparente. Continuer ?</source>
<target>Dit formaat ondersteunt geen transparantie: de afbeelding wordt opgeslagen zoals ze was vóór het toepassen van de transparante kleur. Doorgaan?</target>
</phrase>
<phrase>
<source>Champ de texte</source>
<target>Tekstveld</target>
</phrase>
<phrase>
<source>Clic : positionner à la taille d&apos;origine. Cliquer-glisser : positionner et redimensionner. Clic droit : pivoter de 90°. Ctrl+molette : ajuster la taille.</source>
<target>Klik: op oorspronkelijke grootte plaatsen. Klikken en slepen: plaatsen en formaat wijzigen. Rechtsklik: 90° draaien. Ctrl+muiswiel: formaat aanpassen.</target>
</phrase>
<phrase>
<source>Clic gauche : point suivant ; double-clic ou Entrée : terminer ; clic droit : annuler le dernier point</source>
<target>Linkerklik: volgend punt; dubbelklik of Enter: beëindigen; rechtsklik: laatste punt annuleren</target>
</phrase>
<phrase>
<source>Clic gauche : positionner le coin opposé (Maj = carré, Ctrl = depuis le centre + position libre, Ctrl+Maj = carré centré) ; clic droit : annuler</source>
<target>Linkerklik: tegenoverliggende hoek plaatsen (Shift = vierkant, Ctrl = vanuit het midden + vrije positie, Ctrl+Shift = gecentreerd vierkant); rechtsklik: annuleren</target>
</phrase>
<phrase>
<source>Clic gauche : positionner le coin opposé (Maj = cercle, Ctrl = depuis le centre + position libre, Ctrl+Maj = cercle centré) ; clic droit : annuler</source>
<target>Linkerklik: tegenoverliggende hoek plaatsen (Shift = cirkel, Ctrl = vanuit het midden + vrije positie, Ctrl+Shift = gecentreerde cirkel); rechtsklik: annuleren</target>
</phrase>
<phrase>
<source>Clic gauche : positionner le point de départ (Ctrl = position libre)</source>
<target>Linkerklik: startpunt plaatsen (Ctrl = vrije positie)</target>
</phrase>
<phrase>
<source>Clic gauche : positionner le point final (Ctrl = position libre) ; clic droit : annuler</source>
<target>Linkerklik: eindpunt plaatsen (Ctrl = vrije positie); rechtsklik: annuleren</target>
</phrase>
<phrase>
<source>Clic gauche : positionner le premier coin (Ctrl = point central, position libre)</source>
<target>Linkerklik: eerste hoek plaatsen (Ctrl = middelpunt, vrije positie)</target>
</phrase>
<phrase>
<source>Clic gauche : positionner le premier point (Ctrl = position libre)</source>
<target>Linkerklik: eerste punt plaatsen (Ctrl = vrije positie)</target>
</phrase>
<phrase>
<source>Clic: point anguleux. Cliquer-glisser: point courbe. Clic sur le premier point: fermer. Échap/Entrée: terminer. Clic droit: annuler le dernier point.</source>
<target>Klik: hoekpunt. Klikken en slepen: curvepunt. Klik op het eerste punt: sluiten. Esc/Enter: beëindigen. Rechtsklik: laatste punt annuleren.</target>
</phrase>
<phrase>
<source>Cliquer</source>
<target>Klikken</target>
</phrase>
<phrase>
<source>Cliquer : mode %1</source>
<target>Klikken: modus %1</target>
</phrase>
<phrase>
<source>Cliquez pour choisir une couleur</source>
<target>Klik om een kleur te kiezen</target>
</phrase>
<phrase>
<source>Cliquez sur l&apos;image pour ajouter une couleur. Ajustez la tolérance de chaque couleur avec son curseur, ou cliquez sur × pour la retirer.</source>
<target>Klik op de afbeelding om een kleur toe te voegen. Pas de tolerantie van elke kleur aan met de schuifregelaar, of klik op × om ze te verwijderen.</target>
</phrase>
<phrase>
<source>Cliquez sur l&apos;image pour choisir une couleur</source>
<target>Klik op de afbeelding om een kleur te kiezen</target>
</phrase>
<phrase>
<source>Coins arrondis</source>
<target>Afgeronde hoeken</target>
</phrase>
<phrase>
<source>Composant 1</source>
<target>Component 1</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Composant 2</source>
<target>Component 2</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Convertir %1 en courbe de Bézier</source>
<target>%1 omzetten naar Béziercurve</target>
</phrase>
<phrase>
<source>Convertir %1 en polyligne</source>
<target>%1 omzetten naar polylijn</target>
</phrase>
<phrase>
<source>Convertir en courbe de Bézier</source>
<target>Omzetten naar Béziercurve</target>
</phrase>
<phrase>
<source>Convertir en polyligne</source>
<target>Omzetten naar polylijn</target>
</phrase>
<phrase>
<source>Couleur transparente</source>
<target>Transparante kleur</target>
</phrase>
<phrase>
<source>Couleur transparente...</source>
<target>Transparante kleur...</target>
</phrase>
<phrase>
<source>Courant nominal</source>
<target>Nominale stroom</target>
</phrase>
<phrase>
<source>Deplacer le centre de rotation</source>
<target>Draaipunt verplaatsen</target>
</phrase>
<phrase>
<source>Description</source>
<target>Beschrijving</target>
</phrase>
<phrase>
<source>Distance in pixels between the label and the slave cross reference</source>
<target>Afstand in pixels tussen het label en de slave-kruisverwijzing</target>
</phrase>
<phrase>
<source>Distance label - slave :</source>
<target>Afstand label - slave:</target>
</phrase>
<phrase>
<source>Définir une couleur transparente</source>
<target>Een transparante kleur instellen</target>
</phrase>
<phrase>
<source>Déformer une courbe</source>
<target>Een curve vervormen</target>
</phrase>
<phrase>
<source>Déplacer le centre de rotation d&apos;une image</source>
<target>Het draaipunt van een afbeelding verplaatsen</target>
</phrase>
<phrase>
<source>Déverrouillé : largeur et hauteur peuvent être modifiées indépendamment. Cliquer pour verrouiller.</source>
<target>Ontgrendeld: breedte en hoogte kunnen onafhankelijk worden gewijzigd. Klik om te vergrendelen.</target>
</phrase>
<phrase>
<source>Enregistrer l&apos;image d&apos;origine sous...</source>
<target>Oorspronkelijke afbeelding opslaan als...</target>
</phrase>
<phrase>
<source>Enregistrer l&apos;image sous...</source>
<target>Afbeelding opslaan als...</target>
</phrase>
<phrase>
<source>Erreur</source>
<target>Fout</target>
</phrase>
<phrase>
<source>Exclure de la nomenclature</source>
<target>Uitsluiten van de stuklijst</target>
</phrase>
<phrase>
<source>Faire pivoter %1</source>
<target>%1 roteren</target>
</phrase>
<phrase>
<source>Faire pivoter une image</source>
<target>Een afbeelding draaien</target>
</phrase>
<phrase>
<source>Faites glisser les poignées, ou l&apos;intérieur du cadre, pour ajuster la zone à conserver.</source>
<target>Sleep de handgrepen, of het binnenste van het kader, om het te behouden gebied aan te passen.</target>
</phrase>
<phrase>
<source>Fil</source>
<target>Draad</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Folio</source>
<target>Blad</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Forme fermée</source>
<target>Gesloten vorm</target>
</phrase>
<phrase>
<source>Glisser %1 : rotation (Maj = 15°)</source>
<target>Sleep %1: rotatie (Shift = 15°)</target>
</phrase>
<phrase>
<source>Glisser : ajuster l&apos;arc (Ctrl = position libre, Maj = 15°)</source>
<target>Slepen: de boog aanpassen (Ctrl = vrije positie, Shift = 15°)</target>
</phrase>
<phrase>
<source>Glisser : arrondir les coins (Ctrl = position libre)</source>
<target>Slepen: hoeken afronden (Ctrl = vrije positie)</target>
</phrase>
<phrase>
<source>Glisser : déformer la courbe (Ctrl = position libre, Alt = briser la tangente)</source>
<target>Slepen: de curve vervormen (Ctrl = vrije positie, Alt = de raaklijn breken)</target>
</phrase>
<phrase>
<source>Glisser : déplacer ce point</source>
<target>Slepen: dit punt verplaatsen</target>
</phrase>
<phrase>
<source>Glisser : déplacer le centre de rotation</source>
<target>Slepen: draaipunt verplaatsen</target>
</phrase>
<phrase>
<source>Glisser : déplacer le point (Ctrl = position libre</source>
<target>Slepen: het punt verplaatsen (Ctrl = vrije positie</target>
</phrase>
<phrase>
<source>Glisser : inclinaison (Ctrl = position libre, Maj = 15°)</source>
<target>Slepen: kanteling (Ctrl = vrije positie, Shift = 15°)</target>
</phrase>
<phrase>
<source>Glisser : incliner (Maj = par pas de 15°)</source>
<target>Slepen: kantelen (Shift = stappen van 15°)</target>
</phrase>
<phrase>
<source>Glisser : pivoter (Maj = par pas de 15°)</source>
<target>Slepen: draaien (Shift = stappen van 15°)</target>
</phrase>
<phrase>
<source>Glisser : redimensionner (Ctrl = depuis le centre + position libre, Maj = proportions</source>
<target>Slepen: formaat wijzigen (Ctrl = vanuit het midden + vrije positie, Shift = verhoudingen</target>
</phrase>
<phrase>
<source>Glisser : redimensionner (Maj = conserver les proportions, Ctrl = depuis le centre)</source>
<target>Slepen: formaat wijzigen (Shift = verhoudingen behouden, Ctrl = vanuit het midden)</target>
</phrase>
<phrase>
<source>Glisser : repositionner le centre de rotation (Ctrl = position libre)</source>
<target>Slepen: het draaipunt verplaatsen (Ctrl = vrije positie)</target>
</phrase>
<phrase>
<source>Glisser : rotation (Ctrl = position libre, Maj = 15°)</source>
<target>Slepen: rotatie (Ctrl = vrije positie, Shift = 15°)</target>
</phrase>
<phrase>
<source>Glisser le point violet : arrondir les coins</source>
<target>Sleep het paarse punt: hoeken afronden</target>
</phrase>
<phrase>
<source>Glisser un coin : pivoter (Maj = par pas de 15°) ; glisser un bord : incliner (Maj = par pas de 15°) ; point rouge : déplacer le centre de rotation</source>
<target>Sleep een hoek: draaien (Shift = stappen van 15°); sleep een rand: kantelen (Shift = stappen van 15°); rood punt: draaipunt verplaatsen</target>
</phrase>
<phrase>
<source>Glisser un coin/bord : redimensionner (Ctrl = depuis le centre, Maj = conserver les proportions)</source>
<target>Sleep een hoek/rand: formaat wijzigen (Ctrl = vanuit het midden, Shift = verhoudingen behouden)</target>
</phrase>
<phrase>
<source>Glisser un coin/bord : redimensionner (Ctrl = depuis le centre, Maj = proportions, Alt = détacher en polyligne)</source>
<target>Sleep een hoek/rand: formaat wijzigen (Ctrl = vanuit het midden, Shift = verhoudingen, Alt = losmaken als polylijn)</target>
</phrase>
<phrase>
<source>Glisser un point : le déplacer</source>
<target>Sleep een punt: het verplaatsen</target>
</phrase>
<phrase>
<source>Glisser une extrémité : la déplacer</source>
<target>Sleep een uiteinde: het verplaatsen</target>
</phrase>
<phrase>
<source>Glisser une poignée ou la courbe : déformer (Alt = briser la tangente) ; Alt+glisser un point anguleux : créer des poignées ; clic droit : menu du nœud le plus proche</source>
<target>Sleep een handgreep of de curve: vervormen (Alt = de raaklijn breken); Alt+sleep een hoekpunt: handgrepen creëren; rechtsklik: menu van het dichtstbijzijnde knooppunt</target>
</phrase>
<phrase>
<source>Géométrie</source>
<target>Geometrie</target>
</phrase>
<phrase>
<source>Hauteur</source>
<target>Hoogte</target>
</phrase>
<phrase>
<source>Image BMP (*.bmp)</source>
<target>BMP-afbeelding (*.bmp)</target>
</phrase>
<phrase>
<source>Image Files (*.png *.jpg *.jpeg *.bmp *.svg)</source>
<target>Afbeeldingsbestanden (*.png *.jpg *.jpeg *.bmp *.svg)</target>
</phrase>
<phrase>
<source>Image JPEG (*.jpg *.jpeg)</source>
<target>JPEG-afbeelding (*.jpg *.jpeg)</target>
</phrase>
<phrase>
<source>Image PNG (*.png)</source>
<target>PNG-afbeelding (*.png)</target>
</phrase>
<phrase>
<source>Image source</source>
<target>Bronafbeelding</target>
</phrase>
<phrase>
<source>Image SVG (*.svg)</source>
<target>SVG-afbeelding (*.svg)</target>
</phrase>
<phrase>
<source>Images non incluses dans l&apos;export DXF</source>
<target>Afbeeldingen niet inbegrepen in de DXF-export</target>
<definition>message box title</definition>
</phrase>
<phrase>
<source>Impossible d&apos;enregistrer l&apos;image à cet emplacement.</source>
<target>Kan de afbeelding niet op die locatie opslaan.</target>
</phrase>
<phrase>
<source>Impossible d&apos;enregistrer la nomenclature dans %1.
%2</source>
<target>Kan de stuklijst niet opslaan in %1.
%2</target>
</phrase>
<phrase>
<source>Impossible de charger l&apos;image.</source>
<target>Kan de afbeelding niet laden.</target>
</phrase>
<phrase>
<source>Inclinaison X</source>
<target>Kanteling X</target>
</phrase>
<phrase>
<source>Inclinaison Y</source>
<target>Kanteling Y</target>
</phrase>
<phrase>
<source>Incliner %1</source>
<target>%1 kantelen</target>
</phrase>
<phrase>
<source>Incliner une image</source>
<target>Een afbeelding kantelen</target>
</phrase>
<phrase>
<source>La limite fixée pour cet élément maître est atteinte (Limite: %1).
Voulez-vous tout de même lier ce contact esclave ?</source>
<target>De limiet voor dit hoofdelement is bereikt (Limiet: %1).
Wilt u dit hulpcontact toch koppelen?</target>
</phrase>
<phrase>
<source>Largeur</source>
<target>Breedte</target>
</phrase>
<phrase>
<source>Le format DXF utilisé ici (AC1006) ne permet pas d&apos;inclure d&apos;image. Les images seront représentées uniquement par un rectangle de contour (position, taille, rotation et inclinaison conservées), sans le contenu de l&apos;image.</source>
<target>Het hier gebruikte DXF-formaat (AC1006) laat niet toe afbeeldingen op te nemen. Afbeeldingen worden enkel voorgesteld door een omtrekrechthoek (positie, grootte, rotatie en kanteling behouden), zonder de inhoud van de afbeelding.</target>
<definition>message box content</definition>
</phrase>
<phrase>
<source>Lisse</source>
<target>Vloeiend</target>
</phrase>
<phrase>
<source>Liste de câblage</source>
<target>Bekabelingslijst</target>
<definition>window title</definition>
</phrase>
<phrase>
<source>Liste de câblage (base de données)</source>
<target>Bekabelingslijst (database)</target>
</phrase>
<phrase>
<source>Localisation :</source>
<target>Locatie:</target>
</phrase>
<phrase>
<source>Longueur</source>
<target>Lengte</target>
</phrase>
<phrase>
<source>margin: 5px; font-weight: bold;</source>
<target>margin: 5px; font-weight: bold;</target>
</phrase>
<phrase>
<source>max:</source>
<target>max:</target>
</phrase>
<phrase>
<source>min:</source>
<target>min:</target>
</phrase>
<phrase>
<source>Miroir horizontal</source>
<target>Horizontaal spiegelen</target>
</phrase>
<phrase>
<source>Miroir horizontal d&apos;une image</source>
<target>Horizontaal spiegelen van een afbeelding</target>
</phrase>
<phrase>
<source>Miroir horizontal de %1</source>
<target>Horizontaal spiegelen van %1</target>
</phrase>
<phrase>
<source>Miroir impossible : inclinaison trop extrême pour cette forme</source>
<target>Spiegelen onmogelijk: kanteling te extreem voor deze vorm</target>
</phrase>
<phrase>
<source>Miroir vertical</source>
<target>Verticaal spiegelen</target>
</phrase>
<phrase>
<source>Miroir vertical d&apos;une image</source>
<target>Verticaal spiegelen van een afbeelding</target>
</phrase>
<phrase>
<source>Miroir vertical de %1</source>
<target>Verticaal spiegelen van %1</target>
</phrase>
<phrase>
<source>Modifier l&apos;angle d&apos;un arc</source>
<target>Hoek van een boog wijzigen</target>
</phrase>
<phrase>
<source>Modifier l&apos;angle d&apos;une forme</source>
<target>Hoek van een vorm wijzigen</target>
</phrase>
<phrase>
<source>Modifier l&apos;angle d&apos;une image</source>
<target>De hoek van een afbeelding wijzigen</target>
</phrase>
<phrase>
<source>Modifier l&apos;inclinaison d&apos;une image</source>
<target>De kanteling van een afbeelding wijzigen</target>
</phrase>
<phrase>
<source>Modifier la courbure d&apos;%1</source>
<target>Kromming van %1 wijzigen</target>
</phrase>
<phrase>
<source>Modifier la forme d&apos;%1</source>
<target>Vorm van %1 wijzigen</target>
</phrase>
<phrase>
<source>Modifier la hauteur d&apos;une image</source>
<target>De hoogte van een afbeelding wijzigen</target>
</phrase>
<phrase>
<source>Modifier la largeur d&apos;une image</source>
<target>De breedte van een afbeelding wijzigen</target>
</phrase>
<phrase>
<source>Modifier la longueur d&apos;une ligne</source>
<target>Lengte van een lijn wijzigen</target>
</phrase>
<phrase>
<source>Modifier la taille d&apos;une forme</source>
<target>Grootte van een vorm wijzigen</target>
</phrase>
<phrase>
<source>Modifier le type d&apos;un nœud</source>
<target>Het type van een knooppunt wijzigen</target>
</phrase>
<phrase>
<source>Modifier une image</source>
<target>Een afbeelding wijzigen</target>
</phrase>
<phrase>
<source>Modèle</source>
<target>Model</target>
</phrase>
<phrase>
<source>Nombre maximum de contacts esclaves définis : non défini
</source>
<target>Maximumaantal gedefinieerde hulpcontacten: niet gedefinieerd
</target>
</phrase>
<phrase>
<source>Notes</source>
<target>Notities</target>
</phrase>
<phrase>
<source>Nœud le plus proche</source>
<target>Dichtstbijzijnde knooppunt</target>
</phrase>
<phrase>
<source>pivoter/incliner</source>
<target>draaien/kantelen</target>
</phrase>
<phrase>
<source>Ponter des bornes entre-elles</source>
<target>Klemmen onderling doorverbinden</target>
</phrase>
<phrase>
<source>Rayon X</source>
<target>Straal X</target>
</phrase>
<phrase>
<source>Rayon Y</source>
<target>Straal Y</target>
</phrase>
<phrase>
<source>redimensionner</source>
<target>formaat wijzigen</target>
</phrase>
<phrase>
<source>Redimensionner %1</source>
<target>%1 vergroten/verkleinen</target>
</phrase>
<phrase>
<source>Redimensionner une image</source>
<target>Formaat van een afbeelding wijzigen</target>
</phrase>
<phrase>
<source>Remplacer l&apos;image...</source>
<target>Afbeelding vervangen...</target>
</phrase>
<phrase>
<source>Remplacer une image</source>
<target>Een afbeelding vervangen</target>
</phrase>
<phrase>
<source>Restaurer les proportions</source>
<target>Verhoudingen herstellen</target>
</phrase>
<phrase>
<source>Restaurer les proportions d&apos;une image</source>
<target>De verhoudingen van een afbeelding herstellen</target>
</phrase>
<phrase>
<source>Retirer cette couleur</source>
<target>Deze kleur verwijderen</target>
</phrase>
<phrase>
<source>Revenir à l&apos;image complète, sans rognage</source>
<target>Terug naar de volledige afbeelding, zonder bijsnijden</target>
</phrase>
<phrase>
<source>rgb(%1, %2, %3)</source>
<target>rgb(%1, %2, %3)</target>
</phrase>
<phrase>
<source>Rogner l&apos;image</source>
<target>Afbeelding bijsnijden</target>
</phrase>
<phrase>
<source>Rogner une image</source>
<target>Een afbeelding bijsnijden</target>
</phrase>
<phrase>
<source>Rogner...</source>
<target>Bijsnijden...</target>
</phrase>
<phrase>
<source>Rotation/Inclinaison</source>
<target>Rotatie/kanteling</target>
</phrase>
<phrase>
<source>Réinitialiser</source>
<target>Herinitialiseren</target>
</phrase>
<phrase>
<source>Selectionner une image...</source>
<target>Een afbeelding selecteren...</target>
</phrase>
<phrase>
<source>Supprimer le nœud le plus proche</source>
<target>Het dichtstbijzijnde knooppunt verwijderen</target>
</phrase>
<phrase>
<source>Supprimer un point d&apos;une courbe</source>
<target>Een punt van een curve verwijderen</target>
</phrase>
<phrase>
<source>Symétrique</source>
<target>Symmetrisch</target>
</phrase>
<phrase>
<source>Séparation de potentiel</source>
<target>Potentiaalscheiding</target>
</phrase>
<phrase>
<source>Taille</source>
<target>Grootte</target>
</phrase>
<phrase>
<source>Tension nominale</source>
<target>Nominale spanning</target>
</phrase>
<phrase>
<source>Tolérance pour cette couleur</source>
<target>Tolerantie voor deze kleur</target>
</phrase>
<phrase>
<source>Tous les fichiers (*)</source>
<target>Alle bestanden (*)</target>
</phrase>
<phrase>
<source>Transformer %1</source>
<target>%1 omvormen</target>
</phrase>
<phrase>
<source>Transparence non conservée</source>
<target>Transparantie niet behouden</target>
</phrase>
<phrase>
<source>un arc</source>
<target>een boog</target>
</phrase>
<phrase>
<source>un coin</source>
<target>een hoek</target>
</phrase>
<phrase>
<source>un point</source>
<target>een punt</target>
</phrase>
<phrase>
<source>une courbe</source>
<target>een curve</target>
</phrase>
<phrase>
<source>une extrémité</source>
<target>een uiteinde</target>
</phrase>
<phrase>
<source>une image</source>
<target>een afbeelding</target>
<definition>part of a sentence listing the content of a diagram</definition>
</phrase>
<phrase>
<source>Verrouiller la numérotation automatique</source>
<target>Automatische nummering vergrendelen</target>
</phrase>
<phrase>
<source>Verrouiller la position</source>
<target>Positie vergrendelen</target>
</phrase>
<phrase>
<source>Verrouillé : modifier la largeur ou la hauteur ajuste l&apos;autre pour conserver les proportions. Cliquer pour déverrouiller.</source>
<target>Vergrendeld: het wijzigen van de breedte of hoogte past de andere aan om de verhoudingen te behouden. Klik om te ontgrendelen.</target>
</phrase>
<phrase>
<source>Échec de l&apos;enregistrement</source>
<target>Opslaan mislukt</target>
</phrase>
<phrase>
<source>Édition des nœuds</source>
<target>Knooppunten bewerken</target>
</phrase>
<phrase>
<source>armoire</source>
<target>kast</target>
<definition>cabinet / enclosure</definition>
</phrase>
<phrase>
<source>bobine</source>
<target>spoel</target>
<definition>coil</definition>
</phrase>
<phrase>
<source>borne</source>
<target>klem</target>
<definition>terminal - a connection point on a symbol</definition>
</phrase>
<phrase>
<source>bornier</source>
<target>klemmenstrook</target>
<definition>terminal strip</definition>
</phrase>
<phrase>
<source>cable</source>
<target>kabel</target>
<definition>cable</definition>
</phrase>
<phrase>
<source>calibre</source>
<target>kaliber</target>
<definition>rating - the current rating of a device</definition>
</phrase>
<phrase>
<source>cartouche</source>
<target>titelblok</target>
<definition>title block - the framed information panel</definition>
</phrase>
<phrase>
<source>collection</source>
<target>collectie</target>
<definition>collection - the symbol library</definition>
</phrase>
<phrase>
<source>conducteur</source>
<target>geleider</target>
<definition>conductor - the drawn wire between terminals</definition>
</phrase>
<phrase>
<source>contact</source>
<target>contact</target>
<definition>contact</definition>
</phrase>
<phrase>
<source>contacteur</source>
<target>contacteur</target>
<definition>contactor</definition>
</phrase>
<phrase>
<source>courant</source>
<target>stroom</target>
<definition>current</definition>
</phrase>
<phrase>
<source>disjoncteur</source>
<target>automaat</target>
<definition>circuit breaker</definition>
</phrase>
<phrase>
<source>dossier</source>
<target>map</target>
<definition>folder</definition>
</phrase>
<phrase>
<source>element</source>
<target>element</target>
<definition>element - a symbol placed on a sheet</definition>
</phrase>
<phrase>
<source>esclave</source>
<target>hulpcontact</target>
<definition>slave - the contact block belonging to a master</definition>
</phrase>
<phrase>
<source>fusible</source>
<target>zekering</target>
<definition>fuse</definition>
</phrase>
<phrase>
<source>grille</source>
<target>raster</target>
<definition>grid</definition>
</phrase>
<phrase>
<source>interrupteur</source>
<target>schakelaar</target>
<definition>switch</definition>
</phrase>
<phrase>
<source>maitre</source>
<target>hoofd</target>
<definition>master - the element a cross-reference points from</definition>
</phrase>
<phrase>
<source>modele</source>
<target>sjabloon</target>
<definition>template</definition>
</phrase>
<phrase>
<source>moteur</source>
<target>motor</target>
<definition>motor</definition>
</phrase>
<phrase>
<source>neutre</source>
<target>nul</target>
<definition>neutral</definition>
</phrase>
<phrase>
<source>nomenclature</source>
<target>stuklijst</target>
<definition>parts list / bill of materials</definition>
</phrase>
<phrase>
<source>numerotation</source>
<target>nummering</target>
<definition>numbering - automatic wire and part numbering</definition>
</phrase>
<phrase>
<source>phase</source>
<target>fase</target>
<definition>phase</definition>
</phrase>
<phrase>
<source>potentiel</source>
<target>potentiaal</target>
<definition>potential - conductors electrically joined as one node</definition>
</phrase>
<phrase>
<source>projet</source>
<target>project</target>
<definition>project - the .qet file holding the sheets</definition>
</phrase>
<phrase>
<source>puissance</source>
<target>vermogen</target>
<definition>power</definition>
</phrase>
<phrase>
<source>relais</source>
<target>relais</target>
<definition>relay</definition>
</phrase>
<phrase>
<source>renvoi de folio</source>
<target>bladverwijzing</target>
<definition>sheet cross-reference</definition>
</phrase>
<phrase>
<source>repere</source>
<target>aanduiding</target>
<definition>designation - the identifying mark on a part</definition>
</phrase>
<phrase>
<source>report de folio</source>
<target>bladverwijzing</target>
<definition>sheet cross-reference (the arrow symbol)</definition>
</phrase>
<phrase>
<source>schema</source>
<target>schema</target>
<definition>diagram</definition>
</phrase>
<phrase>
<source>section</source>
<target>doorsnede</target>
<definition>cross-section - conductor size in mm2</definition>
</phrase>
<phrase>
<source>sommaire</source>
<target>inhoudstafel</target>
<definition>summary / table of contents</definition>
</phrase>
<phrase>
<source>tension</source>
<target>spanning</target>
<definition>voltage</definition>
</phrase>
<phrase>
<source>terre</source>
<target>aarde</target>
<definition>earth / ground</definition>
</phrase>
<phrase>
<source>texte</source>
<target>tekst</target>
<definition>text</definition>
</phrase>
<phrase>
<source>transformateur</source>
<target>transformator</target>
<definition>transformer</definition>
</phrase>
</QPH>
+218
View File
@@ -0,0 +1,218 @@
<!DOCTYPE QPH>
<QPH sourcelanguage="fr_FR" language="pl">
<phrase>
<source>armoire</source>
<target>Szafa</target>
<definition>cabinet / enclosure</definition>
</phrase>
<phrase>
<source>bobine</source>
<target>Cewka</target>
<definition>coil</definition>
</phrase>
<phrase>
<source>borne</source>
<target>Zacisk</target>
<definition>terminal - a connection point on a symbol</definition>
</phrase>
<phrase>
<source>bornier</source>
<target>Listwa zaciskowa</target>
<definition>terminal strip</definition>
</phrase>
<phrase>
<source>cable</source>
<target>Kabel</target>
<definition>cable</definition>
</phrase>
<phrase>
<source>calibre</source>
<target>Prąd znamionowy</target>
<definition>rating - the current rating of a device</definition>
</phrase>
<phrase>
<source>cartouche</source>
<target>Tabliczka rysunkowa</target>
<definition>title block - the framed information panel</definition>
</phrase>
<phrase>
<source>champ de texte</source>
<target>Pole tekstowe</target>
<definition>text field</definition>
</phrase>
<phrase>
<source>collection</source>
<target>Biblioteka</target>
<definition>collection - the symbol library</definition>
</phrase>
<phrase>
<source>conducteur</source>
<target>Przewód</target>
<definition>conductor - the drawn wire between terminals</definition>
</phrase>
<phrase>
<source>contact</source>
<target>Styk</target>
<definition>contact</definition>
</phrase>
<phrase>
<source>contacteur</source>
<target>Stycznik</target>
<definition>contactor</definition>
</phrase>
<phrase>
<source>courant</source>
<target>Prąd</target>
<definition>current</definition>
</phrase>
<phrase>
<source>disjoncteur</source>
<target>Wyłącznik nadprądowy</target>
<definition>circuit breaker</definition>
</phrase>
<phrase>
<source>dossier</source>
<target>Folder</target>
<definition>folder</definition>
</phrase>
<phrase>
<source>element</source>
<target>Element</target>
<definition>element - a symbol placed on a sheet</definition>
</phrase>
<phrase>
<source>esclave</source>
<target>Element podrzędny</target>
<definition>slave - the contact block belonging to a master</definition>
</phrase>
<phrase>
<source>fil</source>
<target>Przewód</target>
<definition>wire</definition>
</phrase>
<phrase>
<source>folio</source>
<target>Arkusz</target>
<definition>sheet - one drawing of a set</definition>
</phrase>
<phrase>
<source>fusible</source>
<target>Bezpiecznik</target>
<definition>fuse</definition>
</phrase>
<phrase>
<source>grille</source>
<target>Siatka</target>
<definition>grid</definition>
</phrase>
<phrase>
<source>interrupteur</source>
<target>Wyłącznik</target>
<definition>switch</definition>
</phrase>
<phrase>
<source>maitre</source>
<target>Element główny</target>
<definition>master - the element a cross-reference points from</definition>
</phrase>
<phrase>
<source>modele</source>
<target>Szablon</target>
<definition>template</definition>
</phrase>
<phrase>
<source>moteur</source>
<target>Silnik</target>
<definition>motor</definition>
</phrase>
<phrase>
<source>neutre</source>
<target>Neutralny</target>
<definition>neutral</definition>
</phrase>
<phrase>
<source>nomenclature</source>
<target>Zestawienie materiałów</target>
<definition>parts list / bill of materials</definition>
</phrase>
<phrase>
<source>numerotation</source>
<target>Numeracja</target>
<definition>numbering - automatic wire and part numbering</definition>
</phrase>
<phrase>
<source>phase</source>
<target>Faza</target>
<definition>phase</definition>
</phrase>
<phrase>
<source>potentiel</source>
<target>Potencjał</target>
<definition>potential - conductors electrically joined as one node</definition>
</phrase>
<phrase>
<source>projet</source>
<target>Projekt</target>
<definition>project - the .qet file holding the sheets</definition>
</phrase>
<phrase>
<source>puissance</source>
<target>Moc</target>
<definition>power</definition>
</phrase>
<phrase>
<source>relais</source>
<target>Przekaźnik</target>
<definition>relay</definition>
</phrase>
<phrase>
<source>renvoi de folio</source>
<target>Odnośnik do arkusza</target>
<definition>sheet cross-reference</definition>
</phrase>
<phrase>
<source>repere</source>
<target>Oznaczenie</target>
<definition>designation - the identifying mark on a part</definition>
</phrase>
<phrase>
<source>report de folio</source>
<target>Odnośnik do arkusza</target>
<definition>sheet cross-reference (the arrow symbol)</definition>
</phrase>
<phrase>
<source>schema</source>
<target>Schemat</target>
<definition>diagram</definition>
</phrase>
<phrase>
<source>section</source>
<target>Przekrój</target>
<definition>cross-section - conductor size in mm2</definition>
</phrase>
<phrase>
<source>sommaire</source>
<target>Spis treści</target>
<definition>summary / table of contents</definition>
</phrase>
<phrase>
<source>tension</source>
<target>Napięcie</target>
<definition>voltage</definition>
</phrase>
<phrase>
<source>terre</source>
<target>Uziemienie</target>
<definition>earth / ground</definition>
</phrase>
<phrase>
<source>texte</source>
<target>Tekst</target>
<definition>text</definition>
</phrase>
<phrase>
<source>transformateur</source>
<target>Transformator</target>
<definition>transformer</definition>
</phrase>
</QPH>
File diff suppressed because it is too large Load Diff
+465
View File
@@ -0,0 +1,465 @@
<!DOCTYPE QPH>
<QPH sourcelanguage="fr_FR" language="pt_BR">
<phrase>
<source> Contacts : NO : %1, NC : %2, inverseurs : %3, autres : %4
</source>
<target>Contatos: NA: %1, NF: %2, reversíveis: %3, outros: %4
</target>
</phrase>
<phrase>
<source> Contacts : NO : %1/%2, NC : %3/%4, inverseurs : %5/%6, autres : %7/%8
</source>
<target>Contatos: NA: %1/%2, NF: %3/%4, reversíveis: %5/%6, outros: %7/%8
</target>
</phrase>
<phrase>
<source>Aperçu</source>
<target>Pré-visualização</target>
</phrase>
<phrase>
<source>Arrondir les coins d&apos;%1</source>
<target>Arredondar os cantos de %1</target>
</phrase>
<phrase>
<source>Borne 1</source>
<target>Borne 1</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Borne 2</source>
<target>Borne 2</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Catégorie</source>
<target>Categoria</target>
</phrase>
<phrase>
<source>Ce format ne prend pas en charge la transparence : l&apos;image sera enregistrée telle qu&apos;elle était avant l&apos;application de la couleur transparente. Continuer ?</source>
<target>Este formato não suporta transparência: a imagem será salva como estava antes da aplicação da cor transparente. Continuar?</target>
</phrase>
<phrase>
<source>Champ de texte</source>
<target>Campo de texto</target>
</phrase>
<phrase>
<source>Composant 1</source>
<target>Componente 1</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Composant 2</source>
<target>Componente 2</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Convertir %1 en courbe de Bézier</source>
<target>Converter %1 em curva de Bézier</target>
</phrase>
<phrase>
<source>Convertir en courbe de Bézier</source>
<target>Converter em curva de Bézier</target>
</phrase>
<phrase>
<source>Courant nominal</source>
<target>Corrente nominal</target>
</phrase>
<phrase>
<source>Distance in pixels between the label and the slave cross reference</source>
<target>Distância em pixels entre o rótulo e a referência cruzada do escravo</target>
</phrase>
<phrase>
<source>Distance label - slave :</source>
<target>Distância do rótulo - escravo:</target>
</phrase>
<phrase>
<source>Enregistrer l&apos;image d&apos;origine sous...</source>
<target>Salvar a imagem original como...</target>
</phrase>
<phrase>
<source>Enregistrer l&apos;image sous...</source>
<target>Salvar a imagem como...</target>
</phrase>
<phrase>
<source>Erreur</source>
<target>Erro</target>
</phrase>
<phrase>
<source>Exclure de la nomenclature</source>
<target>Excluir da lista de materiais</target>
</phrase>
<phrase>
<source>Faire pivoter %1</source>
<target>Girar %1</target>
</phrase>
<phrase>
<source>Faire pivoter une image</source>
<target>Girar uma imagem</target>
</phrase>
<phrase>
<source>Fil</source>
<target>Fio</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Folio</source>
<target>Folha</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Hauteur</source>
<target>Altura</target>
</phrase>
<phrase>
<source>Image BMP (*.bmp)</source>
<target>Imagem BMP (*.bmp)</target>
</phrase>
<phrase>
<source>Image JPEG (*.jpg *.jpeg)</source>
<target>Imagem JPEG (*.jpg *.jpeg)</target>
</phrase>
<phrase>
<source>Image PNG (*.png)</source>
<target>Imagem PNG (*.png)</target>
</phrase>
<phrase>
<source>Image SVG (*.svg)</source>
<target>Imagem SVG (*.svg)</target>
</phrase>
<phrase>
<source>Images non incluses dans l&apos;export DXF</source>
<target>Imagens não incluídas na exportação DXF</target>
<definition>message box title</definition>
</phrase>
<phrase>
<source>Impossible d&apos;enregistrer l&apos;image à cet emplacement.</source>
<target>Não foi possível salvar a imagem neste local.</target>
</phrase>
<phrase>
<source>Impossible d&apos;enregistrer la nomenclature dans %1.
%2</source>
<target>Não foi possível salvar a lista de materiais em %1.
%2</target>
</phrase>
<phrase>
<source>Impossible de charger l&apos;image.</source>
<target>Não foi possível carregar a imagem.</target>
</phrase>
<phrase>
<source>Incliner %1</source>
<target>Inclinar %1</target>
</phrase>
<phrase>
<source>Incliner une image</source>
<target>Inclinar uma imagem</target>
</phrase>
<phrase>
<source>La limite fixée pour cet élément maître est atteinte (Limite: %1).
Voulez-vous tout de même lier ce contact esclave ?</source>
<target>O limite definido para este elemento mestre foi atingido (Limite: %1).
Deseja vincular este contato escravo mesmo assim?</target>
</phrase>
<phrase>
<source>Largeur</source>
<target>Largura</target>
</phrase>
<phrase>
<source>Le format DXF utilisé ici (AC1006) ne permet pas d&apos;inclure d&apos;image. Les images seront représentées uniquement par un rectangle de contour (position, taille, rotation et inclinaison conservées), sans le contenu de l&apos;image.</source>
<target>O formato DXF usado aqui (AC1006) não permite incluir imagens. As imagens serão representadas apenas por um retângulo de contorno (posição, tamanho, rotação e inclinação preservados), sem o conteúdo da imagem.</target>
<definition>message box content</definition>
</phrase>
<phrase>
<source>Liste de câblage</source>
<target>Lista de cabeamento</target>
<definition>window title</definition>
</phrase>
<phrase>
<source>Liste de câblage (base de données)</source>
<target>Lista de cabeamento (banco de dados)</target>
</phrase>
<phrase>
<source>margin: 5px; font-weight: bold;</source>
<target>margin: 5px; font-weight: bold;</target>
</phrase>
<phrase>
<source>Modifier l&apos;angle d&apos;un arc</source>
<target>Modificar o ângulo de um arco</target>
</phrase>
<phrase>
<source>Modifier la courbure d&apos;%1</source>
<target>Modificar a curvatura de %1</target>
</phrase>
<phrase>
<source>Modifier la forme d&apos;%1</source>
<target>Modificar a forma de %1</target>
</phrase>
<phrase>
<source>Modèle</source>
<target>Modelo</target>
</phrase>
<phrase>
<source>Nombre maximum de contacts esclaves définis : non défini
</source>
<target>Número máximo de contatos escravos definidos: não definido
</target>
</phrase>
<phrase>
<source>Notes</source>
<target>Observações</target>
</phrase>
<phrase>
<source>Redimensionner %1</source>
<target>Redimensionar %1</target>
</phrase>
<phrase>
<source>Redimensionner une image</source>
<target>Redimensionar uma imagem</target>
</phrase>
<phrase>
<source>Réinitialiser</source>
<target>Redefinir</target>
</phrase>
<phrase>
<source>Selectionner une image...</source>
<target>Selecionar uma imagem...</target>
</phrase>
<phrase>
<source>Séparation de potentiel</source>
<target>Separação de potencial</target>
</phrase>
<phrase>
<source>Taille</source>
<target>Tamanho</target>
</phrase>
<phrase>
<source>Tension nominale</source>
<target>Tensão nominal</target>
</phrase>
<phrase>
<source>Tous les fichiers (*)</source>
<target>Todos os arquivos (*)</target>
</phrase>
<phrase>
<source>Transparence non conservée</source>
<target>Transparência não preservada</target>
</phrase>
<phrase>
<source>une image</source>
<target>uma imagem</target>
<definition>part of a sentence listing the content of a diagram</definition>
</phrase>
<phrase>
<source>Verrouiller la numérotation automatique</source>
<target>Bloquear a numeração automática</target>
</phrase>
<phrase>
<source>Verrouiller la position</source>
<target>Bloquear a posição</target>
</phrase>
<phrase>
<source>Échec de l&apos;enregistrement</source>
<target>Falha ao salvar</target>
</phrase>
<phrase>
<source>armoire</source>
<target>armário</target>
<definition>cabinet / enclosure</definition>
</phrase>
<phrase>
<source>bobine</source>
<target>bobina</target>
<definition>coil</definition>
</phrase>
<phrase>
<source>borne</source>
<target>borne</target>
<definition>terminal - a connection point on a symbol</definition>
</phrase>
<phrase>
<source>bornier</source>
<target>borneira</target>
<definition>terminal strip</definition>
</phrase>
<phrase>
<source>cable</source>
<target>cabo</target>
<definition>cable</definition>
</phrase>
<phrase>
<source>calibre</source>
<target>calibre</target>
<definition>rating - the current rating of a device</definition>
</phrase>
<phrase>
<source>cartouche</source>
<target>bloco de título</target>
<definition>title block - the framed information panel</definition>
</phrase>
<phrase>
<source>collection</source>
<target>coleção</target>
<definition>collection - the symbol library</definition>
</phrase>
<phrase>
<source>conducteur</source>
<target>condutor</target>
<definition>conductor - the drawn wire between terminals</definition>
</phrase>
<phrase>
<source>contact</source>
<target>contato</target>
<definition>contact</definition>
</phrase>
<phrase>
<source>contacteur</source>
<target>contator</target>
<definition>contactor</definition>
</phrase>
<phrase>
<source>courant</source>
<target>corrente</target>
<definition>current</definition>
</phrase>
<phrase>
<source>disjoncteur</source>
<target>disjuntor</target>
<definition>circuit breaker</definition>
</phrase>
<phrase>
<source>dossier</source>
<target>pasta</target>
<definition>folder</definition>
</phrase>
<phrase>
<source>element</source>
<target>elemento</target>
<definition>element - a symbol placed on a sheet</definition>
</phrase>
<phrase>
<source>esclave</source>
<target>escravo</target>
<definition>slave - the contact block belonging to a master</definition>
</phrase>
<phrase>
<source>fusible</source>
<target>fusível</target>
<definition>fuse</definition>
</phrase>
<phrase>
<source>grille</source>
<target>grade</target>
<definition>grid</definition>
</phrase>
<phrase>
<source>interrupteur</source>
<target>interruptor</target>
<definition>switch</definition>
</phrase>
<phrase>
<source>maitre</source>
<target>mestre</target>
<definition>master - the element a cross-reference points from</definition>
</phrase>
<phrase>
<source>modele</source>
<target>modelo</target>
<definition>template</definition>
</phrase>
<phrase>
<source>moteur</source>
<target>motor</target>
<definition>motor</definition>
</phrase>
<phrase>
<source>neutre</source>
<target>neutro</target>
<definition>neutral</definition>
</phrase>
<phrase>
<source>nomenclature</source>
<target>lista de materiais</target>
<definition>parts list / bill of materials</definition>
</phrase>
<phrase>
<source>numerotation</source>
<target>numeração</target>
<definition>numbering - automatic wire and part numbering</definition>
</phrase>
<phrase>
<source>phase</source>
<target>fase</target>
<definition>phase</definition>
</phrase>
<phrase>
<source>potentiel</source>
<target>potencial</target>
<definition>potential - conductors electrically joined as one node</definition>
</phrase>
<phrase>
<source>projet</source>
<target>projeto</target>
<definition>project - the .qet file holding the sheets</definition>
</phrase>
<phrase>
<source>puissance</source>
<target>potência</target>
<definition>power</definition>
</phrase>
<phrase>
<source>relais</source>
<target>relé</target>
<definition>relay</definition>
</phrase>
<phrase>
<source>renvoi de folio</source>
<target>referência de folha</target>
<definition>sheet cross-reference</definition>
</phrase>
<phrase>
<source>repere</source>
<target>designação</target>
<definition>designation - the identifying mark on a part</definition>
</phrase>
<phrase>
<source>report de folio</source>
<target>remissão de folha</target>
<definition>sheet cross-reference (the arrow symbol)</definition>
</phrase>
<phrase>
<source>schema</source>
<target>diagrama</target>
<definition>diagram</definition>
</phrase>
<phrase>
<source>section</source>
<target>seção</target>
<definition>cross-section - conductor size in mm2</definition>
</phrase>
<phrase>
<source>sommaire</source>
<target>sumário</target>
<definition>summary / table of contents</definition>
</phrase>
<phrase>
<source>tension</source>
<target>tensão</target>
<definition>voltage</definition>
</phrase>
<phrase>
<source>terre</source>
<target>terra</target>
<definition>earth / ground</definition>
</phrase>
<phrase>
<source>texte</source>
<target>texto</target>
<definition>text</definition>
</phrase>
<phrase>
<source>transformateur</source>
<target>transformador</target>
<definition>transformer</definition>
</phrase>
</QPH>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+917
View File
@@ -0,0 +1,917 @@
<!DOCTYPE QPH>
<QPH sourcelanguage="fr_FR" language="zh">
<phrase>
<source> Contacts : NO : %1, NC : %2, inverseurs : %3, autres : %4
</source>
<target>触点:常开:%1,常闭:%2,转换:%3,其他:%4
</target>
</phrase>
<phrase>
<source> Contacts : NO : %1/%2, NC : %3/%4, inverseurs : %5/%6, autres : %7/%8
</source>
<target>触点:常开:%1/%2,常闭:%3/%4,转换:%5/%6,其他:%7/%8
</target>
</phrase>
<phrase>
<source> %</source>
<target>%</target>
</phrase>
<phrase>
<source> (Ctrl pendant le glissement = position libre, sans accrochage à la grille)</source>
<target>(拖动时按 Ctrl = 自由位置,不吸附到网格)</target>
</phrase>
<phrase>
<source> -- %1 : mode %2</source>
<target>-- %1:%2 模式</target>
</phrase>
<phrase>
<source> ; point rouge : glisser pour repositionner le centre de rotation</source>
<target>;红点:拖动以重新定位旋转中心</target>
</phrase>
<phrase>
<source> ; point turquoise : arc</source>
<target>;青色点:圆弧</target>
</phrase>
<phrase>
<source> ; un bord : inclinaison</source>
<target>;边:倾斜</target>
</phrase>
<phrase>
<source> °</source>
<target>°</target>
</phrase>
<phrase>
<source> — Cliquer : mode %1</source>
<target>— 单击:%1 模式</target>
</phrase>
<phrase>
<source>, Alt = créer des poignées</source>
<target>,Alt = 创建控制柄</target>
</phrase>
<phrase>
<source>, Alt = détacher en polyligne</source>
<target>,Alt = 分离为多段线</target>
</phrase>
<phrase>
<source>Ajoute une courbe de Bézier sur le folio actuel</source>
<target>在当前图纸上添加贝塞尔曲线</target>
</phrase>
<phrase>
<source>Ajouter un point à une courbe</source>
<target>向曲线添加点</target>
</phrase>
<phrase>
<source>Ajouter une courbe</source>
<target>添加曲线</target>
</phrase>
<phrase>
<source>Angle</source>
<target>角度</target>
</phrase>
<phrase>
<source>Anguleux</source>
<target>角点</target>
</phrase>
<phrase>
<source>Aperçu</source>
<target>预览</target>
</phrase>
<phrase>
<source>Arrondir les coins d&apos;%1</source>
<target>将%1的角变圆</target>
</phrase>
<phrase>
<source>Borne 1</source>
<target>端子1</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Borne 2</source>
<target>端子2</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Catégorie</source>
<target>类别</target>
</phrase>
<phrase>
<source>Ce format ne prend pas en charge la transparence : l&apos;image sera enregistrée telle qu&apos;elle était avant l&apos;application de la couleur transparente. Continuer ?</source>
<target>此格式不支持透明度:图像将按应用透明色之前的状态保存。是否继续?</target>
</phrase>
<phrase>
<source>Champ de texte</source>
<target>文本字段</target>
</phrase>
<phrase>
<source>Clic : positionner à la taille d&apos;origine. Cliquer-glisser : positionner et redimensionner. Clic droit : pivoter de 90°. Ctrl+molette : ajuster la taille.</source>
<target>单击:按原始尺寸放置。单击拖动:放置并调整大小。右键:旋转 90°。Ctrl+滚轮:调整大小。</target>
</phrase>
<phrase>
<source>Clic gauche : point suivant ; double-clic ou Entrée : terminer ; clic droit : annuler le dernier point</source>
<target>左键单击:下一个点;双击或 Enter:结束;右键单击:撤销上一个点</target>
</phrase>
<phrase>
<source>Clic gauche : positionner le coin opposé (Maj = carré, Ctrl = depuis le centre + position libre, Ctrl+Maj = carré centré) ; clic droit : annuler</source>
<target>左键单击:放置对角(Shift = 正方形,Ctrl = 从中心 + 自由位置,Ctrl+Shift = 居中正方形);右键单击:取消</target>
</phrase>
<phrase>
<source>Clic gauche : positionner le coin opposé (Maj = cercle, Ctrl = depuis le centre + position libre, Ctrl+Maj = cercle centré) ; clic droit : annuler</source>
<target>左键单击:放置对角(Shift = 圆形,Ctrl = 从中心 + 自由位置,Ctrl+Shift = 居中圆形);右键单击:取消</target>
</phrase>
<phrase>
<source>Clic gauche : positionner le point de départ (Ctrl = position libre)</source>
<target>左键单击:放置起点(Ctrl = 自由位置)</target>
</phrase>
<phrase>
<source>Clic gauche : positionner le point final (Ctrl = position libre) ; clic droit : annuler</source>
<target>左键单击:放置终点(Ctrl = 自由位置);右键单击:取消</target>
</phrase>
<phrase>
<source>Clic gauche : positionner le premier coin (Ctrl = point central, position libre)</source>
<target>左键单击:放置第一个角(Ctrl = 中心点,自由位置)</target>
</phrase>
<phrase>
<source>Clic gauche : positionner le premier point (Ctrl = position libre)</source>
<target>左键单击:放置第一个点(Ctrl = 自由位置)</target>
</phrase>
<phrase>
<source>Clic: point anguleux. Cliquer-glisser: point courbe. Clic sur le premier point: fermer. Échap/Entrée: terminer. Clic droit: annuler le dernier point.</source>
<target>单击:角点。单击拖动:曲线点。单击第一个点:闭合。Esc/Enter:结束。右键:撤销上一个点。</target>
</phrase>
<phrase>
<source>Cliquer</source>
<target>单击</target>
</phrase>
<phrase>
<source>Cliquer : mode %1</source>
<target>单击:%1 模式</target>
</phrase>
<phrase>
<source>Cliquez pour choisir une couleur</source>
<target>点击选择颜色</target>
</phrase>
<phrase>
<source>Cliquez sur l&apos;image pour ajouter une couleur. Ajustez la tolérance de chaque couleur avec son curseur, ou cliquez sur × pour la retirer.</source>
<target>单击图像以添加颜色。使用滑块调整每种颜色的容差,或单击 × 将其移除。</target>
</phrase>
<phrase>
<source>Cliquez sur l&apos;image pour choisir une couleur</source>
<target>单击图像选择颜色</target>
</phrase>
<phrase>
<source>Coins arrondis</source>
<target>圆角</target>
</phrase>
<phrase>
<source>Composant 1</source>
<target>元件1</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Composant 2</source>
<target>元件2</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Convertir %1 en courbe de Bézier</source>
<target>将 %1 转换为贝塞尔曲线</target>
</phrase>
<phrase>
<source>Convertir %1 en polyligne</source>
<target>将 %1 转换为多段线</target>
</phrase>
<phrase>
<source>Convertir en courbe de Bézier</source>
<target>转换为贝塞尔曲线</target>
</phrase>
<phrase>
<source>Convertir en polyligne</source>
<target>转换为多段线</target>
</phrase>
<phrase>
<source>Couleur transparente</source>
<target>透明色</target>
</phrase>
<phrase>
<source>Couleur transparente...</source>
<target>透明色...</target>
</phrase>
<phrase>
<source>Courant nominal</source>
<target>额定电流</target>
</phrase>
<phrase>
<source>Deplacer le centre de rotation</source>
<target>移动旋转中心</target>
</phrase>
<phrase>
<source>Distance in pixels between the label and the slave cross reference</source>
<target>标签与从站交叉引用之间的像素距离</target>
</phrase>
<phrase>
<source>Distance label - slave :</source>
<target>标签距离 - 从站:</target>
</phrase>
<phrase>
<source>Définir une couleur transparente</source>
<target>设置透明色</target>
</phrase>
<phrase>
<source>Déformer une courbe</source>
<target>变形曲线</target>
</phrase>
<phrase>
<source>Déplacer le centre de rotation d&apos;une image</source>
<target>移动图像的旋转中心</target>
</phrase>
<phrase>
<source>Déverrouillé : largeur et hauteur peuvent être modifiées indépendamment. Cliquer pour verrouiller.</source>
<target>已解锁:宽度和高度可独立修改。单击锁定。</target>
</phrase>
<phrase>
<source>Enregistrer l&apos;image d&apos;origine sous...</source>
<target>原始图像另存为...</target>
</phrase>
<phrase>
<source>Enregistrer l&apos;image sous...</source>
<target>图像另存为...</target>
</phrase>
<phrase>
<source>Erreur</source>
<target>错误</target>
</phrase>
<phrase>
<source>Exclure de la nomenclature</source>
<target>从物料清单中排除</target>
</phrase>
<phrase>
<source>Faire pivoter %1</source>
<target>旋转 %1</target>
</phrase>
<phrase>
<source>Faire pivoter une image</source>
<target>旋转图像</target>
</phrase>
<phrase>
<source>Faites glisser les poignées, ou l&apos;intérieur du cadre, pour ajuster la zone à conserver.</source>
<target>拖动控制柄或框内区域,调整要保留的区域。</target>
</phrase>
<phrase>
<source>Fil</source>
<target>导线</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Folio</source>
<target>图页</target>
<definition>column title</definition>
</phrase>
<phrase>
<source>Forme fermée</source>
<target>闭合形状</target>
</phrase>
<phrase>
<source>Glisser %1 : rotation (Maj = 15°)</source>
<target>拖动 %1:旋转(Shift = 15°)</target>
</phrase>
<phrase>
<source>Glisser : ajuster l&apos;arc (Ctrl = position libre, Maj = 15°)</source>
<target>拖动:调整圆弧(Ctrl = 自由位置,Shift = 15°)</target>
</phrase>
<phrase>
<source>Glisser : arrondir les coins (Ctrl = position libre)</source>
<target>拖动:圆角(Ctrl = 自由位置)</target>
</phrase>
<phrase>
<source>Glisser : déformer la courbe (Ctrl = position libre, Alt = briser la tangente)</source>
<target>拖动:变形曲线(Ctrl = 自由位置,Alt = 断开切线)</target>
</phrase>
<phrase>
<source>Glisser : déplacer ce point</source>
<target>拖动:移动此点</target>
</phrase>
<phrase>
<source>Glisser : déplacer le centre de rotation</source>
<target>拖动:移动旋转中心</target>
</phrase>
<phrase>
<source>Glisser : déplacer le point (Ctrl = position libre</source>
<target>拖动:移动点(Ctrl = 自由位置</target>
</phrase>
<phrase>
<source>Glisser : inclinaison (Ctrl = position libre, Maj = 15°)</source>
<target>拖动:倾斜(Ctrl = 自由位置,Shift = 15°)</target>
</phrase>
<phrase>
<source>Glisser : incliner (Maj = par pas de 15°)</source>
<target>拖动:倾斜(Shift = 以 15° 为步长)</target>
</phrase>
<phrase>
<source>Glisser : pivoter (Maj = par pas de 15°)</source>
<target>拖动:旋转(Shift = 以 15° 为步长)</target>
</phrase>
<phrase>
<source>Glisser : redimensionner (Ctrl = depuis le centre + position libre, Maj = proportions</source>
<target>拖动:调整大小(Ctrl = 从中心 + 自由位置,Shift = 比例</target>
</phrase>
<phrase>
<source>Glisser : redimensionner (Maj = conserver les proportions, Ctrl = depuis le centre)</source>
<target>拖动:调整大小(Shift = 保持比例,Ctrl = 从中心)</target>
</phrase>
<phrase>
<source>Glisser : repositionner le centre de rotation (Ctrl = position libre)</source>
<target>拖动:重新定位旋转中心(Ctrl = 自由位置)</target>
</phrase>
<phrase>
<source>Glisser : rotation (Ctrl = position libre, Maj = 15°)</source>
<target>拖动:旋转(Ctrl = 自由位置,Shift = 15°)</target>
</phrase>
<phrase>
<source>Glisser le point violet : arrondir les coins</source>
<target>拖动紫色点:圆角</target>
</phrase>
<phrase>
<source>Glisser un coin : pivoter (Maj = par pas de 15°) ; glisser un bord : incliner (Maj = par pas de 15°) ; point rouge : déplacer le centre de rotation</source>
<target>拖动角:旋转(Shift = 以 15° 为步长);拖动边:倾斜(Shift = 以 15° 为步长);红点:移动旋转中心</target>
</phrase>
<phrase>
<source>Glisser un coin/bord : redimensionner (Ctrl = depuis le centre, Maj = conserver les proportions)</source>
<target>拖动角/边:调整大小(Ctrl = 从中心,Shift = 保持比例)</target>
</phrase>
<phrase>
<source>Glisser un coin/bord : redimensionner (Ctrl = depuis le centre, Maj = proportions, Alt = détacher en polyligne)</source>
<target>拖动角/边:调整大小(Ctrl = 从中心,Shift = 比例,Alt = 分离为多段线)</target>
</phrase>
<phrase>
<source>Glisser un point : le déplacer</source>
<target>拖动点:移动它</target>
</phrase>
<phrase>
<source>Glisser une extrémité : la déplacer</source>
<target>拖动端点:移动它</target>
</phrase>
<phrase>
<source>Glisser une poignée ou la courbe : déformer (Alt = briser la tangente) ; Alt+glisser un point anguleux : créer des poignées ; clic droit : menu du nœud le plus proche</source>
<target>拖动控制柄或曲线:变形(Alt = 断开切线);Alt+拖动角点:创建控制柄;右键单击:最近节点菜单</target>
</phrase>
<phrase>
<source>Géométrie</source>
<target>几何</target>
</phrase>
<phrase>
<source>Hauteur</source>
<target>高度</target>
</phrase>
<phrase>
<source>Image BMP (*.bmp)</source>
<target>BMP 图像 (*.bmp)</target>
</phrase>
<phrase>
<source>Image Files (*.png *.jpg *.jpeg *.bmp *.svg)</source>
<target>图像文件 (*.png *.jpg *.jpeg *.bmp *.svg)</target>
</phrase>
<phrase>
<source>Image JPEG (*.jpg *.jpeg)</source>
<target>JPEG 图像 (*.jpg *.jpeg)</target>
</phrase>
<phrase>
<source>Image PNG (*.png)</source>
<target>PNG 图像 (*.png)</target>
</phrase>
<phrase>
<source>Image source</source>
<target>源图像</target>
</phrase>
<phrase>
<source>Image SVG (*.svg)</source>
<target>SVG 图像 (*.svg)</target>
</phrase>
<phrase>
<source>Images non incluses dans l&apos;export DXF</source>
<target>图像未包含在 DXF 导出中</target>
<definition>message box title</definition>
</phrase>
<phrase>
<source>Impossible d&apos;enregistrer l&apos;image à cet emplacement.</source>
<target>无法将图像保存到该位置。</target>
</phrase>
<phrase>
<source>Impossible d&apos;enregistrer la nomenclature dans %1.
%2</source>
<target>无法将物料清单保存到 %1。
%2</target>
</phrase>
<phrase>
<source>Impossible de charger l&apos;image.</source>
<target>无法加载图像。</target>
</phrase>
<phrase>
<source>Inclinaison X</source>
<target>X 倾斜</target>
</phrase>
<phrase>
<source>Inclinaison Y</source>
<target>Y 倾斜</target>
</phrase>
<phrase>
<source>Incliner %1</source>
<target>倾斜 %1</target>
</phrase>
<phrase>
<source>Incliner une image</source>
<target>倾斜图像</target>
</phrase>
<phrase>
<source>La limite fixée pour cet élément maître est atteinte (Limite: %1).
Voulez-vous tout de même lier ce contact esclave ?</source>
<target>已达到此主元件的限制(限制:%1)。
是否仍要链接此从属触点?</target>
</phrase>
<phrase>
<source>Largeur</source>
<target>宽度</target>
</phrase>
<phrase>
<source>Le format DXF utilisé ici (AC1006) ne permet pas d&apos;inclure d&apos;image. Les images seront représentées uniquement par un rectangle de contour (position, taille, rotation et inclinaison conservées), sans le contenu de l&apos;image.</source>
<target>此处使用的 DXF 格式 (AC1006) 无法包含图像。图像将仅以轮廓矩形表示(保留位置、大小、旋转和倾斜),不包含图像内容。</target>
<definition>message box content</definition>
</phrase>
<phrase>
<source>Lisse</source>
<target>平滑</target>
</phrase>
<phrase>
<source>Liste de câblage</source>
<target>接线列表</target>
<definition>window title</definition>
</phrase>
<phrase>
<source>Liste de câblage (base de données)</source>
<target>接线列表(数据库)</target>
</phrase>
<phrase>
<source>Longueur</source>
<target>长度</target>
</phrase>
<phrase>
<source>margin: 5px; font-weight: bold;</source>
<target>margin: 5px; font-weight: bold;</target>
</phrase>
<phrase>
<source>Miroir horizontal</source>
<target>水平镜像</target>
</phrase>
<phrase>
<source>Miroir horizontal d&apos;une image</source>
<target>图像水平镜像</target>
</phrase>
<phrase>
<source>Miroir horizontal de %1</source>
<target>%1 的水平镜像</target>
</phrase>
<phrase>
<source>Miroir impossible : inclinaison trop extrême pour cette forme</source>
<target>无法镜像:此形状的倾斜过于极端</target>
</phrase>
<phrase>
<source>Miroir vertical</source>
<target>垂直镜像</target>
</phrase>
<phrase>
<source>Miroir vertical d&apos;une image</source>
<target>图像垂直镜像</target>
</phrase>
<phrase>
<source>Miroir vertical de %1</source>
<target>%1 的垂直镜像</target>
</phrase>
<phrase>
<source>Modifier l&apos;angle d&apos;un arc</source>
<target>修改弧的角度</target>
</phrase>
<phrase>
<source>Modifier l&apos;angle d&apos;une forme</source>
<target>修改形状角度</target>
</phrase>
<phrase>
<source>Modifier l&apos;angle d&apos;une image</source>
<target>修改图像角度</target>
</phrase>
<phrase>
<source>Modifier l&apos;inclinaison d&apos;une image</source>
<target>修改图像倾斜</target>
</phrase>
<phrase>
<source>Modifier la courbure d&apos;%1</source>
<target>修改%1的曲率</target>
</phrase>
<phrase>
<source>Modifier la forme d&apos;%1</source>
<target>修改%1的形状</target>
</phrase>
<phrase>
<source>Modifier la hauteur d&apos;une image</source>
<target>修改图像高度</target>
</phrase>
<phrase>
<source>Modifier la largeur d&apos;une image</source>
<target>修改图像宽度</target>
</phrase>
<phrase>
<source>Modifier la longueur d&apos;une ligne</source>
<target>修改线条长度</target>
</phrase>
<phrase>
<source>Modifier la taille d&apos;une forme</source>
<target>修改形状大小</target>
</phrase>
<phrase>
<source>Modifier le type d&apos;un nœud</source>
<target>修改节点类型</target>
</phrase>
<phrase>
<source>Modifier une image</source>
<target>修改图像</target>
</phrase>
<phrase>
<source>Modèle</source>
<target>型号</target>
</phrase>
<phrase>
<source>Nombre maximum de contacts esclaves définis : non défini
</source>
<target>已定义的从属触点最大数量:未定义
</target>
</phrase>
<phrase>
<source>Notes</source>
<target>备注</target>
</phrase>
<phrase>
<source>Nœud le plus proche</source>
<target>最近节点</target>
</phrase>
<phrase>
<source>pivoter/incliner</source>
<target>旋转/倾斜</target>
</phrase>
<phrase>
<source>Rayon X</source>
<target>X半径</target>
</phrase>
<phrase>
<source>Rayon Y</source>
<target>Y半径</target>
</phrase>
<phrase>
<source>redimensionner</source>
<target>调整大小</target>
</phrase>
<phrase>
<source>Redimensionner %1</source>
<target>调整 %1 大小</target>
</phrase>
<phrase>
<source>Redimensionner une image</source>
<target>调整图像大小</target>
</phrase>
<phrase>
<source>Remplacer l&apos;image...</source>
<target>替换图像...</target>
</phrase>
<phrase>
<source>Remplacer une image</source>
<target>替换图像</target>
</phrase>
<phrase>
<source>Restaurer les proportions</source>
<target>恢复比例</target>
</phrase>
<phrase>
<source>Restaurer les proportions d&apos;une image</source>
<target>恢复图像比例</target>
</phrase>
<phrase>
<source>Retirer cette couleur</source>
<target>移除此颜色</target>
</phrase>
<phrase>
<source>Revenir à l&apos;image complète, sans rognage</source>
<target>恢复为完整图像,不裁剪</target>
</phrase>
<phrase>
<source>rgb(%1, %2, %3)</source>
<target>rgb(%1, %2, %3)</target>
</phrase>
<phrase>
<source>Rogner l&apos;image</source>
<target>裁剪图像</target>
</phrase>
<phrase>
<source>Rogner une image</source>
<target>裁剪图像</target>
</phrase>
<phrase>
<source>Rogner...</source>
<target>裁剪...</target>
</phrase>
<phrase>
<source>Rotation/Inclinaison</source>
<target>旋转/倾斜</target>
</phrase>
<phrase>
<source>Réinitialiser</source>
<target>重置</target>
</phrase>
<phrase>
<source>Selectionner une image...</source>
<target>选择图像...</target>
</phrase>
<phrase>
<source>Supprimer le nœud le plus proche</source>
<target>删除最近节点</target>
</phrase>
<phrase>
<source>Supprimer un point d&apos;une courbe</source>
<target>从曲线删除点</target>
</phrase>
<phrase>
<source>Symétrique</source>
<target>对称</target>
</phrase>
<phrase>
<source>Séparation de potentiel</source>
<target>电位分隔</target>
</phrase>
<phrase>
<source>Taille</source>
<target>大小</target>
</phrase>
<phrase>
<source>Tension nominale</source>
<target>额定电压</target>
</phrase>
<phrase>
<source>Tolérance pour cette couleur</source>
<target>此颜色的容差</target>
</phrase>
<phrase>
<source>Tous les fichiers (*)</source>
<target>所有文件 (*)</target>
</phrase>
<phrase>
<source>Transformer %1</source>
<target>变换 %1</target>
</phrase>
<phrase>
<source>Transparence non conservée</source>
<target>不保留透明度</target>
</phrase>
<phrase>
<source>un arc</source>
<target>一段弧</target>
</phrase>
<phrase>
<source>un coin</source>
<target>一个角</target>
</phrase>
<phrase>
<source>un point</source>
<target>一个点</target>
</phrase>
<phrase>
<source>une courbe</source>
<target>一条曲线</target>
</phrase>
<phrase>
<source>une extrémité</source>
<target>一个端点</target>
</phrase>
<phrase>
<source>une image</source>
<target>一张图像</target>
<definition>part of a sentence listing the content of a diagram</definition>
</phrase>
<phrase>
<source>Verrouiller la numérotation automatique</source>
<target>锁定自动编号</target>
</phrase>
<phrase>
<source>Verrouiller la position</source>
<target>锁定位置</target>
</phrase>
<phrase>
<source>Verrouillé : modifier la largeur ou la hauteur ajuste l&apos;autre pour conserver les proportions. Cliquer pour déverrouiller.</source>
<target>已锁定:修改宽度或高度会调整另一个以保持比例。单击解锁。</target>
</phrase>
<phrase>
<source>Échec de l&apos;enregistrement</source>
<target>保存失败</target>
</phrase>
<phrase>
<source>Édition des nœuds</source>
<target>节点编辑</target>
</phrase>
<phrase>
<source>armoire</source>
<target>机柜</target>
<definition>cabinet / enclosure</definition>
</phrase>
<phrase>
<source>bobine</source>
<target>线圈</target>
<definition>coil</definition>
</phrase>
<phrase>
<source>borne</source>
<target>端子</target>
<definition>terminal - a connection point on a symbol</definition>
</phrase>
<phrase>
<source>bornier</source>
<target>端子排</target>
<definition>terminal strip</definition>
</phrase>
<phrase>
<source>cable</source>
<target>电缆</target>
<definition>cable</definition>
</phrase>
<phrase>
<source>calibre</source>
<target>额定值</target>
<definition>rating - the current rating of a device</definition>
</phrase>
<phrase>
<source>cartouche</source>
<target>标题栏</target>
<definition>title block - the framed information panel</definition>
</phrase>
<phrase>
<source>collection</source>
<target>符号库</target>
<definition>collection - the symbol library</definition>
</phrase>
<phrase>
<source>conducteur</source>
<target>导线</target>
<definition>conductor - the drawn wire between terminals</definition>
</phrase>
<phrase>
<source>contact</source>
<target>触点</target>
<definition>contact</definition>
</phrase>
<phrase>
<source>contacteur</source>
<target>接触器</target>
<definition>contactor</definition>
</phrase>
<phrase>
<source>courant</source>
<target>电流</target>
<definition>current</definition>
</phrase>
<phrase>
<source>disjoncteur</source>
<target>断路器</target>
<definition>circuit breaker</definition>
</phrase>
<phrase>
<source>dossier</source>
<target>文件夹</target>
<definition>folder</definition>
</phrase>
<phrase>
<source>element</source>
<target>元件</target>
<definition>element - a symbol placed on a sheet</definition>
</phrase>
<phrase>
<source>esclave</source>
<target>从</target>
<definition>slave - the contact block belonging to a master</definition>
</phrase>
<phrase>
<source>fusible</source>
<target>熔断器</target>
<definition>fuse</definition>
</phrase>
<phrase>
<source>grille</source>
<target>网格</target>
<definition>grid</definition>
</phrase>
<phrase>
<source>interrupteur</source>
<target>开关</target>
<definition>switch</definition>
</phrase>
<phrase>
<source>maitre</source>
<target>主</target>
<definition>master - the element a cross-reference points from</definition>
</phrase>
<phrase>
<source>modele</source>
<target>模板</target>
<definition>template</definition>
</phrase>
<phrase>
<source>moteur</source>
<target>电动机</target>
<definition>motor</definition>
</phrase>
<phrase>
<source>neutre</source>
<target>中性线</target>
<definition>neutral</definition>
</phrase>
<phrase>
<source>nomenclature</source>
<target>物料清单</target>
<definition>parts list / bill of materials</definition>
</phrase>
<phrase>
<source>numerotation</source>
<target>编号</target>
<definition>numbering - automatic wire and part numbering</definition>
</phrase>
<phrase>
<source>phase</source>
<target>相</target>
<definition>phase</definition>
</phrase>
<phrase>
<source>potentiel</source>
<target>电位</target>
<definition>potential - conductors electrically joined as one node</definition>
</phrase>
<phrase>
<source>projet</source>
<target>项目</target>
<definition>project - the .qet file holding the sheets</definition>
</phrase>
<phrase>
<source>puissance</source>
<target>功率</target>
<definition>power</definition>
</phrase>
<phrase>
<source>relais</source>
<target>继电器</target>
<definition>relay</definition>
</phrase>
<phrase>
<source>renvoi de folio</source>
<target>图纸交叉引用</target>
<definition>sheet cross-reference</definition>
</phrase>
<phrase>
<source>repere</source>
<target>标识</target>
<definition>designation - the identifying mark on a part</definition>
</phrase>
<phrase>
<source>report de folio</source>
<target>图纸交叉引用</target>
<definition>sheet cross-reference (the arrow symbol)</definition>
</phrase>
<phrase>
<source>schema</source>
<target>原理图</target>
<definition>diagram</definition>
</phrase>
<phrase>
<source>section</source>
<target>截面积</target>
<definition>cross-section - conductor size in mm2</definition>
</phrase>
<phrase>
<source>sommaire</source>
<target>目录</target>
<definition>summary / table of contents</definition>
</phrase>
<phrase>
<source>tension</source>
<target>电压</target>
<definition>voltage</definition>
</phrase>
<phrase>
<source>terre</source>
<target>接地</target>
<definition>earth / ground</definition>
</phrase>
<phrase>
<source>texte</source>
<target>文本</target>
<definition>text</definition>
</phrase>
<phrase>
<source>transformateur</source>
<target>变压器</target>
<definition>transformer</definition>
</phrase>
</QPH>
+2968 -1648
View File
File diff suppressed because it is too large Load Diff
+3200 -1489
View File
File diff suppressed because it is too large Load Diff
+2954 -1527
View File
File diff suppressed because it is too large Load Diff
+2915 -1522
View File
File diff suppressed because it is too large Load Diff
+2801 -1380
View File
File diff suppressed because it is too large Load Diff
+2915 -1522
View File
File diff suppressed because it is too large Load Diff
+3097 -1666
View File
File diff suppressed because it is too large Load Diff
+2896 -1571
View File
File diff suppressed because it is too large Load Diff
+2813 -1735
View File
File diff suppressed because it is too large Load Diff
+2860 -1714
View File
File diff suppressed because it is too large Load Diff
+2867 -1706
View File
File diff suppressed because it is too large Load Diff
+2773 -1385
View File
File diff suppressed because it is too large Load Diff
+2903 -1566
View File
File diff suppressed because it is too large Load Diff
+2873 -1584
View File
File diff suppressed because it is too large Load Diff
+2900 -1529
View File
File diff suppressed because it is too large Load Diff
+2879 -1562
View File
File diff suppressed because it is too large Load Diff
+2900 -1579
View File
File diff suppressed because it is too large Load Diff
+2926 -1525
View File
File diff suppressed because it is too large Load Diff
+2798 -1389
View File
File diff suppressed because it is too large Load Diff
+2777 -1699
View File
File diff suppressed because it is too large Load Diff
+3228 -1489
View File
File diff suppressed because it is too large Load Diff
+2842 -1685
View File
File diff suppressed because it is too large Load Diff
+2799 -1386
View File
File diff suppressed because it is too large Load Diff
+2878 -1678
View File
File diff suppressed because it is too large Load Diff
+2862 -1766
View File
File diff suppressed because it is too large Load Diff
+2943 -1526
View File
File diff suppressed because it is too large Load Diff
+2838 -1742
View File
File diff suppressed because it is too large Load Diff
+2859 -1745
View File
File diff suppressed because it is too large Load Diff
+2838 -1742
View File
File diff suppressed because it is too large Load Diff
+2927 -1526
View File
File diff suppressed because it is too large Load Diff
+2897 -1527
View File
File diff suppressed because it is too large Load Diff
+2943 -1526
View File
File diff suppressed because it is too large Load Diff
+2773 -1385
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -62,6 +62,6 @@
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>12.3.0</string> <string>14.0.0</string>
</dict> </dict>
</plist> </plist>
+10 -2
View File
@@ -99,11 +99,14 @@ fi
cmake -S . -B "$BUILD_DIR" -G Ninja \ cmake -S . -B "$BUILD_DIR" -G Ninja \
-DCMAKE_BUILD_TYPE=Release \ -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 \
-DQT_VERSION_MAJOR=$QT_MAJOR \ -DQT_VERSION_MAJOR=$QT_MAJOR \
-DBUILD_WITH_KF=$BUILD_WITH_KF \ -DBUILD_WITH_KF=$BUILD_WITH_KF \
-DBUILD_KF=OFF \ -DBUILD_KF=OFF \
-DQET_EXPORT_PROJECT_DB=ON \ -DQET_EXPORT_PROJECT_DB=ON \
-DPACKAGE_TESTS=OFF -DPACKAGE_TESTS=OFF \
-DQET_ENABLE_SPACEMOUSE=ON \
-DQET_SPACEMOUSE_BACKEND=hid
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "ERROR: cmake configure failed." echo "ERROR: cmake configure failed."
@@ -256,6 +259,7 @@ echo "Install Info.plist and app icon:"
cp -R ${current_dir}/misc/Info.plist $BUNDLE/Contents/ cp -R ${current_dir}/misc/Info.plist $BUNDLE/Contents/
cp -R ${current_dir}/ico/mac_icon/*.icns $BUNDLE/Contents/Resources/ cp -R ${current_dir}/ico/mac_icon/*.icns $BUNDLE/Contents/Resources/
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION r$HEAD" "$BUNDLE/Contents/Info.plist" /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION r$HEAD" "$BUNDLE/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Set :LSMinimumSystemVersion 14.0.0" "$BUNDLE/Contents/Info.plist"
### add missing files ############################################### ### add missing files ###############################################
echo echo
@@ -396,10 +400,12 @@ done
echo "-- Signing main executable..." echo "-- Signing main executable..."
codesign --force --sign "$IDENTITY" --timestamp --options=runtime \ codesign --force --sign "$IDENTITY" --timestamp --options=runtime \
--entitlements "${current_dir}/misc/qelectrotech.entitlements" \
"$BUNDLE/Contents/MacOS/$APPNAME" "$BUNDLE/Contents/MacOS/$APPNAME"
echo "-- Signing bundle..." echo "-- Signing bundle..."
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$BUNDLE" codesign --force --sign "$IDENTITY" --timestamp --options=runtime \
--entitlements "${current_dir}/misc/qelectrotech.entitlements" "$BUNDLE"
echo echo
echo "Verifying bundle signature..." echo "Verifying bundle signature..."
@@ -500,8 +506,10 @@ find "$MOUNT_POINT/$BUNDLE/Contents/PlugIns" \( -name "*.dylib" -o -name "*.so"
codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$lib" codesign --force --sign "$IDENTITY" --timestamp --options=runtime "$lib"
done done
codesign --force --sign "$IDENTITY" --timestamp --options=runtime \ codesign --force --sign "$IDENTITY" --timestamp --options=runtime \
--entitlements "${current_dir}/misc/qelectrotech.entitlements" \
"$MOUNT_POINT/$BUNDLE/Contents/MacOS/$APPNAME" "$MOUNT_POINT/$BUNDLE/Contents/MacOS/$APPNAME"
codesign --force --sign "$IDENTITY" --timestamp --options=runtime \ codesign --force --sign "$IDENTITY" --timestamp --options=runtime \
--entitlements "${current_dir}/misc/qelectrotech.entitlements" \
"$MOUNT_POINT/$BUNDLE" "$MOUNT_POINT/$BUNDLE"
echo "Verifying bundle signature inside DMG..." echo "Verifying bundle signature inside DMG..."
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- 3D mouse with 3DxWare installed: lets QElectroTech load
3Dconnexion's client library, which 3Dconnexion signs, at run time
(sources/spacemouse/connexionbackend.h). The hardened runtime
refuses it otherwise, and the 3D mouse does nothing. -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
+14 -6
View File
@@ -33,7 +33,7 @@ here read the model.
| `qet_diff` | **what an edit actually changed** — element moves, adds, removes, relabels; conductor changes; and folio fields, texts, shapes, images, symbol text fields and terminal strips | | `qet_diff` | **what an edit actually changed** — element moves, adds, removes, relabels; conductor changes; and folio fields, texts, shapes, images, symbol text fields and terminal strips |
| `qet_scan` | sweep a directory of projects, counting nodes carrying an attribute | | `qet_scan` | sweep a directory of projects, counting nodes carrying an attribute |
| `qet_element_info` | a `.elmt`: translated names, terminals, info fields, part counts | | `qet_element_info` | a `.elmt`: translated names, terminals, info fields, part counts |
| `qet_export` | run a headless export (pdf, png, svg, bom, cables, wires, wiring, nets, links, info) | | `qet_export` | run a headless export (pdf, png, svg, dxf, bom, cables, wires, wiring, nets, links, info) |
| `qet_edit` | **change a project** — place, move, rotate, label, wire, number, cross-reference, add text, shapes and images, restyle a symbol's text fields, delete; then diff the result | | `qet_edit` | **change a project** — place, move, rotate, label, wire, number, cross-reference, add text, shapes and images, restyle a symbol's text fields, delete; then diff the result |
| `qet_element_build` | **author a `.elmt`** — draw a new symbol, with terminals to wire it by | | `qet_element_build` | **author a `.elmt`** — draw a new symbol, with terminals to wire it by |
| `qet_project_new` | **start from nothing** — an empty project with a title and folios | | `qet_project_new` | **start from nothing** — an empty project with a title and folios |
@@ -271,6 +271,13 @@ Python, plus the hang guard on `addConductor` and the database refresh in
QElectroTech applies to its own custom-query box, not one invented here. QElectroTech applies to its own custom-query box, not one invented here.
An empty result and a failed query are told apart: `row_count` 0 with no An empty result and a failed query are told apart: `row_count` 0 with no
`error` means nothing matched, and a typo'd column name says so. `error` means nothing matched, and a typo'd column name says so.
- **Texts, shapes and images are in the database too**, one row each in
`drawing_item_view` (`uuid`, `kind`, `folio`, position and size, and a
`description`: the shape type or the text). The uuid is the one saved in
the file, and every `qet_edit` op that takes a text, shape or image
`index` also takes that uuid, which does not shift the way an index
does. `qet_element_build` gives every part of a symbol a uuid as well,
returned in `part_uuids`; `qet_element_info` lists them in `part_list`.
- **`qet_export` isolates its launch.** SingleApplication keys its socket - **`qet_export` isolates its launch.** SingleApplication keys its socket
on `applicationFilePath()`, so a second launch of the same binary path on `applicationFilePath()`, so a second launch of the same binary path
forwards its request to an already-running instance and returns *that* forwards its request to an already-running instance and returns *that*
@@ -295,11 +302,12 @@ Python, plus the hang guard on `addConductor` and the database refresh in
no-op edit. Where an element predates persisted uuids the end cannot be no-op edit. Where an element predates persisted uuids the end cannot be
resolved and keeps a `#`-marked unstable key; the diff then reports resolved and keeps a `#`-marked unstable key; the diff then reports
`unstable_keys` and says so rather than pretending to be comparable. `unstable_keys` and says so rather than pretending to be comparable.
- **Texts, shapes and images have no uuid**, so `qet_diff` cannot say "the same - **Texts, shapes and images are keyed by uuid** when every one on both sides
text, edited": an edited text reads as the old one removed and a new one has one, so an edit or a move reads as a change to that item. A file saved
added, both shown. Shapes and images are keyed by position, so a restyle before they carried a uuid has none; for such a pair (including a legacy
or rescale *is* reported as a change to that item, but a move reads as a file against its first re-save) that kind falls back to position, where an
removal plus an addition. The folio `version` attribute is left out of the edited text reads as removed plus added and a move as a removal plus an
addition. Each section says which it used in `keyed_by`. The folio `version` attribute is left out of the
comparison on purpose: QElectroTech rewrites it on every save, and comparison on purpose: QElectroTech rewrites it on every save, and
including it made every folio of any re-saved project look edited. including it made every folio of any re-saved project look edited.
- **Elements** written before persisted uuids fall back to a positional key, - **Elements** written before persisted uuids fall back to a positional key,
+139 -54
View File
@@ -69,6 +69,7 @@ EXPORT_FORMATS = {
"pdf": "--export-pdf", "pdf": "--export-pdf",
"png": "--export-png", "png": "--export-png",
"svg": "--export-svg", "svg": "--export-svg",
"dxf": "--export-dxf",
"bom": "--export-bom", "bom": "--export-bom",
"cables": "--export-cables", "cables": "--export-cables",
"wires": "--export-wires", "wires": "--export-wires",
@@ -314,32 +315,42 @@ def _plain_text(html: str) -> str:
def _extras(root: ET.Element) -> dict: def _extras(root: ET.Element) -> dict:
"""Everything a folio holds besides elements and conductors. """Everything a folio holds besides elements and conductors.
Independent texts, shapes and images have no uuid, so nothing here can Independent texts, shapes and images are records, not a keyed dict:
say "this is the same text, edited". A change to one therefore reads as files written since they carry a uuid are compared by it (see
the old one removed and a new one added, with both shown -- accurate, _diff_items), so an edited or moved text reads as that text, changed.
if less tidy than a move. Position is the identity they have. Older files have only position to go on, and there a change reads as
the old one removed and a new one added, with both shown.
""" """
folios, texts, shapes, images = {}, {}, {}, {} folios, texts, shapes, images = {}, [], [], []
def record(el, label, value):
return {"uuid": el.get("uuid", ""), "key": tuple(label.values()),
"label": label, "value": value}
for n, d in _folios(root): for n, d in _folios(root):
folios[n] = {f: d.get(f, "") for f in _FOLIO_FIELDS} folios[n] = {f: d.get(f, "") for f in _FOLIO_FIELDS}
for t in d.iter("input"): for t in d.iter("input"):
key = (n, t.get("x", ""), t.get("y", ""), _plain_text(t.get("text", ""))) texts.append(record(
texts[key] = {"rotation": t.get("rotation", "0"), t, {"folio": n, "x": t.get("x", ""), "y": t.get("y", ""),
"font": t.get("font", ""), "color": t.get("color", "")} "text": _plain_text(t.get("text", ""))},
{"rotation": t.get("rotation", "0"),
"font": t.get("font", ""), "color": t.get("color", "")}))
for sh in d.iter("shape"): for sh in d.iter("shape"):
pen, brush = sh.find("pen"), sh.find("brush") pen, brush = sh.find("pen"), sh.find("brush")
key = (n, sh.get("type", ""), sh.get("x1", ""), sh.get("y1", ""), shapes.append(record(
sh.get("x2", ""), sh.get("y2", "")) sh, {"folio": n, "type": sh.get("type", ""),
shapes[key] = { "from": [sh.get("x1", ""), sh.get("y1", "")],
"line_color": pen.get("color", "") if pen is not None else "", "to": [sh.get("x2", ""), sh.get("y2", "")]},
"line_style": pen.get("style", "") if pen is not None else "", {"line_color": pen.get("color", "") if pen is not None else "",
"line_width": pen.get("widthF", "") if pen is not None else "", "line_style": pen.get("style", "") if pen is not None else "",
"fill": (brush.get("color", "") if brush is not None and "line_width": pen.get("widthF", "") if pen is not None else "",
brush.get("style", "") != "NoBrush" else "none"), "fill": (brush.get("color", "") if brush is not None and
"rotation": sh.get("rotation", "0")} brush.get("style", "") != "NoBrush" else "none"),
"rotation": sh.get("rotation", "0")}))
for im in d.iter("image"): for im in d.iter("image"):
key = (n, im.get("x", ""), im.get("y", "")) images.append(record(
images[key] = {"scale": im.get("size", ""), "rotation": im.get("rotation", "")} im, {"folio": n, "x": im.get("x", ""), "y": im.get("y", "")},
{"scale": im.get("size", ""), "rotation": im.get("rotation", "")}))
element_texts = {} element_texts = {}
for n, d in _folios(root): for n, d in _folios(root):
@@ -388,6 +399,32 @@ def _diff_keyed(a: dict, b: dict, label) -> dict:
"changed": changed[:50]} "changed": changed[:50]}
def _diff_items(a: list, b: list) -> dict:
"""_diff_keyed() over _extras() records of one kind.
Keyed on uuid only when every item on both sides has one. A file saved
before these items carried a uuid has none, and the first save by a
current QElectroTech gives them one, so a mixed pair falls back to
position for the whole kind rather than reading as everything removed
and re-added. On uuid, position is part of what is compared, so a move
is a change to that item.
"""
by_uuid = all(r["uuid"] for r in a + b)
def key(r):
return r["uuid"] if by_uuid else str(r["key"])
def keyed(rs):
return {key(r): ({**r["label"], **r["value"]} if by_uuid else r["value"])
for r in rs}
labels = {key(r): ({**r["label"], "uuid": r["uuid"]} if by_uuid else r["label"])
for r in a + b}
out = _diff_keyed(keyed(a), keyed(b), lambda k: labels[k])
out["keyed_by"] = "uuid" if by_uuid else "position"
return out
def _diff_extras(before: ET.Element, after: ET.Element) -> dict: def _diff_extras(before: ET.Element, after: ET.Element) -> dict:
a, b = _extras(before), _extras(after) a, b = _extras(before), _extras(after)
out = {} out = {}
@@ -407,13 +444,9 @@ def _diff_extras(before: ET.Element, after: ET.Element) -> dict:
if len(a["folios"]) != len(b["folios"]) and folio_changes: if len(a["folios"]) != len(b["folios"]) and folio_changes:
out["folios"]["note"] = ("the folio count changed, so changes listed here may be " out["folios"]["note"] = ("the folio count changed, so changes listed here may be "
"later folios shifting position rather than edits") "later folios shifting position rather than edits")
out["texts"] = _diff_keyed(a["texts"], b["texts"], out["texts"] = _diff_items(a["texts"], b["texts"])
lambda k: {"folio": k[0], "x": k[1], "y": k[2], "text": k[3]}) out["shapes"] = _diff_items(a["shapes"], b["shapes"])
out["shapes"] = _diff_keyed(a["shapes"], b["shapes"], out["images"] = _diff_items(a["images"], b["images"])
lambda k: {"folio": k[0], "type": k[1],
"from": [k[2], k[3]], "to": [k[4], k[5]]})
out["images"] = _diff_keyed(a["images"], b["images"],
lambda k: {"folio": k[0], "x": k[1], "y": k[2]})
# Keyed by element, what the field is bound to, and the nth such field. # Keyed by element, what the field is bound to, and the nth such field.
# A field's own text is also compared ("shows"), so relabelling an # A field's own text is also compared ("shows"), so relabelling an
# element shows up here as well as in the element's information. # element shows up here as well as in the element's information.
@@ -594,14 +627,20 @@ def tool_element_info(path: str) -> dict:
ordered, ambiguous = _terminals_in_index_order(list(root.iter("terminal"))) ordered, ambiguous = _terminals_in_index_order(list(root.iter("terminal")))
terminals = [{"index": i, "x": t.get("x"), "y": t.get("y"), terminals = [{"index": i, "x": t.get("x"), "y": t.get("y"),
"orientation": t.get("orientation"), "orientation": t.get("orientation"),
"name": t.get("name", ""), "type": t.get("type", "")} "name": t.get("name", ""), "type": t.get("type", ""),
"uuid": t.get("uuid", "")}
for i, t in enumerate(ordered)] for i, t in enumerate(ordered)]
info_fields = sorted({(i.text or "").strip() info_fields = sorted({(i.text or "").strip()
for i in root.iter("info_name") if (i.text or "").strip()}) for i in root.iter("info_name") if (i.text or "").strip()})
parts = {} parts = {}
part_list = []
desc = root.find("description") desc = root.find("description")
for child in (desc if desc is not None else []): for child in (desc if desc is not None else []):
parts[child.tag] = parts.get(child.tag, 0) + 1 parts[child.tag] = parts.get(child.tag, 0) + 1
if child.tag != "terminal":
# uuid is empty for a part saved before parts carried one; the
# element editor gives it one on the next save.
part_list.append({"type": child.tag, "uuid": child.get("uuid", "")})
return { return {
"file": str(Path(path).expanduser()), "file": str(Path(path).expanduser()),
"type": root.get("type", ""), "link_type": root.get("link_type", ""), "type": root.get("type", ""), "link_type": root.get("link_type", ""),
@@ -614,6 +653,7 @@ def tool_element_info(path: str) -> dict:
"index is undefined" if ambiguous else ""), "index is undefined" if ambiguous else ""),
"info_fields": info_fields, "info_fields": info_fields,
"parts": parts, "parts": parts,
"part_list": part_list,
} }
@@ -851,11 +891,15 @@ def _validate_part(index: int, part) -> str:
for key in spec["required"]: for key in spec["required"]:
if key not in part: if key not in part:
raise ValueError(f"part {index} ({kind}) is missing {key!r}") raise ValueError(f"part {index} ({kind}) is missing {key!r}")
allowed = set(spec["required"]) | set(spec["optional"]) | {"type", "style", "antialias"} allowed = set(spec["required"]) | set(spec["optional"]) | {"type", "style", "antialias", "uuid"}
for key in part: for key in part:
if key not in allowed: if key not in allowed:
raise ValueError(f"part {index} ({kind}): unexpected {key!r}; " raise ValueError(f"part {index} ({kind}): unexpected {key!r}; "
f"allowed: {', '.join(sorted(allowed))}") f"allowed: {', '.join(sorted(allowed))}")
if "uuid" in part and not (isinstance(part["uuid"], str)
and _UUID_RE.fullmatch(part["uuid"])):
raise ValueError(f"part {index} ({kind}): uuid must look like "
f"{{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}}, got {part['uuid']!r}")
if kind == "polygon": if kind == "polygon":
pts = part["points"] pts = part["points"]
if not isinstance(pts, list) or len(pts) < 2: if not isinstance(pts, list) or len(pts) < 2:
@@ -866,9 +910,19 @@ def _validate_part(index: int, part) -> str:
return kind return kind
def _part_element(part: dict) -> ET.Element: def _part_uuid(part: dict) -> str:
"""The caller's uuid for this part, braced as QElectroTech writes it, or
a new one. Every part carries one, as the element editor saves them."""
given = part.get("uuid")
if given:
return given if given.startswith("{") else "{" + given + "}"
return "{" + str(__import__("uuid").uuid4()) + "}"
def _part_element(part: dict, uuid: str) -> ET.Element:
kind = part["type"] kind = part["type"]
node = ET.Element(kind) node = ET.Element(kind)
node.set("uuid", uuid)
if kind == "polygon": if kind == "polygon":
for n, (px, py) in enumerate(part["points"], start=1): for n, (px, py) in enumerate(part["points"], start=1):
node.set(f"x{n}", _fmt(px)) node.set(f"x{n}", _fmt(px))
@@ -960,8 +1014,11 @@ def tool_element_build(output: str, names: dict, parts: list,
ET.SubElement(kind, "kindInformation", {"name": key}).text = str(informations[key]) ET.SubElement(kind, "kindInformation", {"name": key}).text = str(informations[key])
ET.SubElement(root, "informations") ET.SubElement(root, "informations")
description = ET.SubElement(root, "description") description = ET.SubElement(root, "description")
for part in parts: part_uuids = [_part_uuid(part) for part in parts]
description.append(_part_element(part)) if len(set(part_uuids)) != len(part_uuids):
raise ValueError("two parts were given the same uuid")
for part, part_uuid in zip(parts, part_uuids):
description.append(_part_element(part, part_uuid))
for t in terminals: for t in terminals:
attrs = {"x": _fmt(t["x"]), "y": _fmt(t["y"]), attrs = {"x": _fmt(t["x"]), "y": _fmt(t["y"]),
"orientation": t["orientation"], "orientation": t["orientation"],
@@ -985,6 +1042,8 @@ def tool_element_build(output: str, names: dict, parts: list,
# caller listed them in. # caller listed them in.
"terminal_index_order": [t["name"] or f"({t['x']},{t['y']})" "terminal_index_order": [t["name"] or f"({t['x']},{t['y']})"
for t in check["terminals"]], for t in check["terminals"]],
# In the order the parts were given.
"part_uuids": part_uuids,
"verified": check} "verified": check}
@@ -1034,8 +1093,9 @@ OPS = {
("value", "str")]), ("value", "str")]),
"remove_plc_io": ("removePlcIO", [("folio", "folio"), ("element", "elmt"), "remove_plc_io": ("removePlcIO", [("folio", "folio"), ("element", "elmt"),
("index", "folio")]), ("index", "folio")]),
# Texts and shapes have no uuid; they are addressed by index into a # Texts, shapes and images are addressed by "index": their index in a
# position-sorted listing, and add_text/add_shape return that index so # position-sorted listing, which add_text/add_shape return, or their
# uuid, which does not shift when another one is added. So
# it can be named as "$id". Indexes shift when one is added or deleted. # it can be named as "$id". Indexes shift when one is added or deleted.
"delete_conductor": ("deleteConductor", [("folio", "folio"), ("element", "elmt"), "delete_conductor": ("deleteConductor", [("folio", "folio"), ("element", "elmt"),
("terminal", "num")]), ("terminal", "num")]),
@@ -1091,38 +1151,38 @@ OPS = {
("to_folio", "folio"), ("x", "num"), ("y", "num")]), ("to_folio", "folio"), ("x", "num"), ("y", "num")]),
"add_text": ("addText", [("folio", "folio"), ("text", "str"), "add_text": ("addText", [("folio", "folio"), ("text", "str"),
("x", "num"), ("y", "num")]), ("x", "num"), ("y", "num")]),
"set_text": ("setTextContent", [("folio", "folio"), ("index", "folio"), "set_text": ("setTextContent", [("folio", "folio"), ("index", "text"),
("text", "str")]), ("text", "str")]),
"set_text_color": ("setTextColor", [("folio", "folio"), ("index", "folio"), "set_text_color": ("setTextColor", [("folio", "folio"), ("index", "text"),
("color", "str")]), ("color", "str")]),
"rotate_text": ("setTextRotation", [("folio", "folio"), ("index", "folio"), "rotate_text": ("setTextRotation", [("folio", "folio"), ("index", "text"),
("angle", "num")]), ("angle", "num")]),
"delete_text": ("deleteText", [("folio", "folio"), ("index", "folio")]), "delete_text": ("deleteText", [("folio", "folio"), ("index", "text")]),
"add_shape": ("addShape", [("folio", "folio"), ("shape", "str"), "add_shape": ("addShape", [("folio", "folio"), ("shape", "str"),
("x1", "num"), ("y1", "num"), ("x1", "num"), ("y1", "num"),
("x2", "num"), ("y2", "num")]), ("x2", "num"), ("y2", "num")]),
"set_shape": ("setShapeProperty", [("folio", "folio"), ("index", "folio"), "set_shape": ("setShapeProperty", [("folio", "folio"), ("index", "shape"),
("property", "str"), ("value", "str")]), ("property", "str"), ("value", "str")]),
"add_image": ("addImage", [("folio", "folio"), ("file", "str"), "add_image": ("addImage", [("folio", "folio"), ("file", "str"),
("x", "num"), ("y", "num")]), ("x", "num"), ("y", "num")]),
"add_pdf_page": ("addPdfPage", [("folio", "folio"), ("file", "str"), "add_pdf_page": ("addPdfPage", [("folio", "folio"), ("file", "str"),
("page", "num"), ("dpi", "num"), ("page", "num"), ("dpi", "num"),
("x", "num"), ("y", "num")]), ("x", "num"), ("y", "num")]),
"scale_image": ("setImageScale", [("folio", "folio"), ("index", "folio"), "scale_image": ("setImageScale", [("folio", "folio"), ("index", "image"),
("factor", "num")]), ("factor", "num")]),
"rotate_image": ("setImageRotation", [("folio", "folio"), ("index", "folio"), "rotate_image": ("setImageRotation", [("folio", "folio"), ("index", "image"),
("angle", "num")]), ("angle", "num")]),
"delete_image": ("deleteImage", [("folio", "folio"), ("index", "folio")]), "delete_image": ("deleteImage", [("folio", "folio"), ("index", "image")]),
"delete_shape": ("deleteShape", [("folio", "folio"), ("index", "folio")]), "delete_shape": ("deleteShape", [("folio", "folio"), ("index", "shape")]),
"add_polygon": ("addPolygon", [("folio", "folio"), ("points", "points"), "add_polygon": ("addPolygon", [("folio", "folio"), ("points", "points"),
("closed", "bool")]), ("closed", "bool")]),
"set_shape_polygon": ("setShapePolygon", [("folio", "folio"), ("index", "folio"), "set_shape_polygon": ("setShapePolygon", [("folio", "folio"), ("index", "shape"),
("points", "points")]), ("points", "points")]),
"add_path": ("addPath", [("folio", "folio"), ("nodes", "nodes"), "add_path": ("addPath", [("folio", "folio"), ("nodes", "nodes"),
("closed", "bool")]), ("closed", "bool")]),
"set_shape_path_nodes": ("setShapePathNodes", [("folio", "folio"), ("index", "folio"), "set_shape_path_nodes": ("setShapePathNodes", [("folio", "folio"), ("index", "shape"),
("nodes", "nodes")]), ("nodes", "nodes")]),
"set_shape_closed": ("setShapeClosed", [("folio", "folio"), ("index", "folio"), "set_shape_closed": ("setShapeClosed", [("folio", "folio"), ("index", "shape"),
("closed", "bool")]), ("closed", "bool")]),
"add_table": ("addTable", [("folio", "folio"), ("kind", "str"), "add_table": ("addTable", [("folio", "folio"), ("kind", "str"),
("name", "str"), ("query", "str")]), ("name", "str"), ("query", "str")]),
@@ -1161,6 +1221,9 @@ _REQUIRED_METHODS = sorted({m for m, _ in OPS.values() if m} |
_MARKER = "QETEDIT " _MARKER = "QETEDIT "
_UUID_RE = re.compile(r"\{?[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\}?")
def _js(value) -> str: def _js(value) -> str:
"""A JSON literal is a JavaScript literal for every type used here.""" """A JSON literal is a JavaScript literal for every type used here."""
@@ -1175,11 +1238,15 @@ def _build_script(operations: list, output: str) -> str:
JavaScript exception. JavaScript exception.
""" """
refs: set[str] = set() refs: set[str] = set()
# Resolvers a uuid reference needs; required only when one is used, so
# an index-only edit still runs on a build that predates them.
uuid_methods: set[str] = set()
folio_js = "0"
lines = [ lines = [
"// generated by qet-mcp; do not edit", "// generated by qet-mcp; do not edit",
"var R = {};", # $name -> value from an earlier op "var R = {};", # $name -> value from an earlier op
"var missing = [];", "var missing = [];",
f"var need = {_js(_REQUIRED_METHODS)};", "var need = @NEED@;",
"for (var i = 0; i < need.length; i++) {", "for (var i = 0; i < need.length; i++) {",
" if (typeof qet[need[i]] !== 'function') missing.push(need[i]);", " if (typeof qet[need[i]] !== 'function') missing.push(need[i]);",
"}", "}",
@@ -1228,6 +1295,18 @@ def _build_script(operations: list, output: str) -> str:
raise ValueError(f"operation {op_index}: {key!r} must be a non-empty list of " raise ValueError(f"operation {op_index}: {key!r} must be a non-empty list of "
f"integer indices, got {value!r}") f"integer indices, got {value!r}")
return _js(value) return _js(value)
if kind in ("text", "shape", "image"):
# A uuid names the item for good; it is turned into the index
# the call takes at run time, by the item's own folio.
if isinstance(value, str) and _UUID_RE.fullmatch(value):
method = {"text": "textIndex", "shape": "shapeIndex",
"image": "imageIndex"}[kind]
uuid_methods.add(method)
return f"qet.{method}({folio_js}, {_js(value)})"
if not isinstance(value, int) or isinstance(value, bool):
raise ValueError(f"operation {op_index}: {key!r} must be a {kind} index, "
f"its uuid, or a \"$name\" reference, got {value!r}")
return _js(value)
if kind == "folio": if kind == "folio":
if not isinstance(value, int) or isinstance(value, bool): if not isinstance(value, int) or isinstance(value, bool):
raise ValueError(f"operation {op_index}: {key!r} must be a folio index " raise ValueError(f"operation {op_index}: {key!r} must be a folio index "
@@ -1314,6 +1393,7 @@ def _build_script(operations: list, output: str) -> str:
for key, kind in spec: for key, kind in spec:
if key not in op: if key not in op:
raise ValueError(f"operation {i} ({name}) is missing {key!r}") raise ValueError(f"operation {i} ({name}) is missing {key!r}")
folio_js = args[0] if args else "0"
args.append(ref_or(op[key], kind, i, key)) args.append(ref_or(op[key], kind, i, key))
ident = op.get("id") ident = op.get("id")
@@ -1359,7 +1439,8 @@ def _build_script(operations: list, output: str) -> str:
lines.append(f" qet.log({_js(_MARKER)} + JSON.stringify(" lines.append(f" qet.log({_js(_MARKER)} + JSON.stringify("
"{kind: 'save', result: saved, stopped_early: stop}));") "{kind: 'save', result: saved, stopped_early: stop}));")
lines.append("}") lines.append("}")
return "\n".join(lines) + "\n" need = _js(sorted(set(_REQUIRED_METHODS) | uuid_methods))
return "\n".join(line.replace("@NEED@", need) for line in lines) + "\n"
def _parse_script_output(text: str) -> dict: def _parse_script_output(text: str) -> dict:
@@ -2106,7 +2187,7 @@ TOOLS = [
}, },
{ {
"name": "qet_export", "name": "qet_export",
"description": "Run a QElectroTech export headlessly (pdf, png, svg, bom, " "description": "Run a QElectroTech export headlessly (pdf, png, svg, dxf, bom, "
"cables, wires, wiring, nets, links, info). Launches the " "cables, wires, wiring, nets, links, info). Launches the "
"binary in an isolated sandbox so it cannot be captured by, " "binary in an isolated sandbox so it cannot be captured by, "
"or capture, a running QElectroTech.", "or capture, a running QElectroTech.",
@@ -2268,10 +2349,12 @@ TOOLS = [
"that element; set_element_text takes " + "that element; set_element_text takes " +
", ".join(ELEMENT_TEXT_PROPERTIES) + ". A field bound with source " ", ".join(ELEMENT_TEXT_PROPERTIES) + ". A field bound with source "
"\"info\" follows set_label/set_info. Indexes shift on delete. " "\"info\" follows set_label/set_info. Indexes shift on delete. "
"Texts and shapes have no uuid: add_text/add_shape return an " "Texts, shapes and images: add_text/add_shape/add_image return an "
"index you can name as \"$id\", and the other text/shape ops " "index you can name as \"$id\", and the other text/shape/image "
"take it as \"index\". Indexes shift when one is added or " "ops take it as \"index\". Indexes shift when one is added or "
"deleted. Shapes: " + ", ".join(SHAPES) + "; set_shape takes " + ", ".join(SHAPE_PROPERTIES) + "deleted; \"index\" also accepts the item's uuid (from the "
"project database's drawing_item_view, or a saved file), which "
"does not. Shapes: " + ", ".join(SHAPES) + "; set_shape takes " + ", ".join(SHAPE_PROPERTIES) +
" (fill accepts a colour or \"none\"). " " (fill accepts a colour or \"none\"). "
"add_shape's own \"polygon\" is always the degenerate two-point " "add_shape's own \"polygon\" is always the degenerate two-point "
"form (it shares add_shape's p1/p2 shape); add_polygon takes as " "form (it shares add_shape's p1/p2 shape); add_polygon takes as "
@@ -2291,7 +2374,7 @@ TOOLS = [
"shapes are listed by current on-folio position, so changing one " "shapes are listed by current on-folio position, so changing one "
"shape's points can reorder it relative to the others -- re-list " "shape's points can reorder it relative to the others -- re-list "
"before addressing one by index again if more than one is being " "before addressing one by index again if more than one is being "
"edited in the same run. " "edited in the same run, or address it by uuid. "
"Tables: add_table places a BOM/nomenclature or summary table " "Tables: add_table places a BOM/nomenclature or summary table "
"(kind is \"nomenclature\" or \"summary\") built from a query " "(kind is \"nomenclature\" or \"summary\") built from a query "
"against a project database view -- run qet.query() (the " "against a project database view -- run qet.query() (the "
@@ -2512,7 +2595,9 @@ TOOLS = [
'line x1,y1,x2,y2; rect/ellipse/arc x,y,width,height ' 'line x1,y1,x2,y2; rect/ellipse/arc x,y,width,height '
"(arc also start,angle); circle x,y,diameter; polygon " "(arc also start,angle); circle x,y,diameter; polygon "
'points:[[x,y],...] and closed; text x,y,text with optional ' 'points:[[x,y],...] and closed; text x,y,text with optional '
"size, rotation, color. Any part may carry style and antialias. " "size, rotation, color. Any part may carry style and antialias, "
"and a uuid to name it by; parts without one get a new uuid, "
"returned in part_uuids. "
"Coordinates are the element's own, with (0,0) at its origin.", "Coordinates are the element's own, with (0,0) at its origin.",
"items": {"type": "object"}, "items": {"type": "object"},
}, },
+168 -5
View File
@@ -54,6 +54,7 @@ needs_examples = unittest.skipUnless(have_binary and have_examples,
COIL = "common://10_electric/10_allpole/310_relays_contactors_contacts/01_coils/bobine_ka_a_remanence.elmt" COIL = "common://10_electric/10_allpole/310_relays_contactors_contacts/01_coils/bobine_ka_a_remanence.elmt"
SLAVE = ("common://10_electric/10_allpole/310_relays_contactors_contacts/" SLAVE = ("common://10_electric/10_allpole/310_relays_contactors_contacts/"
"02_contacts_cross_referencing/15_protection_contacts/contact_relais_nf_esclave.elmt") "02_contacts_cross_referencing/15_protection_contacts/contact_relais_nf_esclave.elmt")
SHARED_UUID = "{11111111-2222-3333-4444-555555555555}"
# The shipped "going/coming arrow" pair -- a folio-jump link, next_report on # The shipped "going/coming arrow" pair -- a folio-jump link, next_report on
# one folio linked to previous_report on the next, one terminal each. Same # one folio linked to previous_report on the next, one terminal each. Same
# category of element as issue #974's custom "naechste_folie_rechts.elmt" / # category of element as issue #974's custom "naechste_folie_rechts.elmt" /
@@ -328,6 +329,29 @@ class EditValidation(unittest.TestCase):
with self.assertRaisesRegex(ValueError, "list of strings"): with self.assertRaisesRegex(ValueError, "list of strings"):
self.build([{"op": "add_autonum", "kind": "conductor", "name": "a", "parts": "string:W"}]) self.build([{"op": "add_autonum", "kind": "conductor", "name": "a", "parts": "string:W"}])
def test_a_uuid_index_is_resolved_at_run_time(self):
u = "{11111111-1111-4111-8111-111111111111}"
script = self.build([{"op": "set_shape", "folio": 2, "index": u,
"property": "fill", "value": "none"},
{"op": "delete_text", "folio": 1, "index": u},
{"op": "delete_image", "folio": 0, "index": u}])
self.assertIn(f'qet.setShapeProperty(2, qet.shapeIndex(2, "{u}"), ', script)
self.assertIn(f'qet.deleteText(1, qet.textIndex(1, "{u}"))', script)
self.assertIn(f'qet.deleteImage(0, qet.imageIndex(0, "{u}"))', script)
need = json.loads(re.search(r"var need = (\[.*?\]);", script).group(1))
self.assertTrue({"shapeIndex", "textIndex", "imageIndex"} <= set(need))
def test_an_index_only_edit_does_not_need_the_uuid_resolvers(self):
script = self.build([{"op": "delete_shape", "folio": 0, "index": 3}])
need = json.loads(re.search(r"var need = (\[.*?\]);", script).group(1))
self.assertFalse({"shapeIndex", "textIndex", "imageIndex"} & set(need))
def test_a_string_index_that_is_not_a_uuid_is_refused(self):
for bad in ("3", "{nope}", "11111111-1111"):
with self.subTest(index=bad):
with self.assertRaises(ValueError):
self.build([{"op": "delete_shape", "folio": 0, "index": bad}])
def test_indexed_references_and_element_lists(self): def test_indexed_references_and_element_lists(self):
script = self.build([ script = self.build([
{"op": "add_folio", "id": "f"}, {"op": "add_folio", "id": "f"},
@@ -536,6 +560,30 @@ class ElementBuild(unittest.TestCase):
{"x": 0, "y": -20, "orientation": "n", "name": "high"}]) {"x": 0, "y": -20, "orientation": "n", "name": "high"}])
self.assertEqual(r["terminal_index_order"], ["high", "low"]) self.assertEqual(r["terminal_index_order"], ["high", "low"])
def test_every_part_gets_its_own_uuid(self):
parts = [{"type": "line", "x1": 0, "y1": 0, "x2": 10, "y2": 0},
{"type": "rect", "x": 0, "y": 0, "width": 5, "height": 5},
{"type": "text", "x": 0, "y": 0, "text": "K"}]
r = self.build(parts=parts)
desc = ET.parse(self.out).getroot().find("description")
written = [e.get("uuid") for e in desc if e.tag != "terminal"]
self.assertEqual(written, r["part_uuids"])
self.assertEqual(len(set(written)), 3)
self.assertEqual([p["uuid"] for p in r["verified"]["part_list"]], written)
def test_a_given_part_uuid_is_kept_and_braced(self):
r = self.build(parts=[{"type": "line", "x1": 0, "y1": 0, "x2": 1, "y2": 0,
"uuid": "11111111-1111-4111-8111-111111111111"}])
self.assertEqual(r["part_uuids"], ["{11111111-1111-4111-8111-111111111111}"])
def test_bad_or_repeated_part_uuids_are_refused(self):
line = {"type": "line", "x1": 0, "y1": 0, "x2": 1, "y2": 0}
u = "{11111111-1111-4111-8111-111111111111}"
for parts in ([{**line, "uuid": "nope"}], [{**line, "uuid": u}, {**line, "uuid": u}]):
with self.subTest(parts=parts):
with self.assertRaises(ValueError):
self.build(parts=parts)
def test_output_reads_back(self): def test_output_reads_back(self):
r = self.build(names={"en": "Coil", "fr": "Bobine"}) r = self.build(names={"en": "Coil", "fr": "Bobine"})
self.assertEqual(r["verified"]["names"], {"en": "Coil", "fr": "Bobine"}) self.assertEqual(r["verified"]["names"], {"en": "Coil", "fr": "Bobine"})
@@ -632,14 +680,17 @@ class Diff(unittest.TestCase):
def project(self, name, *, texts=(), shapes=(), images=(), author="", version="1", def project(self, name, *, texts=(), shapes=(), images=(), author="", version="1",
strips=(), conductors=()): strips=(), conductors=()):
inputs = "".join(f'<input x="{x}" y="{y}" rotation="0" font="f" ' # An optional last member of each tuple is the item's uuid.
uid = lambda u: f' uuid="{u[0]}"' if u else ""
inputs = "".join(f'<input x="{x}" y="{y}" rotation="0" font="f"{uid(u)} '
f'text="&lt;html&gt;&lt;body&gt;&lt;p&gt;{t}&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;"/>' f'text="&lt;html&gt;&lt;body&gt;&lt;p&gt;{t}&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;"/>'
for x, y, t in texts) for x, y, t, *u in texts)
shp = "".join(f'<shape type="Rectangle" x1="{a}" y1="{b}" x2="{c}" y2="{d}">' shp = "".join(f'<shape type="Rectangle" x1="{a}" y1="{b}" x2="{c}" y2="{d}"{uid(u)}>'
f'<pen color="{pc}" style="SolidLine" widthF="1"/>' f'<pen color="{pc}" style="SolidLine" widthF="1"/>'
f'<brush color="#fff" style="SolidPattern"/></shape>' f'<brush color="#fff" style="SolidPattern"/></shape>'
for a, b, c, d, pc in shapes) for a, b, c, d, pc, *u in shapes)
img = "".join(f'<image x="{x}" y="{y}" size="{s}" rotation="0"/>' for x, y, s in images) img = "".join(f'<image x="{x}" y="{y}" size="{s}" rotation="0"{uid(u)}/>'
for x, y, s, *u in images)
st = "".join(f'<terminal_strip><terminal_strip_data uuid="{u}"><informations>' st = "".join(f'<terminal_strip><terminal_strip_data uuid="{u}"><informations>'
f'<information name="name">{n}</information></informations>' f'<information name="name">{n}</information></informations>'
f'</terminal_strip_data><layout/></terminal_strip>' for u, n in strips) f'</terminal_strip_data><layout/></terminal_strip>' for u, n in strips)
@@ -669,6 +720,39 @@ class Diff(unittest.TestCase):
self.assertEqual([t["text"] for t in d["removed"]], ["note"]) self.assertEqual([t["text"] for t in d["removed"]], ["note"])
self.assertEqual([t["text"] for t in d["added"]], ["note EDITED"]) self.assertEqual([t["text"] for t in d["added"]], ["note EDITED"])
U1 = "{11111111-1111-4111-8111-111111111111}"
U2 = "{22222222-2222-4222-8222-222222222222}"
def test_edited_text_with_a_uuid_is_a_change_to_that_text(self):
a = self.project("a.qet", texts=[(1, 2, "note", self.U1)])
b = self.project("b.qet", texts=[(1, 2, "note EDITED", self.U1)])
d = m.tool_diff(a, b)["texts"]
self.assertEqual(d["keyed_by"], "uuid")
self.assertEqual((d["added"], d["removed"]), ([], []))
self.assertEqual(d["changed"][0]["changed"]["text"], ["note", "note EDITED"])
def test_moved_shape_with_a_uuid_is_a_change_to_that_shape(self):
a = self.project("a.qet", shapes=[(0, 0, 5, 5, "#000000", self.U1),
(9, 9, 12, 12, "#000000", self.U2)])
b = self.project("b.qet", shapes=[(100, 0, 105, 5, "#000000", self.U1),
(9, 9, 12, 12, "#000000", self.U2)])
d = m.tool_diff(a, b)["shapes"]
self.assertEqual((d["added"], d["removed"]), ([], []))
self.assertEqual(len(d["changed"]), 1)
self.assertEqual(d["changed"][0]["item"]["uuid"], self.U1)
self.assertEqual(d["changed"][0]["changed"]["from"], [["0", "0"], ["100", "0"]])
def test_a_file_without_uuids_against_its_resave_is_keyed_by_position(self):
"""The first save by a current QElectroTech adds uuids to a legacy
file: that must not read as every item removed and re-added."""
a = self.project("a.qet", texts=[(1, 2, "note")], images=[(3, 3, 1)])
b = self.project("b.qet", texts=[(1, 2, "note", self.U1)], images=[(3, 3, 1, self.U2)])
d = m.tool_diff(a, b)
for k in ("texts", "images"):
with self.subTest(section=k):
self.assertEqual(d[k]["keyed_by"], "position")
self.assertFalse(d[k]["added"] or d[k]["removed"] or d[k]["changed"])
def test_shape_restyle_is_a_change_to_that_item(self): def test_shape_restyle_is_a_change_to_that_item(self):
a = self.project("a.qet", shapes=[(0, 0, 5, 5, "#000000")]) a = self.project("a.qet", shapes=[(0, 0, 5, 5, "#000000")])
b = self.project("b.qet", shapes=[(0, 0, 5, 5, "#ff0000")]) b = self.project("b.qet", shapes=[(0, 0, 5, 5, "#ff0000")])
@@ -1975,6 +2059,85 @@ class Integration(unittest.TestCase):
self.assertEqual(len(d["shapes"]["added"]), 1) self.assertEqual(len(d["shapes"]["added"]), 1)
self.assertEqual(len(d["images"]["added"]), 1) self.assertEqual(len(d["images"]["added"]), 1)
def test_drawing_items_are_in_the_database_and_addressable_by_uuid(self):
base = self.sb.new()
r = self.ok(self.sb.edit(base, [
{"op": "add_text", "folio": 0, "text": "note", "x": 50, "y": 50},
{"op": "add_shape", "folio": 0, "shape": "rectangle", "x1": 10, "y1": 10, "x2": 200, "y2": 120}]))
root = ET.parse(r["output"]).getroot()
shape_uuid = next(root.iter("shape")).get("uuid")
text_uuid = next(root.iter("input")).get("uuid")
rows = m.tool_query(BINARY, r["output"],
"SELECT uuid, kind, folio, description FROM drawing_item_view "
"ORDER BY kind")["rows"]
self.assertEqual([(x["uuid"], x["kind"], x["folio"]) for x in rows],
[(shape_uuid, "shape", 1), (text_uuid, "text", 1)])
self.assertEqual(rows[1]["description"], "note")
# A second shape placed above the first shifts its index to 1; the
# uuid still names it.
r2 = self.ok(self.sb.edit(r["output"], [
{"op": "add_shape", "folio": 0, "shape": "line", "x1": 0, "y1": 0, "x2": 5, "y2": 0},
{"op": "set_shape", "folio": 0, "index": shape_uuid, "property": "fill", "value": "#00ff00"}],
out="out2.qet"))
d = r2["diff"]["shapes"]
self.assertEqual(d["keyed_by"], "uuid")
self.assertEqual([c["item"]["uuid"] for c in d["changed"]], [shape_uuid])
self.assertEqual(len(d["added"]), 1)
def _old_file(self, same_shape_uuid=False):
"""A folio with a text and two shapes, as a version without
drawing-item uuids wrote it -- or, with same_shape_uuid, hand-edited
so that both shapes claim the same one."""
r = self.ok(self.sb.edit(self.sb.new(), [
{"op": "add_text", "folio": 0, "text": "note", "x": 50, "y": 50},
{"op": "add_shape", "folio": 0, "shape": "rectangle", "x1": 10, "y1": 10, "x2": 200, "y2": 120},
{"op": "add_shape", "folio": 0, "shape": "line", "x1": 0, "y1": 0, "x2": 5, "y2": 0}]))
tree = ET.parse(r["output"])
shapes = list(tree.getroot().iter("shape"))
for item in [*tree.getroot().iter("input"), *shapes]:
del item.attrib["uuid"]
if same_shape_uuid:
for shape in shapes:
shape.set("uuid", SHARED_UUID)
tree.write(self.sb.p("old.qet"), encoding="utf-8", xml_declaration=True)
return self.sb.p("old.qet")
def _db_uuids(self, project):
rows = m.tool_query(BINARY, project, "SELECT uuid, kind FROM drawing_item_view")["rows"]
return sorted((x["uuid"], x["kind"]) for x in rows)
@staticmethod
def _file_uuids(project):
root = ET.parse(project).getroot()
return sorted((item.get("uuid"), kind)
for tag, kind in (("shape", "shape"), ("input", "text"))
for item in root.iter(tag))
def test_a_file_without_drawing_uuids_gets_the_same_ones_on_every_open(self):
old = self._old_file()
first = self._db_uuids(old)
self.assertEqual(len(first), 3)
self.assertEqual(len({u for u, _ in first}), 3, "each item gets its own")
self.assertEqual(self._db_uuids(old), first)
def test_the_first_save_writes_those_uuids_and_a_reopen_keeps_them(self):
old = self._old_file()
derived = self._db_uuids(old)
saved = self.ok(self.sb.edit(old, [{"op": "add_folio"}], out="saved.qet"))["output"]
self.assertEqual(self._file_uuids(saved), derived)
self.assertEqual(self._db_uuids(saved), derived)
# Saving the same old file again writes the same uuids (#754).
again = self.ok(self.sb.edit(old, [{"op": "add_folio"}], out="again.qet"))["output"]
self.assertEqual(self._file_uuids(again), derived)
def test_a_uuid_repeated_in_a_hand_edited_file_names_one_item_only(self):
old = self._old_file(same_shape_uuid=True)
shapes = [u for u, kind in self._db_uuids(old) if kind == "shape"]
self.assertEqual(len(set(shapes)), 2)
self.assertIn(SHARED_UUID, shapes)
self.assertEqual([u for u, kind in self._db_uuids(old) if kind == "shape"], shapes)
# ---- polygon and path shapes ---- # ---- polygon and path shapes ----
def test_add_polygon_with_more_than_two_points(self): def test_add_polygon_with_more_than_two_points(self):
+659
View File
@@ -0,0 +1,659 @@
#!/usr/bin/env python3
# Copyright 2006-2026 The QElectroTech Team
# This file is part of QElectroTech.
#
# QElectroTech is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# QElectroTech is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
r"""Record raw USB reports from a 3Dconnexion 3D mouse, for QElectroTech.
QElectroTech's Windows/macOS 3D mouse support reads the device directly
over USB, so it has to decode each device model's raw reports itself.
This records what your device sends while you make a few guided
movements, and saves it to one file you can attach to discussion #599.
Nothing is sent anywhere. One recording per device model is enough, from
any of the three systems: the device sends the same reports on all of them.
It takes about two minutes. Each step says what to do: press Enter, make
the movement and hold it until the next prompt. Push firmly.
Linux
Python 3 is already installed.
1. Download: curl -LO https://raw.githubusercontent.com/qelectrotech/qelectrotech-source-mirror/master/misc/spacemouse-capture.py
2. List: sudo python3 spacemouse-capture.py --list
3. Record: sudo python3 spacemouse-capture.py --seconds 3
If --list shows several devices (a Logitech receiver shows up as
several), add --device /dev/hidrawN with the 3D mouse's line.
sudo is needed because /dev/hidraw* is readable by root only.
spacenavd can keep running; if the recording comes out empty, stop it
(sudo systemctl stop spacenavd) and try again.
macOS
Python 3 comes with the Xcode command line tools; if "python3" asks
to install them, accept. Open Terminal, then:
1. Download: curl -LO https://raw.githubusercontent.com/qelectrotech/qelectrotech-source-mirror/master/misc/spacemouse-capture.py
2. Record: python3 spacemouse-capture.py --seconds 3
No sudo. If it says another program holds the device, quit 3DxWare
(or uninstall it) and try again. If it says macOS refused access,
allow Terminal in System Settings > Privacy & Security > Input
Monitoring, and try again. (Rather not use Terminal? The SpaceMouse
Check app does the same with a window: see discussion #599.)
Windows
Install Python 3 from https://www.python.org/downloads/ (tick "Add
python.exe to PATH") or from the Microsoft Store. Open a Command
Prompt (Windows key, type cmd, Enter), then:
1. cd %USERPROFILE%\Downloads
2. Download: curl -LO https://raw.githubusercontent.com/qelectrotech/qelectrotech-source-mirror/master/misc/spacemouse-capture.py
3. Record: python spacemouse-capture.py --seconds 3
No administrator rights needed, and 3DxWare can keep running.
Windows does not give out the device's report descriptor, so a
recording made on Linux or macOS is a little more complete.
Every system
--list only show the 3D mice found
--device PATH pick one, if several are found (a path from --list)
--seconds N multiply the time per step (3 triples it)
-o FILE where to save (default: spacemouse-capture-<id>.json)
The file is saved in the current folder: attach it to discussion #599.
Only Python's standard library is used.
"""
import argparse
import datetime
import glob
import json
import os
import platform
import select
import subprocess
import sys
import time
VENDORS = {0x046D: 'Logitech (older 3Dconnexion)', 0x256F: '3Dconnexion'}
STEPS = [
('rest', 'Do not touch the device.', 3),
('right', 'Push the cap to the RIGHT and hold it, then let go.', 4),
('left', 'Push the cap to the LEFT and hold it, then let go.', 4),
('away', 'Push the cap AWAY from you and hold it, then let go.', 4),
('toward', 'Pull the cap TOWARDS you and hold it, then let go.', 4),
('down', 'Press the cap DOWN and hold it, then let go.', 4),
('up', 'Lift the cap UP and hold it, then let go.', 4),
('twist_cw', 'TWIST the cap CLOCKWISE (seen from above) and hold, then let go.', 4),
('twist_ccw', 'TWIST the cap ANTICLOCKWISE and hold, then let go.', 4),
('tilt_away', 'TILT the cap AWAY from you and hold, then let go.', 4),
('tilt_right', 'TILT the cap to the RIGHT and hold, then let go.', 4),
('buttons', 'Press each button once, slowly, one at a time, in any order.', 15),
]
# Processes that may also be reading the device.
OTHER_READERS = ('3dconnexion', '3dx', 'spacenavd') # 3DxWare: 3DconnexionHelper, 3DxNLServer...
def other_readers():
if sys.platform == 'win32':
try:
out = subprocess.run(['tasklist', '/fo', 'csv', '/nh'], capture_output=True,
text=True, timeout=10).stdout
except (OSError, subprocess.SubprocessError):
return ['unknown']
names = {line.split('","')[0].strip('"') for line in out.splitlines() if line}
return sorted(n for n in names if any(r in n.lower() for r in OTHER_READERS))
try:
out = subprocess.run(['ps', '-A', '-o', 'comm='], capture_output=True,
text=True, timeout=5).stdout
except (OSError, subprocess.SubprocessError):
return ['unknown']
names = {os.path.basename(line.strip()) for line in out.splitlines()}
return sorted(n for n in names if any(r in n.lower() for r in OTHER_READERS))
# --- Linux: /dev/hidraw ----------------------------------------------------
class HidrawDevice:
backend = 'hidraw'
def __init__(self, path, name='?', vendor='?', product='?', sysfs=None):
self.path, self.name, self.vendor, self.product = path, name, vendor, product
self.sysfs = sysfs
self.fd = None
@staticmethod
def find():
found = []
for sysdir in sorted(glob.glob('/sys/class/hidraw/hidraw*')):
try:
with open(os.path.join(sysdir, 'device', 'uevent')) as f:
uevent = dict(line.strip().split('=', 1) for line in f if '=' in line)
except OSError:
continue
# HID_ID=0003:0000256F:0000C635 (bus:vendor:product)
try:
_bus, vendor, product = (int(x, 16) for x in uevent.get('HID_ID', '').split(':'))
except ValueError:
continue
if vendor not in VENDORS:
continue
found.append(HidrawDevice('/dev/' + os.path.basename(sysdir),
uevent.get('HID_NAME', '?'),
'%04x' % vendor, '%04x' % product, sysdir))
return found
def descriptor(self):
if not self.sysfs:
return 'unknown'
try:
with open(os.path.join(self.sysfs, 'device', 'report_descriptor'), 'rb') as f:
return f.read().hex()
except OSError as e:
return 'unreadable: %s' % e
def open(self):
try:
self.fd = os.open(self.path, os.O_RDONLY | os.O_NONBLOCK)
except PermissionError:
sys.exit('Permission denied on %s -- run with sudo.' % self.path)
def record(self, seconds):
"""Read every report arriving within `seconds`; return [[ms, hex], ...]."""
reports = []
start = time.monotonic()
while True:
left = seconds - (time.monotonic() - start)
if left <= 0:
return reports
ready, _, _ = select.select([self.fd], [], [], left)
if not ready:
continue
try:
data = os.read(self.fd, 64)
except BlockingIOError:
continue
if not data: # only a test FIFO with no writer does this
time.sleep(0.01)
continue
reports.append([round((time.monotonic() - start) * 1000, 1), data.hex()])
def drain(self):
"""Drop reports queued while waiting for Enter."""
while True:
try:
if not os.read(self.fd, 64):
return
except BlockingIOError:
return
def close(self):
os.close(self.fd)
def empty_hint(self):
return 'Try stopping spacenavd first: sudo systemctl stop spacenavd'
# --- macOS: IOKit, the same calls hidapi's mac backend makes ----------------
class MacHid:
"""ctypes bindings to the few CoreFoundation/IOKit calls needed."""
def __init__(self):
import ctypes as c
self.c = c
cf = c.CDLL('/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation')
io = c.CDLL('/System/Library/Frameworks/IOKit.framework/IOKit')
vp, i32, u32, idx = c.c_void_p, c.c_int32, c.c_uint32, c.c_long
def fn(lib, name, res, *args):
f = getattr(lib, name)
f.restype, f.argtypes = res, list(args)
return f
self.CFStringCreateWithCString = fn(cf, 'CFStringCreateWithCString', vp, vp, c.c_char_p, u32)
self.CFStringGetCString = fn(cf, 'CFStringGetCString', c.c_bool, vp, c.c_char_p, idx, u32)
self.CFGetTypeID = fn(cf, 'CFGetTypeID', c.c_ulong, vp)
self.CFNumberGetTypeID = fn(cf, 'CFNumberGetTypeID', c.c_ulong)
self.CFStringGetTypeID = fn(cf, 'CFStringGetTypeID', c.c_ulong)
self.CFDataGetTypeID = fn(cf, 'CFDataGetTypeID', c.c_ulong)
self.CFNumberGetValue = fn(cf, 'CFNumberGetValue', c.c_bool, vp, idx, vp)
self.CFDataGetLength = fn(cf, 'CFDataGetLength', idx, vp)
self.CFDataGetBytePtr = fn(cf, 'CFDataGetBytePtr', vp, vp)
self.CFSetGetCount = fn(cf, 'CFSetGetCount', idx, vp)
self.CFSetGetValues = fn(cf, 'CFSetGetValues', None, vp, c.POINTER(vp))
self.CFRunLoopGetCurrent = fn(cf, 'CFRunLoopGetCurrent', vp)
self.CFRunLoopRunInMode = fn(cf, 'CFRunLoopRunInMode', i32, vp, c.c_double, c.c_bool)
self.kCFRunLoopDefaultMode = vp.in_dll(cf, 'kCFRunLoopDefaultMode').value
self.IOHIDManagerCreate = fn(io, 'IOHIDManagerCreate', vp, vp, u32)
self.IOHIDManagerSetDeviceMatching = fn(io, 'IOHIDManagerSetDeviceMatching', None, vp, vp)
self.IOHIDManagerScheduleWithRunLoop = fn(io, 'IOHIDManagerScheduleWithRunLoop', None, vp, vp, vp)
self.IOHIDManagerCopyDevices = fn(io, 'IOHIDManagerCopyDevices', vp, vp)
self.IOHIDDeviceGetProperty = fn(io, 'IOHIDDeviceGetProperty', vp, vp, vp)
self.IOHIDDeviceOpen = fn(io, 'IOHIDDeviceOpen', i32, vp, u32)
self.IOHIDDeviceClose = fn(io, 'IOHIDDeviceClose', i32, vp, u32)
self.IOHIDDeviceScheduleWithRunLoop = fn(io, 'IOHIDDeviceScheduleWithRunLoop', None, vp, vp, vp)
self.IOHIDDeviceUnscheduleFromRunLoop = fn(io, 'IOHIDDeviceUnscheduleFromRunLoop', None, vp, vp, vp)
# void (*)(void *ctx, IOReturn, void *sender, IOHIDReportType, uint32_t id, uint8_t *, CFIndex)
self.ReportCallback = c.CFUNCTYPE(None, vp, i32, vp, c.c_int, u32, c.POINTER(c.c_uint8), idx)
self.IOHIDDeviceRegisterInputReportCallback = fn(
io, 'IOHIDDeviceRegisterInputReportCallback', None, vp, vp, idx, self.ReportCallback, vp)
def cfstr(self, s):
return self.CFStringCreateWithCString(None, s.encode(), 0x08000100) # UTF-8
def prop(self, dev, key):
"""A device property as int, str or bytes, or None."""
c = self.c
ref = self.IOHIDDeviceGetProperty(dev, self.cfstr(key))
if not ref:
return None
t = self.CFGetTypeID(ref)
if t == self.CFNumberGetTypeID():
v = c.c_int64()
self.CFNumberGetValue(ref, 4, c.byref(v)) # kCFNumberSInt64Type
return v.value
if t == self.CFStringGetTypeID():
buf = c.create_string_buffer(256)
return buf.value.decode('utf-8', 'replace') if self.CFStringGetCString(
ref, buf, len(buf), 0x08000100) else None
if t == self.CFDataGetTypeID():
return c.string_at(self.CFDataGetBytePtr(ref), self.CFDataGetLength(ref))
return None
class MacDevice:
backend = 'iokit'
_hid = None
def __init__(self, ref, name, vendor, product, location):
self.ref, self.name, self.vendor, self.product = ref, name, vendor, product
self.path = 'iokit:%08x' % location
self.reports = []
self.start = time.monotonic()
@classmethod
def hid(cls):
if cls._hid is None:
cls._hid = MacHid()
return cls._hid
@classmethod
def find(cls):
h = cls.hid()
mgr = h.IOHIDManagerCreate(None, 0)
h.IOHIDManagerSetDeviceMatching(mgr, None)
h.IOHIDManagerScheduleWithRunLoop(mgr, h.CFRunLoopGetCurrent(), h.kCFRunLoopDefaultMode)
devset = h.IOHIDManagerCopyDevices(mgr)
if not devset:
return []
n = h.CFSetGetCount(devset)
refs = (h.c.c_void_p * n)()
h.CFSetGetValues(devset, refs)
found = []
for ref in refs:
vendor = h.prop(ref, 'VendorID')
if vendor not in VENDORS:
continue
# The same test as QET's SpaceMouseHid::isSpaceMouse(): Generic
# Desktop / Multi-axis Controller, or no usage at all. This also
# skips Logitech's ordinary mice and keyboards.
usage = (h.prop(ref, 'PrimaryUsagePage') or 0, h.prop(ref, 'PrimaryUsage') or 0)
if usage not in ((1, 8), (0, 0)):
continue
found.append(cls(ref, h.prop(ref, 'Product') or '?', '%04x' % vendor,
'%04x' % (h.prop(ref, 'ProductID') or 0),
h.prop(ref, 'LocationID') or 0))
return sorted(found, key=lambda d: d.path)
def descriptor(self):
d = self.hid().prop(self.ref, 'ReportDescriptor')
return d.hex() if d else 'unreadable'
def open(self):
h = self.hid()
ret = h.IOHIDDeviceOpen(self.ref, 0) & 0xFFFFFFFF # kIOHIDOptionsTypeNone: shared
if ret == 0xE00002C5:
sys.exit('The device is held exclusively by another program '
'(kIOReturnExclusiveAccess). Quit 3DxWare and try again.')
if ret in (0xE00002E2, 0xE00002C1):
sys.exit('macOS refused access (0x%08X). Allow Terminal under System Settings > '
'Privacy & Security > Input Monitoring, then try again.' % ret)
if ret:
sys.exit('Could not open the device (IOReturn 0x%08X).' % ret)
size = h.prop(self.ref, 'MaxInputReportSize') or 64
self.buf = h.c.create_string_buffer(size)
def on_report(_ctx, _result, _sender, _type, _id, data, length):
self.reports.append([round((time.monotonic() - self.start) * 1000, 1),
h.c.string_at(data, length).hex()])
self.callback = h.ReportCallback(on_report) # must outlive the device
h.IOHIDDeviceRegisterInputReportCallback(self.ref, self.buf, size, self.callback, None)
h.IOHIDDeviceScheduleWithRunLoop(self.ref, h.CFRunLoopGetCurrent(), h.kCFRunLoopDefaultMode)
def record(self, seconds):
h = self.hid()
self.reports, self.start = [], time.monotonic()
while True:
left = seconds - (time.monotonic() - self.start)
if left <= 0:
return self.reports
h.CFRunLoopRunInMode(h.kCFRunLoopDefaultMode, left, False)
def drain(self):
self.hid().CFRunLoopRunInMode(self.hid().kCFRunLoopDefaultMode, 0.05, False)
self.reports = []
def close(self):
h = self.hid()
h.IOHIDDeviceUnscheduleFromRunLoop(self.ref, h.CFRunLoopGetCurrent(), h.kCFRunLoopDefaultMode)
h.IOHIDDeviceClose(self.ref, 0)
def empty_hint(self):
return ('Quit 3DxWare if it is running, or allow Terminal under System Settings > '
'Privacy & Security > Input Monitoring, and try again.')
# --- Windows: hid.dll, the same calls hidapi's Windows backend makes ---------
class WinHid:
"""ctypes bindings to the SetupAPI/HID/kernel32 calls needed."""
def __init__(self):
import ctypes as c
from ctypes import wintypes as w
self.c, self.w = c, w
self.hid = c.WinDLL('hid')
self.setupapi = c.WinDLL('setupapi')
self.k32 = c.WinDLL('kernel32', use_last_error=True)
k, sa = self.k32, self.setupapi
class GUID(c.Structure):
_fields_ = [('Data1', w.DWORD), ('Data2', w.WORD), ('Data3', w.WORD),
('Data4', c.c_ubyte * 8)]
class InterfaceData(c.Structure):
_fields_ = [('cbSize', w.DWORD), ('InterfaceClassGuid', GUID),
('Flags', w.DWORD), ('Reserved', c.c_void_p)]
class Attributes(c.Structure):
_fields_ = [('Size', w.ULONG), ('VendorID', w.USHORT), ('ProductID', w.USHORT),
('VersionNumber', w.USHORT)]
class Caps(c.Structure):
_fields_ = [('Usage', w.USHORT), ('UsagePage', w.USHORT),
('InputReportByteLength', w.USHORT), ('OutputReportByteLength', w.USHORT),
('FeatureReportByteLength', w.USHORT), ('Reserved', w.USHORT * 17),
('rest', w.USHORT * 10)]
class Overlapped(c.Structure):
_fields_ = [('Internal', c.c_void_p), ('InternalHigh', c.c_void_p),
('Offset', w.DWORD), ('OffsetHigh', w.DWORD), ('hEvent', w.HANDLE)]
self.GUID, self.InterfaceData, self.Attributes = GUID, InterfaceData, Attributes
self.Caps, self.Overlapped = Caps, Overlapped
k.CreateFileW.restype = w.HANDLE
k.CreateFileW.argtypes = [w.LPCWSTR, w.DWORD, w.DWORD, c.c_void_p, w.DWORD, w.DWORD, w.HANDLE]
k.CreateEventW.restype = w.HANDLE
k.CreateEventW.argtypes = [c.c_void_p, w.BOOL, w.BOOL, w.LPCWSTR]
k.ReadFile.argtypes = [w.HANDLE, c.c_void_p, w.DWORD, c.c_void_p, c.c_void_p]
k.GetOverlappedResult.argtypes = [w.HANDLE, c.c_void_p, c.POINTER(w.DWORD), w.BOOL]
k.WaitForSingleObject.argtypes = [w.HANDLE, w.DWORD]
k.WaitForSingleObject.restype = w.DWORD
k.CancelIo.argtypes = [w.HANDLE]
k.CloseHandle.argtypes = [w.HANDLE]
sa.SetupDiGetClassDevsW.restype = w.HANDLE
sa.SetupDiGetClassDevsW.argtypes = [c.c_void_p, w.LPCWSTR, w.HWND, w.DWORD]
sa.SetupDiEnumDeviceInterfaces.argtypes = [w.HANDLE, c.c_void_p, c.c_void_p, w.DWORD, c.c_void_p]
sa.SetupDiGetDeviceInterfaceDetailW.argtypes = [w.HANDLE, c.c_void_p, c.c_void_p, w.DWORD,
c.POINTER(w.DWORD), c.c_void_p]
sa.SetupDiDestroyDeviceInfoList.argtypes = [w.HANDLE]
self.hid.HidD_GetHidGuid.argtypes = [c.c_void_p]
self.hid.HidD_GetAttributes.argtypes = [w.HANDLE, c.c_void_p]
self.hid.HidD_GetPreparsedData.argtypes = [w.HANDLE, c.POINTER(c.c_void_p)]
self.hid.HidD_FreePreparsedData.argtypes = [c.c_void_p]
self.hid.HidP_GetCaps.argtypes = [c.c_void_p, c.c_void_p]
self.hid.HidD_GetProductString.argtypes = [w.HANDLE, c.c_void_p, w.ULONG]
INVALID = (2 ** 64 - 1, 2 ** 32 - 1, -1) # INVALID_HANDLE_VALUE, 64/32-bit
def paths(self):
"""Every HID interface path on the system."""
c, w = self.c, self.w
guid = self.GUID()
self.hid.HidD_GetHidGuid(c.byref(guid))
info = self.setupapi.SetupDiGetClassDevsW(c.byref(guid), None, None, 0x12) # PRESENT|INTERFACE
paths = []
i = 0
while True:
data = self.InterfaceData()
data.cbSize = c.sizeof(data)
if not self.setupapi.SetupDiEnumDeviceInterfaces(info, None, c.byref(guid), i, c.byref(data)):
break
i += 1
needed = w.DWORD()
self.setupapi.SetupDiGetDeviceInterfaceDetailW(info, c.byref(data), None, 0, c.byref(needed), None)
buf = c.create_string_buffer(needed.value)
# SP_DEVICE_INTERFACE_DETAIL_DATA_W: DWORD cbSize, then the path.
c.cast(buf, c.POINTER(w.DWORD))[0] = 8 if c.sizeof(c.c_void_p) == 8 else 6
if self.setupapi.SetupDiGetDeviceInterfaceDetailW(info, c.byref(data), buf, needed, None, None):
paths.append(c.wstring_at(c.addressof(buf) + 4))
self.setupapi.SetupDiDestroyDeviceInfoList(info)
return paths
def open(self, path, access):
# FILE_SHARE_READ|WRITE, OPEN_EXISTING, FILE_FLAG_OVERLAPPED
h = self.k32.CreateFileW(path, access, 3, None, 3, 0x40000000, None)
return None if h is None or h in self.INVALID else h
def describe(self, h):
"""(vendor, product, usage_page, usage, input_length, name) of an open handle."""
c = self.c
attrs = self.Attributes()
attrs.Size = c.sizeof(attrs)
if not self.hid.HidD_GetAttributes(h, c.byref(attrs)):
return None
page = usage = length = 0
pre = c.c_void_p()
if self.hid.HidD_GetPreparsedData(h, c.byref(pre)):
caps = self.Caps()
self.hid.HidP_GetCaps(pre, c.byref(caps))
page, usage, length = caps.UsagePage, caps.Usage, caps.InputReportByteLength
self.hid.HidD_FreePreparsedData(pre)
name = c.create_unicode_buffer(128)
if not self.hid.HidD_GetProductString(h, name, c.sizeof(name)):
name.value = '?'
return attrs.VendorID, attrs.ProductID, page, usage, length, name.value
class WinDevice:
backend = 'windows-hid'
_hid = None
def __init__(self, path, name, vendor, product, length):
self.path, self.name, self.vendor, self.product = path, name, vendor, product
self.length = length or 64
self.handle = None
@classmethod
def hid(cls):
if cls._hid is None:
cls._hid = WinHid()
return cls._hid
@classmethod
def find(cls):
h = cls.hid()
found = []
for path in h.paths():
handle = h.open(path, 0) # no access: enough to read attributes
if handle is None:
continue
try:
d = h.describe(handle)
finally:
h.k32.CloseHandle(handle)
if not d:
continue
vendor, product, page, usage, length, name = d
# The same test as QET's SpaceMouseHid::isSpaceMouse().
if vendor not in VENDORS or (page, usage) not in ((1, 8), (0, 0)):
continue
found.append(cls(path, name, '%04x' % vendor, '%04x' % product, length))
return found
def descriptor(self):
# Windows only gives out a parsed form of it.
return ''
def open(self):
h = self.hid()
self.handle = h.open(self.path, 0x80000000) # GENERIC_READ
if self.handle is None:
sys.exit('Could not open the device (error %d).' % h.c.get_last_error())
self.event = h.k32.CreateEventW(None, True, False, None)
self.buf = h.c.create_string_buffer(self.length)
self.pending = False
self.start = time.monotonic()
def _read(self, wait_ms):
"""One report if it arrives within wait_ms, else None."""
h = self.hid()
c, w = h.c, h.w
if not self.pending:
self.ov = h.Overlapped()
self.ov.hEvent = self.event
h.k32.ReadFile(self.handle, self.buf, self.length, None, c.byref(self.ov))
self.pending = True
if h.k32.WaitForSingleObject(self.event, max(0, int(wait_ms))) != 0:
return None
self.pending = False
n = w.DWORD()
if not h.k32.GetOverlappedResult(self.handle, c.byref(self.ov), c.byref(n), False):
return None
data = self.buf.raw[:n.value]
# Windows puts a report ID in front even when the device has none;
# hidapi drops that 0, so QET never sees it.
return data[1:] if data[:1] == b'\0' else data
def record(self, seconds):
reports = []
start = time.monotonic()
while True:
left = seconds - (time.monotonic() - start)
if left <= 0:
return reports
data = self._read(left * 1000)
if data:
reports.append([round((time.monotonic() - start) * 1000, 1), data.hex()])
def drain(self):
while self._read(0):
pass
def close(self):
h = self.hid()
if self.pending:
h.k32.CancelIo(self.handle)
h.k32.CloseHandle(self.event)
h.k32.CloseHandle(self.handle)
def empty_hint(self):
return 'Check the cable, push the cap firmly, and try again.'
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('--list', action='store_true', help='only list matching devices')
ap.add_argument('--device', help='device path from --list, if more than one is found')
ap.add_argument('--descriptor', help=argparse.SUPPRESS) # testing without a device
ap.add_argument('--yes', action='store_true', help=argparse.SUPPRESS) # no Enter prompts
ap.add_argument('-o', '--output', help='output file (default: spacemouse-capture-<product>.json)')
ap.add_argument('--seconds', type=float, default=1.0,
help='multiply each step\'s recording time (default: 1.0, e.g. 3 triples it)')
args = ap.parse_args()
Device = {'darwin': MacDevice, 'win32': WinDevice}.get(sys.platform, HidrawDevice)
devices = Device.find()
if args.list:
for d in devices:
print('%s %s:%s %s' % (d.path, d.vendor, d.product, d.name))
if not devices:
print('No 3Dconnexion device found.')
return 0 if devices else 1
if args.device:
dev = next((d for d in devices if d.path == args.device), None)
if dev is None:
if Device is not HidrawDevice:
sys.exit('No device %s (try --list)' % args.device)
dev = HidrawDevice(args.device)
elif len(devices) == 1:
dev = devices[0]
elif not devices:
sys.exit('No 3Dconnexion device found. Is it plugged in? (try --list)')
else:
sys.exit('Several devices found, pick one with --device:\n' +
'\n'.join(' %s %s' % (d.path, d.name) for d in devices))
if args.descriptor:
with open(args.descriptor, 'rb') as f:
descriptor = f.read().hex()
else:
descriptor = dev.descriptor()
dev.open()
print('Recording from %s (%s, %s:%s).' % (dev.path, dev.name, dev.vendor, dev.product))
readers = other_readers()
if readers:
print('Also running: %s. If nothing gets recorded, quit it and try again.' % ', '.join(readers))
print('For each step, press Enter, do the movement, and wait for the next prompt.\n')
result = {
'tool': 'spacemouse-capture.py 2',
'date': datetime.datetime.now(datetime.timezone.utc).isoformat(timespec='seconds'),
'system': platform.platform(),
'backend': dev.backend,
'other_readers': readers,
'device': {'name': dev.name, 'vendor': dev.vendor, 'product': dev.product},
'report_descriptor': descriptor,
'steps': [],
}
try:
for i, (key, text, seconds) in enumerate(STEPS, 1):
seconds = seconds * args.seconds
print('[%d/%d] %s' % (i, len(STEPS), text))
if not args.yes:
input(' Press Enter to start (%.0f s)... ' % seconds)
dev.drain()
reports = dev.record(seconds)
print(' %d reports recorded.\n' % len(reports))
result['steps'].append({'step': key, 'instruction': text, 'reports': reports})
except KeyboardInterrupt:
print('\nStopped early -- saving what was recorded so far.')
finally:
dev.close()
out = args.output or 'spacemouse-capture-%s.json' % dev.product
with open(out, 'w') as f:
json.dump(result, f, indent=1)
total = sum(len(s['reports']) for s in result['steps'])
print('Saved %s (%d reports in total).' % (out, total))
if total == 0:
print('Nothing was recorded. ' + dev.empty_hint())
else:
print('Please attach this file to discussion #599. Thank you!')
return 0
if __name__ == '__main__':
sys.exit(main())
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
#Based on raspberry pi 5 8 Gb Trixie #Based on raspberry pi 5 8 Gb Trixie
#sudo apt install git ssh rsync libqt5svg5-dev qt5-qmake qtbase5-dev libkf5widgetsaddons-dev libkf5coreaddons-dev pkgconf libqt5waylandclient5-dev libqt5waylandcompositor5-dev g++ make #sudo apt install git ssh rsync libqt5svg5-dev qt5-qmake qtbase5-dev libkf5widgetsaddons-dev libkf5coreaddons-dev libsqlite3-dev pkgconf libqt5waylandclient5-dev libqt5waylandcompositor5-dev g++ make
#mkdir -p AppImage/0.100.0/aarch64 #mkdir -p AppImage/0.100.0/aarch64
# Get GIT sources # Get GIT sources
#git clone --recursive https://github.com/qelectrotech/qelectrotech-source-mirror.git #git clone --recursive https://github.com/qelectrotech/qelectrotech-source-mirror.git
@@ -0,0 +1,940 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "elementpickerpopup.h"
#include "elementscollectionwidget.h"
#include <QAction>
#include <QGuiApplication>
#include <QHBoxLayout>
#include <QListWidget>
#include <QMenu>
#include <QPushButton>
#include <QToolButton>
#include <QCloseEvent>
#include <QCursor>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QLabel>
#include <QLineEdit>
#include <QListView>
#include <QScreen>
#include <QSizeGrip>
#include <QStandardItemModel>
#include <QTimer>
#include <QVBoxLayout>
#include <QDir>
#include <QFileInfo>
#include <QSettings>
#include <limits>
#include "../qetapp.h"
#include "../shortcutmanager.h"
#include "elementslocation.h"
//The palette is read from disk each time the picker opens. It is meant as
//a shortlist, and a large custom collection would otherwise make opening
//slow and the grid unusable.
static const int max_palette_entries = 60;
/**
@brief ElementPickerPopup::ElementPickerPopup
@param source : the collection widget whose model the search runs against
@param parent
*/
ElementPickerPopup::ElementPickerPopup(ElementsCollectionWidget *source,
QWidget *parent) :
QFrame(parent, Qt::Popup),
m_source(source)
{
setFrameShape(QFrame::StyledPanel);
setFrameShadow(QFrame::Raised);
setMinimumWidth(340);
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(6, 6, 6, 6);
layout->setSpacing(4);
//Command row, shown when the picker is opened as the shortcut bar
m_commands = new QWidget(this);
m_commands_layout = new QHBoxLayout(m_commands);
m_commands_layout->setContentsMargins(0, 0, 0, 0);
m_commands_layout->setSpacing(2);
m_commands->hide();
//SolidWorks: right-click the bar to customise it
m_commands->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_commands, &QWidget::customContextMenuRequested, this, [this](const QPoint &pos) {
QMenu menu;
menu.addAction(tr("Personnaliser la barre…"), this, &ElementPickerPopup::startCustomising);
menu.exec(m_commands->mapToGlobal(pos));
});
//Customising, shown instead of everything else. Two lists that
//commands are dragged between: the bar, left to right, and the
//commands that are not on it.
m_editor = new QWidget(this);
auto *editor_layout = new QVBoxLayout(m_editor);
editor_layout->setContentsMargins(0, 0, 0, 0);
auto *help = new QLabel(
tr("Glissez les commandes et les éléments dans la barre, hors de la "
"barre, ou d'une place à l'autre. Un double-clic fait passer une "
"commande ou un élément d'une liste à l'autre."), m_editor);
help->setWordWrap(true);
m_edit_row = new QListWidget(m_editor);
m_edit_row->setFlow(QListView::LeftToRight);
m_edit_row->setWrapping(false);
m_edit_row->setIconSize(QSize(24, 24));
m_edit_row->setFixedHeight(44);
m_edit_available = new QListWidget(m_editor);
m_edit_available->setIconSize(QSize(20, 20));
m_edit_available->setMinimumHeight(220);
for (QListWidget *list : {m_edit_row, m_edit_available}) {
list->setDragDropMode(QAbstractItemView::DragDrop);
list->setDefaultDropAction(Qt::MoveAction);
list->setSelectionMode(QAbstractItemView::SingleSelection);
}
//A drop copies the item as it was in the other list, so a command
//dragged onto the bar would keep its text and one dragged off would
//have none. Set the text for the list it has landed in.
//Deferred: a drop inserts the row first and copies the item's data
//into it afterwards.
auto relabel = [this](QListWidget *list, bool icon_only) {
return [this, list, icon_only]() {
QTimer::singleShot(0, this, [list, icon_only]() {
for (int i = 0 ; i < list->count() ; ++i) {
QListWidgetItem *item = list->item(i);
item->setText(icon_only && !item->icon().isNull()
? QString() : item->toolTip());
}
});
};
};
connect(m_edit_row->model(), &QAbstractItemModel::rowsInserted,
this, relabel(m_edit_row, true));
connect(m_edit_available->model(), &QAbstractItemModel::rowsInserted,
this, relabel(m_edit_available, false));
//An element dragged off the bar onto the commands is removed from
//the bar, not listed as a command
connect(m_edit_available->model(), &QAbstractItemModel::rowsInserted, this, [this]() {
QTimer::singleShot(0, this, [this]() {
for (int i = m_edit_available->count() - 1 ; i >= 0 ; --i) {
if (ShortcutBarSettings::isElement(
m_edit_available->item(i)->data(Qt::UserRole).toString())) {
delete m_edit_available->takeItem(i);
}
}
});
});
connect(m_edit_row, &QListWidget::itemDoubleClicked, this, [this](QListWidgetItem *item) {
const QString id = item->data(Qt::UserRole).toString();
delete item;
if (!ShortcutBarSettings::isElement(id)) {
m_edit_available->addItem(barItem(id, false));
}
});
connect(m_edit_available, &QListWidget::itemDoubleClicked, this, [this](QListWidgetItem *item) {
const QString id = item->data(Qt::UserRole).toString();
delete item;
m_edit_row->addItem(barItem(id, true));
});
//Elements to pin: the same ranked search as the picker, dragged
//onto the bar. Copied, not moved, so a result can be pinned and
//still be seen in the list.
m_edit_symbols_box = new QWidget(m_editor);
auto *symbols_layout = new QVBoxLayout(m_edit_symbols_box);
symbols_layout->setContentsMargins(0, 0, 0, 0);
m_edit_symbols_search = new QLineEdit(m_edit_symbols_box);
m_edit_symbols_search->setPlaceholderText(tr("Rechercher un élément…"));
m_edit_symbols_search->setClearButtonEnabled(true);
m_edit_symbols = new QListWidget(m_edit_symbols_box);
m_edit_symbols->setIconSize(QSize(32, 32));
m_edit_symbols->setMinimumHeight(220);
m_edit_symbols->setDragDropMode(QAbstractItemView::DragOnly);
m_edit_symbols->setDefaultDropAction(Qt::CopyAction);
m_edit_symbols->setSelectionMode(QAbstractItemView::SingleSelection);
symbols_layout->addWidget(new QLabel(tr("Éléments :"), m_edit_symbols_box));
symbols_layout->addWidget(m_edit_symbols_search);
symbols_layout->addWidget(m_edit_symbols);
auto *symbols_timer = new QTimer(this);
symbols_timer->setSingleShot(true);
symbols_timer->setInterval(300);
connect(m_edit_symbols_search, &QLineEdit::textChanged, this,
[symbols_timer]() { symbols_timer->start(); });
connect(symbols_timer, &QTimer::timeout, this, &ElementPickerPopup::runSymbolSearch);
connect(m_edit_symbols, &QListWidget::itemDoubleClicked, this, [this](QListWidgetItem *item) {
m_edit_row->addItem(barItem(item->data(Qt::UserRole).toString(), true));
});
auto *defaults = new QPushButton(tr("Valeurs par défaut"), m_editor);
auto *cancel = new QPushButton(tr("Annuler"), m_editor);
auto *done = new QPushButton(tr("Terminé"), m_editor);
done->setDefault(true);
connect(defaults, &QPushButton::clicked, this, [this]() {
fillCustomising(ShortcutBarSettings::defaultIds(m_context));
});
connect(cancel, &QPushButton::clicked, this, [this]() { finishCustomising(false); });
connect(done, &QPushButton::clicked, this, [this]() { finishCustomising(true); });
auto *editor_buttons = new QHBoxLayout();
editor_buttons->addWidget(defaults);
editor_buttons->addStretch();
editor_buttons->addWidget(cancel);
editor_buttons->addWidget(done);
editor_layout->addWidget(help);
editor_layout->addWidget(new QLabel(tr("Dans la barre :"), m_editor));
editor_layout->addWidget(m_edit_row);
auto *lists = new QHBoxLayout();
auto *commands_column = new QVBoxLayout();
commands_column->addWidget(new QLabel(tr("Autres commandes :"), m_editor));
commands_column->addWidget(m_edit_available);
lists->addLayout(commands_column);
lists->addWidget(m_edit_symbols_box);
editor_layout->addLayout(lists);
editor_layout->addLayout(editor_buttons);
m_editor->hide();
m_search = new QLineEdit(this);
m_search->setPlaceholderText(tr("Rechercher un élément…"));
m_search->setClearButtonEnabled(true);
m_model = new QStandardItemModel(this);
m_view = new QListView(this);
m_view->setModel(m_model);
m_view->setIconSize(QSize(40, 40));
m_view->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
m_view->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_view->setMinimumHeight(260);
m_hint = new QLabel(tr("Entrée pour insérer · Échap pour fermer"), this);
m_hint->setEnabled(false);
//Wrapped rather than cut off when the bar is made narrow
m_hint->setWordWrap(true);
//The bar's width is the user's: dragging the grip wraps the tiles
//onto more rows, and the width is kept for next time
m_grip = new QSizeGrip(this);
m_grip->setToolTip(tr("Glisser pour changer la largeur de la barre"));
m_grip->installEventFilter(this);
auto *hint_row = new QHBoxLayout();
hint_row->addWidget(m_hint, 1);
hint_row->addWidget(m_grip, 0, Qt::AlignBottom | Qt::AlignRight);
layout->addWidget(m_commands);
layout->addWidget(m_search);
layout->addWidget(m_view);
layout->addLayout(hint_row);
layout->addWidget(m_editor);
//Search as you type, after a short idle: shorter than the dock's
//500 ms, since the popup is opened and closed in a few seconds, but
//the collection is large enough that every keystroke would show.
auto *timer = new QTimer(this);
timer->setSingleShot(true);
timer->setInterval(300);
connect(m_search, &QLineEdit::textChanged, this,
[timer]() { timer->start(); });
connect(timer, &QTimer::timeout, this, &ElementPickerPopup::runSearch);
connect(m_view, &QListView::doubleClicked, this,
[this](const QModelIndex &) { chooseCurrent(); });
}
/**
@brief ElementPickerPopup::popUpAt
Show the picker at @a global_pos, kept on screen, with the search field
focused and any previous query cleared.
@param global_pos
*/
void ElementPickerPopup::popUpAt(const QPoint &global_pos)
{
m_bar_mode = false;
setCommands({});
show(global_pos);
}
/**
@brief ElementPickerPopup::popUpShortcutBar
Show the picker at @a global_pos with the shortcut bar's commands for
@a context above it.
@param global_pos
@param context
*/
void ElementPickerPopup::popUpShortcutBar(const QPoint &global_pos,
ShortcutBarSettings::Context context)
{
m_bar_mode = true;
m_context = context;
setCommands(ShortcutBarSettings::ids(context));
show(global_pos);
}
/**
@brief ElementPickerPopup::commandAction
@return the action registered under @a id by the window this popup
belongs to, or nullptr
*/
QAction *ElementPickerPopup::commandAction(const QString &id) const
{
return ShortcutManager::instance().action(id, parentWidget());
}
/**
@brief ElementPickerPopup::show
Reset the search and show at @a global_pos, kept on screen.
*/
void ElementPickerPopup::show(const QPoint &global_pos)
{
m_search->clear();
m_model->clear();
showPalette();
keepOnScreen(global_pos);
QFrame::show();
m_search->setFocus();
}
/**
@brief ElementPickerPopup::keepOnScreen
Fit the popup to its contents and move it to @a global_pos, kept fully on
the screen that point is on: opening at the cursor near a right or bottom
edge would otherwise push it off.
*/
void ElementPickerPopup::keepOnScreen(const QPoint &global_pos)
{
adjustSize();
//The sizes the user gave the customising window and the bar
if (m_customising) {
const QSize size = ShortcutBarSettings::editorSize();
if (size.isValid()) {
resize(size.expandedTo(minimumSizeHint()));
}
} else if (m_bar_mode && ShortcutBarSettings::barWidth() > 0) {
resize(qMax(ShortcutBarSettings::barWidth(), minimumSizeHint().width()),
height());
}
QPoint pos = global_pos;
if (QScreen *screen = QGuiApplication::screenAt(global_pos)) {
const QRect avail = screen->availableGeometry();
pos.setX(qBound(avail.left(),
pos.x(), avail.right() - width()));
pos.setY(qBound(avail.top(),
pos.y(), avail.bottom() - height()));
}
move(pos);
}
/**
@brief ElementPickerPopup::setCommands
Show @a ids as a row of buttons above the search field, or hide the row
when there are none. A disabled command keeps its place, greyed out, so
the row looks the same every time for a given selection. An id no live
action carries, or a pinned element that no longer exists, is skipped.
Clicking a button closes the picker first, then triggers the action: a
command such as "add a line" starts a mode on the folio, which needs the
focus the popup holds. A pinned element is placed, as if chosen from the
picker.
@param ids : command ids and pinned elements' collection paths
*/
void ElementPickerPopup::setCommands(const QStringList &ids)
{
while (QLayoutItem *item = m_commands_layout->takeAt(0)) {
delete item->widget();
delete item;
}
qDeleteAll(m_tiles);
m_tiles.clear();
m_tile_rows = new QVBoxLayout();
m_tile_rows->setSpacing(2);
m_commands_layout->addLayout(m_tile_rows);
for (const QString &id : ids)
{
if (ShortcutBarSettings::isElement(id))
{
const ElementsLocation location(id);
if (!location.exist()) {
continue;
}
auto *button = new QToolButton(m_commands);
button->setAutoRaise(true);
button->setIconSize(QSize(24, 24));
button->setIcon(location.icon());
button->setToolTip(location.name());
button->setFocusPolicy(Qt::NoFocus);
connect(button, &QToolButton::clicked, this, [this, location]() {
hide();
emit elementChosen(location);
});
m_tiles << button;
continue;
}
QAction *action = commandAction(id);
if (!action) {
continue;
}
auto *button = new QToolButton(m_commands);
button->setAutoRaise(true);
button->setIconSize(QSize(24, 24));
const QString text = action->text().remove(QLatin1Char('&'));
if (action->icon().isNull()) {
button->setText(text);
button->setToolButtonStyle(Qt::ToolButtonTextOnly);
} else {
button->setIcon(action->icon());
}
const QKeySequence key = action->shortcut();
QString tip = key.isEmpty()
? text
: QStringLiteral("%1 (%2)").arg(
text, key.toString(QKeySequence::NativeText));
//The status tip says what the command does, which a short
//name such as "Orienter les textes" does not
if (!action->statusTip().isEmpty()) {
tip += QLatin1Char('\n') + action->statusTip();
}
button->setToolTip(tip);
button->setEnabled(action->isEnabled());
button->setFocusPolicy(Qt::NoFocus);
connect(button, &QToolButton::clicked, this, [this, action]() {
hide();
action->trigger();
});
m_tiles << button;
}
m_commands_layout->addStretch();
if (m_bar_mode)
{
auto *customise = new QToolButton(m_commands);
customise->setAutoRaise(true);
customise->setText(QStringLiteral("…"));
customise->setToolTip(tr("Personnaliser la barre…"));
customise->setFocusPolicy(Qt::NoFocus);
connect(customise, &QToolButton::clicked, this, &ElementPickerPopup::startCustomising);
m_commands_layout->addWidget(customise, 0, Qt::AlignTop);
}
layoutTiles(ShortcutBarSettings::barWidth());
m_grip->setVisible(m_bar_mode);
//An empty bar still shows, so it can be customised back
m_commands->setVisible(m_bar_mode || !ids.isEmpty());
}
/**
@brief ElementPickerPopup::layoutTiles
Put the bar's tiles in rows no wider than the bar at @a bar_width, in
their order, as many per row as fit. A @a bar_width of 0 or less, the
width the user never set, keeps them on one row.
*/
void ElementPickerPopup::layoutTiles(int bar_width)
{
if (!m_tile_rows) {
return;
}
//Emptying a row layout leaves its buttons alone; they go into the
//new rows below
while (QLayoutItem *item = m_tile_rows->takeAt(0)) {
delete item->layout();
}
//Room left beside the tiles: the popup's margins and the "…" button
const int limit = bar_width > 0
? bar_width - 12 - 30
: std::numeric_limits<int>::max();
QHBoxLayout *row = nullptr;
int used = 0;
for (QToolButton *tile : std::as_const(m_tiles))
{
const int w = tile->sizeHint().width() + m_tile_rows->spacing();
if (!row || (used + w > limit && used > 0)) {
row = new QHBoxLayout();
row->setSpacing(2);
row->addStretch();
m_tile_rows->addLayout(row);
used = 0;
}
//Before the stretch, which stays last so the row is left-aligned
row->insertWidget(row->count() - 1, tile);
used += w;
}
}
/**
@brief ElementPickerPopup::eventFilter
Drive the size grip by hand. QSizeGrip asks the window manager to do the
resize, and a popup is a window the window manager does not manage, so
on X11 nothing would happen. Only the width follows the mouse: the tiles
re-flow into rows as it changes, the height follows the rows, and the
width is saved when the grip is let go.
*/
bool ElementPickerPopup::eventFilter(QObject *watched, QEvent *event)
{
if (watched != m_grip) {
return QFrame::eventFilter(watched, event);
}
switch (event->type())
{
case QEvent::MouseButtonPress: {
auto *me = static_cast<QMouseEvent *>(event);
if (me->button() != Qt::LeftButton) {
break;
}
m_grip_active = true;
m_grip_press_x = me->globalPosition().toPoint().x();
m_grip_press_width = width();
return true;
}
case QEvent::MouseMove: {
if (!m_grip_active) {
break;
}
auto *me = static_cast<QMouseEvent *>(event);
const int w = qMax(minimumWidth(), m_grip_press_width
+ me->globalPosition().toPoint().x() - m_grip_press_x);
layoutTiles(w);
layout()->activate();
resize(w, sizeHint().height());
return true;
}
case QEvent::MouseButtonRelease:
if (!m_grip_active) {
break;
}
m_grip_active = false;
ShortcutBarSettings::setBarWidth(width());
return true;
default:
break;
}
return QFrame::eventFilter(watched, event);
}
/**
@brief ElementPickerPopup::barItem
@return a list item for @a id, a command or a pinned element: icon only,
for the bar row, or icon and text, for the other lists. The id is in
UserRole.
*/
QListWidgetItem *ElementPickerPopup::barItem(const QString &id, bool icon_only) const
{
QString text = id;
QIcon icon;
if (ShortcutBarSettings::isElement(id)) {
const ElementsLocation location(id);
if (location.exist()) {
text = location.name();
icon = location.icon();
}
} else if (QAction *action = commandAction(id)) {
text = action->text().remove(QLatin1Char('&'));
icon = action->icon();
}
auto *item = new QListWidgetItem();
item->setData(Qt::UserRole, id);
item->setToolTip(text);
if (!icon.isNull()) {
item->setIcon(icon);
}
if (!icon_only || item->icon().isNull()) {
item->setText(text);
}
return item;
}
/**
@brief ElementPickerPopup::startCustomising
Turn the bar into a small window holding the two lists. A Qt::Popup
closes as soon as the mouse is pressed outside it and keeps the mouse
grab during a drag, so the window is re-shown as a tool window for the
time of the edit.
*/
void ElementPickerPopup::startCustomising()
{
const QPoint where = pos();
m_customising = true;
hide();
setWindowFlags(Qt::Tool);
setWindowTitle(tr("Personnaliser la barre de raccourcis : %1")
.arg(ShortcutBarSettings::title(m_context)));
fillCustomising(ShortcutBarSettings::ids(m_context));
m_commands->hide();
setPickerVisible(false);
m_grip->hide();
//Elements are pinned to the empty-folio bar only: with something
//selected, the bar is for acting on it, not for adding more
m_edit_symbols_search->clear();
m_edit_symbols->clear();
m_edit_symbols_box->setVisible(m_context == ShortcutBarSettings::Canvas);
m_editor->show();
keepOnScreen(where);
QFrame::show();
activateWindow();
}
/**
@brief ElementPickerPopup::fillCustomising
Put @a ids on the bar row, and every other available command in the
list below it.
*/
void ElementPickerPopup::fillCustomising(const QStringList &ids)
{
m_edit_row->clear();
m_edit_available->clear();
for (const QString &id : ids) {
m_edit_row->addItem(barItem(id, true));
}
for (const QString &id : ShortcutBarSettings::availableIds()) {
if (!ids.contains(id)) {
m_edit_available->addItem(barItem(id, false));
}
}
m_edit_available->sortItems();
}
/**
@brief ElementPickerPopup::runSymbolSearch
Fill the customising window's element list with the ranked hits for its
search field. Elements embedded in a project are left out: their path
names the project as it is loaded now, so it would not find them again
in a later session.
*/
void ElementPickerPopup::runSymbolSearch()
{
m_edit_symbols->clear();
if (!m_source) {
return;
}
const QVector<ElementSearchHit> hits =
m_source->rankedSearch(m_edit_symbols_search->text());
for (const ElementSearchHit &hit : hits)
{
if (ElementsLocation(hit.path).isProject()) {
continue;
}
auto *item = new QListWidgetItem(hit.icon, hit.name);
item->setData(Qt::UserRole, hit.path);
item->setToolTip(QStringLiteral("%1\n%2").arg(hit.name, hit.folder));
m_edit_symbols->addItem(item);
if (m_edit_symbols->count() >= max_palette_entries) {
break;
}
}
}
/**
@brief ElementPickerPopup::finishCustomising
Leave the edit, saving the bar row when @a save, and show the bar again
at the cursor so the result can be seen and used straight away. The
window's size is kept either way.
*/
void ElementPickerPopup::finishCustomising(bool save)
{
if (save)
{
QStringList ids;
for (int i = 0 ; i < m_edit_row->count() ; ++i) {
ids << m_edit_row->item(i)->data(Qt::UserRole).toString();
}
ShortcutBarSettings::setIds(m_context, ids);
}
ShortcutBarSettings::setEditorSize(size());
m_customising = false;
hide();
m_editor->hide();
setPickerVisible(true);
setWindowFlags(Qt::Popup);
setWindowTitle(QString());
//At the cursor, like any other opening of the bar
popUpShortcutBar(QCursor::pos(), m_context);
}
/**
@brief ElementPickerPopup::closeEvent
Closing the customising window from its title bar cancels the edit,
like the Annuler button, rather than leaving the bar half-edited.
*/
void ElementPickerPopup::closeEvent(QCloseEvent *event)
{
if (m_customising) {
event->ignore();
finishCustomising(false);
return;
}
QFrame::closeEvent(event);
}
/**
@brief ElementPickerPopup::setPickerVisible
Show or hide the search field, the results and the hint line.
*/
void ElementPickerPopup::setPickerVisible(bool visible)
{
m_search->setVisible(visible);
m_view->setVisible(visible);
m_hint->setVisible(visible);
}
/**
@brief ElementPickerPopup::runSearch
*/
void ElementPickerPopup::runSearch()
{
m_model->clear();
if (!m_source) {
return;
}
if (m_search->text().isEmpty()) {
const bool was_hidden = m_view->isHidden();
showPalette();
if (was_hidden != m_view->isHidden()) {
keepOnScreen(pos());
}
return;
}
if (m_view->isHidden()) {
m_view->show();
keepOnScreen(pos());
}
m_palette_mode = false;
m_view->setViewMode(QListView::ListMode);
m_view->setGridSize(QSize());
m_view->setIconSize(QSize(40, 40));
const QVector<ElementSearchHit> hits = m_source->rankedSearch(m_search->text());
for (const ElementSearchHit &hit : hits)
{
auto *item = new QStandardItem(hit.name);
item->setIcon(hit.icon);
item->setEditable(false);
item->setToolTip(hit.folder);
item->setData(hit.path, Qt::UserRole + 2);
m_model->appendRow(item);
}
//Preselect the best hit so Enter works straight from the search field
//without arrowing down first -- that is the whole point of the popup.
if (m_model->rowCount()) {
m_view->setCurrentIndex(m_model->index(0, 0));
}
m_hint->setText(hits.isEmpty()
? tr("Aucun résultat")
: tr("Entrée pour insérer · Échap pour fermer"));
}
/**
@brief ElementPickerPopup::chooseCurrent
Emit the highlighted element and close.
*/
void ElementPickerPopup::chooseCurrent()
{
const QModelIndex index = m_view->currentIndex();
if (!index.isValid()) {
return;
}
const QString path = index.data(Qt::UserRole + 2).toString();
if (path.isEmpty()) {
return;
}
ElementsLocation location(path);
if (!location.exist()) {
return;
}
//Close before emitting: placement mode wants the focus, and a popup
//still up would keep the grab.
hide();
emit elementChosen(location);
}
/**
@brief ElementPickerPopup::keyPressEvent
Drive the list from the search field, so the hands never leave the
keyboard: Up/Down move the selection, Enter places, Esc closes.
@param event
*/
void ElementPickerPopup::keyPressEvent(QKeyEvent *event)
{
switch (event->key())
{
case Qt::Key_Escape:
if (m_customising) {
finishCustomising(false);
} else {
hide();
}
return;
case Qt::Key_Return:
case Qt::Key_Enter:
if (m_customising && m_edit_symbols_search->hasFocus()) {
//Enter in the element search pins the best hit rather
//than closing the window. Searched now, not on the
//timer, so a quick Enter after typing gets this text.
runSymbolSearch();
if (QListWidgetItem *item = m_edit_symbols->item(0)) {
m_edit_row->addItem(barItem(item->data(Qt::UserRole).toString(), true));
}
} else if (m_customising) {
finishCustomising(true);
} else {
chooseCurrent();
}
return;
case Qt::Key_Down:
case Qt::Key_Up:
case Qt::Key_PageDown:
case Qt::Key_PageUp:
if (!m_customising && m_model->rowCount()) {
//Forwarded rather than focus-switched, so typing carries
//on going to the search field.
QCoreApplication::sendEvent(m_view, event);
return;
}
break;
default:
break;
}
QFrame::keyPressEvent(event);
}
/**
@brief ElementPickerPopup::showPalette
With an empty search field, show the quick palette as an icon grid.
The palette is a folder, not a config file. QET already has a user-owned,
drag-populated, icon-rendering collection -- so a palette is just a
directory whose subfolders are categories and whose contents are entries.
That gives set-up (drag into the folder), ordering (the 01_/02_ filename
convention the shipped collection already uses), team sharing
(companyElementsDir) and version control for free, with no schema, parser,
merge rules or "reset to defaults" to build or maintain.
Only the path lives in QSettings, defaulting to the user's custom
collection.
*/
void ElementPickerPopup::showPalette()
{
m_palette_mode = true;
m_model->clear();
//Elements pinned to the bar replace the folder grid: the bar already
//shows the user's shortlist, and a second one below it is noise.
//The search still works; the list comes back as soon as one types.
if (m_bar_mode && ShortcutBarSettings::hasElements(m_context)) {
m_view->hide();
m_hint->setText(tr("Tapez pour rechercher un élément · Échap pour fermer"));
return;
}
m_view->show();
m_view->setViewMode(QListView::IconMode);
m_view->setIconSize(QSize(48, 48));
m_view->setGridSize(QSize(92, 84));
m_view->setResizeMode(QListView::Adjust);
m_view->setWordWrap(true);
m_view->setMovement(QListView::Static);
QSettings settings;
const QString path = settings.value(
QStringLiteral("elementscollection/palette-path"),
QETApp::customElementsDir()).toString();
const int count = loadPaletteDir(path, QString(), 0);
if (!count) {
m_hint->setText(
tr("Palette vide — glissez des éléments dans votre collection "
"personnelle, ou tapez pour rechercher"));
} else {
m_hint->setText(tr("Entrée pour insérer · Échap pour fermer"));
m_view->setCurrentIndex(m_model->index(0, 0));
}
}
/**
@brief ElementPickerPopup::loadPaletteDir
Add every .elmt under @a dir_path to the grid, recursing into subfolders.
@param dir_path
@param prefix : folder path so far, shown as the entry's tooltip
@param depth : recursion guard -- a palette is a shortlist, and the whole
shipped collection would not be usable as a grid anyway
@return how many entries were added
*/
int ElementPickerPopup::loadPaletteDir(const QString &dir_path,
const QString &prefix, int depth)
{
if (depth > 3 || m_model->rowCount() >= max_palette_entries) {
return 0;
}
QDir dir(dir_path);
if (!dir.exists()) {
return 0;
}
int added = 0;
//Sorted by name, which is what makes the 01_/02_ filename convention
//work as the ordering mechanism.
const QFileInfoList entries = dir.entryInfoList(
QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name);
for (const QFileInfo &info : entries)
{
if (m_model->rowCount() >= max_palette_entries) {
break;
}
if (info.isDir()) {
added += loadPaletteDir(info.absoluteFilePath(),
prefix.isEmpty()
? info.fileName()
: prefix + QStringLiteral(" / ")
+ info.fileName(),
depth + 1);
continue;
}
if (info.suffix().compare(QStringLiteral("elmt"),
Qt::CaseInsensitive) != 0) {
continue;
}
ElementsLocation location(info.absoluteFilePath());
if (!location.exist()) {
continue;
}
auto *item = new QStandardItem(location.name().isEmpty()
? info.completeBaseName()
: location.name());
item->setIcon(location.icon());
item->setEditable(false);
item->setToolTip(prefix.isEmpty() ? info.completeBaseName() : prefix);
item->setData(info.absoluteFilePath(), Qt::UserRole + 2);
item->setTextAlignment(Qt::AlignHCenter | Qt::AlignTop);
m_model->appendRow(item);
++added;
}
return added;
}
@@ -0,0 +1,121 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef ELEMENTPICKERPOPUP_H
#define ELEMENTPICKERPOPUP_H
#include "elementslocation.h"
#include "../shortcutbarsettings.h"
#include <QFrame>
class ElementsCollectionWidget;
class QLineEdit;
class QListView;
class QStandardItemModel;
class QLabel;
class QAction;
class QHBoxLayout;
class QListWidget;
class QListWidgetItem;
class QSizeGrip;
class QToolButton;
class QVBoxLayout;
/**
@brief A cursor-anchored element picker.
Opens where the mouse is, focused on its search field: type to filter,
Enter to place, Esc to close. It is a recall tool, as opposed to the
Collections dock, which is a browsing tool.
It deliberately does not build its own ElementsCollectionModel. Loading the
collection is already the slow part of startup and a second copy would
double it, so the picker asks the dock's widget to run the query -- see
ElementsCollectionWidget::rankedSearch(). That also means the two always
agree on results and ranking.
*/
class ElementPickerPopup : public QFrame
{
Q_OBJECT
public:
explicit ElementPickerPopup(ElementsCollectionWidget *source,
QWidget *parent = nullptr);
void popUpAt(const QPoint &global_pos);
void popUpShortcutBar(const QPoint &global_pos,
ShortcutBarSettings::Context context);
signals:
/// Emitted when the user picks an element; the popup has closed
void elementChosen(const ElementsLocation &location);
protected:
void keyPressEvent(QKeyEvent *event) override;
void closeEvent(QCloseEvent *event) override;
bool eventFilter(QObject *watched, QEvent *event) override;
private:
void runSearch();
void chooseCurrent();
void showPalette();
void show(const QPoint &global_pos);
void setCommands(const QStringList &ids);
void layoutTiles(int bar_width);
QAction *commandAction(const QString &id) const;
QListWidgetItem *barItem(const QString &id, bool icon_only) const;
void startCustomising();
void fillCustomising(const QStringList &ids);
void finishCustomising(bool save);
void runSymbolSearch();
void setPickerVisible(bool visible);
void keepOnScreen(const QPoint &global_pos);
int loadPaletteDir(const QString &dir_path, const QString &prefix,
int depth);
ElementsCollectionWidget *m_source = nullptr;
QLineEdit *m_search = nullptr;
QListView *m_view = nullptr;
QStandardItemModel *m_model = nullptr;
QLabel *m_hint = nullptr;
QWidget *m_commands = nullptr;
QHBoxLayout *m_commands_layout = nullptr;
/// The bar's tiles, commands and elements, in order
QList<QToolButton *> m_tiles;
QVBoxLayout *m_tile_rows = nullptr;
QSizeGrip *m_grip = nullptr;
/// The size grip is being dragged, from this x at this width
bool m_grip_active = false;
int m_grip_press_x = 0;
int m_grip_press_width = 0;
/// Opened as the shortcut bar, as opposed to the plain picker
bool m_bar_mode = false;
ShortcutBarSettings::Context m_context = ShortcutBarSettings::Canvas;
/// Customising the bar in place
bool m_customising = false;
QWidget *m_editor = nullptr;
QListWidget *m_edit_row = nullptr;
QListWidget *m_edit_available = nullptr;
/// Element search, for pinning elements to the bar
QWidget *m_edit_symbols_box = nullptr;
QLineEdit *m_edit_symbols_search = nullptr;
QListWidget *m_edit_symbols = nullptr;
bool m_palette_mode = true;
};
#endif // ELEMENTPICKERPOPUP_H
@@ -49,6 +49,14 @@ ElementsCollectionModel::ElementsCollectionModel(QObject *parent) :
*/ */
ElementsCollectionModel::~ElementsCollectionModel() ElementsCollectionModel::~ElementsCollectionModel()
{ {
// Without cancel(), the wait below runs the whole queued
// QtConcurrent::map() to completion, so closing this dialog on a
// large collection blocks until every remaining item has been
// processed -- a visible hang on the button pressed precisely to
// stop the work. cancel() drops the not-yet-started items so the
// wait that follows (still needed, so an in-flight item can't
// dereference this object after it's gone) is short.
m_future.cancel();
m_future.waitForFinished(); m_future.waitForFinished();
} }
@@ -431,7 +439,7 @@ void ElementsCollectionModel::addLocation(const ElementsLocation& location)
collection_name); collection_name);
} }
} }
// ANPASSUNG: Makros und Custom Collection werden hier behandelt! // Macros and Custom Collection are handled here
else if (location.isCustomCollection() || location.isMacrosCollection()) { else if (location.isCustomCollection() || location.isMacrosCollection()) {
QList <ElementCollectionItem *> child_list; QList <ElementCollectionItem *> child_list;
@@ -495,6 +503,10 @@ void ElementsCollectionModel::addProject(QETProject *project, bool set_data)
connect(project->embeddedElementCollection(), connect(project->embeddedElementCollection(),
&XmlElementCollection::directoryRemoved, &XmlElementCollection::directoryRemoved,
this, &ElementsCollectionModel::itemRemovedFromCollection); this, &ElementsCollectionModel::itemRemovedFromCollection);
connect(project, &QETProject::projectTitleChanged,
this, &ElementsCollectionModel::projectNameChanged);
connect(project, &QETProject::projectFilePathChanged,
this, &ElementsCollectionModel::projectNameChanged);
} }
/** /**
@@ -526,6 +538,10 @@ void ElementsCollectionModel::removeProject(QETProject *project)
&XmlElementCollection::directoryRemoved, &XmlElementCollection::directoryRemoved,
this, this,
&ElementsCollectionModel::itemRemovedFromCollection); &ElementsCollectionModel::itemRemovedFromCollection);
disconnect(project, &QETProject::projectTitleChanged,
this, &ElementsCollectionModel::projectNameChanged);
disconnect(project, &QETProject::projectFilePathChanged,
this, &ElementsCollectionModel::projectNameChanged);
} }
} }
@@ -647,7 +663,7 @@ QModelIndex ElementsCollectionModel::indexFromLocation(
if (eci->type() == FileElementCollectionItem::Type) { if (eci->type() == FileElementCollectionItem::Type) {
if (FileElementCollectionItem *feci = static_cast<FileElementCollectionItem *>(eci)) { if (FileElementCollectionItem *feci = static_cast<FileElementCollectionItem *>(eci)) {
// ANPASSUNG: Makro-Prüfung hinzugefügt, damit das Modell den Pfad im Baum findet! // Macro check added so the model finds the path in the tree
if ( (location.isCommonCollection() && feci->isCommonCollection()) || if ( (location.isCommonCollection() && feci->isCommonCollection()) ||
(location.isCompanyCollection() && feci->isCompanyCollection()) || (location.isCompanyCollection() && feci->isCompanyCollection()) ||
(location.isMacrosCollection() && feci->isMacrosCollection()) || (location.isMacrosCollection() && feci->isMacrosCollection()) ||
@@ -766,3 +782,15 @@ void ElementsCollectionModel::updateItem(const QString& path)
eci->setUpData(); eci->setUpData();
} }
} }
/**
@brief ElementsCollectionModel::projectNameChanged
Update the displayed name of the collection of project,
when its title or its file path changed.
@param project
*/
void ElementsCollectionModel::projectNameChanged(QETProject *project)
{
if (XmlProjectElementCollectionItem *xpeci = m_project_hash.value(project))
xpeci->updateProjectName();
}
@@ -73,6 +73,7 @@ class ElementsCollectionModel : public QStandardItemModel
void elementIntegratedToCollection (const QString& path); void elementIntegratedToCollection (const QString& path);
void itemRemovedFromCollection (const QString& path); void itemRemovedFromCollection (const QString& path);
void updateItem (const QString& path); void updateItem (const QString& path);
void projectNameChanged (QETProject *project);
private: private:
QList <QETProject *> m_project_list; QList <QETProject *> m_project_list;
@@ -35,6 +35,12 @@
#include <QCheckBox> #include <QCheckBox>
#include <QDesktopServices> #include <QDesktopServices>
#include <QSettings>
#include <QShortcut>
#include <QListView>
#include <QStandardItemModel>
#include <QSet>
#include <algorithm>
#include <QDialog> #include <QDialog>
#include <QDialogButtonBox> #include <QDialogButtonBox>
#include <QFileDialog> #include <QFileDialog>
@@ -49,6 +55,32 @@
#include <QStatusBar> #include <QStatusBar>
#include <QLineEdit> #include <QLineEdit>
namespace {
/**
@brief The SearchResultsView class
The flat list of search results. Rows are not backed by the collection
model, so the drag is built here from the path each row carries, with
the same content and pixmap as a drag from the tree.
*/
class SearchResultsView : public QListView
{
public:
using QListView::QListView;
protected:
void startDrag(Qt::DropActions supportedActions) override
{
const QString path =
currentIndex().data(Qt::UserRole + 2).toString();
if (path.isEmpty()) {
QListView::startDrag(supportedActions);
return;
}
ElementsTreeView::execElementDrag(this, ElementsLocation(path));
}
};
}
/** /**
@brief ElementsCollectionWidget::ElementsCollectionWidget @brief ElementsCollectionWidget::ElementsCollectionWidget
Default constructor. Default constructor.
@@ -217,8 +249,26 @@ void ElementsCollectionWidget::setUpWidget()
m_tab_widget->addTab(m_tree_view, tr("Collections")); m_tab_widget->addTab(m_tree_view, tr("Collections"));
m_tab_widget->addTab(m_macros_tree_view, tr("Modèles")); m_tab_widget->addTab(m_macros_tree_view, tr("Modèles"));
//Flat ranked search results.
//The tree search hides non-matching rows, so hits stay scattered
//through five levels of expanded folders -- searching "diode" leaves
//you scrolling a tree to find them. This shows the same matches as a
//ranked list instead, and takes the tab widget's place while a search
//is active.
m_search_model = new QStandardItemModel(this);
m_search_results = new SearchResultsView(this);
m_search_results->setModel(m_search_model);
m_search_results->setDragDropMode(QAbstractItemView::DragOnly);
m_search_results->setIconSize(QSize(50, 50));
m_search_results->setUniformItemSizes(false);
m_search_results->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
m_search_results->setContextMenuPolicy(Qt::CustomContextMenu);
m_search_results->setMouseTracking(true);
m_search_results->hide();
m_main_vlayout->addWidget(m_search_field); m_main_vlayout->addWidget(m_search_field);
m_main_vlayout->addWidget(m_tab_widget); m_main_vlayout->addWidget(m_tab_widget);
m_main_vlayout->addWidget(m_search_results);
m_progress_bar = new QProgressBar(this); m_progress_bar = new QProgressBar(this);
m_progress_bar->setFormat(QObject::tr("chargement %p% (%v sur %m)")); m_progress_bar->setFormat(QObject::tr("chargement %p% (%v sur %m)"));
@@ -266,15 +316,7 @@ void ElementsCollectionWidget::setUpConnection()
this, &ElementsCollectionWidget::dirProperties); this, &ElementsCollectionWidget::dirProperties);
connect(m_tree_view, &QTreeView::doubleClicked, connect(m_tree_view, &QTreeView::doubleClicked,
[this](const QModelIndex &index) [this](const QModelIndex &index) { this->activateIndex(index); });
{
this->m_index_at_context_menu = index ;
ElementCollectionItem *eci = elementCollectionItemForIndex(index);
if (eci && eci->collectionPath().endsWith(".qetmak")) {
return; // Do nothing on double click for macros
}
this->editElement();
});
connect(m_tree_view, &QTreeView::entered, connect(m_tree_view, &QTreeView::entered,
[this] (const QModelIndex &index) { [this] (const QModelIndex &index) {
@@ -284,19 +326,58 @@ void ElementsCollectionWidget::setUpConnection()
qde->statusBar()->showMessage(eci->localName()); qde->statusBar()->showMessage(eci->localName());
}); });
//Enter on the highlighted item does the same as a double click, so a
//run of elements can be placed without leaving the keyboard. Bound as
//a shortcut on the view rather than by reimplementing keyPressEvent,
//which would mean subclassing ElementsTreeView for one key.
for (const auto key : {Qt::Key_Return, Qt::Key_Enter}) {
auto *sc = new QShortcut(QKeySequence(key), m_tree_view);
sc->setContext(Qt::WidgetShortcut);
connect(sc, &QShortcut::activated, this, [this]() {
this->activateIndex(m_tree_view->currentIndex());
});
}
//The flat results list carries the collection path directly, so it
//does not go through activateIndex() -- there is no tree index behind
//a row to look an ElementCollectionItem up from.
auto place_from_results = [this](const QModelIndex &index) {
const QString path = index.data(Qt::UserRole + 2).toString();
if (path.isEmpty()) {
return;
}
ElementsLocation location(path);
if (location.exist()) {
emit insertElementRequested(location);
}
};
connect(m_search_results, &QListView::doubleClicked, this, place_from_results);
for (const auto key : {Qt::Key_Return, Qt::Key_Enter}) {
auto *sc = new QShortcut(QKeySequence(key), m_search_results);
sc->setContext(Qt::WidgetShortcut);
connect(sc, &QShortcut::activated, this, [this, place_from_results]() {
place_from_results(m_search_results->currentIndex());
});
}
//Down from the search field moves into the results, so the whole
//type-then-place run happens without touching the mouse.
auto *to_results = new QShortcut(QKeySequence(Qt::Key_Down), m_search_field);
to_results->setContext(Qt::WidgetShortcut);
connect(to_results, &QShortcut::activated, this, [this]() {
if (!m_search_results->isVisible() || !m_search_model->rowCount()) {
return;
}
m_search_results->setFocus();
if (!m_search_results->currentIndex().isValid()) {
m_search_results->setCurrentIndex(m_search_model->index(0, 0));
}
});
connect(m_macros_tree_view, &QTreeView::customContextMenuRequested, connect(m_macros_tree_view, &QTreeView::customContextMenuRequested,
this, &ElementsCollectionWidget::customContextMenu); this, &ElementsCollectionWidget::customContextMenu);
connect(m_macros_tree_view, &QTreeView::doubleClicked, connect(m_macros_tree_view, &QTreeView::doubleClicked,
[this](const QModelIndex &index) [this](const QModelIndex &index) { this->activateIndex(index); });
{
this->m_index_at_context_menu = index ;
ElementCollectionItem *eci = elementCollectionItemForIndex(index);
if (eci && eci->collectionPath().endsWith(".qetmak")) {
return; // Do nothing on double click for macros
}
this->editElement();
});
connect(m_macros_tree_view, &QTreeView::entered, connect(m_macros_tree_view, &QTreeView::entered,
[this] (const QModelIndex &index) { [this] (const QModelIndex &index) {
@@ -405,6 +486,60 @@ void ElementsCollectionWidget::openDir()
} }
/**
@brief ElementsCollectionWidget::activateIndex
What a double click (or Enter) on @a index does.
Historically this opened the element editor, which is the slowest action
available on a symbol you are most likely about to place. Placing is now
the default and editing has moved to the context menu, where it already
was. The old behaviour is preserved behind a preference for anyone who
relies on it.
@param index
*/
void ElementsCollectionWidget::activateIndex(const QModelIndex &index)
{
m_index_at_context_menu = index;
ElementCollectionItem *eci = elementCollectionItemForIndex(index);
if (!eci) {
return;
}
//Macros are placed, never edited, and were already skipped here.
const bool is_macro = eci->collectionPath().endsWith(".qetmak");
QSettings settings;
const bool insert = settings.value(
QStringLiteral("elementscollection/double-click-inserts"), true).toBool();
if (insert) {
insertCurrentElement();
return;
}
if (!is_macro) {
editElement();
}
}
/**
@brief ElementsCollectionWidget::insertCurrentElement
Ask for the current item to be placed on the folio.
*/
void ElementsCollectionWidget::insertCurrentElement()
{
ElementCollectionItem *eci =
elementCollectionItemForIndex(m_index_at_context_menu);
if (!(eci && eci->isElement())) {
return;
}
ElementsLocation location(eci->collectionPath());
if (!location.exist()) {
return;
}
emit insertElementRequested(location);
}
/** /**
@brief ElementsCollectionWidget::editElement @brief ElementsCollectionWidget::editElement
Edit the element represented by the current selected item Edit the element represented by the current selected item
@@ -415,10 +550,19 @@ void ElementsCollectionWidget::editElement()
if ( !(eci && eci->isElement()) ) return; if ( !(eci && eci->isElement()) ) return;
// Prevent the element editor from opening for macros editLocation(ElementsLocation(eci->collectionPath()));
if (eci->collectionPath().endsWith(".qetmak")) return; }
ElementsLocation location(eci->collectionPath()); /**
@brief ElementsCollectionWidget::editLocation
Open the element editor on @a location. Macros have no editor and are
ignored.
@param location
*/
void ElementsCollectionWidget::editLocation(const ElementsLocation &location)
{
// Prevent the element editor from opening for macros
if (!location.exist() || location.path().endsWith(".qetmak")) return;
QETApp *app = QETApp::instance(); QETApp *app = QETApp::instance();
app->openElementLocations(QList<ElementsLocation>() << location); app->openElementLocations(QList<ElementsLocation>() << location);
@@ -932,6 +1076,7 @@ void ElementsCollectionWidget::search()
//Reset the search //Reset the search
if (text.isEmpty()) if (text.isEmpty())
{ {
clearFlatResults();
QModelIndex current_index = m_tree_view->currentIndex(); QModelIndex current_index = m_tree_view->currentIndex();
m_tree_view->reset(); m_tree_view->reset();
@@ -958,10 +1103,20 @@ void ElementsCollectionWidget::search()
return; return;
} }
QSettings settings;
if (settings.value(QStringLiteral("elementscollection/search-flat-list"),
true).toBool())
{
showFlatResults(rankedSearch(text, m_showed_index));
return;
}
//Tree search: hide what does not match, expand what does.
clearFlatResults();
hideCollection(true); hideCollection(true);
const QStringList text_list = text.split("+", Qt::SkipEmptyParts); const QStringList text_list = text.split("+", Qt::SkipEmptyParts);
QModelIndexList match_index; QModelIndexList match_index;
for (QString txt : text_list) { for (const QString &txt : text_list) {
match_index << m_model->match(m_showed_index.isValid() match_index << m_model->match(m_showed_index.isValid()
? m_model->index(0,0,m_showed_index) ? m_model->index(0,0,m_showed_index)
: m_model->index(0,0), : m_model->index(0,0),
@@ -972,10 +1127,157 @@ void ElementsCollectionWidget::search()
| Qt::MatchRecursive); | Qt::MatchRecursive);
} }
for(QModelIndex index : match_index) for (const QModelIndex &index : std::as_const(match_index))
showAndExpandItem(index); showAndExpandItem(index);
} }
/**
@brief ElementsCollectionWidget::rankedSearch
Run the collection search for @a text and return the element hits, best
first. The hits carry no model index, so a list that is not backed by the
collection model can show them too.
@param text : search text, "+" separating terms
@param within : restrict the search to this item's children, or the
whole collection when invalid
@return ranked hits, best first
*/
QVector<ElementSearchHit> ElementsCollectionWidget::rankedSearch(
const QString &text,
const QModelIndex &within)
{
QVector<ElementSearchHit> hits;
if (!m_model || text.size() < 3) {
return hits;
}
const QStringList terms = text.split("+", Qt::SkipEmptyParts);
QModelIndexList matches;
for (const QString &term : terms) {
matches << m_model->match(within.isValid()
? m_model->index(0, 0, within)
: m_model->index(0, 0),
Qt::UserRole+1,
QVariant(term),
-1,
Qt::MatchContains
| Qt::MatchRecursive);
}
const QString needle = terms.value(0).toLower();
QVector<QPair<int, ElementSearchHit>> scored;
QSet<QString> seen;
for (const QModelIndex &index : std::as_const(matches))
{
ElementCollectionItem *eci = elementCollectionItemForIndex(index);
if (!(eci && eci->isElement())) {
continue;
}
const QString path = eci->collectionPath();
//match() is run once per "+"-separated term, so the same element
//can arrive several times.
if (path.isEmpty() || seen.contains(path)) {
continue;
}
seen.insert(path);
ElementSearchHit hit;
hit.path = path;
hit.name = index.data(Qt::DisplayRole).toString();
hit.icon = qvariant_cast<QIcon>(index.data(Qt::DecorationRole));
//Where it lives, so two similarly-named symbols are tellable apart
QStringList parts;
for (QModelIndex p = index.parent(); p.isValid(); p = p.parent()) {
parts.prepend(p.data(Qt::DisplayRole).toString());
}
hit.folder = parts.join(QStringLiteral(" / "));
const QString hay = index.data(Qt::UserRole + 1).toString().toLower();
scored.append({rankMatch(needle, hit.name, hay), hit});
}
std::stable_sort(scored.begin(), scored.end(),
[](const QPair<int, ElementSearchHit> &a,
const QPair<int, ElementSearchHit> &b) {
return a.first > b.first;
});
hits.reserve(scored.size());
for (const auto &p : std::as_const(scored)) {
hits.append(p.second);
}
return hits;
}
/**
@brief ElementsCollectionWidget::rankMatch
Score a hit so the list can be ordered by how well it matches.
The tree search treats every hit equally, which is fine when they stay in
place but useless in a ranked list: typing "diode" should not put an
element whose *description* mentions diodes above one actually called
"Diode". Name beats element-info field, earlier beats later, shorter beats
longer.
@param needle : lower-cased search text
@param name : the element's display name
@param haystack : the full indexed string (name + every info field)
@return a score, higher is better
*/
int ElementsCollectionWidget::rankMatch(const QString &needle,
const QString &name,
const QString &haystack)
{
const QString n = name.toLower();
int score = 0;
if (n == needle) {
score = 1000;
} else if (n.startsWith(needle)) {
score = 800;
} else if (n.contains(needle)) {
score = 600 - qMin(n.indexOf(needle), 99);
} else if (haystack.contains(needle)) {
//Matched only on an info field -- manufacturer, reference,
//description. Still a real hit, just a weaker one.
score = 300;
}
//Among equally-placed hits prefer the shorter name: "Diode" over
//"Diode Zener bidirectional".
return score - qMin(n.size(), 99);
}
/**
@brief ElementsCollectionWidget::showFlatResults
Replace the tree with a flat list of @a hits, in the order given.
@param hits : from rankedSearch()
*/
void ElementsCollectionWidget::showFlatResults(const QVector<ElementSearchHit> &hits)
{
m_search_model->clear();
for (const ElementSearchHit &hit : hits)
{
auto *item = new QStandardItem(hit.icon, hit.name);
item->setEditable(false);
item->setToolTip(hit.folder);
item->setData(hit.path, Qt::UserRole + 2);
m_search_model->appendRow(item);
}
m_tab_widget->hide();
m_search_results->show();
}
/**
@brief ElementsCollectionWidget::clearFlatResults
Put the tree back when the search field is emptied.
*/
void ElementsCollectionWidget::clearFlatResults()
{
m_search_model->clear();
m_search_results->hide();
m_tab_widget->show();
}
/** /**
@brief ElementsCollectionWidget::hideCollection @brief ElementsCollectionWidget::hideCollection
Hide all collection displayed in this tree Hide all collection displayed in this tree
@@ -43,6 +43,22 @@ class ElementsTreeView;
and all action needed to use this widget. and all action needed to use this widget.
This is the element collection widget used in the diagram editor. This is the element collection widget used in the diagram editor.
*/ */
class QListView;
class QStandardItemModel;
/**
@brief One ranked hit from a collection search.
Carries everything a result row needs, so a list that is not backed by
the collection model can show it.
*/
struct ElementSearchHit
{
QString path; ///< collection path, enough to build an ElementsLocation
QString name; ///< display name
QString folder; ///< where it lives, for telling similar names apart
QIcon icon;
};
class ElementsCollectionWidget : public QWidget class ElementsCollectionWidget : public QWidget
{ {
Q_OBJECT Q_OBJECT
@@ -56,6 +72,8 @@ class ElementsCollectionWidget : public QWidget
void removeProject (QETProject *project); void removeProject (QETProject *project);
void highlightUnusedElement(); void highlightUnusedElement();
void setCurrentLocation(const ElementsLocation &location); void setCurrentLocation(const ElementsLocation &location);
QVector<ElementSearchHit> rankedSearch(const QString &text,
const QModelIndex &within = QModelIndex());
protected: protected:
void leaveEvent(QEvent *event) override; void leaveEvent(QEvent *event) override;
@@ -86,9 +104,26 @@ class ElementsCollectionWidget : public QWidget
public slots: public slots:
void reload(); void reload();
void loadingFinished(); void loadingFinished();
void insertCurrentElement();
void editLocation(const ElementsLocation &location);
signals:
/**
Emitted when the user asks for an element to be placed on the
current folio. Whoever hosts this widget decides which view
receives it -- the dock is inside a diagram editor, but the
picker popup is not, so the widget must not reach for an
ancestor editor itself.
*/
void insertElementRequested(const ElementsLocation &location);
private: private:
void locationWasSaved(const ElementsLocation& location); void locationWasSaved(const ElementsLocation& location);
void activateIndex(const QModelIndex &index);
void showFlatResults(const QVector<ElementSearchHit> &hits);
void clearFlatResults();
static int rankMatch(const QString &needle, const QString &name,
const QString &haystack);
private: private:
@@ -100,6 +135,9 @@ class ElementsCollectionWidget : public QWidget
ElementsTreeView *m_tree_view; ElementsTreeView *m_tree_view;
ElementsTreeView *m_macros_tree_view = nullptr; ElementsTreeView *m_macros_tree_view = nullptr;
QTabWidget *m_tab_widget = nullptr; QTabWidget *m_tab_widget = nullptr;
/// Flat ranked results, shown in place of the tree while searching
QListView *m_search_results = nullptr;
QStandardItemModel *m_search_model = nullptr;
QVBoxLayout *m_main_vlayout; QVBoxLayout *m_main_vlayout;
QMenu *m_context_menu; QMenu *m_context_menu;
QModelIndex m_index_at_context_menu; QModelIndex m_index_at_context_menu;
@@ -653,9 +653,16 @@ QDomElement ElementsLocation::xml() const
if (!m_project) if (!m_project)
{ {
QFile file (m_file_system_path); QFile file (m_file_system_path);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return QDomElement();
QDomDocument docu; QDomDocument docu;
if (docu.setContent(&file)) if (docu.setContent(&file))
{
file.close();
return docu.documentElement(); return docu.documentElement();
}
file.close();
} }
else else
{ {
@@ -82,13 +82,27 @@ void ElementsTreeView::startDrag(Qt::DropActions supportedActions)
@param location : location to use for create the content of the QDrag @param location : location to use for create the content of the QDrag
*/ */
void ElementsTreeView::startElementDrag(const ElementsLocation &location) void ElementsTreeView::startElementDrag(const ElementsLocation &location)
{
execElementDrag(this, location);
}
/**
@brief ElementsTreeView::execElementDrag
Build and run the QDrag for @a location, from @a source.
Static so that a view which is not an ElementsTreeView -- the flat list
of search results -- starts exactly the same drag as the tree.
@param source : the widget the drag starts from
@param location : location to use for create the content of the QDrag
*/
void ElementsTreeView::execElementDrag(QWidget *source,
const ElementsLocation &location)
{ {
if (! location.exist()) return; if (! location.exist()) return;
#if QT_VERSION < QT_VERSION_CHECK(6, 2, 0) #if QT_VERSION < QT_VERSION_CHECK(6, 2, 0)
QDrag* drag = new QDrag(this); QDrag* drag = new QDrag(source);
#else #else
QScopedPointer<QDrag> drag(new QDrag(this)); QScopedPointer<QDrag> drag(new QDrag(source));
#endif #endif
QString location_str = location.toString(); QString location_str = location.toString();
@@ -209,7 +223,7 @@ void ElementsTreeView::startElementDrag(const ElementsLocation &location)
&elmt_creation_state)); &elmt_creation_state));
if (elmt_creation_state) { return; } if (elmt_creation_state) { return; }
QPixmap elmt_pixmap(QET::Palette::forPalette(temp_elmt->pixmap(), palette())); QPixmap elmt_pixmap(QET::Palette::forPalette(temp_elmt->pixmap(), source->palette()));
QPoint elmt_hotspot(temp_elmt->hotspot()); QPoint elmt_hotspot(temp_elmt->hotspot());
//Adjust the size of the pixmap if he is too big //Adjust the size of the pixmap if he is too big
@@ -32,6 +32,8 @@ class ElementsTreeView : public QTreeView
{ {
public: public:
ElementsTreeView(QWidget *parent = nullptr); ElementsTreeView(QWidget *parent = nullptr);
static void execElementDrag(QWidget *source,
const ElementsLocation &location);
protected: protected:
void startDrag(Qt::DropActions supportedActions) override; void startDrag(Qt::DropActions supportedActions) override;
@@ -162,7 +162,7 @@ QString FileElementCollectionItem::localName()
else if (m_path == QETApp::customElementsDirN()) else if (m_path == QETApp::customElementsDirN())
setText(QObject::tr("Collection utilisateur")); setText(QObject::tr("Collection utilisateur"));
else if (m_path == macrosPath) else if (m_path == macrosPath)
setText(QObject::tr("Makros")); setText(QObject::tr("Macros"));
else else
setText(QObject::tr("Collection inconnue")); setText(QObject::tr("Collection inconnue"));
} }
@@ -21,6 +21,8 @@
#include "../qetproject.h" #include "../qetproject.h"
#include "xmlelementcollection.h" #include "xmlelementcollection.h"
#include <QFileInfo>
/** /**
@brief XmlProjectElementCollectionItem::XmlProjectElementCollectionItem @brief XmlProjectElementCollectionItem::XmlProjectElementCollectionItem
Constructor Constructor
@@ -58,10 +60,7 @@ QString XmlProjectElementCollectionItem::localName()
return text(); return text();
if (isCollectionRoot()) { if (isCollectionRoot()) {
if (m_project->title().isEmpty()) updateProjectName();
setText(QObject::tr("Projet sans titre"));
else
setText(m_project->title());
} }
else { else {
ElementsLocation location (embeddedPath(), m_project); ElementsLocation location (embeddedPath(), m_project);
@@ -71,6 +70,25 @@ QString XmlProjectElementCollectionItem::localName()
return text(); return text();
} }
/**
@brief XmlProjectElementCollectionItem::updateProjectName
Set the displayed name of the collection root from the project :
its title, or its file name when it has no title, like the project panel.
Does nothing if this item is not the root of the collection.
*/
void XmlProjectElementCollectionItem::updateProjectName()
{
if (!isCollectionRoot() || !m_project)
return;
if (!m_project->title().isEmpty())
setText(m_project->title());
else if (!m_project->filePath().isEmpty())
setText(QFileInfo(m_project->filePath()).completeBaseName());
else
setText(QObject::tr("Projet sans titre"));
}
/** /**
@brief XmlProjectElementCollectionItem::name @brief XmlProjectElementCollectionItem::name
@return The collection name of this item @return The collection name of this item
@@ -45,6 +45,7 @@ class XmlProjectElementCollectionItem : public ElementCollectionItem
bool isCollectionRoot() const override; bool isCollectionRoot() const override;
void addChildAtPath(const QString &collection_name) override; void addChildAtPath(const QString &collection_name) override;
QETProject * project() const; QETProject * project() const;
void updateProjectName();
void setProject (QETProject *project, void setProject (QETProject *project,
bool set_data = true, bool set_data = true,
@@ -46,10 +46,11 @@ FreeTerminalEditor::FreeTerminalEditor(QETProject *project, QWidget *parent) :
connect(m_project, &QObject::destroyed, this, &FreeTerminalEditor::reload); connect(m_project, &QObject::destroyed, this, &FreeTerminalEditor::reload);
} }
//Disabled the move if the table is currently edited (yellow cell) //Disable the move button while cells are edited (yellow), re-evaluate on every change
connect(m_model, &FreeTerminalModel::dataChanged, this, [=] { connect(m_model, &FreeTerminalModel::dataChanged, this, &FreeTerminalEditor::selectionChanged);
this->setDisabledMove();
}); connect(ui->m_table_view->selectionModel(), &QItemSelectionModel::selectionChanged,
this, &FreeTerminalEditor::selectionChanged);
connect(ui->m_table_view, &QAbstractItemView::doubleClicked, this, [=](const QModelIndex &index) connect(ui->m_table_view, &QAbstractItemView::doubleClicked, this, [=](const QModelIndex &index)
{ {
@@ -102,7 +103,7 @@ void FreeTerminalEditor::reload()
QString str(strip->installation() + " " + strip->location() + " " + strip->name()); QString str(strip->installation() + " " + strip->location() + " " + strip->name());
ui->m_move_in_cb->addItem(str, strip->uuid()); ui->m_move_in_cb->addItem(str, strip->uuid());
} }
setDisabledMove(false); selectionChanged();
} }
} }
@@ -273,10 +274,19 @@ void FreeTerminalEditor::on_m_move_pb_clicked()
reload(); reload();
} }
void FreeTerminalEditor::selectionChanged()
{
const bool has_selection = !ui->m_table_view->selectionModel()->selectedIndexes().isEmpty();
const bool has_pending = !m_model->modifiedModelRealTerminalData().isEmpty();
setDisabledMove(!has_selection || has_pending);
}
void FreeTerminalEditor::setDisabledMove(bool b) void FreeTerminalEditor::setDisabledMove(bool b)
{ {
ui->m_move_label->setDisabled(b); ui->m_move_label->setDisabled(b);
ui->m_move_in_cb->setDisabled(b); ui->m_move_in_cb->setDisabled(b);
ui->m_move_pb->setDisabled(b); ui->m_move_pb->setDisabled(b);
ui->m_move_pb->setToolTip(b ? tr("Appliquez ou annulez les modifications en cours avant de déplacer")
: tr("Déplacer les bornes sélectionnées vers le bornier choisi"));
} }
@@ -182,7 +182,7 @@ bool FreeTerminalModel::setData(const QModelIndex &index, const QVariant &value,
modified_ = true; modified_ = true;
modified_cell = FUNCTION_CELL; modified_cell = FUNCTION_CELL;
} }
else if (column_ == LED_CELL) else if (column_ == LED_CELL && mrtd.led_ != value.toBool())
{ {
mrtd.led_ = value.toBool(); mrtd.led_ = value.toBool();
modified_ = true; modified_ = true;
+49
View File
@@ -0,0 +1,49 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef ALIGNMENT_H
#define ALIGNMENT_H
#include "textgrid.h"
#include <QPointF>
/**
The geometry behind the align commands, kept free of any scene so it
can be tested on its own.
*/
namespace Alignment
{
/**
@return the movement that puts p on a grid of x_grid by y_grid,
divided by divisor as TextGrid::snap() does, or a null point when
p is already on it. Unlike Diagram::snapToGrid(), this never looks
at the keyboard: a command run from a shortcut with Ctrl in it must
not quietly round to the pixel instead.
Less than a millionth of a pixel counts as on the grid: positions
that went through arithmetic carry residues of that size, and
qFuzzyIsNull() (1e-12) is too strict to absorb them.
*/
inline QPointF gridOffset(const QPointF &p, int x_grid, int y_grid, qreal divisor = 1)
{
const QPointF offset = TextGrid::snap(p, x_grid, y_grid, divisor) - p;
auto clean = [](qreal v) { return qAbs(v) < 1e-6 ? 0.0 : v; };
return QPointF(clean(offset.x()), clean(offset.y()));
}
}
#endif // ALIGNMENT_H
@@ -0,0 +1,78 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "renumberelementscommand.h"
#include "../qetproject.h"
#include "../qetgraphicsitem/element.h"
RenumberElementsCommand::RenumberElementsCommand(
QETProject *project,
QVector<ElementChange> changes,
QHash<QString, NumerotationContext> old_ctx,
QHash<QString, NumerotationContext> new_ctx,
const QString &text)
: QUndoCommand(text)
, project_(project)
, changes_(std::move(changes))
, old_ctx_(std::move(old_ctx))
, new_ctx_(std::move(new_ctx))
{
}
void RenumberElementsCommand::undo()
{
apply(false);
}
void RenumberElementsCommand::redo()
{
// QUndoStack calls redo() once immediately after push(); keep standard behaviour.
if (first_redo_) {
first_redo_ = false;
}
apply(true);
}
void RenumberElementsCommand::apply(bool use_new)
{
if (!project_) return;
// Restore project contexts
const auto &ctx = use_new ? new_ctx_ : old_ctx_;
for (auto it = ctx.constBegin(); it != ctx.constEnd(); ++it) {
project_->addElementAutoNum(it.key(), it.value());
}
// Apply per-element changes
for (const ElementChange &c : changes_) {
if (!c.element) continue;
const bool frozen = use_new ? c.new_frozen : c.old_frozen;
const auto &infos = use_new ? c.new_infos : c.old_infos;
const auto &seq = use_new ? c.new_seq : c.old_seq;
// Temporarily unfreeze so that label/infos update correctly.
const bool was_frozen = c.element->isFreezeLabel();
if (was_frozen) c.element->freezeLabel(false);
c.element->rSequenceStruct() = seq;
c.element->setElementInformations(infos);
c.element->freezeLabel(frozen);
}
}
+70
View File
@@ -0,0 +1,70 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef RENUMBERELEMENTSCOMMAND_H
#define RENUMBERELEMENTSCOMMAND_H
#include <QUndoCommand>
#include <QHash>
#include <QVector>
#include "../diagramcontext.h"
#include "assignvariables.h" // defines autonum::sequentialNumbers
class Element;
class QETProject;
/**
* @brief Undoable renumbering of element labels and sequence structs.
*/
class RenumberElementsCommand final : public QUndoCommand
{
public:
struct ElementChange
{
Element *element = nullptr;
DiagramContext old_infos;
DiagramContext new_infos;
autonum::sequentialNumbers old_seq;
autonum::sequentialNumbers new_seq;
bool old_frozen = false;
bool new_frozen = false;
};
RenumberElementsCommand(
QETProject *project,
QVector<ElementChange> changes,
QHash<QString, NumerotationContext> old_ctx,
QHash<QString, NumerotationContext> new_ctx,
const QString &text);
void undo() override;
void redo() override;
private:
void apply(bool use_new);
private:
QETProject *project_ = nullptr;
QVector<ElementChange> changes_;
QHash<QString, NumerotationContext> old_ctx_;
QHash<QString, NumerotationContext> new_ctx_;
bool first_redo_ = true;
};
#endif // RENUMBERELEMENTSCOMMAND_H
@@ -23,6 +23,7 @@
#include "formulaautonumberingw.h" #include "formulaautonumberingw.h"
#include "numparteditorw.h" #include "numparteditorw.h"
#include "qdebug.h" #include "qdebug.h"
#include "renumberelementsdialog.h"
#include "ui_autonumberingmanagementw.h" #include "ui_autonumberingmanagementw.h"
#include "ui_formulaautonumberingw.h" #include "ui_formulaautonumberingw.h"
@@ -48,6 +49,8 @@ AutoNumberingManagementW::AutoNumberingManagementW(QETProject *project,
ui->m_selected_folios_le->setDisabled(true); ui->m_selected_folios_le->setDisabled(true);
ui->m_selected_folios_le->setReadOnly(true); ui->m_selected_folios_le->setReadOnly(true);
ui->m_apply_project_rb->setChecked(true); ui->m_apply_project_rb->setChecked(true);
// Enabled only when project is in "Under Development" status and not read-only.
ui->m_renumber_elements_pb->setEnabled(ui->m_status_cb->currentIndex() == 0 && project_ && !project_->isReadOnly());
setProjectContext(); setProjectContext();
} }
@@ -87,6 +90,7 @@ void AutoNumberingManagementW::on_m_status_cb_currentIndexChanged(int index)
ui->m_both_conductor_rb->setChecked(true); ui->m_both_conductor_rb->setChecked(true);
ui->m_both_element_rb->setChecked(true); ui->m_both_element_rb->setChecked(true);
ui->m_both_folio_rb->setChecked(true); ui->m_both_folio_rb->setChecked(true);
ui->m_renumber_elements_pb->setEnabled(true);
} }
//Installing //Installing
else if (index == 1) { else if (index == 1) {
@@ -96,15 +100,31 @@ void AutoNumberingManagementW::on_m_status_cb_currentIndexChanged(int index)
ui->m_new_conductor_rb->setChecked(true); ui->m_new_conductor_rb->setChecked(true);
ui->m_new_element_rb->setChecked(true); ui->m_new_element_rb->setChecked(true);
ui->m_new_folio_rb->setChecked(true); ui->m_new_folio_rb->setChecked(true);
ui->m_renumber_elements_pb->setEnabled(true);
} }
//Built //Built
else if (index == 2) { else if (index == 2) {
ui->m_disable_conductor_rb->setChecked(true); ui->m_disable_conductor_rb->setChecked(true);
ui->m_disable_element_rb->setChecked(true); ui->m_disable_element_rb->setChecked(true);
ui->m_disable_folio_rb->setChecked(true); ui->m_disable_folio_rb->setChecked(true);
ui->m_renumber_elements_pb->setEnabled(false);
} }
} }
void AutoNumberingManagementW::on_m_renumber_elements_pb_clicked()
{
if (!project_ || project_->isReadOnly()) return;
// Only allowed during "Under Development"
// if (ui->m_status_cb->currentIndex() != 1) return;
QStringList titles = project_->elementAutoNum().keys();
titles.sort(Qt::CaseInsensitive);
RenumberElementsDialog dlg(titles, this);
if (dlg.exec() != QDialog::Accepted) return;
project_->renumberElementsBySchemeTitle(dlg.selectedSchemeTitle());
}
/** /**
@brief AutoNumberingManagementW::on_m_apply_folios_rb_clicked @brief AutoNumberingManagementW::on_m_apply_folios_rb_clicked
Set From Folios Combobox Set From Folios Combobox

Some files were not shown because too many files have changed in this diff Show More