Compare commits

..

78 Commits

Author SHA1 Message Date
Laurent Trinques d7052e396b Merge pull request #591 from ispyisail/feature-dynamictext-drag-resize
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m32s
Add drag-to-resize for dynamic element text width (#577 phase 1)
2026-09-21 12:19:22 +02:00
Laurent Trinques e123c55754 Merge pull request #969 from ispyisail/feature/qet-mcp-server
Add misc/qet-mcp: a Model Context Protocol server over QET projects
2026-09-21 12:00:34 +02:00
Laurent Trinques 268aba60eb Merge pull request #970 from ispyisail/feature/scripting-draw-api
Scripting API: draw, cross-reference and query a project
2026-09-21 11:57:48 +02:00
Laurent Trinques 313f9533a8 Merge pull request #967 from Kellermorph/fix-conductor-style
fix: inherit conductor line style (pen style) when linking cross-references
2026-09-21 11:55:05 +02:00
Laurent Trinques 3d1cb671c1 Merge pull request #966 from Kellermorph/place-makro-fix
fix: correct macro placement position mismatch
2026-09-21 09:21:34 +02:00
Laurent Trinques c0fc093b4e Merge pull request #968 from Kellermorph/background-drawing-selection
Add diagram background color picker with adaptive border/titleblock
2026-09-21 09:20:31 +02:00
ispyisail 46c35d2297 Let a script query the project database
Every structural question this API could answer, it answered by walking
live objects. The project builds a SQLite database that already knows
most of them, and nothing outside the application could reach it.

  tables()      what is queryable, tables and views
  query(sql)    rows, one object per row
  queryError()  why the last one returned nothing

This is not a new door. QElectroTech already ships a "Requête SQL
personnalisée" box in the element-query dialog where a user types
arbitrary SQL, guarded by projectDataBase::isReadOnlySelect(); query()
goes through projectDataBase::newQuery(), which applies that same rule
and returns the same rejection message. A script gets what a user
already has, and neither can write: DELETE, UPDATE and a chained
"SELECT 1; DROP TABLE" are all refused before reaching SQLite.

An empty result and a failure are told apart. query() returns no rows
for both, so queryError() carries the reason -- a refusal, or SQLite's
own message for a bad column -- and is empty when the query simply
matched nothing. Conflating those is how a silent typo in a column name
becomes "there are no such elements".

No updateDB() before querying, and that is a measured decision rather
than an omission. A script that has just edited something is the
expected caller, so a stale cache was the obvious hazard; but
projectDataBase maintains itself incrementally through addElement(),
elementInfoChanged(), addConductor() and the rest, which the undo
commands behind every edit already call. Tested both ways on the cases
most likely to go stale -- an element added and labelled, a conductor
property changed -- each queried immediately afterwards through both the
table and the view. Identical counts with the rebuild and without it,
and updateDB() repopulates every table, so calling it per query would
have been real cost for no benefit. The comment says so, so it is not
added back on the assumption it must be needed.

The views are the surface to depend on: element_nomenclature_view,
project_summary_view and wiring_list_view exist to be queried. The
tables are how the cache is arranged today and a column may move --
which is why tables() lists both and the header says which is which.

Verified against examples/industrial.qet, the largest shipped project:
618 elements counted, the busiest wire numbers ranked (0VDC 93 times,
24V2 64), and duplicate element labels found by GROUP BY ... HAVING --
V6 seven times, V5 six -- which is a design-rule question no tool here
could previously ask. Qt 6.10.2, ctest matches master.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 19:14:53 +12:00
ispyisail 2adc58c1c0 Let a script add the text and shapes a folio carries
The drawing furniture beside the circuit: a free-standing note, a line,
a rectangle, an ellipse, a polygon.

  texts()  addText()  setTextContent()  setTextColor()
           setTextRotation()  deleteText()
  shapes() addShape()  deleteShape()

Added with the same AddGraphicsObjectCommand the corresponding GUI tools
use, and changed through the plainText/color/rotation properties those
items already publish, so a script's note undoes like a hand-placed one.

These are addressed by index into a listing sorted by position, reading
order, because they have no better identity: unlike an element they carry
no uuid, and unlike a conductor they have no terminal to be named by.
Position is what they have and it persists, so the ordering survives a
save and reload -- verified by listing before and after, including a
rotated text whose bounding box moves. It does not survive adding or
deleting one: indexes after that point shift the way a list's do, which
is why texts() and shapes() exist rather than a caller keeping a handle.

The sort is on sceneBoundingRect(), not pos(). A QetShapeItem keeps its
geometry in its line/rect/polygon and leaves pos() at the origin, so
sorting on pos() put three shapes drawn in three different places all at
(0, 0) and made every shape index refer to whichever the set yielded
first -- which is what the first version of this did, and the test that
caught it was asking for three shapes and getting index 0 three times.

Path is deliberately not offered: it is built by successive clicks and
has no two-point form to give here.

Verified headlessly: three texts added bottom-up and listed in reading
order, edited, recoloured, rotated, one deleted; three shapes added,
listed with their real geometry, the middle one deleted and the right one
gone; unknown shape name, invalid colour and out-of-range index all
decline with a reason. Saved, reloaded, both listings identical.

Qt 6.10.2, build clean, ctest matches master, qet-lint clean on the
generated project, qet-coherence-check clean on the example corpus.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 19:00:04 +12:00
ispyisail 82262c5980 Let a script number a conductor and link a cross-reference
Two gaps left over from the drawing verbs. A script could create a
conductor but not say what it was -- no number, colour, section or
formula -- and could not link a master to its slave, although
LinkElementCommand has been there all along and nothing bound it.

  conductors()            what is on this folio, and how to address it
  conductorProperty()
  setConductorProperty()  num, formula, function, bus, cable,
                          tension_protocol, conductor_color,
                          conductor_section, color, text_color
  elementLinkType()       simple / master / slave / next_report / ...
  linkedElements()
  linkElements()          two folio indices: a master and its slave are
                          normally on different folios
  unlinkElement()

The property names are the ones the .qet file uses for the same fields,
so what a script sets is what a reader of the file sees rather than a
third spelling invented here.

A property is applied to every conductor of the same electrical
potential, not to the one conductor named. That is the rule the
application already follows -- SearchAndReplaceWorker pushes one
QPropertyUndoCommand per conductor of relatedPotentialConductors()
inside a macro -- because a wire number describes a potential, not one
drawn segment; setting it on one and leaving the rest of the potential
disagreeing would produce a file no GUI action could have produced.

Linking asks LinkElementCommand::isLinkable() rather than re-deriving
its rules, so a script cannot make a link the GUI would refuse: master
to master, a PLC master to a non-PLC slave, a next-report to another
next-report, or anything to an already-taken target.

A conductor is addressed as "the conductor on terminal i of element U".
It has no identity of its own to use instead: conductors carry no
persisted uuid, and the terminal1/terminal2 ids in the file are
folio-scoped integers QElectroTech renumbers on every save. Since the
change is potential-wide, any terminal of the potential names it equally
well, so in practice a potential is addressed from one of its leaves; a
terminal carrying several conductors names none of them and is refused
rather than guessed at.

Verified headlessly. Conductor: num, section and colour set from one end
of a potential and read back from the other, saved and reloaded, present
in the XML. Propagation shown to discriminate, which took two tries --
the first attempt wired A.0-B.0 and B.1-C.0 and saw no propagation,
correctly, because a coil's two terminals are opposite ends of the coil
and not one potential. Wiring a real hub at A.0 instead, a number set
via the B leaf appears on the C conductor too, in memory and in the
saved file. Cross-reference: a master on one folio linked to a slave on
another, linkedElements() agreeing from both ends, surviving save and
reload with link_uuid written on both folios; master-to-master,
self-link, unlink and relink all behave. Unknown property, invalid
colour, bare terminal and ambiguous terminal all decline with a reason.

Qt 6.10.2, build clean, ctest matches master, qet-coherence-check clean
on the example corpus, qet-lint clean on the generated projects.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 18:48:40 +12:00
ispyisail 6dcb6a2a8f Key a conductor by something that survives a save
qet_diff keyed each conductor on its raw terminal1/terminal2 pair, with
a comment claiming that pair was "stable within a folio". It is stable
within a folio; it is not stable across a save. QElectroTech reassigns
those folio-scoped integer ids on every write, in whatever order it
serialises the elements, so one untouched conductor of ArduinoLCD.qet
goes from terminal1="1" terminal2="16" to terminal1="34" terminal2="15".

Diffing a project against a re-saved copy of itself therefore reported
29 of its 47 conductors as removed and 29 as added, with nothing
changed. That is the main thing this tool is for, so the conductor half
of the answer was noise in exactly the case it was wanted.

The format has two addressing schemes and a file can hold both at once.
Older conductors use the integer ids with no element1/element2; current
ones use terminal uuids from the .elmt definition plus element1/element2
naming the placed instances. A terminal uuid alone is not an identity --
it belongs to the definition, so two coils of one type share it and a
conductor between them keys as a self-loop -- so an end is identified by
the (instance, terminal) pair, taken from the conductor where it carries
one and resolved through the folio's elements where it does not.

Where an element predates persisted uuids there is nothing stable to key
on. Keying those on terminal geometry alone collapsed nine distinct
conductors of schema_indus.qet onto a single key, which is worse than
the instability it was meant to fix, so such ends stay unresolved, keep
a "#"-marked key, and the diff reports unstable_keys and says in words
that added/removed may not mean what they look like.

Measured over the 24 shipped example projects, 3190 conductors: 0
colliding keys, against 8 for the geometry-only key. On a re-saved but
otherwise untouched project: 0 added, 0 removed, against 29 and 29
before this change. A project with two conductors genuinely added still
reports exactly two added and none removed, so the check still
discriminates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 18:38:46 +12:00
ispyisail c740cdf1ac Let a script wire, label and rotate, not only place
The scripting API (bugtracker #162) could place an element and move it,
and could count conductors but not make one. So a script could put a
coil and a motor on a folio and had no way to connect them, which is
most of what drawing is. This adds the missing verbs:

  addConductor()      wire terminal i of one element to terminal j of another
  rotateElement()
  setElementInfo()    any information key
  setElementLabel()   the label key, by name, since it is the one people want
  addFolio()
  setFolioTitle()
  elementUuids()      what is on this folio
  elementName()
  elementTerminals()  which terminal index is which, before wiring it

Each goes through the command the GUI already uses, so a script's edits
undo like manual ones and reach the project database the same way:
ConductorCreator (the drag-a-rectangle-over-terminals path, which is
what makes a new conductor inherit an existing potential's properties
and join auto-numbering), ChangeElementInformationCommand,
QETProject::addNewDiagram(), ChangeTitleBlockCommand. rotateElement()
pushes the same QPropertyUndoCommand on "rotation" that
RotateSelectionCommand pushes for an Element, rather than
RotateSelectionCommand itself, which works on the diagram's selection
and would mean rewriting the user's selection to rotate one element.

Terminals are addressed by index, not uuid. Terminal::uuid() is a
property of the catalog .elmt definition: empty for most of the
installed base, and where present, identical across every instance of
that element -- two coils of the same type placed side by side have
byte-identical terminal uuids, so a uuid cannot say which coil's A1 is
meant. elementTerminals() exists so a script can see the indexing
instead of guessing it.

The one real hazard is that ConductorCreator asks the user which
potential to inherit from when the two terminals sit on two different
existing ones, and it asks with a plain modal QDialog that
QET::QetMessageBox's non-interactive mode does not cover -- so under
headless --run there is nobody to answer and the call never returns.
Measured: with the check removed, that one call hangs until killed;
with it, it declines in 0.4 s. addConductor() therefore refuses that
case, the same way and for the same reason addElement() already refuses
the import-conflict dialog.

To make that check without duplicating the condition, existingPotential()
becomes static over an explicit terminal list and ConductorCreator gains
a public needsPotentialChoice() predicate. Behaviour of the GUI path is
unchanged; setUpPropertieToUse() passes m_terminals_list to the same code
it called before.

Verified headlessly against a copy of examples/ArduinoLCD.qet: new folio
titled, two coils placed, wired, labelled, an info key set and the
element rotated; saved, reloaded, and the conductor, label, title and
rotation (persisted as orientation="1") all read back. Re-saving the
result is byte-identical. qet-lint clean on the generated project;
qet-coherence-check clean on it and on the 24-project example corpus,
and shown to report 9 findings on a deliberately broken copy of the same
file, so the clean result discriminates. Qt 6.10.2, ctest identical to
master (the 61 failures are the vendored KDE ECM suite, present on both).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 17:57:29 +12:00
ispyisail aab0a18506 Add misc/qet-mcp: a Model Context Protocol server over QET projects
A small stdio MCP server that lets an assistant read a project, ask what
an edit actually changed, and sweep a corpus. Standard library only --
Python 3.9+, no third-party dependencies, and the MCP SDK is not
required. Nothing in the build or the application refers to it; it sits
in misc/ beside make_icon_themes.py and is inert unless run.

It exists because verifying a change by screenshot is unreliable, and
that unreliability produced two wrong conclusions in a single review
session. A drag of a multi-element selection looked like it had left the
symbols behind and detached their labels; diffing the saved file showed
all four elements had moved by an identical (0,-80) and no label had
moved at all. An Apply button looked like it did nothing; it was
disabled because a required field was empty. Both times the pixels
misled and the file told the truth, so these tools read the file.

Seven tools: qet_project_info, qet_elements, qet_conductors, qet_diff,
qet_scan, qet_element_info and qet_export. Only qet_export launches
QElectroTech; everything else parses the .qet or .elmt directly, which
needs no display and cannot be confused by a dialog.

Two behaviours of QElectroTech are carried inside the tool rather than
left for the caller to rediscover. SingleApplication keys its socket on
applicationFilePath(), so a second launch of the same path forwards its
request to a running instance and returns that process's answer with no
error; qet_export therefore copies the binary to a unique temporary
path, gives it a private HOME and runs it offscreen. A symlink would not
do, because applicationFilePath() resolves it back. And the CLI matches
its export flags by exact string (cli_export.cpp:828) with the project
and output as positional arguments (:862, :882), so --export-bom=out.csv
is not recognised as an export at all and the run starts the interface
and hangs headless; the tool uses the positional form.

Worth recording for anyone extending this: the project database would be
a better query surface than the XML, but it is not reachable from
outside the application. projectDataBase::newQuery() and
isReadOnlySelect() are C++-internal and the JavaScript scripting API
exposes no SQL binding. A --query CLI verb, or a scripting binding,
would let this expose the guarded read-only SELECT surface instead.

Verified against the shipped examples: qet_scan reports 3190 conductors
across the 24 example projects with no cable value, and qet_diff
reproduces the four-element move above from the two saved files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 17:33:17 +12:00
Kellermorph ebab58e4b3 Add diagram background color picker with adaptive border/titleblock
Replace the white/grey toggle (m_grey_background) with a full color
picker widget (DiagramBgColorToolButton) in the Affichage toolbar,
matching the existing ConductorColorToolButton UX:

- Preset colors: White, Off-white, Light grey, Grey, Dark grey, Black
- Recently used colors section
- "Autre couleur..." opens QColorDialog for any custom color
- "Couleur système" restores the default dark-mode inverted background

New features:
- Diagram::m_custom_background_color flag: when the user picks a
  custom color, PaletteGraphicsView skips lightness inversion so the
  chosen color is displayed as-is
- Border and titleblock text/lines automatically switch between black
  and white based on Diagram::background_color.lightness(), so a dark
  background always shows a visible light border and titleblock content
- "Couleur système" restores Qt::white + re-enables inversion

Files changed:
- New: sources/ui/diagrambgcolorbutton.h/.cpp
- sources/diagram.h/.cpp: added static m_custom_background_color flag
- sources/palettegraphicsview.cpp: skip inversion when custom bg active
- sources/bordertitleblock.cpp: adaptive border pen color
- sources/titleblocktemplate.cpp: adaptive ink color for cell borders/text
- sources/qetdiagrameditor.h/.cpp: replace toggle with new widget
- cmake/qet_compilation_vars.cmake: register new source files
2026-09-20 22:58:08 +02:00
Kellermorph 45ca53c7a8 fix: inherit conductor line style (pen style) when linking cross-references
ApplyForEqualAttributes() was missing the 'style' attribute, causing
dashed/dash-dotted line styles to be lost when potentials are merged
via folio reports. Only color and other properties were copied.

Add style copy in single-element case and equality check in
multi-element case, matching the existing pattern for other attributes.
2026-09-20 21:56:43 +02:00
Kellermorph 2efce1752d fix: correct macro placement position mismatch
Fix macro elements jumping to upper-left corner instead of being placed
at the correct drop position.

Root cause: The preview offset used itemsBoundingRect() (all items
including children), while Diagram::fromXml() computed its translation
offset from top-level items only. This mismatch caused fromXml to
translate elements to the wrong position.

Changes:
- Compute top-level-only bounding rect in dummy diagram constructor
  to get the correct m_items_top_left reference point
- Pass final_pos + m_items_top_left to fromXml() so the internal
  translation yields the intended final position
- Add braces around single-statement for-loop in fromXml
- Remove empty else block leftovers from debug cleanup
2026-09-20 21:24:38 +02:00
Laurent Trinques c256e2dd1a Merge pull request #958 from bhangart/fix/pin-fetchcontent-dependencies
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m37s
Fix/pin fetchcontent dependencies
2026-09-20 20:13:37 +02:00
Laurent Trinques 8e3a7a6ab6 Merge pull request #927 from ispyisail/fix/923-snap-element-text
Snap a device's text to the grid when it is dragged (#923)
2026-09-20 20:10:13 +02:00
Laurent Trinques 740271c993 Merge pull request #938 from arummler/fix-translation-syntax
Fix various singular/plural cosntructions
2026-09-20 20:08:53 +02:00
Laurent Trinques 1bec2700bd Merge pull request #924 from Kellermorph/auto-numbering-new-project
Add global auto-numbering rules to QElectroTech settings
2026-09-20 20:07:16 +02:00
Laurent Trinques 268e9c7bd9 Merge pull request #959 from jp2images/fix-dark-canvas-item-updates
Keep the dark canvas on QGraphicsView's own update path
2026-09-20 20:02:50 +02:00
Laurent Trinques 264a0f677f Merge pull request #964 from jp2images/remove-stray-sbom-files
Remove two files that slipped into #944
2026-09-20 20:02:27 +02:00
Laurent Trinques 9ccfb5e116 Merge pull request #963 from jp2images/fix-hover-tint-light-face
Darken the hover ink on a light button face
2026-09-20 19:45:28 +02:00
Jeff Patterson 04463f0954 Darken the hover ink on a light button face
QETStyle::hoverColor() lightened the highlight color until it read at
3.5:1 against the Light role. On a dark face that is the way to go; on a
light face lightening only fades the ink, so with a pale platform accent
that QET keeps (macOS's green selection color, black selection text) the
loop ran to white and every hovered line-art icon vanished. The ink now
moves away from the face, darker on a light face, lighter on a dark one,
and falls back to the button text color if twenty steps are not enough.

The hover test gets a row with that accent on each palette, and a new
test sweeps accents across hues and lightness on both palettes and
requires the hover ink to read at 3:1 on the face.

Fixes #962
2026-09-20 12:45:07 -05:00
Laurent Trinques 3245919c19 Merge pull request #961 from jp2images/fix-small-page-icons
Give every configuration page an icon at page size
2026-09-20 19:44:00 +02:00
Jeff Patterson 55ad2eccbf Remove two files that slipped into #944
sources/qetsbom.cpp and sources/qetsbom.h were untracked local files
that a directory-wide add swept into the rebuilt #944 commit. Nothing
references them; the build does not compile them.
2026-09-20 12:43:57 -05:00
Jeff Patterson 8b2548d601 Keep the dark canvas on QGraphicsView's own update path
Laurent found that moving an element on a #954 build left its terminals'
help lines behind at every step, on both palettes. The view listened to
QGraphicsScene::changed() so that render() would keep the scene's updates
flowing, and any receiver on that signal puts the scene on its Qt 4.4
compatibility path, which erases a moved item's own old rect only:
children bigger than their parent stay on screen. Master hides that with
FullViewportUpdate, which repaints the whole viewport on every change.

Paint the inverted folio through QGraphicsView::paintEvent() instead, with
IndirectPainting set for that call and the draw hooks painting into a
viewport-sized image, and hand the scene the viewport when the items are
drawn so it records where each item was painted, including a child whose
geometry is set while its parent paints. The listener and the
full-viewport update go. Two tests move a parent with a sheet-wide child,
read the backing store, and require the repaint to be a partial one.
2026-09-20 12:43:50 -05:00
Laurent Trinques c3aeb0bf83 Merge pull request #944 from jp2images/fix-stylesheet-palette-switch
Refresh style-sheet widgets after a live palette switch
2026-09-20 19:36:26 +02:00
Jeff Patterson 16d89f64fd Refresh style-sheet widgets after a live palette switch
Switching the system between light and dark while QET runs changed
the palette of every plain widget but left widgets that carry a style
sheet in the colors they were created with: the folio tab bar stayed
light in dark mode, and after a dark-to-light switch its Add folio and
chevron buttons hovered as a near-black box with the icon lost inside
it. QApplication::setPalette() does not reach a widget with a style
sheet; QStyleSheetStyle resolved its palette once, when the sheet was
applied, and keeps it. Seventeen call sites set a sheet on a widget and
five .ui files carry one, so any of them could show the stale palette.

QET::Palette::refreshStyleSheets() re-applies each such widget's own
sheet, which makes QStyleSheetStyle resolve it against the palette now
in force. QETApp::useSystemPalette() calls it after installing the
palette, so both the OS color scheme change and the "use system
colors" setting are covered.

tests/qttest/tst_qetpalette: a tab widget with the folio tab bar's
sheet is still drawn in the old colors after setPalette(), which is the
defect, and follows the palette after refreshStyleSheets(), in both
directions.

Fixes #943.
2026-09-20 12:20:01 -05:00
Jeff Patterson ac78c51b70 Give every configuration page an icon at page size
The settings and project dialogs list their pages with 64 or 128 pixel
icons, and two pages had theirs at 22 pixels only: the terminal-strip
page and the shortcuts page, which borrowed configure-toolbars. Both get
a 128 pixel icon drawn in the style of the other page icons, the
shortcuts page under its own name, configure-shortcuts. The SVG sources
sit beside the PNGs.

On a dark palette the Printing and Export pages were small too: their
128 pixel icons exist in the light theme only, and Qt inherits by name,
not by size, so the dark theme's small copies were scaled up instead.
make_icon_themes.py now aliases the light files of the sizes a dark name
lacks, when they read on the dark window at 3:1.

A test asks the theme for every page icon at 128 pixels, on both
palettes.

Fixes #960
2026-09-20 10:44:09 -05:00
Beat Hangartner 2e27f77b28 Added more verbose description of the difference between lightweight and annotated tags as comment. 2026-09-20 14:21:36 +02:00
Laurent Trinques 83fb525e27 Merge pull request #939 from elevatormind/backtrace-windows-disable
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m39s
Fix issue #937
2026-09-20 14:03:43 +02:00
Laurent Trinques d1808c7a89 Fix Draw the folio with inverted lightness on a dark palette- #954 2026-09-20 13:07:57 +02:00
Laurent Trinques 4e4f029d2d Merge pull request #957 from qelectrotech/revert-955-revert-954-feature-dark-canvas
Revert "Revert "Draw the folio with inverted lightness on a dark palette""
2026-09-20 13:05:38 +02:00
Laurent Trinques 242f134e0f Revert "Revert "Draw the folio with inverted lightness on a dark palette"" 2026-09-20 13:05:13 +02:00
Magnus Hellströmer 2dc88df29c feat(build): Enable backtrace detection on Windows 2026-09-20 12:48:10 +02:00
Magnus Hellströmer fd8135264f fix(build): Remove stale Windows backtrace comments 2026-09-20 12:48:10 +02:00
Magnus Hellströmer 827cd2a91e fix(build): support backtrace in MSYS2 2026-09-20 12:48:10 +02:00
Magnus Hellströmer cdd25189b5 fix(build): guard backtrace detection on Windows 2026-09-20 12:48:04 +02:00
Laurent Trinques f1313f5895 Merge pull request #955 from qelectrotech/revert-954-feature-dark-canvas
Revert "Draw the folio with inverted lightness on a dark palette"
2026-09-20 07:20:03 +02:00
Laurent Trinques 75fafd5acc Revert "Draw the folio with inverted lightness on a dark palette" 2026-09-20 06:57:56 +02:00
Laurent Trinques 3394c1246c Merge pull request #954 from jp2images/feature-dark-canvas
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m12s
Draw the folio with inverted lightness on a dark palette
2026-09-20 06:44:09 +02:00
Jeff Patterson 3e34a6d55f Fill the dark canvas buffer and keep one scene connection
QGraphicsView::render() paints only what the scene draws, so a scene
without a background brush left the off-screen buffer uninitialized
and the inversion turned that memory into noise. The buffer is now
filled white first, which the inversion turns into the Base color.

listenToScene() connected a new receiver on every setScene() call and
never dropped the previous scene's. It now keeps a single connection
and replaces it.

Test in tst_qetpalette: paletteViewFillsWhatTheSceneLeavesBlank.
2026-09-19 18:27:00 -05:00
Jeff Patterson 50792ba1ad Repaint the whole dark canvas when the application palette changes
After a live light/dark switch only the sheet changed colors; the
viewport around it kept the previous palette. Qt repaints a widget on
an application palette change only when the widget's own palette
changed with it, and under the folio tab widget's style sheet it does
not, so the only repaints came from the scene and covered the scene
rectangle alone. PaletteGraphicsView now watches the application
object for ApplicationPaletteChange, the one receiver Qt always
notifies, and repaints its whole viewport.

The test paletteViewFollowsTheApplicationUnderAStyleSheet now also
requires a full-viewport repaint after each switch, before anything
asks the view for a rendering.
2026-09-19 18:27:00 -05:00
Jeff Patterson 6404612014 Follow the application palette, not the view's, on the dark canvas
The folio tab widget carries a style sheet, and QStyleSheetStyle pins
the palette of every widget under it to the application palette in
force when the sheet was applied. After a live light/dark switch the
view's own palette() is therefore stale: the folio kept its dark sheet
after a switch to light, and kept its white sheet after a switch to
dark. PaletteGraphicsView now reads the application palette both for
the decision to invert and for the sheet and ink colors.

Test in tst_qetpalette: paletteViewFollowsTheApplicationUnderAStyleSheet
puts the view in a tab widget with a style sheet and switches the
application palette to dark and back.
2026-09-19 18:27:00 -05:00
Jeff Patterson eaf15faaa3 Move the dark canvas into PaletteGraphicsView and test it directly
The inverted painting, the rubber band replay and the changed()
receiver lived in DiagramView, which the unit tests cannot link, so the
update-flag regression was only covered through a stand-in view. They
now live in PaletteGraphicsView, a QGraphicsView subclass with no
other dependency, and DiagramView derives from it. The view tells a
subclass through paintingInverted(bool) when it renders for an
inverted display; DiagramView forwards that to the diagram. The grid
dot rule moves out of Diagram::drawBackground into
QET::Palette::gridDotColor().

tst_qetpalette now links the real class: gridDotColorSoftensInvertedDots,
paletteViewFollowsThePalette (light sheet, dark sheet at text contrast
with a red box still red and the paintingInverted calls in order, back
to light), paletteViewKeepsSceneUpdatesFlowing (three whole-scene
updates and a selection each repaint, scene set after construction),
paletteViewDrawsTheRubberBand.
2026-09-19 18:27:00 -05:00
Jeff Patterson b8c9e670c4 Keep scene updates flowing on the dark canvas and soften its grid
On a dark palette the folio view paints through QGraphicsView::render()
instead of onto its viewport. In that case QGraphicsView never clears
the scene's "update everything" flag, and while the flag is set every
further QGraphicsScene::update() and item update is dropped: from the
second Diagram::update() on, the grid toggle, the white/gray toggle and
even a selection waited for an unrelated repaint. With a receiver on
QGraphicsScene::changed() the scene clears the flag before it emits, so
DiagramView now connects an empty receiver in its constructor.

While the view paints for inversion, Diagram draws the grid dots a
third of the way from the sheet color to black, so they come out as a
soft gray on the dark sheet instead of as bright as the ink. Printing
and export never take that path.

Test in tst_qetpalette: sceneUpdatesReachARenderedView.
2026-09-19 18:26:45 -05:00
Jeff Patterson 85dc638a42 Draw the folio with inverted lightness on a dark palette
On a dark palette the folio stayed a white sheet with black ink, and
the white/gray toggle only darkened the sheet while the ink stayed
black. DiagramView now renders each repaint into an image and inverts
its lightness before blitting it: white becomes the palette's Base,
black becomes its Text, and colored conductors and elements keep their
hue. The document, printing and export are untouched; only the screen
rendering changes, and only while the palette is dark.

QET::Palette::invertLightness does the inversion in one integer pass
(adding 255 - max - min to the three channels inverts the HSL lightness
and keeps hue and saturation), then stretches the result between the
sheet and ink colors through three lookup tables. A 4K viewport costs
about 9 ms in a release build. QGraphicsView::render() skips the
selection rubber band, so the view draws it again after the inversion.

Tests in tst_qetpalette: invertLightnessMapsSheetAndInk,
invertedViewReadsOnDarkSheet, invertLightnessSpeed.
2026-09-19 18:26:45 -05:00
Laurent Trinques dfe56cddbd Merge pull request #928 from ispyisail/fix/903-escape-cancels-text-tool
Let Escape cancel the text tool, like every other placement (#903)
2026-09-19 23:43:31 +02:00
Beat Hangartner 3f397f5f78 Explain why fetching by git tag is a supply chain risk
The pinning comment stated that a tag is mutable but not what an attacker
does with that, so the trade-off was hard to judge for anyone reviewing or
later undoing the pins. Spell out the mechanism: a tag is a name pointing
at a commit, anyone with push access upstream can force-push it elsewhere,
and FetchContent resolves it at build time, so a stolen maintainer account
or CI token makes every fresh build compile the attacker's code while
nothing changes here and the tag name still reads correctly. A commit hash
is derived from the content and cannot be moved that way.

Name the two cases where this was actually exploited: tj-actions/changed-
files in March 2025 (CVE-2025-30066), where tags v1 through v45.0.7 were
retargeted to a commit leaking CI secrets into build logs across more than
23,000 repositories, and aquasecurity/trivy-action in March 2026
(CVE-2026-33634), where 76 of 77 version tags were force-pushed to a
credential stealer for about twelve hours. Both were GitHub Actions rather
than CMake dependencies, which the comment says, because the point is the
shared mechanism of resolving a tag at build time.

Also document how to upgrade a pin, including that git ls-remote reports
the tag object for an annotated tag and the commit on the "^{}" line.

The note lives in fetch_pugixml.cmake, which fetch_kdeaddons.cmake and
fetch_singleapplication.cmake already refer to. Comments only; no build
behaviour changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 23:36:20 +02:00
Beat Hangartner 7ec13cbc1a Pin fetched dependencies to commit hashes instead of git tags
CMake fetches pugixml, SingleApplication and the three KDE Frameworks
modules by git tag. A tag is a mutable pointer that its owner can move,
so two builds of the same QElectroTech commit can silently get different
third-party sources, and a compromised upstream account can change what
every builder downloads without anything changing in this repository.
Pinning each dependency to the commit its tag currently points at closes
that, while keeping the tag name in a trailing comment so the intended
version stays readable.

No versions change. Every pinned commit is the one its tag resolves to,
checked with git ls-remote and confirmed by fetching each one and
verifying that git describe reports exactly the tag. The three KDE
modules live in separate repositories and therefore need separate
commits, so the single KF_GIT_TAG variable becomes three per-module
variables; passing -DKF_GIT_TAG=<ref> still selects one ref for all
three, unpinned, exactly as before, and KF_GIT_TAG stays defined so the
build summary in define_definitions.cmake is unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 23:05:39 +02:00
Laurent Trinques ef1fbd79b4 Merge pull request #942 from elevatormind/gitignore-ide-addition
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m1s
chore: ignore IDE settings and cache files
2026-09-19 20:25:12 +02:00
Laurent Trinques eb7abdfbc6 Merge pull request #946 from jp2images/feature-panels-follow-palette
Let the Collections and Projects panels follow the palette
2026-09-19 20:24:05 +02:00
Laurent Trinques f574d28340 Merge pull request #947 from jp2images/fix-dark-theme-light-icons
Redraw the Add PDF and folio icons as SVGs, stop inverting page-shaped icons in the dark theme
2026-09-19 20:22:29 +02:00
Laurent Trinques 31f90e2c46 Merge pull request #949 from jp2images/fix-menubar-test-macos
Make the menu bar keyboard test pass on macOS
2026-09-19 20:20:58 +02:00
Laurent Trinques 45bd8256b3 Merge pull request #950 from jp2images/feature-icon-hover
Give icons a hover state through a proxy style
2026-09-19 20:18:20 +02:00
Jeff Patterson 4e43cde063 Give icons a hover state through a proxy style
Hovering a tool button changed only its frame, and on the dark palette
Fusion's hover frame is too faint to notice, so nothing told the user
which button was under the pointer (GitHub #870, PR 3 of the plan).

QETStyle wraps the running style. For QIcon::Active it returns line-art
icons tinted in the palette's highlight color, lightened until the tint
reads at 3:1 on the Light role, which is the top of Fusion's hover
gradient. Colored icons keep their colors, using the same line-art rule
as misc/make_icon_themes.py. Every other mode goes to the base style.

Fusion also asks for QIcon::Active for the icon of a highlighted menu
item and paints it on the highlight bar, where the tint would vanish.
The generated pixmap is cached per icon and cannot tell a menu from a
toolbar, so drawControl(CE_MenuItem) hands the base style an icon whose
Active pixmap is in the HighlightedText color instead.

QETApp::initStyle() installs the proxy on every platform, keeping the
base style's object name so the Fusion checks still match.

tests/qttest/tst_qeticons: hovering changes the icon ink to the tint and
stays at 3:1 on both palettes, on a raised button and on a checked one
drawn sunken, where the frame gives no hover cue at all; a colored icon is unchanged on hover; a
highlighted menu item's icon reads at 3:1 on the highlight bar.
inkcontrast.h gains background() and ink() helpers for those checks.
2026-09-19 12:26:06 -05:00
Jeff Patterson aef56fe41d Make the menu bar keyboard test pass on macOS
tst_menubarkeyboard's control case presses Alt+F and expects the File
menu to open. On macOS it never did, for two reasons unrelated to the
F10 shortcut it guards: a QMenuBar is native there, so its menus live
in the system menu bar where QTest key events do not reach them, and Qt
does not turn "&File" into an Alt+F mnemonic on macOS at all. The test
has failed on every Mac build since bd6bed8d6 added it.

On macOS the test now uses an in-window menu bar and switches auto
mnemonics on for its own process, which runs the same QMenuBar code the
other platforms exercise. What it still does not prove on macOS is the
native bar: QETMainWindow::activateMenuBar() calls setActiveAction() on
a bar the system draws, and only F10 in the running application can
say what that does there. Other platforms are unchanged.

Fixes #948.
2026-09-19 12:24:57 -05:00
Jeff Patterson 0395f90f75 Let the Collections and Projects panels follow the palette
Both panels forced a light palette on themselves (white rows, black
text, their own selection blue) so that element previews, which are
black line art drawn for the white sheet, would stay visible on a dark
desktop (bugtracker 335). On a dark palette the two docks were the
only white windows left.

The forced palettes are gone. Element previews are now kept as drawn,
on a transparent background, and adapted where they are shown:
ElementPreviewDelegate, installed on the collection tree, hands the
view a copy with its lightness inverted when the palette is dark
(QET::Palette::forPalette), so black ink becomes the palette's light
gray while colored icons such as folders stay as they are; the drag
pixmap is adapted the same way. A light palette shows the previews
untouched. This fixes bugtracker 335 on every dark desktop rather than
masking it with a white panel.

The preview cache stored the old white-sheet pictures; it records the
format now and drops a cache written before this change once. The
amber "show this directory" highlight sets black text so it reads on
both palettes. The Projects panel only shows icons from the icon
theme, which has a dark variant, so nothing else changes there.

tests/qttest/tst_qetpalette: the line-art rule tells ink from color;
inversion keeps hue and alpha; a preview reads at 3:1 on the Base
color of both palettes; in a tree on the dark palette the delegate
inverts a line-art icon and leaves a colored one alone.

Fixes #945.
2026-09-19 12:22:24 -05:00
Jeff Patterson a4d5b7a12a Draw the folio icons as SVGs
The folio icons (Add, Remove, Properties, New folio, Title block
template) were anti-aliased gray page drawings, and the previous commit
left them untouched on the dark palette, where their soft gray fills
read blurry next to the line-art icons. They are now pixel-grid SVGs in
ico/scalable/ in the style of the Add PDF icon: a landscape sheet with
a title block line, a plus or minus badge in the corner, text lines for
properties, a filled title block for the template. Same 24 pixel canvas
as pdf-import.svg, same currentColor recoloring for the dark theme.

Only the 22 pixel PNGs go: five files leave ico/22x22 and both .qrc
files, and the alias list in misc/make_icon_themes.py that exposed
three of them under a second name is down to conductor2.png. The 16
pixel files stay, so menus and the projects panel keep their icons at
that size, and the 128 pixel diagram.png stays for the configuration
page list.

tests/qttest/tst_qeticons: every file in ico/scalable/ resolves in both
themes at 22, 24, 32 and 64 pixels, dark ink on light and light ink on
dark, with no 22 pixel PNG left beside it; the light-art check reads
the folio family at 16 pixels, where the page art remains.
2026-09-19 12:02:55 -05:00
Jeff Patterson ba29dc45a0 Draw the Add PDF icon as an SVG
The "Add PDF" action used ico/22x22/pdf-import.png, a white page with a
red PDF mark that sat apart from its neighbors "Add text" and "Add
image", both gray line art in a square frame with a plus. It is now
ico/scalable/pdf-import.svg, the same pixel design as insert-image.png:
a frame, the letters PDF, a plus in the corner. One file serves every
slot and stays sharp on high-DPI screens; the PNG is gone from both
.qrc files.

The canvas is 24 pixels with the art offset by one, like the Breeze
SVGs already in the theme. Fusion's toolbar slot is 24 pixels: a 22
pixel PNG is drawn unscaled inside it, but a scalable icon is rendered
at the slot size, and a 22 pixel grid stretched to 24 puts every one
pixel line between pixels and reads blurry.

The file uses currentColor like the Breeze SVGs already in the theme,
so misc/make_icon_themes.py produces the dark copy the same way. A new
ico/scalable/ folder holds QET's own vector icons; the direction
arummler asked for in #690.

tests/qttest/tst_qeticons: the icon resolves in both themes at 16, 22,
32 and 64 pixels, dark ink on the light theme and light ink on the dark
one, and no 22 pixel PNG remains.
2026-09-19 12:02:54 -05:00
Jeff Patterson d06c7be606 Leave light icons out of the dark theme
misc/make_icon_themes.py sorted icons by saturation alone, so a white
page with a small red mark counted as line art and its dark copy turned
the page black: the PDF import icon read black on black (#919,
Kellermorph), and the folio, diagram and label icons came out as dark
pages with a light border.

An icon whose visible pixels are at least 30% near white is now "light
art" and inherits from the qet theme untouched; it already reads on a
dark toolbar. The generator also removes dark files it no longer
produces, so a reclassified icon falls back to the light theme instead
of keeping a stale copy. Thirteen files leave ico/themes/qet-dark.

tests/qttest/tst_qeticons: every dark theme file, taken as its mean
visible color, reaches 3:1 on the dark palette's window color; the
lightest-pixel check it replaces let a black page with a light border
through. Asking the dark theme for pdf-import, diagram, label, the
folio icons and diagram_bg returns the light art.
2026-09-19 12:02:54 -05:00
Magnus Hellströmer 56cbd6e23d chore: ignore IDE settings and cache files 2026-09-19 18:10:37 +02:00
Laurent Trinques 8bbc2da5c7 Merge pull request #940 from Kellermorph/german-translation
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m17s
Update German translation
2026-09-19 14:11:15 +02:00
Kellermorph 7fa1ff35b8 Add global auto-numbering rules to QElectroTech settings
Add a 'Numérotation auto' tab to the global settings page (Settings >
Nouveau projet) where users can define default auto-numbering rules
for Conducteurs, Eléments, and Folios. These rules are automatically
transferred to every new project created.

Changes:
- Add NumerotationContext::saveToSettings()/loadFromSettings() static
  helpers for persisting named numerotation contexts via QSettings
- Add 'Numérotation auto' tab to NewDiagramPage with three sub-tabs
  using SelectAutonumW widgets (same UI as project properties)
- Add save/remove/persist slots for conductor, element, and folio
  contexts with immediate QSettings persistence on every change
- NewDiagramPage::applyConf() saves autonum settings when editing
  global defaults (no project)
- QETProject constructor loads global autonum settings from QSettings
  for new empty projects
2026-09-19 13:47:28 +02:00
Kellermorph 4b8db6be0e Update German translation 2026-09-19 13:25:39 +02:00
Laurent Trinques a6b4c3c673 Merge pull request #930 from ispyisail/feat/923-forum-auto-conductor-shortcut
List auto conductor creation in the Shortcuts page
2026-09-19 13:04:44 +02:00
Laurent Trinques 8f0ee06a4f Merge pull request #929 from ispyisail/feat/461-conductor-color-quick-access
Add a one-click conductor colour to the toolbar (#461)
2026-09-19 13:00:14 +02:00
Andre Rummler 7e3420b1a5 Avoid constructing sentences. 2026-09-19 10:39:54 +02:00
Andre Rummler 183035d5ac Replace enumerator list. 2026-09-19 09:15:28 +02:00
Andre Rummler eb4109bb11 Replace manual enumerator sentence with qt trasnlator automatism. 2026-09-19 09:08:25 +02:00
Andre Rummler 717bf57677 Improve plural marking from previous commit. 2026-09-19 09:08:00 +02:00
Andre Rummler 861e5de25e Use qt own enumeration translation function. Fix plural forms. 2026-09-19 08:31:34 +02:00
ispyisail bf7bff595e Merge branch 'master' into revive/591-dynamic-text-drag-resize
Bringing the drag-to-resize work up to date with current master (662
commits) before asking for review again. Both files auto-merged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CaKympWT3owLotCpEN2CFj
2026-09-19 11:30:31 +12:00
ispyisail 70599c4ae1 List auto conductor creation in the Shortcuts page
"Est il possible dans les raccourcis d'ajouter un pour création
automatique de conducteur ? Je n'utilise pas par défaut, mais
ponctuellement c'est très pratique." -- oc67, an electrician, on the
forum (viewtopic.php?pid=23296).

The action itself has existed for a long time: m_auto_conductor is a
checkable QAction in the Schéma toolbar and the Project menu. It was
simply never handed to ShortcutManager, so it did not appear in
Configuration > Raccourcis and there was no way to reach it from the
keyboard. This registers it.

No default sequence is set. That is the request read literally -- he
asked for it to be *in* the shortcuts list so he can bind it himself --
and it avoids spending one of the few free keys on a setting many people
never touch. The Shortcuts page already treats "no shortcut" as a normal
state: it renders an empty field and its quick filter can list actions
with and without a binding separately.

Verified on a virtual display. With shortcuts/diagrameditor.auto_conductor
set to Ctrl+Alt+A:

  Configuration > Raccourcis, filtered on "conducteur", lists
  "Création automatique de conducteur(s)" under "Éditeur de schémas"
  showing that binding.

  Mouse parked away from the toolbar, pressing it twice: the toolbar
  button changes on each press and returns to its starting appearance
  after the second, so the key toggles the setting exactly as clicking
  the button does.

Two things that misled the first run, recorded so the next person does
not repeat them: F7 is already registered to panel.move_diagram_downx100
in the elements panel, and a screenshot taken with the pointer resting on
the button shows its hover state, not its checked state.

ctest 12/12, Qt 6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CaKympWT3owLotCpEN2CFj
2026-09-19 09:18:04 +12:00
ispyisail e42ebdc861 Add a conductor colour button to the Schéma toolbar (#461)
An electrician on the forum draws 400 V and 24 V circuits in the same
folio and wants the colour to be one click away
(qelectrotech.org/forum, viewtopic pid=23296). Today the quickest route
is F2, which opens a colour dialog and refuses to act unless exactly one
conductor is selected, so colouring a run means one conductor, one
dialog, at a time.

This adds a swatch button beside the auto-conductor actions. Picking a
colour does two things:

  - recolours every conductor currently selected, as ONE undo step;
  - becomes the colour of the next conductor drawn, through the
    LastUsedStyle mechanism #888 already added and Conductor's
    constructor already reads.

Either half is useful alone: with nothing selected it just sets the pen
for what comes next.

The menu lists the colours the trade names -- the three phases, neutral,
earth, and the ones used for control and extra-low-voltage circuits --
then any custom colours picked this session, then the full colour
dialog. A colour already in the standard list is not repeated under
"recently used".

Nothing is written to the project or to QSettings. That is deliberate:
it is the same session-scoped "what did I just use" idea as
LastUsedStyle, so it adds no persisted state and no file-format change.
Named presets stored per project -- what #461 actually asks for -- are a
larger feature that needs a maintainer decision first; the question is
still open on that issue since 21 June.

Verified on a virtual display against examples/Habitat-Schemas_developpes.qet,
reading colours back from the saved project rather than the screen:

  select all on folio 1, pick Rouge
      21 conductors {none:1, #ff5500:2, #ff0000:6, #00aa00:5, #0000ff:7}
      -> all 21 #ff0000
  one Ctrl+Z
      -> back to the original five-colour mix, exactly
  pick Marron with nothing selected, then draw a conductor
      -> the new conductor is #7b3f00

ctest 12/12, Qt 6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CaKympWT3owLotCpEN2CFj
2026-09-19 09:02:58 +12:00
ispyisail 3c1fb2fe72 Let Escape cancel the text tool, like every other placement (#903)
Issue #903 reported that Escape stopped cancelling an in-progress
placement. PR #899 fixed that by letting Escape through to the active
tool whenever Diagram::eventInterfaceIsRunning(), and 863ac5f0a tightened
it to isRunning() so a second Escape cannot retrigger an abort already in
flight. That covers seven of the eight tools. It cannot cover the eighth.

eventInterfaceIsRunning() is m_event_interface && isRunning(), and
isRunning() returns m_running. DiagramEventAddText is the only class
under sources/diagramevent/ that never sets m_running, so the guard reads
false for the whole time the tool is armed and DiagramView::keyPressEvent
keeps swallowing Escape for its own selection/focus handling.

It is also the only one of the eight with no RightButton branch --
right-clicking the folio with the text tool armed opens the folio context
menu. So this tool currently has no way to cancel at all: the only way
out is to pick a different tool, and any stray click drops a text field
the user did not want.

The tool is armed from the moment it is attached, so m_running is set in
the constructor and cleared where the text is placed, before finish().

Measured on a virtual display against a fixture holding one free text
field, counting diagram-level text fields in the saved project:

  arm the text tool, click            2 fields   places, as it should
  arm it, press Escape, then click    2 fields   before  -- not cancelled
                                      1 field    after   -- cancelled

The same run on master's rectangle tool cancels correctly, which is what
made the text tool look fixed when it was not.

ctest 12/12, Qt 6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 08:32:58 +12:00
ispyisail 95d0e523fe Snap a device text to the grid when it is dragged (#923)
Dragging an element's text -- its label, article number, any of its
information fields -- moved it in free one-unit steps while everything
else in the editor snapped to the grid. Reported by pki791 in #923 for
labels moved with Shift.

QET moves a text with the mouse along five paths. Four snap and let Ctrl
place freely:

  DiagramTextItem::mouseMoveEvent        an independent text
  ElementTextItemGroup::mouseMoveEvent   a group of element texts
  ElementTextsMover::continueMovement    every OTHER selected element text
  QetGraphicsItem::setPos                elements, images, shapes

DynamicElementTextItem::mouseMoveEvent, the text actually under the
cursor, ended "setPos(new_pos)" with no grid and no modifier check. It is
otherwise the same function as the group's, which is why this reads as an
omission rather than a decision: the line this adds is that function's,
character for character.

The inconsistency was visible in one gesture. With two element texts
selected and one of them dragged, ElementTextsMover skips the driver item
and snaps the rest, so the text under the cursor was the only one on the
folio that did not land on the grid.

Verified on a virtual display (Xvfb + openbox) against a two-lamp fixture,
grid 10, reading the saved positions rather than the screen:

  Shift+drag the label      before (32.95, -11.55) -> (7.95, 23.45) off-grid
                            after                  -> (10, 20)      on-grid
  the co-selected label     (10, -10) -> (50, 20) on-grid, before and after
  Shift to grab, then Ctrl  -> (7.95, 23.45) off-grid, free placement kept

The last line matters: moving an element text needs Shift at press, and
the modifier is read at move time, so Ctrl still places freely -- press
with Shift, hold Ctrl to drag. Holding both from the press is a different
gesture, reserved by DiagramView::isCtrlShifting() for the view's mode
switch, and does not move the text at all. Nothing that was possible
before is lost.

Worth knowing when reviewing: 470 of the 492 element texts in the 24
example projects (95.5 %) sit off the grid today, because element
definitions place their default text at fractional offsets. The first
drag of almost any existing label will pull it onto the grid, by at most
half a grid step.

ctest 12/12, Qt 6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 08:06:45 +12:00
ispyisail 81449faffd Add drag-to-resize for dynamic element text width (#577 phase 1)
Adds two QetGraphicsHandlerItem grip handles at the left/right edges of a
selected DynamicElementTextItem's frameRect(), reusing the exact same
handle class, scene-event-filter wiring, and live-drag-then-undo-on-release
pattern QetShapeItem already uses for its own diagram-level resize handles
(sources/qetgraphicsitem/qetshapeitem.cpp).

- Handles are created/destroyed on ItemSelectedHasChanged, matching
  QetShapeItem's convention (and ElementPrimitiveDecorator's, for the
  element editor's own primitives).
- Position is recomputed in paint() rather than hooked to specific
  mutators, since textWidth/font/text/rotation can all move frameRect()
  and there's no single itemChange notification that covers all of them.
- The drag delta is resolved through mapFromScene() into the item's own
  local coordinates, so a rotated text box still resizes along its own
  baseline rather than along the scene's x-axis.
- setTextWidth() is called live during the drag for immediate visual
  feedback (matching how QetShapeItem's handlerMouseMoveEvent live-updates
  geometry); only on release is a QPropertyUndoCommand pushed -- the exact
  same command the properties-panel width spinbox already uses
  (sources/ui/dynamicelementtextmodel.cpp), so no new undo-command class
  or XML was needed.
- The original textWidth() value is preserved as-is (including -1, the
  "auto" sentinel) for the undo command's old_value, separately from the
  concrete baseline used for the live drag's delta math -- otherwise an
  undo would replace "auto width" with a synthesized fixed width instead
  of actually restoring the auto-sizing state.

Scoped to DynamicElementTextItem per the discussion's phase 1 (the
buildable, no-new-XML piece); IndependentTextItem and the element editor's
PartText/PartDynamicTextField have no serialized width property to resize
yet and are left as explicitly out-of-scope follow-ups.

Verified headlessly (Xvfb + xdotool + scrot): selecting an element's
label text shows the two handles, dragging one live-resizes the text
(confirmed via the properties panel's width field updating in real time),
and undo/redo correctly restores the exact original width including the
auto-width (-1) case.
2026-08-01 17:51:26 +12:00
107 changed files with 6040 additions and 499 deletions
+8
View File
@@ -8,3 +8,11 @@ doc/*
QElectroTech.tag QElectroTech.tag
!doc/doc-utils !doc/doc-utils
lang/*.qm lang/*.qm
# -- IDE settings/intermediate files --
# zed
.zed
.cache
# VS Code
.vscode
+3 -1
View File
@@ -113,7 +113,9 @@ if(Backtrace_FOUND)
include_directories(${Backtrace_INCLUDE_DIRS}) include_directories(${Backtrace_INCLUDE_DIRS})
add_compile_definitions(QET_CRASH_BACKTRACE) add_compile_definitions(QET_CRASH_BACKTRACE)
else() else()
message(STATUS "backtrace() not available: crash dumps will carry the log ring without a backtrace") message(STATUS
"backtrace() not available: crash dumps will carry the log ring "
"without a backtrace")
endif() endif()
find_package(SQLite3 REQUIRED) find_package(SQLite3 REQUIRED)
+16
View File
@@ -271,6 +271,22 @@ mkdir build && cd build
cmake .. -G Ninja -DBUILD_WITH_KF=OFF -DCMAKE_BUILD_TYPE=Release cmake .. -G Ninja -DBUILD_WITH_KF=OFF -DCMAKE_BUILD_TYPE=Release
cmake --build . cmake --build .
``` ```
If `libbacktrace` is installed in your MSYS2 environment, pass the following
cached CMake variables when configuring. They are only necessary in that
case, because `FindBacktrace` needs them to locate the library:
```sh
cmake .. -G Ninja -DBUILD_WITH_KF=OFF -DCMAKE_BUILD_TYPE=Release \
-DBacktrace_INCLUDE_DIR=/c/msys64/clang64/include \
-DBacktrace_LIBRARY=/c/msys64/clang64/lib/libbacktrace.a
```
`Backtrace_INCLUDE_DIR` must point to the directory containing `backtrace.h`,
and `Backtrace_LIBRARY` to the `libbacktrace.a` file. The paths above are the
usual locations for the MSYS2 `clang64` environment; adjust them if your
installation uses a different prefix.
(KF6 isn't packaged in MSYS2 either, hence `-DBUILD_WITH_KF=OFF` again.) (KF6 isn't packaged in MSYS2 either, hence `-DBUILD_WITH_KF=OFF` again.)
Using the Qt Online Installer's bundled MinGW kit instead: point Using the Qt Online Installer's bundled MinGW kit instead: point
+28 -5
View File
@@ -23,8 +23,31 @@ if(BUILD_WITH_KF)
if(BUILD_KF) if(BUILD_KF)
if(NOT DEFINED KF_GIT_TAG) # v6.10.0 is a more or less random version, taken as an conservative
# this is a more or less random version, taken as an conservative approach # approach. Pinned to the commits v6.10.0 points at, not to the tags
# themselves; see the note in fetch_pugixml.cmake. Each module lives in its
# own repository, so the same v6.10.0 release is a different commit in
# each.
#
# KDE uses annotated tags, so "git ls-remote <repo> 'refs/tags/v6.10.0*'"
# prints two hashes per module: refs/tags/v6.10.0 is the tag object (the
# tagger, the date and the tag message) and refs/tags/v6.10.0^{} is the
# commit that object points at. The hashes below are the "^{}" ones, i.e.
# the commits. Lightweight tags, such as pugixml's v1.15 and
# SingleApplication's v3.2.0, have no tag object and print only the
# commit line.
set(KF_ECM_GIT_COMMIT 7dd28cc56c339c3f8fb356f7c53c0e8f61433d81) # v6.10.0
set(KF_KCOREADDONS_GIT_COMMIT c569f974dab24b4784ad186a3db4b76b2fa36612) # v6.10.0
set(KF_KWIDGETSADDONS_GIT_COMMIT 1abbed8a280d6626c59fb197f2c4667d2b1e7445) # v6.10.0
if(DEFINED KF_GIT_TAG)
# Explicit override: -DKF_GIT_TAG=<ref> selects one ref for all three
# modules, unpinned, exactly as it did before.
set(KF_ECM_GIT_COMMIT ${KF_GIT_TAG})
set(KF_KCOREADDONS_GIT_COMMIT ${KF_GIT_TAG})
set(KF_KWIDGETSADDONS_GIT_COMMIT ${KF_GIT_TAG})
else()
# Keep KF_GIT_TAG defined: define_definitions.cmake reports it.
set(KF_GIT_TAG v6.10.0) set(KF_GIT_TAG v6.10.0)
endif() endif()
# using a function in order to limit the scope of the variables # using a function in order to limit the scope of the variables
@@ -53,19 +76,19 @@ if(BUILD_WITH_KF)
FetchContent_Declare( FetchContent_Declare(
ecm ecm
GIT_REPOSITORY https://invent.kde.org/frameworks/extra-cmake-modules.git GIT_REPOSITORY https://invent.kde.org/frameworks/extra-cmake-modules.git
GIT_TAG ${KF_GIT_TAG}) GIT_TAG ${KF_ECM_GIT_COMMIT})
FetchContent_MakeAvailable(ecm) FetchContent_MakeAvailable(ecm)
FetchContent_Declare( FetchContent_Declare(
kcoreaddons kcoreaddons
GIT_REPOSITORY https://invent.kde.org/frameworks/kcoreaddons.git GIT_REPOSITORY https://invent.kde.org/frameworks/kcoreaddons.git
GIT_TAG ${KF_GIT_TAG}) GIT_TAG ${KF_KCOREADDONS_GIT_COMMIT})
FetchContent_MakeAvailable(kcoreaddons) FetchContent_MakeAvailable(kcoreaddons)
FetchContent_Declare( FetchContent_Declare(
kwidgetsaddons kwidgetsaddons
GIT_REPOSITORY https://invent.kde.org/frameworks/kwidgetsaddons.git GIT_REPOSITORY https://invent.kde.org/frameworks/kwidgetsaddons.git
GIT_TAG ${KF_GIT_TAG}) GIT_TAG ${KF_KWIDGETSADDONS_GIT_COMMIT})
FetchContent_MakeAvailable(kwidgetsaddons) FetchContent_MakeAvailable(kwidgetsaddons)
endfunction() endfunction()
qet_make_kf_available() qet_make_kf_available()
+42 -1
View File
@@ -22,10 +22,51 @@ option(BUILD_PUGIXML "Build pugixml library, use system one otherwise" YES)
if(BUILD_PUGIXML) if(BUILD_PUGIXML)
# Pinned to the commit v1.15 points at, not to the tag itself.
#
# A git tag is only a named pointer to a commit, and anyone with push access
# to the upstream repository can move it (git push --force) to any other
# commit. FetchContent fetches whatever the tag points at when the build
# runs, so if a maintainer account or CI token is compromised, the attacker
# can retarget a well-known release tag to malicious code: every fresh build
# of QElectroTech then compiles it, while nothing changes in this repository
# and the tag name still looks correct. A commit hash cannot be moved, because
# it is derived from the content: different code always has a different hash.
#
# This attack has been used in the wild:
# - March 2025, tj-actions/changed-files (CVE-2025-30066): tags v1 through
# v45.0.7 were retargeted to a commit that dumped CI secrets into build
# logs, affecting more than 23,000 repositories.
# - March 2026, aquasecurity/trivy-action (CVE-2026-33634): 76 of 77
# version tags were force-pushed to a credential stealer and stayed
# malicious for about 12 hours.
# Both were GitHub Actions rather than CMake dependencies, but the mechanism
# is the same one FetchContent relies on here: resolving a git tag at build
# time.
#
# To upgrade, look up the commit the new tag points at with
# git ls-remote <repo> 'refs/tags/<tag>*', check that it is the release you
# expect, and update both the hash and the trailing tag comment.
#
# How many lines that prints depends on which of the two kinds of tag
# upstream created:
# - A lightweight tag is nothing but a ref pointing straight at the commit,
# so ls-remote prints a single line, "refs/tags/<tag>", and its hash is
# the commit to pin. pugixml tags this way, which is why the v1.15 hash
# below is what "git ls-remote ... refs/tags/v1.15" reports directly;
# SingleApplication (v3.2.0) does the same.
# - An annotated tag is a git object in its own right, carrying a tagger,
# a date, a message and optionally a GPG signature, and pointing at the
# commit. ls-remote then prints two lines: "refs/tags/<tag>" is the tag
# object and "refs/tags/<tag>^{}" is that object dereferenced, i.e. the
# commit. The KDE Frameworks modules tag this way, so for them it is the
# "^{}" hash that belongs in the pin; the other hash identifies the tag
# object itself, which is not the source revision and changes whenever
# upstream re-creates the tag, even over the very same commit.
FetchContent_Declare( FetchContent_Declare(
pugixml pugixml
GIT_REPOSITORY https://github.com/zeux/pugixml.git GIT_REPOSITORY https://github.com/zeux/pugixml.git
GIT_TAG v1.15) GIT_TAG ee86beb30e4973f5feffe3ce63bfa4fbadf72f38) # v1.15
set(PUGIXML_INSTALL OFF CACHE INTERNAL "") set(PUGIXML_INSTALL OFF CACHE INTERNAL "")
FetchContent_MakeAvailable(pugixml) FetchContent_MakeAvailable(pugixml)
else() else()
+7 -1
View File
@@ -31,9 +31,15 @@ if(EXISTS "${CMAKE_SOURCE_DIR}/SingleApplication/CMakeLists.txt")
set(FETCHCONTENT_SOURCE_DIR_SINGLEAPPLICATION "${CMAKE_SOURCE_DIR}/SingleApplication") set(FETCHCONTENT_SOURCE_DIR_SINGLEAPPLICATION "${CMAKE_SOURCE_DIR}/SingleApplication")
endif() endif()
# Pinned to the commit v3.2.0 points at, not to the tag itself; see the note in
# fetch_pugixml.cmake. v3.2.0 is a lightweight tag, a ref pointing straight at
# the commit, so "git ls-remote <repo> refs/tags/v3.2.0" prints that commit and
# nothing else. An annotated tag, as KDE uses in fetch_kdeaddons.cmake, would
# print the tag object under refs/tags/v3.2.0 as well, with the commit on the
# refs/tags/v3.2.0^{} line.
FetchContent_Declare( FetchContent_Declare(
SingleApplication SingleApplication
GIT_REPOSITORY https://github.com/itay-grudev/SingleApplication.git GIT_REPOSITORY https://github.com/itay-grudev/SingleApplication.git
GIT_TAG v3.2.0) GIT_TAG aede311d28d20179216c5419b581087be2a8409f) # v3.2.0
set(QT_DEFAULT_MAJOR_VERSION 6) set(QT_DEFAULT_MAJOR_VERSION 6)
FetchContent_MakeAvailable(SingleApplication) FetchContent_MakeAvailable(SingleApplication)
+10
View File
@@ -248,8 +248,12 @@ set(QET_SRC_FILES
${QET_DIR}/sources/qet.h ${QET_DIR}/sources/qet.h
${QET_DIR}/sources/qeticons.cpp ${QET_DIR}/sources/qeticons.cpp
${QET_DIR}/sources/qeticons.h ${QET_DIR}/sources/qeticons.h
${QET_DIR}/sources/palettegraphicsview.cpp
${QET_DIR}/sources/palettegraphicsview.h
${QET_DIR}/sources/qetpalette.cpp ${QET_DIR}/sources/qetpalette.cpp
${QET_DIR}/sources/qetpalette.h ${QET_DIR}/sources/qetpalette.h
${QET_DIR}/sources/qetstyle.cpp
${QET_DIR}/sources/qetstyle.h
${QET_DIR}/sources/qetinformation.cpp ${QET_DIR}/sources/qetinformation.cpp
${QET_DIR}/sources/qetinformation.h ${QET_DIR}/sources/qetinformation.h
${QET_DIR}/sources/qetmainwindow.cpp ${QET_DIR}/sources/qetmainwindow.cpp
@@ -434,6 +438,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/ElementsCollection/elementcollectionhandler.h ${QET_DIR}/sources/ElementsCollection/elementcollectionhandler.h
${QET_DIR}/sources/ElementsCollection/elementcollectionitem.cpp ${QET_DIR}/sources/ElementsCollection/elementcollectionitem.cpp
${QET_DIR}/sources/ElementsCollection/elementcollectionitem.h ${QET_DIR}/sources/ElementsCollection/elementcollectionitem.h
${QET_DIR}/sources/ElementsCollection/elementpreviewdelegate.cpp
${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/elementscollectionwidget.cpp ${QET_DIR}/sources/ElementsCollection/elementscollectionwidget.cpp
@@ -699,6 +705,10 @@ set(QET_SRC_FILES
${QET_DIR}/sources/ui/contactgroupselectiondialog.h ${QET_DIR}/sources/ui/contactgroupselectiondialog.h
${QET_DIR}/sources/ui/conductorpropertiesdialog.cpp ${QET_DIR}/sources/ui/conductorpropertiesdialog.cpp
${QET_DIR}/sources/ui/conductorpropertiesdialog.h ${QET_DIR}/sources/ui/conductorpropertiesdialog.h
${QET_DIR}/sources/ui/conductorcolortoolbutton.cpp
${QET_DIR}/sources/ui/conductorcolortoolbutton.h
${QET_DIR}/sources/ui/diagrambgcolorbutton.cpp
${QET_DIR}/sources/ui/diagrambgcolorbutton.h
${QET_DIR}/sources/ui/conductorpropertieswidget.cpp ${QET_DIR}/sources/ui/conductorpropertieswidget.cpp
${QET_DIR}/sources/ui/conductorpropertieswidget.h ${QET_DIR}/sources/ui/conductorpropertieswidget.h
${QET_DIR}/sources/ui/configsaveloaderwidget.cpp ${QET_DIR}/sources/ui/configsaveloaderwidget.cpp
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

+29
View File
@@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
<!-- keyboard body with a lighter top edge -->
<rect x="6" y="30" width="116" height="68" rx="8" fill="#31363b"/>
<rect x="6" y="30" width="116" height="34" rx="8" fill="#4d4d4d"/>
<rect x="6" y="46" width="116" height="18" fill="#31363b"/>
<!-- key rows -->
<g fill="#eff0f1">
<rect x="14" y="38" width="8" height="8" rx="1.5"/><rect x="24" y="38" width="8" height="8" rx="1.5"/>
<rect x="34" y="38" width="8" height="8" rx="1.5"/><rect x="44" y="38" width="8" height="8" rx="1.5"/>
<rect x="54" y="38" width="8" height="8" rx="1.5"/><rect x="64" y="38" width="8" height="8" rx="1.5"/>
<rect x="74" y="38" width="8" height="8" rx="1.5"/><rect x="84" y="38" width="8" height="8" rx="1.5"/>
<rect x="94" y="38" width="8" height="8" rx="1.5"/><rect x="104" y="38" width="10" height="8" rx="1.5"/>
<rect x="14" y="50" width="12" height="8" rx="1.5"/><rect x="28" y="50" width="8" height="8" rx="1.5"/>
<rect x="38" y="50" width="8" height="8" rx="1.5"/><rect x="48" y="50" width="8" height="8" rx="1.5"/>
<rect x="58" y="50" width="8" height="8" rx="1.5"/><rect x="68" y="50" width="8" height="8" rx="1.5"/>
<rect x="78" y="50" width="8" height="8" rx="1.5"/><rect x="88" y="50" width="8" height="8" rx="1.5"/>
<rect x="98" y="50" width="16" height="8" rx="1.5"/>
<rect x="14" y="62" width="16" height="8" rx="1.5"/><rect x="32" y="62" width="8" height="8" rx="1.5"/>
<rect x="42" y="62" width="8" height="8" rx="1.5"/><rect x="52" y="62" width="8" height="8" rx="1.5"/>
<rect x="62" y="62" width="8" height="8" rx="1.5"/><rect x="72" y="62" width="8" height="8" rx="1.5"/>
<rect x="82" y="62" width="8" height="8" rx="1.5"/><rect x="92" y="62" width="22" height="8" rx="1.5"/>
<rect x="14" y="74" width="10" height="8" rx="1.5"/><rect x="26" y="74" width="10" height="8" rx="1.5"/>
<rect x="38" y="74" width="52" height="8" rx="1.5"/>
<rect x="92" y="74" width="10" height="8" rx="1.5"/><rect x="104" y="74" width="10" height="8" rx="1.5"/>
</g>
<!-- the two keys of a shortcut, pressed -->
<rect x="14" y="62" width="16" height="8" rx="1.5" fill="#3daee9"/>
<rect x="42" y="62" width="8" height="8" rx="1.5" fill="#3daee9"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

+48
View File
@@ -0,0 +1,48 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
<!-- DIN rail -->
<rect x="6" y="86" width="116" height="18" rx="3" fill="#7f8c8d"/>
<rect x="6" y="92" width="116" height="6" fill="#4d4d4d"/>
<!-- six terminal blocks: L, L, N, PE, L, spare -->
<g fill="#eff0f1">
<rect x="12" y="26" width="16" height="66" rx="2"/>
<rect x="30" y="26" width="16" height="66" rx="2"/>
<rect x="48" y="26" width="16" height="66" rx="2"/>
<rect x="66" y="26" width="16" height="66" rx="2"/>
<rect x="84" y="26" width="16" height="66" rx="2"/>
<rect x="102" y="26" width="16" height="66" rx="2"/>
</g>
<!-- right-hand shading of each block -->
<g fill="#bdc3c7">
<rect x="24" y="28" width="4" height="62"/>
<rect x="42" y="28" width="4" height="62"/>
<rect x="60" y="28" width="4" height="62"/>
<rect x="78" y="28" width="4" height="62"/>
<rect x="96" y="28" width="4" height="62"/>
<rect x="114" y="28" width="4" height="62"/>
</g>
<!-- colored marker bands -->
<rect x="12" y="26" width="16" height="10" rx="2" fill="#7f8c8d"/>
<rect x="30" y="26" width="16" height="10" rx="2" fill="#7f8c8d"/>
<rect x="48" y="26" width="16" height="10" rx="2" fill="#3daee9"/>
<rect x="66" y="26" width="16" height="10" rx="2" fill="#27ae60"/>
<rect x="70" y="26" width="8" height="10" fill="#fdbc4b"/>
<rect x="84" y="26" width="16" height="10" rx="2" fill="#7f8c8d"/>
<rect x="102" y="26" width="16" height="10" rx="2" fill="#da4453"/>
<!-- screws -->
<g fill="#31363b">
<circle cx="20" cy="50" r="4.5"/><circle cx="20" cy="74" r="4.5"/>
<circle cx="38" cy="50" r="4.5"/><circle cx="38" cy="74" r="4.5"/>
<circle cx="56" cy="50" r="4.5"/><circle cx="56" cy="74" r="4.5"/>
<circle cx="74" cy="50" r="4.5"/><circle cx="74" cy="74" r="4.5"/>
<circle cx="92" cy="50" r="4.5"/><circle cx="92" cy="74" r="4.5"/>
<circle cx="110" cy="50" r="4.5"/><circle cx="110" cy="74" r="4.5"/>
</g>
<g fill="#eff0f1">
<rect x="16" y="49" width="8" height="2"/><rect x="16" y="73" width="8" height="2"/>
<rect x="34" y="49" width="8" height="2"/><rect x="34" y="73" width="8" height="2"/>
<rect x="52" y="49" width="8" height="2"/><rect x="52" y="73" width="8" height="2"/>
<rect x="70" y="49" width="8" height="2"/><rect x="70" y="73" width="8" height="2"/>
<rect x="88" y="49" width="8" height="2"/><rect x="88" y="73" width="8" height="2"/>
<rect x="106" y="49" width="8" height="2"/><rect x="106" y="73" width="8" height="2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

+3
View File
@@ -216,6 +216,9 @@ Clarification:
ico/128x128/diagram.png by the QElectroTech team (License CC BY-ND 3.0) ico/128x128/diagram.png by the QElectroTech team (License CC BY-ND 3.0)
ico/128x128/document-export.png by the QElectroTech team (License CC BY-ND 3.0) ico/128x128/document-export.png by the QElectroTech team (License CC BY-ND 3.0)
ico/128x128/project.png by the QElectroTech team (License CC BY-ND 3.0) ico/128x128/project.png by the QElectroTech team (License CC BY-ND 3.0)
ico/128x128/terminalstrip.png and configure-shortcuts.png by Jeff Patterson from the QElectroTech team (License CC BY-ND 3.0), rendered from the .svg files beside them
ico/scalable/pdf-import.svg by Jeff Patterson from the QElectroTech team (License CC BY-ND 3.0), laid out like ico/22x22/insert-image.png
ico/scalable/diagram.svg, folio-new.svg, folio-delete.svg, folio-properties.svg, label.svg by Jeff Patterson from the QElectroTech team (License CC BY-ND 3.0), the folio icons redrawn in the same style
ico/256x256/* by Nuri from the QElectroTech team (License CC BY-ND 3.0) ico/256x256/* by Nuri from the QElectroTech team (License CC BY-ND 3.0)
ico/breeze-icons/* by Nuri from the QElectroTech team (License CC BY-ND 3.0) ico/breeze-icons/* by Nuri from the QElectroTech team (License CC BY-ND 3.0)
ico/diagram.png by Nuri from the QElectroTech team (License CC BY-ND 3.0) ico/diagram.png by Nuri from the QElectroTech team (License CC BY-ND 3.0)
+16 -25
View File
@@ -148,15 +148,8 @@
<file alias="themes/qet/22x22/conductor-reset.png">22x22/conductor2.png</file> <file alias="themes/qet/22x22/conductor-reset.png">22x22/conductor2.png</file>
<file alias="themes/qet/22x22/configure-toolbars.png">22x22/configure-toolbars.png</file> <file alias="themes/qet/22x22/configure-toolbars.png">22x22/configure-toolbars.png</file>
<file alias="themes/qet/22x22/configure.png">22x22/configure.png</file> <file alias="themes/qet/22x22/configure.png">22x22/configure.png</file>
<file alias="themes/qet/22x22/diagram.png">22x22/diagram.png</file>
<file alias="themes/qet/22x22/diagram_add.png">22x22/diagram_add.png</file>
<file alias="themes/qet/22x22/folio-new.png">22x22/diagram_add.png</file>
<file alias="themes/qet/22x22/diagram_bg.png">22x22/diagram_bg.png</file> <file alias="themes/qet/22x22/diagram_bg.png">22x22/diagram_bg.png</file>
<file alias="themes/qet/22x22/diagram_del.png">22x22/diagram_del.png</file>
<file alias="themes/qet/22x22/folio-delete.png">22x22/diagram_del.png</file>
<file alias="themes/qet/22x22/dialog-cancel.png">22x22/dialog-cancel.png</file> <file alias="themes/qet/22x22/dialog-cancel.png">22x22/dialog-cancel.png</file>
<file alias="themes/qet/22x22/dialog-information.png">22x22/dialog-information.png</file>
<file alias="themes/qet/22x22/folio-properties.png">22x22/dialog-information.png</file>
<file alias="themes/qet/22x22/dialog-ok.png">22x22/dialog-ok.png</file> <file alias="themes/qet/22x22/dialog-ok.png">22x22/dialog-ok.png</file>
<file alias="themes/qet/22x22/document-close.png">22x22/document-close.png</file> <file alias="themes/qet/22x22/document-close.png">22x22/document-close.png</file>
<file alias="themes/qet/22x22/document-export.png">22x22/document-export.png</file> <file alias="themes/qet/22x22/document-export.png">22x22/document-export.png</file>
@@ -207,7 +200,6 @@
<file alias="themes/qet/22x22/guides.png">22x22/guides.png</file> <file alias="themes/qet/22x22/guides.png">22x22/guides.png</file>
<file alias="themes/qet/22x22/hotspot.png">22x22/hotspot.png</file> <file alias="themes/qet/22x22/hotspot.png">22x22/hotspot.png</file>
<file alias="themes/qet/22x22/insert-image.png">22x22/insert-image.png</file> <file alias="themes/qet/22x22/insert-image.png">22x22/insert-image.png</file>
<file alias="themes/qet/22x22/label.png">22x22/label.png</file>
<file alias="themes/qet/22x22/landscape.png">22x22/landscape.png</file> <file alias="themes/qet/22x22/landscape.png">22x22/landscape.png</file>
<file alias="themes/qet/22x22/line.png">22x22/line.png</file> <file alias="themes/qet/22x22/line.png">22x22/line.png</file>
<file alias="themes/qet/22x22/list-add.png">22x22/list-add.png</file> <file alias="themes/qet/22x22/list-add.png">22x22/list-add.png</file>
@@ -218,7 +210,6 @@
<file alias="themes/qet/22x22/object-locked.png">22x22/object-locked.png</file> <file alias="themes/qet/22x22/object-locked.png">22x22/object-locked.png</file>
<file alias="themes/qet/22x22/object-rotate-right.png">22x22/object-rotate-right.png</file> <file alias="themes/qet/22x22/object-rotate-right.png">22x22/object-rotate-right.png</file>
<file alias="themes/qet/22x22/object-unlocked.png">22x22/object-unlocked.png</file> <file alias="themes/qet/22x22/object-unlocked.png">22x22/object-unlocked.png</file>
<file alias="themes/qet/22x22/pdf-import.png">22x22/pdf-import.png</file>
<file alias="themes/qet/22x22/polygon.png">22x22/polygon.png</file> <file alias="themes/qet/22x22/polygon.png">22x22/polygon.png</file>
<file alias="themes/qet/22x22/portrait.png">22x22/portrait.png</file> <file alias="themes/qet/22x22/portrait.png">22x22/portrait.png</file>
<file alias="themes/qet/22x22/preferences-desktop-user.png">22x22/preferences-desktop-user.png</file> <file alias="themes/qet/22x22/preferences-desktop-user.png">22x22/preferences-desktop-user.png</file>
@@ -268,12 +259,20 @@
<file alias="themes/qet/48x48/user-away-extended.png">48x48/user-away-extended.png</file> <file alias="themes/qet/48x48/user-away-extended.png">48x48/user-away-extended.png</file>
<file alias="themes/qet/48x48/user-away.png">48x48/user-away.png</file> <file alias="themes/qet/48x48/user-away.png">48x48/user-away.png</file>
<file alias="themes/qet/48x48/view-pim-journal.png">48x48/view-pim-journal.png</file> <file alias="themes/qet/48x48/view-pim-journal.png">48x48/view-pim-journal.png</file>
<file alias="themes/qet/128x128/configure-shortcuts.png">128x128/configure-shortcuts.png</file>
<file alias="themes/qet/128x128/diagram.png">128x128/diagram.png</file> <file alias="themes/qet/128x128/diagram.png">128x128/diagram.png</file>
<file alias="themes/qet/128x128/document-export.png">128x128/document-export.png</file> <file alias="themes/qet/128x128/document-export.png">128x128/document-export.png</file>
<file alias="themes/qet/128x128/plasmagik.png">128x128/plasmagik.png</file> <file alias="themes/qet/128x128/plasmagik.png">128x128/plasmagik.png</file>
<file alias="themes/qet/128x128/printer.png">128x128/printer.png</file> <file alias="themes/qet/128x128/printer.png">128x128/printer.png</file>
<file alias="themes/qet/128x128/project.png">128x128/project.png</file> <file alias="themes/qet/128x128/project.png">128x128/project.png</file>
<file alias="themes/qet/128x128/settings.png">128x128/settings.png</file> <file alias="themes/qet/128x128/settings.png">128x128/settings.png</file>
<file alias="themes/qet/128x128/terminalstrip.png">128x128/terminalstrip.png</file>
<file alias="themes/qet/scalable/diagram.svg">scalable/diagram.svg</file>
<file alias="themes/qet/scalable/folio-delete.svg">scalable/folio-delete.svg</file>
<file alias="themes/qet/scalable/folio-new.svg">scalable/folio-new.svg</file>
<file alias="themes/qet/scalable/folio-properties.svg">scalable/folio-properties.svg</file>
<file alias="themes/qet/scalable/label.svg">scalable/label.svg</file>
<file alias="themes/qet/scalable/pdf-import.svg">scalable/pdf-import.svg</file>
<file alias="themes/qet/scalable/edit-opacity.svg">breeze-icons/scalable/apps/hidef/edit-opacity.svg</file> <file alias="themes/qet/scalable/edit-opacity.svg">breeze-icons/scalable/apps/hidef/edit-opacity.svg</file>
<file alias="themes/qet/scalable/image-flip-horizontal.svg">breeze-icons/scalable/apps/hidef/image-flip-horizontal-symbolic.svg</file> <file alias="themes/qet/scalable/image-flip-horizontal.svg">breeze-icons/scalable/apps/hidef/image-flip-horizontal-symbolic.svg</file>
<file alias="themes/qet/scalable/image-flip-vertical.svg">breeze-icons/scalable/apps/hidef/image-flip-vertical-symbolic.svg</file> <file alias="themes/qet/scalable/image-flip-vertical.svg">breeze-icons/scalable/apps/hidef/image-flip-vertical-symbolic.svg</file>
@@ -290,7 +289,6 @@
<file>themes/qet-dark/16x16/conductor-edit.png</file> <file>themes/qet-dark/16x16/conductor-edit.png</file>
<file>themes/qet-dark/16x16/configure-toolbars.png</file> <file>themes/qet-dark/16x16/configure-toolbars.png</file>
<file>themes/qet-dark/16x16/configure.png</file> <file>themes/qet-dark/16x16/configure.png</file>
<file>themes/qet-dark/16x16/diagram.png</file>
<file>themes/qet-dark/16x16/dialog-cancel.png</file> <file>themes/qet-dark/16x16/dialog-cancel.png</file>
<file>themes/qet-dark/16x16/dialog-ok.png</file> <file>themes/qet-dark/16x16/dialog-ok.png</file>
<file>themes/qet-dark/16x16/document-export.png</file> <file>themes/qet-dark/16x16/document-export.png</file>
@@ -331,9 +329,6 @@
<file>themes/qet-dark/16x16/folder-open.png</file> <file>themes/qet-dark/16x16/folder-open.png</file>
<file>themes/qet-dark/16x16/folder-show-all.png</file> <file>themes/qet-dark/16x16/folder-show-all.png</file>
<file>themes/qet-dark/16x16/folder.png</file> <file>themes/qet-dark/16x16/folder.png</file>
<file>themes/qet-dark/16x16/folio-delete.png</file>
<file>themes/qet-dark/16x16/folio-new.png</file>
<file>themes/qet-dark/16x16/folio-properties.png</file>
<file>themes/qet-dark/16x16/folio-ref-coming.png</file> <file>themes/qet-dark/16x16/folio-ref-coming.png</file>
<file>themes/qet-dark/16x16/go-company.png</file> <file>themes/qet-dark/16x16/go-company.png</file>
<file>themes/qet-dark/16x16/go-down-double.png</file> <file>themes/qet-dark/16x16/go-down-double.png</file>
@@ -348,7 +343,6 @@
<file>themes/qet-dark/16x16/item-copy.png</file> <file>themes/qet-dark/16x16/item-copy.png</file>
<file>themes/qet-dark/16x16/item-move.png</file> <file>themes/qet-dark/16x16/item-move.png</file>
<file>themes/qet-dark/16x16/kdenlive-show-video.png</file> <file>themes/qet-dark/16x16/kdenlive-show-video.png</file>
<file>themes/qet-dark/16x16/label.png</file>
<file>themes/qet-dark/16x16/list-add.png</file> <file>themes/qet-dark/16x16/list-add.png</file>
<file>themes/qet-dark/16x16/list-remove.png</file> <file>themes/qet-dark/16x16/list-remove.png</file>
<file>themes/qet-dark/16x16/masquer.png</file> <file>themes/qet-dark/16x16/masquer.png</file>
@@ -381,15 +375,7 @@
<file>themes/qet-dark/22x22/arrow-right.png</file> <file>themes/qet-dark/22x22/arrow-right.png</file>
<file>themes/qet-dark/22x22/configure-toolbars.png</file> <file>themes/qet-dark/22x22/configure-toolbars.png</file>
<file>themes/qet-dark/22x22/configure.png</file> <file>themes/qet-dark/22x22/configure.png</file>
<file>themes/qet-dark/22x22/diagram.png</file>
<file>themes/qet-dark/22x22/diagram_add.png</file>
<file>themes/qet-dark/22x22/folio-new.png</file>
<file>themes/qet-dark/22x22/diagram_bg.png</file>
<file>themes/qet-dark/22x22/diagram_del.png</file>
<file>themes/qet-dark/22x22/folio-delete.png</file>
<file>themes/qet-dark/22x22/dialog-cancel.png</file> <file>themes/qet-dark/22x22/dialog-cancel.png</file>
<file>themes/qet-dark/22x22/dialog-information.png</file>
<file>themes/qet-dark/22x22/folio-properties.png</file>
<file>themes/qet-dark/22x22/dialog-ok.png</file> <file>themes/qet-dark/22x22/dialog-ok.png</file>
<file>themes/qet-dark/22x22/document-export.png</file> <file>themes/qet-dark/22x22/document-export.png</file>
<file>themes/qet-dark/22x22/document-import.png</file> <file>themes/qet-dark/22x22/document-import.png</file>
@@ -425,7 +411,6 @@
<file>themes/qet-dark/22x22/go-up.png</file> <file>themes/qet-dark/22x22/go-up.png</file>
<file>themes/qet-dark/22x22/grid.png</file> <file>themes/qet-dark/22x22/grid.png</file>
<file>themes/qet-dark/22x22/insert-image.png</file> <file>themes/qet-dark/22x22/insert-image.png</file>
<file>themes/qet-dark/22x22/label.png</file>
<file>themes/qet-dark/22x22/landscape.png</file> <file>themes/qet-dark/22x22/landscape.png</file>
<file>themes/qet-dark/22x22/line.png</file> <file>themes/qet-dark/22x22/line.png</file>
<file>themes/qet-dark/22x22/list-add.png</file> <file>themes/qet-dark/22x22/list-add.png</file>
@@ -436,7 +421,6 @@
<file>themes/qet-dark/22x22/object-locked.png</file> <file>themes/qet-dark/22x22/object-locked.png</file>
<file>themes/qet-dark/22x22/object-rotate-right.png</file> <file>themes/qet-dark/22x22/object-rotate-right.png</file>
<file>themes/qet-dark/22x22/object-unlocked.png</file> <file>themes/qet-dark/22x22/object-unlocked.png</file>
<file>themes/qet-dark/22x22/pdf-import.png</file>
<file>themes/qet-dark/22x22/polygon.png</file> <file>themes/qet-dark/22x22/polygon.png</file>
<file>themes/qet-dark/22x22/portrait.png</file> <file>themes/qet-dark/22x22/portrait.png</file>
<file>themes/qet-dark/22x22/preferences-desktop-user.png</file> <file>themes/qet-dark/22x22/preferences-desktop-user.png</file>
@@ -472,8 +456,13 @@
<file>themes/qet-dark/48x48/user-away.png</file> <file>themes/qet-dark/48x48/user-away.png</file>
<file>themes/qet-dark/48x48/view-pim-journal.png</file> <file>themes/qet-dark/48x48/view-pim-journal.png</file>
<file>themes/qet-dark/128x128/plasmagik.png</file> <file>themes/qet-dark/128x128/plasmagik.png</file>
<file>themes/qet-dark/128x128/printer.png</file>
<file>themes/qet-dark/128x128/settings.png</file> <file>themes/qet-dark/128x128/settings.png</file>
<file>themes/qet-dark/scalable/diagram.svg</file>
<file>themes/qet-dark/scalable/folio-delete.svg</file>
<file>themes/qet-dark/scalable/folio-new.svg</file>
<file>themes/qet-dark/scalable/folio-properties.svg</file>
<file>themes/qet-dark/scalable/label.svg</file>
<file>themes/qet-dark/scalable/pdf-import.svg</file>
<file>themes/qet-dark/scalable/edit-opacity.svg</file> <file>themes/qet-dark/scalable/edit-opacity.svg</file>
<file>themes/qet-dark/scalable/image-flip-horizontal.svg</file> <file>themes/qet-dark/scalable/image-flip-horizontal.svg</file>
<file>themes/qet-dark/scalable/image-flip-vertical.svg</file> <file>themes/qet-dark/scalable/image-flip-vertical.svg</file>
@@ -482,5 +471,7 @@
<file>themes/qet-dark/scalable/ellipse-to-bezier.svg</file> <file>themes/qet-dark/scalable/ellipse-to-bezier.svg</file>
<file>themes/qet-dark/scalable/rect-to-bezier.svg</file> <file>themes/qet-dark/scalable/rect-to-bezier.svg</file>
<file>themes/qet-dark/scalable/rect-to-polyline.svg</file> <file>themes/qet-dark/scalable/rect-to-polyline.svg</file>
<file alias="themes/qet-dark/128x128/document-export.png">128x128/document-export.png</file>
<file alias="themes/qet-dark/128x128/printer.png">128x128/printer.png</file>
</qresource> </qresource>
</RCC> </RCC>
+12
View File
@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<g transform="translate(1,1)">
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="M 3 5 h 1 v 1 h -1 Z M 3 6 h 1 v 1 h -1 Z M 3 7 h 1 v 1 h -1 Z M 3 8 h 1 v 1 h -1 Z M 3 9 h 1 v 1 h -1 Z M 3 10 h 1 v 1 h -1 Z M 3 11 h 1 v 1 h -1 Z M 3 12 h 1 v 1 h -1 Z M 3 13 h 1 v 1 h -1 Z M 3 14 h 1 v 1 h -1 Z M 3 15 h 1 v 1 h -1 Z M 3 16 h 1 v 1 h -1 Z M 4 5 h 1 v 1 h -1 Z M 4 13 h 1 v 1 h -1 Z M 4 16 h 1 v 1 h -1 Z M 5 5 h 1 v 1 h -1 Z M 5 13 h 1 v 1 h -1 Z M 5 16 h 1 v 1 h -1 Z M 6 5 h 1 v 1 h -1 Z M 6 13 h 1 v 1 h -1 Z M 6 16 h 1 v 1 h -1 Z M 7 5 h 1 v 1 h -1 Z M 7 13 h 1 v 1 h -1 Z M 7 16 h 1 v 1 h -1 Z M 8 5 h 1 v 1 h -1 Z M 8 13 h 1 v 1 h -1 Z M 8 16 h 1 v 1 h -1 Z M 9 5 h 1 v 1 h -1 Z M 9 13 h 1 v 1 h -1 Z M 9 16 h 1 v 1 h -1 Z M 10 5 h 1 v 1 h -1 Z M 10 13 h 1 v 1 h -1 Z M 10 16 h 1 v 1 h -1 Z M 11 5 h 1 v 1 h -1 Z M 11 13 h 1 v 1 h -1 Z M 11 14 h 1 v 1 h -1 Z M 11 15 h 1 v 1 h -1 Z M 11 16 h 1 v 1 h -1 Z M 12 5 h 1 v 1 h -1 Z M 12 13 h 1 v 1 h -1 Z M 12 16 h 1 v 1 h -1 Z M 13 5 h 1 v 1 h -1 Z M 13 13 h 1 v 1 h -1 Z M 13 16 h 1 v 1 h -1 Z M 14 5 h 1 v 1 h -1 Z M 14 13 h 1 v 1 h -1 Z M 14 16 h 1 v 1 h -1 Z M 15 5 h 1 v 1 h -1 Z M 15 13 h 1 v 1 h -1 Z M 15 16 h 1 v 1 h -1 Z M 16 5 h 1 v 1 h -1 Z M 16 13 h 1 v 1 h -1 Z M 16 16 h 1 v 1 h -1 Z M 17 5 h 1 v 1 h -1 Z M 17 13 h 1 v 1 h -1 Z M 17 16 h 1 v 1 h -1 Z M 18 5 h 1 v 1 h -1 Z M 18 6 h 1 v 1 h -1 Z M 18 7 h 1 v 1 h -1 Z M 18 8 h 1 v 1 h -1 Z M 18 9 h 1 v 1 h -1 Z M 18 10 h 1 v 1 h -1 Z M 18 11 h 1 v 1 h -1 Z M 18 12 h 1 v 1 h -1 Z M 18 13 h 1 v 1 h -1 Z M 18 14 h 1 v 1 h -1 Z M 18 15 h 1 v 1 h -1 Z M 18 16 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+12
View File
@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<g transform="translate(1,1)">
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="M 3 5 h 1 v 1 h -1 Z M 3 6 h 1 v 1 h -1 Z M 3 7 h 1 v 1 h -1 Z M 3 8 h 1 v 1 h -1 Z M 3 9 h 1 v 1 h -1 Z M 3 10 h 1 v 1 h -1 Z M 3 11 h 1 v 1 h -1 Z M 3 12 h 1 v 1 h -1 Z M 3 13 h 1 v 1 h -1 Z M 3 14 h 1 v 1 h -1 Z M 3 15 h 1 v 1 h -1 Z M 3 16 h 1 v 1 h -1 Z M 4 5 h 1 v 1 h -1 Z M 4 13 h 1 v 1 h -1 Z M 4 16 h 1 v 1 h -1 Z M 5 5 h 1 v 1 h -1 Z M 5 13 h 1 v 1 h -1 Z M 5 16 h 1 v 1 h -1 Z M 6 5 h 1 v 1 h -1 Z M 6 13 h 1 v 1 h -1 Z M 6 16 h 1 v 1 h -1 Z M 7 5 h 1 v 1 h -1 Z M 7 13 h 1 v 1 h -1 Z M 7 16 h 1 v 1 h -1 Z M 8 5 h 1 v 1 h -1 Z M 8 13 h 1 v 1 h -1 Z M 8 16 h 1 v 1 h -1 Z M 9 5 h 1 v 1 h -1 Z M 9 13 h 1 v 1 h -1 Z M 9 16 h 1 v 1 h -1 Z M 10 5 h 1 v 1 h -1 Z M 10 13 h 1 v 1 h -1 Z M 10 16 h 1 v 1 h -1 Z M 11 5 h 1 v 1 h -1 Z M 11 13 h 1 v 1 h -1 Z M 11 14 h 1 v 1 h -1 Z M 11 15 h 1 v 1 h -1 Z M 11 16 h 1 v 1 h -1 Z M 12 5 h 1 v 1 h -1 Z M 12 13 h 1 v 1 h -1 Z M 12 16 h 1 v 1 h -1 Z M 13 5 h 1 v 1 h -1 Z M 14 5 h 1 v 1 h -1 Z M 14 17 h 1 v 1 h -1 Z M 15 5 h 1 v 1 h -1 Z M 15 17 h 1 v 1 h -1 Z M 16 5 h 1 v 1 h -1 Z M 16 17 h 1 v 1 h -1 Z M 17 5 h 1 v 1 h -1 Z M 17 17 h 1 v 1 h -1 Z M 18 5 h 1 v 1 h -1 Z M 18 6 h 1 v 1 h -1 Z M 18 7 h 1 v 1 h -1 Z M 18 8 h 1 v 1 h -1 Z M 18 9 h 1 v 1 h -1 Z M 18 10 h 1 v 1 h -1 Z M 18 11 h 1 v 1 h -1 Z M 18 12 h 1 v 1 h -1 Z M 18 17 h 1 v 1 h -1 Z M 19 17 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+12
View File
@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<g transform="translate(1,1)">
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="M 3 5 h 1 v 1 h -1 Z M 3 6 h 1 v 1 h -1 Z M 3 7 h 1 v 1 h -1 Z M 3 8 h 1 v 1 h -1 Z M 3 9 h 1 v 1 h -1 Z M 3 10 h 1 v 1 h -1 Z M 3 11 h 1 v 1 h -1 Z M 3 12 h 1 v 1 h -1 Z M 3 13 h 1 v 1 h -1 Z M 3 14 h 1 v 1 h -1 Z M 3 15 h 1 v 1 h -1 Z M 3 16 h 1 v 1 h -1 Z M 4 5 h 1 v 1 h -1 Z M 4 13 h 1 v 1 h -1 Z M 4 16 h 1 v 1 h -1 Z M 5 5 h 1 v 1 h -1 Z M 5 13 h 1 v 1 h -1 Z M 5 16 h 1 v 1 h -1 Z M 6 5 h 1 v 1 h -1 Z M 6 13 h 1 v 1 h -1 Z M 6 16 h 1 v 1 h -1 Z M 7 5 h 1 v 1 h -1 Z M 7 13 h 1 v 1 h -1 Z M 7 16 h 1 v 1 h -1 Z M 8 5 h 1 v 1 h -1 Z M 8 13 h 1 v 1 h -1 Z M 8 16 h 1 v 1 h -1 Z M 9 5 h 1 v 1 h -1 Z M 9 13 h 1 v 1 h -1 Z M 9 16 h 1 v 1 h -1 Z M 10 5 h 1 v 1 h -1 Z M 10 13 h 1 v 1 h -1 Z M 10 16 h 1 v 1 h -1 Z M 11 5 h 1 v 1 h -1 Z M 11 13 h 1 v 1 h -1 Z M 11 14 h 1 v 1 h -1 Z M 11 15 h 1 v 1 h -1 Z M 11 16 h 1 v 1 h -1 Z M 12 5 h 1 v 1 h -1 Z M 12 13 h 1 v 1 h -1 Z M 12 16 h 1 v 1 h -1 Z M 13 5 h 1 v 1 h -1 Z M 14 5 h 1 v 1 h -1 Z M 14 17 h 1 v 1 h -1 Z M 15 5 h 1 v 1 h -1 Z M 15 17 h 1 v 1 h -1 Z M 16 5 h 1 v 1 h -1 Z M 16 17 h 1 v 1 h -1 Z M 17 5 h 1 v 1 h -1 Z M 17 14 h 1 v 1 h -1 Z M 17 15 h 1 v 1 h -1 Z M 17 16 h 1 v 1 h -1 Z M 17 17 h 1 v 1 h -1 Z M 17 18 h 1 v 1 h -1 Z M 17 19 h 1 v 1 h -1 Z M 18 5 h 1 v 1 h -1 Z M 18 6 h 1 v 1 h -1 Z M 18 7 h 1 v 1 h -1 Z M 18 8 h 1 v 1 h -1 Z M 18 9 h 1 v 1 h -1 Z M 18 10 h 1 v 1 h -1 Z M 18 11 h 1 v 1 h -1 Z M 18 12 h 1 v 1 h -1 Z M 18 17 h 1 v 1 h -1 Z M 19 17 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+12
View File
@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<g transform="translate(1,1)">
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="M 3 5 h 1 v 1 h -1 Z M 3 6 h 1 v 1 h -1 Z M 3 7 h 1 v 1 h -1 Z M 3 8 h 1 v 1 h -1 Z M 3 9 h 1 v 1 h -1 Z M 3 10 h 1 v 1 h -1 Z M 3 11 h 1 v 1 h -1 Z M 3 12 h 1 v 1 h -1 Z M 3 13 h 1 v 1 h -1 Z M 3 14 h 1 v 1 h -1 Z M 3 15 h 1 v 1 h -1 Z M 3 16 h 1 v 1 h -1 Z M 4 5 h 1 v 1 h -1 Z M 4 13 h 1 v 1 h -1 Z M 4 16 h 1 v 1 h -1 Z M 5 5 h 1 v 1 h -1 Z M 5 13 h 1 v 1 h -1 Z M 5 16 h 1 v 1 h -1 Z M 6 5 h 1 v 1 h -1 Z M 6 8 h 1 v 1 h -1 Z M 6 10 h 1 v 1 h -1 Z M 6 13 h 1 v 1 h -1 Z M 6 16 h 1 v 1 h -1 Z M 7 5 h 1 v 1 h -1 Z M 7 8 h 1 v 1 h -1 Z M 7 10 h 1 v 1 h -1 Z M 7 13 h 1 v 1 h -1 Z M 7 16 h 1 v 1 h -1 Z M 8 5 h 1 v 1 h -1 Z M 8 8 h 1 v 1 h -1 Z M 8 10 h 1 v 1 h -1 Z M 8 13 h 1 v 1 h -1 Z M 8 16 h 1 v 1 h -1 Z M 9 5 h 1 v 1 h -1 Z M 9 8 h 1 v 1 h -1 Z M 9 10 h 1 v 1 h -1 Z M 9 13 h 1 v 1 h -1 Z M 9 16 h 1 v 1 h -1 Z M 10 5 h 1 v 1 h -1 Z M 10 8 h 1 v 1 h -1 Z M 10 10 h 1 v 1 h -1 Z M 10 13 h 1 v 1 h -1 Z M 10 16 h 1 v 1 h -1 Z M 11 5 h 1 v 1 h -1 Z M 11 8 h 1 v 1 h -1 Z M 11 10 h 1 v 1 h -1 Z M 11 13 h 1 v 1 h -1 Z M 11 14 h 1 v 1 h -1 Z M 11 15 h 1 v 1 h -1 Z M 11 16 h 1 v 1 h -1 Z M 12 5 h 1 v 1 h -1 Z M 12 8 h 1 v 1 h -1 Z M 12 10 h 1 v 1 h -1 Z M 12 13 h 1 v 1 h -1 Z M 12 16 h 1 v 1 h -1 Z M 13 5 h 1 v 1 h -1 Z M 13 8 h 1 v 1 h -1 Z M 13 10 h 1 v 1 h -1 Z M 13 13 h 1 v 1 h -1 Z M 13 16 h 1 v 1 h -1 Z M 14 5 h 1 v 1 h -1 Z M 14 8 h 1 v 1 h -1 Z M 14 10 h 1 v 1 h -1 Z M 14 13 h 1 v 1 h -1 Z M 14 16 h 1 v 1 h -1 Z M 15 5 h 1 v 1 h -1 Z M 15 8 h 1 v 1 h -1 Z M 15 10 h 1 v 1 h -1 Z M 15 13 h 1 v 1 h -1 Z M 15 16 h 1 v 1 h -1 Z M 16 5 h 1 v 1 h -1 Z M 16 13 h 1 v 1 h -1 Z M 16 16 h 1 v 1 h -1 Z M 17 5 h 1 v 1 h -1 Z M 17 13 h 1 v 1 h -1 Z M 17 16 h 1 v 1 h -1 Z M 18 5 h 1 v 1 h -1 Z M 18 6 h 1 v 1 h -1 Z M 18 7 h 1 v 1 h -1 Z M 18 8 h 1 v 1 h -1 Z M 18 9 h 1 v 1 h -1 Z M 18 10 h 1 v 1 h -1 Z M 18 11 h 1 v 1 h -1 Z M 18 12 h 1 v 1 h -1 Z M 18 13 h 1 v 1 h -1 Z M 18 14 h 1 v 1 h -1 Z M 18 15 h 1 v 1 h -1 Z M 18 16 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+12
View File
@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<g transform="translate(1,1)">
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="M 3 5 h 1 v 1 h -1 Z M 3 6 h 1 v 1 h -1 Z M 3 7 h 1 v 1 h -1 Z M 3 8 h 1 v 1 h -1 Z M 3 9 h 1 v 1 h -1 Z M 3 10 h 1 v 1 h -1 Z M 3 11 h 1 v 1 h -1 Z M 3 12 h 1 v 1 h -1 Z M 3 13 h 1 v 1 h -1 Z M 3 14 h 1 v 1 h -1 Z M 3 15 h 1 v 1 h -1 Z M 3 16 h 1 v 1 h -1 Z M 4 5 h 1 v 1 h -1 Z M 4 13 h 1 v 1 h -1 Z M 4 14 h 1 v 1 h -1 Z M 4 15 h 1 v 1 h -1 Z M 4 16 h 1 v 1 h -1 Z M 5 5 h 1 v 1 h -1 Z M 5 13 h 1 v 1 h -1 Z M 5 14 h 1 v 1 h -1 Z M 5 15 h 1 v 1 h -1 Z M 5 16 h 1 v 1 h -1 Z M 6 5 h 1 v 1 h -1 Z M 6 13 h 1 v 1 h -1 Z M 6 14 h 1 v 1 h -1 Z M 6 15 h 1 v 1 h -1 Z M 6 16 h 1 v 1 h -1 Z M 7 5 h 1 v 1 h -1 Z M 7 13 h 1 v 1 h -1 Z M 7 14 h 1 v 1 h -1 Z M 7 15 h 1 v 1 h -1 Z M 7 16 h 1 v 1 h -1 Z M 8 5 h 1 v 1 h -1 Z M 8 13 h 1 v 1 h -1 Z M 8 14 h 1 v 1 h -1 Z M 8 15 h 1 v 1 h -1 Z M 8 16 h 1 v 1 h -1 Z M 9 5 h 1 v 1 h -1 Z M 9 13 h 1 v 1 h -1 Z M 9 14 h 1 v 1 h -1 Z M 9 15 h 1 v 1 h -1 Z M 9 16 h 1 v 1 h -1 Z M 10 5 h 1 v 1 h -1 Z M 10 13 h 1 v 1 h -1 Z M 10 14 h 1 v 1 h -1 Z M 10 15 h 1 v 1 h -1 Z M 10 16 h 1 v 1 h -1 Z M 11 5 h 1 v 1 h -1 Z M 11 13 h 1 v 1 h -1 Z M 11 14 h 1 v 1 h -1 Z M 11 15 h 1 v 1 h -1 Z M 11 16 h 1 v 1 h -1 Z M 12 5 h 1 v 1 h -1 Z M 12 13 h 1 v 1 h -1 Z M 12 14 h 1 v 1 h -1 Z M 12 15 h 1 v 1 h -1 Z M 12 16 h 1 v 1 h -1 Z M 13 5 h 1 v 1 h -1 Z M 13 13 h 1 v 1 h -1 Z M 13 14 h 1 v 1 h -1 Z M 13 15 h 1 v 1 h -1 Z M 13 16 h 1 v 1 h -1 Z M 14 5 h 1 v 1 h -1 Z M 14 13 h 1 v 1 h -1 Z M 14 14 h 1 v 1 h -1 Z M 14 15 h 1 v 1 h -1 Z M 14 16 h 1 v 1 h -1 Z M 15 5 h 1 v 1 h -1 Z M 15 13 h 1 v 1 h -1 Z M 15 14 h 1 v 1 h -1 Z M 15 15 h 1 v 1 h -1 Z M 15 16 h 1 v 1 h -1 Z M 16 5 h 1 v 1 h -1 Z M 16 13 h 1 v 1 h -1 Z M 16 14 h 1 v 1 h -1 Z M 16 15 h 1 v 1 h -1 Z M 16 16 h 1 v 1 h -1 Z M 17 5 h 1 v 1 h -1 Z M 17 13 h 1 v 1 h -1 Z M 17 14 h 1 v 1 h -1 Z M 17 15 h 1 v 1 h -1 Z M 17 16 h 1 v 1 h -1 Z M 18 5 h 1 v 1 h -1 Z M 18 6 h 1 v 1 h -1 Z M 18 7 h 1 v 1 h -1 Z M 18 8 h 1 v 1 h -1 Z M 18 9 h 1 v 1 h -1 Z M 18 10 h 1 v 1 h -1 Z M 18 11 h 1 v 1 h -1 Z M 18 12 h 1 v 1 h -1 Z M 18 13 h 1 v 1 h -1 Z M 18 14 h 1 v 1 h -1 Z M 18 15 h 1 v 1 h -1 Z M 18 16 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#4d4d4d;
}
</style>
</defs>
<g transform="translate(1,1)">
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="M 3 3 h 16 v 1 h -16 Z M 3 4 h 1 v 15 h -1 Z M 4 18 h 10 v 1 h -10 Z M 18 4 h 1 v 8 h -1 Z M 16 14 h 1 v 5 h -1 Z M 14 16 h 5 v 1 h -5 Z" class="ColorScheme-Text"/>
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="M 5 7 h 1 v 1 h -1 Z M 6 7 h 1 v 1 h -1 Z M 7 7 h 1 v 1 h -1 Z M 5 8 h 1 v 1 h -1 Z M 7 8 h 1 v 1 h -1 Z M 5 9 h 1 v 1 h -1 Z M 6 9 h 1 v 1 h -1 Z M 7 9 h 1 v 1 h -1 Z M 5 10 h 1 v 1 h -1 Z M 5 11 h 1 v 1 h -1 Z M 9 7 h 1 v 1 h -1 Z M 10 7 h 1 v 1 h -1 Z M 9 8 h 1 v 1 h -1 Z M 11 8 h 1 v 1 h -1 Z M 9 9 h 1 v 1 h -1 Z M 11 9 h 1 v 1 h -1 Z M 9 10 h 1 v 1 h -1 Z M 11 10 h 1 v 1 h -1 Z M 9 11 h 1 v 1 h -1 Z M 10 11 h 1 v 1 h -1 Z M 13 7 h 1 v 1 h -1 Z M 14 7 h 1 v 1 h -1 Z M 15 7 h 1 v 1 h -1 Z M 13 8 h 1 v 1 h -1 Z M 13 9 h 1 v 1 h -1 Z M 14 9 h 1 v 1 h -1 Z M 13 10 h 1 v 1 h -1 Z M 13 11 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 667 B

+12
View File
@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#dcdcdc;
}
</style>
</defs>
<g transform="translate(1,1)">
<path style="fill:#dcdcdc;fill-opacity:1;stroke:none" d="M 3 5 h 1 v 1 h -1 Z M 3 6 h 1 v 1 h -1 Z M 3 7 h 1 v 1 h -1 Z M 3 8 h 1 v 1 h -1 Z M 3 9 h 1 v 1 h -1 Z M 3 10 h 1 v 1 h -1 Z M 3 11 h 1 v 1 h -1 Z M 3 12 h 1 v 1 h -1 Z M 3 13 h 1 v 1 h -1 Z M 3 14 h 1 v 1 h -1 Z M 3 15 h 1 v 1 h -1 Z M 3 16 h 1 v 1 h -1 Z M 4 5 h 1 v 1 h -1 Z M 4 13 h 1 v 1 h -1 Z M 4 16 h 1 v 1 h -1 Z M 5 5 h 1 v 1 h -1 Z M 5 13 h 1 v 1 h -1 Z M 5 16 h 1 v 1 h -1 Z M 6 5 h 1 v 1 h -1 Z M 6 13 h 1 v 1 h -1 Z M 6 16 h 1 v 1 h -1 Z M 7 5 h 1 v 1 h -1 Z M 7 13 h 1 v 1 h -1 Z M 7 16 h 1 v 1 h -1 Z M 8 5 h 1 v 1 h -1 Z M 8 13 h 1 v 1 h -1 Z M 8 16 h 1 v 1 h -1 Z M 9 5 h 1 v 1 h -1 Z M 9 13 h 1 v 1 h -1 Z M 9 16 h 1 v 1 h -1 Z M 10 5 h 1 v 1 h -1 Z M 10 13 h 1 v 1 h -1 Z M 10 16 h 1 v 1 h -1 Z M 11 5 h 1 v 1 h -1 Z M 11 13 h 1 v 1 h -1 Z M 11 14 h 1 v 1 h -1 Z M 11 15 h 1 v 1 h -1 Z M 11 16 h 1 v 1 h -1 Z M 12 5 h 1 v 1 h -1 Z M 12 13 h 1 v 1 h -1 Z M 12 16 h 1 v 1 h -1 Z M 13 5 h 1 v 1 h -1 Z M 13 13 h 1 v 1 h -1 Z M 13 16 h 1 v 1 h -1 Z M 14 5 h 1 v 1 h -1 Z M 14 13 h 1 v 1 h -1 Z M 14 16 h 1 v 1 h -1 Z M 15 5 h 1 v 1 h -1 Z M 15 13 h 1 v 1 h -1 Z M 15 16 h 1 v 1 h -1 Z M 16 5 h 1 v 1 h -1 Z M 16 13 h 1 v 1 h -1 Z M 16 16 h 1 v 1 h -1 Z M 17 5 h 1 v 1 h -1 Z M 17 13 h 1 v 1 h -1 Z M 17 16 h 1 v 1 h -1 Z M 18 5 h 1 v 1 h -1 Z M 18 6 h 1 v 1 h -1 Z M 18 7 h 1 v 1 h -1 Z M 18 8 h 1 v 1 h -1 Z M 18 9 h 1 v 1 h -1 Z M 18 10 h 1 v 1 h -1 Z M 18 11 h 1 v 1 h -1 Z M 18 12 h 1 v 1 h -1 Z M 18 13 h 1 v 1 h -1 Z M 18 14 h 1 v 1 h -1 Z M 18 15 h 1 v 1 h -1 Z M 18 16 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#dcdcdc;
}
</style>
</defs>
<g transform="translate(1,1)">
<path style="fill:#dcdcdc;fill-opacity:1;stroke:none" d="M 3 5 h 1 v 1 h -1 Z M 3 6 h 1 v 1 h -1 Z M 3 7 h 1 v 1 h -1 Z M 3 8 h 1 v 1 h -1 Z M 3 9 h 1 v 1 h -1 Z M 3 10 h 1 v 1 h -1 Z M 3 11 h 1 v 1 h -1 Z M 3 12 h 1 v 1 h -1 Z M 3 13 h 1 v 1 h -1 Z M 3 14 h 1 v 1 h -1 Z M 3 15 h 1 v 1 h -1 Z M 3 16 h 1 v 1 h -1 Z M 4 5 h 1 v 1 h -1 Z M 4 13 h 1 v 1 h -1 Z M 4 16 h 1 v 1 h -1 Z M 5 5 h 1 v 1 h -1 Z M 5 13 h 1 v 1 h -1 Z M 5 16 h 1 v 1 h -1 Z M 6 5 h 1 v 1 h -1 Z M 6 13 h 1 v 1 h -1 Z M 6 16 h 1 v 1 h -1 Z M 7 5 h 1 v 1 h -1 Z M 7 13 h 1 v 1 h -1 Z M 7 16 h 1 v 1 h -1 Z M 8 5 h 1 v 1 h -1 Z M 8 13 h 1 v 1 h -1 Z M 8 16 h 1 v 1 h -1 Z M 9 5 h 1 v 1 h -1 Z M 9 13 h 1 v 1 h -1 Z M 9 16 h 1 v 1 h -1 Z M 10 5 h 1 v 1 h -1 Z M 10 13 h 1 v 1 h -1 Z M 10 16 h 1 v 1 h -1 Z M 11 5 h 1 v 1 h -1 Z M 11 13 h 1 v 1 h -1 Z M 11 14 h 1 v 1 h -1 Z M 11 15 h 1 v 1 h -1 Z M 11 16 h 1 v 1 h -1 Z M 12 5 h 1 v 1 h -1 Z M 12 13 h 1 v 1 h -1 Z M 12 16 h 1 v 1 h -1 Z M 13 5 h 1 v 1 h -1 Z M 14 5 h 1 v 1 h -1 Z M 14 17 h 1 v 1 h -1 Z M 15 5 h 1 v 1 h -1 Z M 15 17 h 1 v 1 h -1 Z M 16 5 h 1 v 1 h -1 Z M 16 17 h 1 v 1 h -1 Z M 17 5 h 1 v 1 h -1 Z M 17 17 h 1 v 1 h -1 Z M 18 5 h 1 v 1 h -1 Z M 18 6 h 1 v 1 h -1 Z M 18 7 h 1 v 1 h -1 Z M 18 8 h 1 v 1 h -1 Z M 18 9 h 1 v 1 h -1 Z M 18 10 h 1 v 1 h -1 Z M 18 11 h 1 v 1 h -1 Z M 18 12 h 1 v 1 h -1 Z M 18 17 h 1 v 1 h -1 Z M 19 17 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#dcdcdc;
}
</style>
</defs>
<g transform="translate(1,1)">
<path style="fill:#dcdcdc;fill-opacity:1;stroke:none" d="M 3 5 h 1 v 1 h -1 Z M 3 6 h 1 v 1 h -1 Z M 3 7 h 1 v 1 h -1 Z M 3 8 h 1 v 1 h -1 Z M 3 9 h 1 v 1 h -1 Z M 3 10 h 1 v 1 h -1 Z M 3 11 h 1 v 1 h -1 Z M 3 12 h 1 v 1 h -1 Z M 3 13 h 1 v 1 h -1 Z M 3 14 h 1 v 1 h -1 Z M 3 15 h 1 v 1 h -1 Z M 3 16 h 1 v 1 h -1 Z M 4 5 h 1 v 1 h -1 Z M 4 13 h 1 v 1 h -1 Z M 4 16 h 1 v 1 h -1 Z M 5 5 h 1 v 1 h -1 Z M 5 13 h 1 v 1 h -1 Z M 5 16 h 1 v 1 h -1 Z M 6 5 h 1 v 1 h -1 Z M 6 13 h 1 v 1 h -1 Z M 6 16 h 1 v 1 h -1 Z M 7 5 h 1 v 1 h -1 Z M 7 13 h 1 v 1 h -1 Z M 7 16 h 1 v 1 h -1 Z M 8 5 h 1 v 1 h -1 Z M 8 13 h 1 v 1 h -1 Z M 8 16 h 1 v 1 h -1 Z M 9 5 h 1 v 1 h -1 Z M 9 13 h 1 v 1 h -1 Z M 9 16 h 1 v 1 h -1 Z M 10 5 h 1 v 1 h -1 Z M 10 13 h 1 v 1 h -1 Z M 10 16 h 1 v 1 h -1 Z M 11 5 h 1 v 1 h -1 Z M 11 13 h 1 v 1 h -1 Z M 11 14 h 1 v 1 h -1 Z M 11 15 h 1 v 1 h -1 Z M 11 16 h 1 v 1 h -1 Z M 12 5 h 1 v 1 h -1 Z M 12 13 h 1 v 1 h -1 Z M 12 16 h 1 v 1 h -1 Z M 13 5 h 1 v 1 h -1 Z M 14 5 h 1 v 1 h -1 Z M 14 17 h 1 v 1 h -1 Z M 15 5 h 1 v 1 h -1 Z M 15 17 h 1 v 1 h -1 Z M 16 5 h 1 v 1 h -1 Z M 16 17 h 1 v 1 h -1 Z M 17 5 h 1 v 1 h -1 Z M 17 14 h 1 v 1 h -1 Z M 17 15 h 1 v 1 h -1 Z M 17 16 h 1 v 1 h -1 Z M 17 17 h 1 v 1 h -1 Z M 17 18 h 1 v 1 h -1 Z M 17 19 h 1 v 1 h -1 Z M 18 5 h 1 v 1 h -1 Z M 18 6 h 1 v 1 h -1 Z M 18 7 h 1 v 1 h -1 Z M 18 8 h 1 v 1 h -1 Z M 18 9 h 1 v 1 h -1 Z M 18 10 h 1 v 1 h -1 Z M 18 11 h 1 v 1 h -1 Z M 18 12 h 1 v 1 h -1 Z M 18 17 h 1 v 1 h -1 Z M 19 17 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#dcdcdc;
}
</style>
</defs>
<g transform="translate(1,1)">
<path style="fill:#dcdcdc;fill-opacity:1;stroke:none" d="M 3 5 h 1 v 1 h -1 Z M 3 6 h 1 v 1 h -1 Z M 3 7 h 1 v 1 h -1 Z M 3 8 h 1 v 1 h -1 Z M 3 9 h 1 v 1 h -1 Z M 3 10 h 1 v 1 h -1 Z M 3 11 h 1 v 1 h -1 Z M 3 12 h 1 v 1 h -1 Z M 3 13 h 1 v 1 h -1 Z M 3 14 h 1 v 1 h -1 Z M 3 15 h 1 v 1 h -1 Z M 3 16 h 1 v 1 h -1 Z M 4 5 h 1 v 1 h -1 Z M 4 13 h 1 v 1 h -1 Z M 4 16 h 1 v 1 h -1 Z M 5 5 h 1 v 1 h -1 Z M 5 13 h 1 v 1 h -1 Z M 5 16 h 1 v 1 h -1 Z M 6 5 h 1 v 1 h -1 Z M 6 8 h 1 v 1 h -1 Z M 6 10 h 1 v 1 h -1 Z M 6 13 h 1 v 1 h -1 Z M 6 16 h 1 v 1 h -1 Z M 7 5 h 1 v 1 h -1 Z M 7 8 h 1 v 1 h -1 Z M 7 10 h 1 v 1 h -1 Z M 7 13 h 1 v 1 h -1 Z M 7 16 h 1 v 1 h -1 Z M 8 5 h 1 v 1 h -1 Z M 8 8 h 1 v 1 h -1 Z M 8 10 h 1 v 1 h -1 Z M 8 13 h 1 v 1 h -1 Z M 8 16 h 1 v 1 h -1 Z M 9 5 h 1 v 1 h -1 Z M 9 8 h 1 v 1 h -1 Z M 9 10 h 1 v 1 h -1 Z M 9 13 h 1 v 1 h -1 Z M 9 16 h 1 v 1 h -1 Z M 10 5 h 1 v 1 h -1 Z M 10 8 h 1 v 1 h -1 Z M 10 10 h 1 v 1 h -1 Z M 10 13 h 1 v 1 h -1 Z M 10 16 h 1 v 1 h -1 Z M 11 5 h 1 v 1 h -1 Z M 11 8 h 1 v 1 h -1 Z M 11 10 h 1 v 1 h -1 Z M 11 13 h 1 v 1 h -1 Z M 11 14 h 1 v 1 h -1 Z M 11 15 h 1 v 1 h -1 Z M 11 16 h 1 v 1 h -1 Z M 12 5 h 1 v 1 h -1 Z M 12 8 h 1 v 1 h -1 Z M 12 10 h 1 v 1 h -1 Z M 12 13 h 1 v 1 h -1 Z M 12 16 h 1 v 1 h -1 Z M 13 5 h 1 v 1 h -1 Z M 13 8 h 1 v 1 h -1 Z M 13 10 h 1 v 1 h -1 Z M 13 13 h 1 v 1 h -1 Z M 13 16 h 1 v 1 h -1 Z M 14 5 h 1 v 1 h -1 Z M 14 8 h 1 v 1 h -1 Z M 14 10 h 1 v 1 h -1 Z M 14 13 h 1 v 1 h -1 Z M 14 16 h 1 v 1 h -1 Z M 15 5 h 1 v 1 h -1 Z M 15 8 h 1 v 1 h -1 Z M 15 10 h 1 v 1 h -1 Z M 15 13 h 1 v 1 h -1 Z M 15 16 h 1 v 1 h -1 Z M 16 5 h 1 v 1 h -1 Z M 16 13 h 1 v 1 h -1 Z M 16 16 h 1 v 1 h -1 Z M 17 5 h 1 v 1 h -1 Z M 17 13 h 1 v 1 h -1 Z M 17 16 h 1 v 1 h -1 Z M 18 5 h 1 v 1 h -1 Z M 18 6 h 1 v 1 h -1 Z M 18 7 h 1 v 1 h -1 Z M 18 8 h 1 v 1 h -1 Z M 18 9 h 1 v 1 h -1 Z M 18 10 h 1 v 1 h -1 Z M 18 11 h 1 v 1 h -1 Z M 18 12 h 1 v 1 h -1 Z M 18 13 h 1 v 1 h -1 Z M 18 14 h 1 v 1 h -1 Z M 18 15 h 1 v 1 h -1 Z M 18 16 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+12
View File
@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#dcdcdc;
}
</style>
</defs>
<g transform="translate(1,1)">
<path style="fill:#dcdcdc;fill-opacity:1;stroke:none" d="M 3 5 h 1 v 1 h -1 Z M 3 6 h 1 v 1 h -1 Z M 3 7 h 1 v 1 h -1 Z M 3 8 h 1 v 1 h -1 Z M 3 9 h 1 v 1 h -1 Z M 3 10 h 1 v 1 h -1 Z M 3 11 h 1 v 1 h -1 Z M 3 12 h 1 v 1 h -1 Z M 3 13 h 1 v 1 h -1 Z M 3 14 h 1 v 1 h -1 Z M 3 15 h 1 v 1 h -1 Z M 3 16 h 1 v 1 h -1 Z M 4 5 h 1 v 1 h -1 Z M 4 13 h 1 v 1 h -1 Z M 4 14 h 1 v 1 h -1 Z M 4 15 h 1 v 1 h -1 Z M 4 16 h 1 v 1 h -1 Z M 5 5 h 1 v 1 h -1 Z M 5 13 h 1 v 1 h -1 Z M 5 14 h 1 v 1 h -1 Z M 5 15 h 1 v 1 h -1 Z M 5 16 h 1 v 1 h -1 Z M 6 5 h 1 v 1 h -1 Z M 6 13 h 1 v 1 h -1 Z M 6 14 h 1 v 1 h -1 Z M 6 15 h 1 v 1 h -1 Z M 6 16 h 1 v 1 h -1 Z M 7 5 h 1 v 1 h -1 Z M 7 13 h 1 v 1 h -1 Z M 7 14 h 1 v 1 h -1 Z M 7 15 h 1 v 1 h -1 Z M 7 16 h 1 v 1 h -1 Z M 8 5 h 1 v 1 h -1 Z M 8 13 h 1 v 1 h -1 Z M 8 14 h 1 v 1 h -1 Z M 8 15 h 1 v 1 h -1 Z M 8 16 h 1 v 1 h -1 Z M 9 5 h 1 v 1 h -1 Z M 9 13 h 1 v 1 h -1 Z M 9 14 h 1 v 1 h -1 Z M 9 15 h 1 v 1 h -1 Z M 9 16 h 1 v 1 h -1 Z M 10 5 h 1 v 1 h -1 Z M 10 13 h 1 v 1 h -1 Z M 10 14 h 1 v 1 h -1 Z M 10 15 h 1 v 1 h -1 Z M 10 16 h 1 v 1 h -1 Z M 11 5 h 1 v 1 h -1 Z M 11 13 h 1 v 1 h -1 Z M 11 14 h 1 v 1 h -1 Z M 11 15 h 1 v 1 h -1 Z M 11 16 h 1 v 1 h -1 Z M 12 5 h 1 v 1 h -1 Z M 12 13 h 1 v 1 h -1 Z M 12 14 h 1 v 1 h -1 Z M 12 15 h 1 v 1 h -1 Z M 12 16 h 1 v 1 h -1 Z M 13 5 h 1 v 1 h -1 Z M 13 13 h 1 v 1 h -1 Z M 13 14 h 1 v 1 h -1 Z M 13 15 h 1 v 1 h -1 Z M 13 16 h 1 v 1 h -1 Z M 14 5 h 1 v 1 h -1 Z M 14 13 h 1 v 1 h -1 Z M 14 14 h 1 v 1 h -1 Z M 14 15 h 1 v 1 h -1 Z M 14 16 h 1 v 1 h -1 Z M 15 5 h 1 v 1 h -1 Z M 15 13 h 1 v 1 h -1 Z M 15 14 h 1 v 1 h -1 Z M 15 15 h 1 v 1 h -1 Z M 15 16 h 1 v 1 h -1 Z M 16 5 h 1 v 1 h -1 Z M 16 13 h 1 v 1 h -1 Z M 16 14 h 1 v 1 h -1 Z M 16 15 h 1 v 1 h -1 Z M 16 16 h 1 v 1 h -1 Z M 17 5 h 1 v 1 h -1 Z M 17 13 h 1 v 1 h -1 Z M 17 14 h 1 v 1 h -1 Z M 17 15 h 1 v 1 h -1 Z M 17 16 h 1 v 1 h -1 Z M 18 5 h 1 v 1 h -1 Z M 18 6 h 1 v 1 h -1 Z M 18 7 h 1 v 1 h -1 Z M 18 8 h 1 v 1 h -1 Z M 18 9 h 1 v 1 h -1 Z M 18 10 h 1 v 1 h -1 Z M 18 11 h 1 v 1 h -1 Z M 18 12 h 1 v 1 h -1 Z M 18 13 h 1 v 1 h -1 Z M 18 14 h 1 v 1 h -1 Z M 18 15 h 1 v 1 h -1 Z M 18 16 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<defs id="defs3051">
<style type="text/css" id="current-color-scheme">
.ColorScheme-Text {
color:#dcdcdc;
}
</style>
</defs>
<g transform="translate(1,1)">
<path style="fill:#dcdcdc;fill-opacity:1;stroke:none" d="M 3 3 h 16 v 1 h -16 Z M 3 4 h 1 v 15 h -1 Z M 4 18 h 10 v 1 h -10 Z M 18 4 h 1 v 8 h -1 Z M 16 14 h 1 v 5 h -1 Z M 14 16 h 5 v 1 h -5 Z" class="ColorScheme-Text"/>
<path style="fill:#dcdcdc;fill-opacity:1;stroke:none" d="M 5 7 h 1 v 1 h -1 Z M 6 7 h 1 v 1 h -1 Z M 7 7 h 1 v 1 h -1 Z M 5 8 h 1 v 1 h -1 Z M 7 8 h 1 v 1 h -1 Z M 5 9 h 1 v 1 h -1 Z M 6 9 h 1 v 1 h -1 Z M 7 9 h 1 v 1 h -1 Z M 5 10 h 1 v 1 h -1 Z M 5 11 h 1 v 1 h -1 Z M 9 7 h 1 v 1 h -1 Z M 10 7 h 1 v 1 h -1 Z M 9 8 h 1 v 1 h -1 Z M 11 8 h 1 v 1 h -1 Z M 9 9 h 1 v 1 h -1 Z M 11 9 h 1 v 1 h -1 Z M 9 10 h 1 v 1 h -1 Z M 11 10 h 1 v 1 h -1 Z M 9 11 h 1 v 1 h -1 Z M 10 11 h 1 v 1 h -1 Z M 13 7 h 1 v 1 h -1 Z M 14 7 h 1 v 1 h -1 Z M 15 7 h 1 v 1 h -1 Z M 13 8 h 1 v 1 h -1 Z M 13 9 h 1 v 1 h -1 Z M 14 9 h 1 v 1 h -1 Z M 13 10 h 1 v 1 h -1 Z M 13 11 h 1 v 1 h -1 Z" class="ColorScheme-Text"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+195 -190
View File
File diff suppressed because it is too large Load Diff
+95 -9
View File
@@ -34,10 +34,27 @@ expects, so QIcon::fromTheme("name") finds them. The dark theme only holds
the icons that need a dark variant: black line art. Colored icons are not the icons that need a dark variant: black line art. Colored icons are not
touched; the dark theme inherits them from the light one. touched; the dark theme inherits them from the light one.
Qt inherits by name, not by size: once a name has any file in the dark
theme, the parent theme is never consulted for that name, and a size the
dark theme lacks is served by scaling the nearest dark file. An icon that
is line art at 22 pixels and colored at 128 (the printer) would then come
out as the 22 pixel copy scaled up on a dark palette. So for every name
the dark theme holds, the .qrc also aliases the light files of the sizes
the dark theme does not have, when they read on the dark window (3:1,
measured as tests/qttest/tst_qeticons.cpp does); a light file that does
not is left out, and Qt scales the nearest dark copy as before.
An icon counts as line art when fewer than 20% of its visible pixels are An icon counts as line art when fewer than 20% of its visible pixels are
saturated. Its dark copy keeps hue and alpha and inverts lightness, scaled saturated. Its dark copy keeps hue and alpha and inverts lightness, scaled
so pure black becomes (220, 220, 220), the dark palette's text color. so pure black becomes (220, 220, 220), the dark palette's text color.
An icon drawn as a light object is left alone even when it is not
saturated: a page sheet, a printer body, the white half of the background
swatch. Such art already reads on a dark toolbar, and inverting it would
turn the white body black (the PDF import icon, GitHub #919). The test is
the share of visible pixels that are near white: 30% or more and the icon
inherits from the light theme untouched.
Requires Pillow. Idempotent: running it twice changes nothing. Requires Pillow. Idempotent: running it twice changes nothing.
""" """
@@ -64,16 +81,21 @@ SIZES = ["16x16", "22x22", "32x32", "48x48", "128x128"]
# Table entries in sources/qeticons.cpp that pair a 16 pixel file with a # Table entries in sources/qeticons.cpp that pair a 16 pixel file with a
# 22 pixel file of another name. The theme needs one name per icon, so # 22 pixel file of another name. The theme needs one name per icon, so
# the 22 pixel file is exposed under the 16 pixel name as well. # the 22 pixel file is exposed under the 16 pixel name as well. The folio
# icons that used to be listed here are SVGs now (ico/scalable/).
ALIASES = { ALIASES = {
"22x22/conductor2.png": "conductor-reset", "22x22/conductor2.png": "conductor-reset",
"22x22/diagram_add.png": "folio-new",
"22x22/diagram_del.png": "folio-delete",
"22x22/dialog-information.png": "folio-properties",
} }
# SVG icons referenced from sources/qeticons.cpp. # SVG icons referenced from sources/qeticons.cpp. ico/scalable/ holds the
# ones drawn for QET as vectors; one file serves every size.
SVGS = [ SVGS = [
"scalable/diagram.svg",
"scalable/folio-delete.svg",
"scalable/folio-new.svg",
"scalable/folio-properties.svg",
"scalable/label.svg",
"scalable/pdf-import.svg",
"breeze-icons/scalable/apps/hidef/edit-opacity.svg", "breeze-icons/scalable/apps/hidef/edit-opacity.svg",
"breeze-icons/scalable/apps/hidef/image-flip-horizontal-symbolic.svg", "breeze-icons/scalable/apps/hidef/image-flip-horizontal-symbolic.svg",
"breeze-icons/scalable/apps/hidef/image-flip-vertical-symbolic.svg", "breeze-icons/scalable/apps/hidef/image-flip-vertical-symbolic.svg",
@@ -85,8 +107,12 @@ SVGS = [
] ]
SATURATED_FRACTION = 0.20 # at or above this an icon is "colored" SATURATED_FRACTION = 0.20 # at or above this an icon is "colored"
WHITE_LIGHTNESS = 0.85 # a visible pixel this light counts as white
WHITE_FRACTION = 0.30 # at or above this an icon is "light art"
INK = 220 / 255.0 # lightness of pure black after inversion INK = 220 / 255.0 # lightness of pure black after inversion
SVG_INK = "#dcdcdc" SVG_INK = "#dcdcdc"
DARK_WINDOW = (53, 53, 53) # QET::Palette::fusionDark() window color
DARK_RATIO = 3.0 # what tst_qeticons requires of a dark theme file
def visible_pixels(image): def visible_pixels(image):
@@ -107,6 +133,38 @@ def is_line_art(image):
return saturated / len(pixels) < SATURATED_FRACTION return saturated / len(pixels) < SATURATED_FRACTION
def has_light_fill(image):
"""True when the icon is mostly white: a page, a sheet, a light body."""
pixels = visible_pixels(image)
if not pixels:
return False
white = sum(1 for r, g, b, _ in pixels
if colorsys.rgb_to_hls(r / 255, g / 255, b / 255)[1] > WHITE_LIGHTNESS)
return white / len(pixels) >= WHITE_FRACTION
def relative_luminance(rgb):
def linear(c):
c /= 255.0
return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
r, g, b = rgb
return 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b)
def reads_on_dark(image):
"""True when the icon's mean visible color reaches DARK_RATIO against
the dark window, the test tst_qeticons applies to every dark file."""
pixels = visible_pixels(image)
if not pixels:
return False
n = len(pixels)
mean = (sum(p[0] for p in pixels) // n, sum(p[1] for p in pixels) // n,
sum(p[2] for p in pixels) // n)
lighter, darker = sorted((relative_luminance(mean), relative_luminance(DARK_WINDOW)),
reverse=True)
return (lighter + 0.05) / (darker + 0.05) >= DARK_RATIO
def invert_lightness(image): def invert_lightness(image):
"""Invert lightness, keeping hue, saturation and alpha. """Invert lightness, keeping hue, saturation and alpha.
@@ -177,7 +235,7 @@ def main():
light = [] # (alias, source) pairs, paths relative to ico/ light = [] # (alias, source) pairs, paths relative to ico/
dark = [] # paths relative to ico/, files exist on disk dark = [] # paths relative to ico/, files exist on disk
changed = 0 changed = 0
line_art = colored = 0 line_art = colored = light_art = 0
for size in SIZES: for size in SIZES:
folder = ICO / size folder = ICO / size
@@ -188,7 +246,10 @@ def main():
names.append(ALIASES[rel]) names.append(ALIASES[rel])
image = Image.open(png).convert("RGBA") image = Image.open(png).convert("RGBA")
art = is_line_art(image) art = is_line_art(image)
if art: if art and has_light_fill(image):
art = False
light_art += 1
elif art:
line_art += 1 line_art += 1
dark_image = None dark_image = None
else: else:
@@ -213,6 +274,29 @@ def main():
changed += write_if_changed(target, text) changed += write_if_changed(target, text)
dark.append(f"themes/qet-dark/scalable/{name}") dark.append(f"themes/qet-dark/scalable/{name}")
# Complete each dark name with the light files of its other sizes
# that read on a dark window (see the module docstring): aliases, no
# copies.
dark_sizes = {}
for path in dark:
size, name = path.split("/")[2:]
dark_sizes.setdefault(name, set()).add(size)
dark_aliases = []
for alias, source in light:
size, name = alias.split("/")[2:]
if name in dark_sizes and size not in dark_sizes[name] \
and reads_on_dark(Image.open(ICO / source).convert("RGBA")):
dark_aliases.append((f"themes/qet-dark/{size}/{name}", source))
# Drop dark files from an earlier run that are no longer generated, so
# a reclassified icon falls back to the light theme instead of keeping
# a stale copy.
for stale in sorted((THEMES / "qet-dark").glob("*/*")):
if stale.is_file() and stale.name != "index.theme" \
and str(stale.relative_to(ICO)) not in dark:
stale.unlink()
changed += 1
dirs = SIZES + ["scalable"] dirs = SIZES + ["scalable"]
changed += write_if_changed(THEMES / "qet" / "index.theme", changed += write_if_changed(THEMES / "qet" / "index.theme",
index_theme("qet", "QElectroTech icons", dirs)) index_theme("qet", "QElectroTech icons", dirs))
@@ -228,11 +312,13 @@ def main():
qrc.append(f' <file alias="{alias}">{source}</file>') qrc.append(f' <file alias="{alias}">{source}</file>')
for path in dark: for path in dark:
qrc.append(f" <file>{path}</file>") qrc.append(f" <file>{path}</file>")
for alias, source in dark_aliases:
qrc.append(f' <file alias="{alias}">{source}</file>')
qrc += [" </qresource>", "</RCC>", ""] qrc += [" </qresource>", "</RCC>", ""]
changed += write_if_changed(QRC, "\n".join(qrc)) changed += write_if_changed(QRC, "\n".join(qrc))
print(f"{line_art} line-art icons, {colored} colored icons, {len(SVGS)} SVGs; " print(f"{line_art} line-art icons, {light_art} light icons, {colored} colored icons, "
f"{changed} files written") f"{len(SVGS)} SVGs; {changed} files written or removed")
if __name__ == "__main__": if __name__ == "__main__":
+130
View File
@@ -0,0 +1,130 @@
# qet-mcp — a Model Context Protocol server for QElectroTech projects
A small stdio MCP server that lets an AI assistant read and verify
QElectroTech projects: what is in a project, what an edit actually
changed, and what a whole corpus of projects contains.
It has **no third-party dependencies** — Python 3.9+ and the standard
library only. The MCP SDK is not required.
## Why
Verifying a change by screenshot is unreliable, and this tool exists
because that unreliability produced two wrong conclusions in one review
session:
- A drag of a multi-element selection *looked* like it had left the
symbols behind and detached their labels. Diffing the saved file showed
all four elements had moved by an identical `(0, -80)` and **no label
had moved at all**. A bug report was one step away from being filed.
- An "Apply" button *looked* like it did nothing. It was disabled,
because a required field was empty.
Both times the pixels misled and the model told the truth. So the tools
here read the model.
## Tools
| Tool | What it answers |
|---|---|
| `qet_project_info` | title, format version, folios, element and conductor counts |
| `qet_elements` | placed elements: uuid, type, position, label, information bag |
| `qet_conductors` | conductors and their documentation fields; filter by attribute |
| `qet_diff` | **what an edit actually changed** — moves with deltas, adds, removes, relabels, conductor field changes |
| `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_export` | run a headless export (pdf, png, svg, bom, cables, wires, wiring, nets, links, info) |
Only `qet_export` launches QElectroTech. Everything else parses the file
directly, which is faster, needs no display, and cannot be confused by a
dialog.
## Running it
```bash
# list the tools and exit
misc/qet-mcp/qet_mcp.py --list
# speak MCP on stdin/stdout
misc/qet-mcp/qet_mcp.py
```
Register it with an MCP client, for example:
```json
{
"mcpServers": {
"qet": {
"command": "python3",
"args": ["/path/to/qelectrotech/misc/qet-mcp/qet_mcp.py"]
}
}
}
```
## Worked examples
**What did that edit change?**
```json
{"name": "qet_diff", "arguments": {"before": "a.qet", "after": "b.qet"}}
```
```json
"elements": { "moved_count": 4,
"distinct_move_deltas": [[0.0, -80.0]],
"relabelled": [], "info_changed": [] }
```
Four elements moved by one uniform delta; nothing was relabelled. That is
the answer a screenshot gave wrongly.
**How much of a corpus uses a field?**
```json
{"name": "qet_scan",
"arguments": {"directory": "examples", "tag": "conductor", "attribute": "cable"}}
```
```json
{ "files": 24, "total": 3190, "non_empty": 0, "distinct_values": [] }
```
Across the shipped examples: 3190 conductors, not one with a cable value.
## Notes and limits
- **The project database is not reachable from outside the application.**
`projectDataBase::newQuery()` and `isReadOnlySelect()` are C++-internal
and the JavaScript scripting API exposes no SQL binding, so structural
queries here are done over the XML. A `--query` CLI verb, or a scripting
binding, would let this server expose the guarded read-only SQL surface
instead, and would be a better foundation.
- **`qet_export` isolates its launch.** SingleApplication keys its socket
on `applicationFilePath()`, so a second launch of the same binary path
forwards its request to an already-running instance and returns *that*
process's answer with no error. The tool copies the binary to a unique
temporary path, gives it a private `HOME`, and runs it on the offscreen
platform. A symlink would not work: `applicationFilePath()` resolves it
back to the real path.
- **The CLI matches its flags exactly.** `--export-bom out.csv` is the
supported form; `--export-bom=out.csv` is not recognised as an export
at all, so the application starts its interface instead and a headless
run hangs. The tool uses the positional form.
- **Conductor identity is the hard part of `qet_diff`.** A conductor names
its ends with `terminal1`/`terminal2`, and the project format has two
schemes: folio-scoped integer ids in older files, terminal-definition
uuids plus `element1`/`element2` in newer ones. The integer ids are
**renumbered on every save**, so keying on them made all 47 conductors of
an untouched `ArduinoLCD.qet` read as 29 removed and 29 re-added the
moment the other side had been through QElectroTech. Ends are now keyed
by owning element uuid plus terminal, which is stable across a save:
measured at 0 colliding keys over 3190 conductors in the 24 shipped
examples, and 0 churn on a re-saved but otherwise untouched project.
Where an element predates persisted uuids the end cannot be resolved and
keeps a `#`-marked unstable key; the diff then reports `unstable_keys`
and says so rather than pretending to be comparable.
- **Elements** written before persisted uuids fall back to a positional key,
which makes a move in such a file read as a remove plus an add rather
than as a move.
- Read-only by design. Nothing here writes to a project.
Binary file not shown.
+721
View File
@@ -0,0 +1,721 @@
#!/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/>.
"""
qet-mcp — a Model Context Protocol server over QElectroTech projects.
WHY THIS EXISTS
Verifying a QET change by screenshot is unreliable. Twice in one review
session a screenshot was read as showing a defect that the saved file
proved had not happened: once "dragging a multi-selection leaves the
symbols behind and detaches their labels" (the XML showed all four
elements moved and no label moved), and once "Apply does nothing" (Apply
was disabled because a required field was empty). Both times the pixels
misled and the model told the truth.
So the primary tools here read the *model*, not the screen, and the
primary tool is qet_diff: do the thing, then ask what actually changed.
DESIGN
Most tools parse the .qet XML directly and never launch QElectroTech.
That is deliberate: it is fast, deterministic, needs no display, and
cannot be confused by a dialog. Only qet_export shells out to the
binary, and it carries the launch traps with it (see _run_qet).
The project database would be a better query surface than XML, but it is
not reachable from outside the application: projectDataBase::newQuery()
and isReadOnlySelect() are C++-internal and the JavaScript scripting API
exposes no SQL binding. Until it does, structure lives here.
PROTOCOL
Line-delimited JSON-RPC 2.0 on stdin/stdout, per MCP's stdio transport.
Nothing but protocol goes to stdout; diagnostics go to stderr.
No third-party dependencies — the MCP SDK is not assumed to be present.
"""
from __future__ import annotations
import json
import os
import shutil
import subprocess
import sys
import tempfile
import xml.etree.ElementTree as ET
from pathlib import Path
SERVER_NAME = "qet-mcp"
SERVER_VERSION = "0.1.0"
DEFAULT_PROTOCOL = "2025-06-18"
EXPORT_FORMATS = {
"pdf": "--export-pdf",
"png": "--export-png",
"svg": "--export-svg",
"bom": "--export-bom",
"cables": "--export-cables",
"wires": "--export-wires",
"wiring": "--export-wiring",
"nets": "--export-nets",
"links": "--export-links",
"info": "--info",
}
# --------------------------------------------------------------------------
# model reading
# --------------------------------------------------------------------------
def _root(path: str) -> ET.Element:
p = Path(path).expanduser()
if not p.is_file():
raise ValueError(f"no such file: {p}")
try:
return ET.parse(p).getroot()
except ET.ParseError as exc:
raise ValueError(f"{p.name} is not parseable XML: {exc}") from exc
def _element_info(el: ET.Element) -> dict:
"""The <elementInformations> bag, as a plain dict."""
out = {}
bag = el.find("elementInformations")
if bag is not None:
for info in bag.findall("elementInformation"):
name = info.get("name")
if name:
out[name] = (info.text or "").strip()
return out
def _folios(root: ET.Element):
"""Yield (index, diagram) for each folio, 1-based as the UI numbers them."""
for i, d in enumerate(root.iter("diagram"), start=1):
yield i, d
def _elements(root: ET.Element):
for i, d in _folios(root):
for el in d.iter("element"):
yield i, el
def _conductors(root: ET.Element):
for i, d in _folios(root):
index = _terminal_index(d)
for c in d.iter("conductor"):
yield i, c, index
def _element_row(folio: int, el: ET.Element) -> dict:
info = _element_info(el)
etype = el.get("type", "")
return {
"folio": folio,
"uuid": el.get("uuid", ""),
"type": etype,
"name": etype.rsplit("/", 1)[-1].removesuffix(".elmt"),
"x": el.get("x"),
"y": el.get("y"),
"label": info.get("label", ""),
"info": info,
}
def _terminal_index(diagram: ET.Element) -> dict:
"""Map a folio's terminal ids to an identity that survives a save.
A conductor names its ends with terminal1/terminal2, which are plain
integers scoped to the folio -- and QElectroTech reassigns them on every
write, in whatever order it happens to serialise the elements. The same
untouched conductor comes back as terminal1="1" terminal2="16" before a
save and terminal1="34" terminal2="15" after one. Keying a conductor on
that pair, which this tool used to do, made every conductor in the file
read as removed-and-re-added whenever the "after" side had been through
QElectroTech -- which is the common case for "what did that edit change",
so the conductor half of the diff was noise precisely when it was needed.
So resolve each id to (owning element uuid, terminal position and
orientation inside that element). Element uuids are persisted and
stable; the terminal's local geometry comes from the element definition
and does not move when the element moves. That pair is the same basis
QET's own Terminal::stableUuid() uses for terminals with no uuid of
their own, and it is stable for the same reasons.
Conductors in the corpus carry no element1/element2 attribute -- 0 of
47 in ArduinoLCD.qet, 0 of 67 in 741.qet -- so this mapping has to be
built from the elements rather than read off the conductor.
"""
index = {}
for el in diagram.iter("element"):
uuid = el.get("uuid", "")
if not uuid:
# Old enough to predate persisted element uuids. Leaving these
# ids unresolved is deliberate: keyed on terminal geometry
# alone, every element of the same type collapses together --
# in schema_indus.qet that merged nine distinct conductors onto
# one key, which is worse than the instability it was meant to
# fix. An unresolved end keeps them apart and stays visibly
# marked with a "#" so the caller can see the diff is on the
# unstable footing that file forces.
continue
for t in el.iter("terminal"):
tid = t.get("id")
if tid is None:
continue
index[tid] = (f"{uuid}@{t.get('x','?')},{t.get('y','?')}"
f",{t.get('orientation','?')}")
return index
def _conductor_key(folio: int, c: ET.Element, index: dict) -> str:
"""Identify a conductor by its two ends, in whichever scheme it uses.
The project format has two, and a file can hold both at once -- the
same folio, after an edit, carries legacy conductors and new ones:
- legacy: terminal1/terminal2 are the folio-scoped integer ids, and
there is no element1/element2. Resolve them through index.
- current: terminal1/terminal2 are terminal uuids from the element
*definition*, with element1/element2 naming the placed instances.
The terminal uuid alone is not an identity -- two coils of the same
type have the same one on both ends, so a conductor between them
would key as a self-loop -- so it is the (instance, terminal) pair
that identifies an end.
"""
ends = []
for elem_attr, term_attr, name_attr in (("element1", "terminal1", "terminalname1"),
("element2", "terminal2", "terminalname2")):
tid = c.get(term_attr, "?")
owner = c.get(elem_attr)
if owner:
ends.append(f"{owner}/{tid or c.get(name_attr, '?')}")
else:
# An id with no element behind it stays visible as itself
# rather than silently collapsing conductors onto one key.
ends.append(index.get(tid, f"#{tid}"))
# A conductor is undirected: whichever end QET happens to write first,
# it is the same connection.
return f"{folio}:" + "--".join(sorted(ends))
def _conductor_row(folio: int, c: ET.Element, index: dict | None = None) -> dict:
return {
"folio": folio,
"uuid": c.get("uuid", ""),
"key": _conductor_key(folio, c, index or {}),
"num": c.get("num", ""),
"formula": c.get("formula", ""),
"cable": c.get("cable", ""),
"bus": c.get("bus", ""),
"function": c.get("function", ""),
"color": c.get("conductor_color", ""),
"section": c.get("conductor_section", ""),
"type": c.get("type", ""),
}
# --------------------------------------------------------------------------
# tools
# --------------------------------------------------------------------------
def tool_project_info(path: str) -> dict:
root = _root(path)
folios = []
for i, d in _folios(root):
folios.append({
"index": i,
"title": d.get("title", ""),
"elements": sum(1 for _ in d.iter("element")),
"conductors": sum(1 for _ in d.iter("conductor")),
})
return {
"file": str(Path(path).expanduser()),
"title": root.get("title", ""),
"version": root.get("version", ""),
"folio_count": len(folios),
"element_count": sum(f["elements"] for f in folios),
"conductor_count": sum(f["conductors"] for f in folios),
"folios": folios,
}
def tool_elements(path: str, folio: int | None = None,
name_contains: str | None = None, limit: int = 200) -> dict:
rows = []
for i, el in _elements(_root(path)):
if folio is not None and i != folio:
continue
row = _element_row(i, el)
if name_contains and name_contains.lower() not in row["name"].lower():
continue
rows.append(row)
return {"count": len(rows), "truncated": len(rows) > limit,
"elements": rows[:limit]}
def tool_conductors(path: str, folio: int | None = None,
attribute: str | None = None,
non_empty: bool = False, limit: int = 200) -> dict:
rows = []
for i, c, ix in _conductors(_root(path)):
if folio is not None and i != folio:
continue
row = _conductor_row(i, c, ix)
if attribute is not None:
value = c.get(attribute, "")
if non_empty and not value.strip():
continue
row["value"] = value
rows.append(row)
return {"count": len(rows), "truncated": len(rows) > limit,
"conductors": rows[:limit]}
def tool_diff(before: str, after: str) -> dict:
"""Structural diff of two .qet files.
This is the tool that answers "what did that edit actually change",
which is the question a screenshot answers badly.
"""
# Key on uuid where there is one. Files written before conductors and
# elements carried persisted uuids fall back to a positional key, which
# is why a move in such a file reads as remove+add rather than a move.
a_el, b_el = {}, {}
for i, e in _elements(_root(before)):
r = _element_row(i, e)
a_el[r["uuid"] or f"{i}:{r['x']},{r['y']}:{r['name']}"] = r
for i, e in _elements(_root(after)):
r = _element_row(i, e)
b_el[r["uuid"] or f"{i}:{r['x']},{r['y']}:{r['name']}"] = r
moved, relabelled, changed_info = [], [], []
for k, a in a_el.items():
b = b_el.get(k)
if b is None:
continue
if (a["x"], a["y"]) != (b["x"], b["y"]):
moved.append({
"uuid": k, "name": a["name"], "folio": a["folio"],
"from": [a["x"], a["y"]], "to": [b["x"], b["y"]],
"delta": [_num(b["x"]) - _num(a["x"]),
_num(b["y"]) - _num(a["y"])],
})
if a["label"] != b["label"]:
relabelled.append({"uuid": k, "name": a["name"],
"from": a["label"], "to": b["label"]})
if a["info"] != b["info"]:
changed_info.append({"uuid": k, "name": a["name"],
"from": a["info"], "to": b["info"]})
a_co = {r["key"]: r for i, c, ix in _conductors(_root(before))
for r in [_conductor_row(i, c, ix)]}
b_co = {r["key"]: r for i, c, ix in _conductors(_root(after))
for r in [_conductor_row(i, c, ix)]}
# An end that could not be resolved to an element is keyed on the
# folio-scoped integer id, which QElectroTech reassigns on every write.
# Say so rather than presenting the result as if it were comparable:
# in such a file an untouched conductor can read as removed and re-added.
shaky = sum(1 for k in set(a_co) | set(b_co) if "#" in k)
unstable = {} if not shaky else {
"unstable_keys": shaky,
"warning": "some conductors sit on elements with no persisted uuid, so "
"they are keyed on folio-scoped terminal ids that "
"QElectroTech renumbers on save; added/removed entries "
"marked with # may be the same conductor, not a change",
}
conductor_changes = []
for k, a in a_co.items():
b = b_co.get(k)
if b is None:
continue
fields = {f: [a[f], b[f]] for f in
("num", "formula", "cable", "bus", "color", "section",
"function", "type")
if a[f] != b[f]}
if fields:
conductor_changes.append({"key": k, "changed": fields})
deltas = sorted({tuple(m["delta"]) for m in moved})
return {
"elements": {
"before": len(a_el), "after": len(b_el),
"added": sorted(set(b_el) - set(a_el))[:50],
"removed": sorted(set(a_el) - set(b_el))[:50],
"moved": moved[:100],
"moved_count": len(moved),
"distinct_move_deltas": [list(d) for d in deltas],
"relabelled": relabelled[:50],
"info_changed": changed_info[:50],
},
"conductors": {
"before": len(a_co), "after": len(b_co),
"added": sorted(set(b_co) - set(a_co))[:50],
"removed": sorted(set(a_co) - set(b_co))[:50],
"changed": conductor_changes[:100],
"changed_count": len(conductor_changes),
**unstable,
},
}
def _num(v) -> float:
try:
return float(v)
except (TypeError, ValueError):
return 0.0
def tool_scan(directory: str, tag: str = "conductor",
attribute: str = "cable", recursive: bool = True) -> dict:
"""Sweep every .qet in a directory, counting how many <tag> carry a
non-empty `attribute`.
This is the corpus question: "3190 conductors, 0 cable values across
25 projects" is exactly one call to this tool.
"""
d = Path(directory).expanduser()
if not d.is_dir():
raise ValueError(f"not a directory: {d}")
files = sorted(d.rglob("*.qet") if recursive else d.glob("*.qet"))
total = non_empty = 0
per_file, values, unreadable = [], {}, []
for f in files:
try:
root = ET.parse(f).getroot()
except ET.ParseError as exc:
unreadable.append({"file": f.name, "error": str(exc)})
continue
n = ne = 0
for node in root.iter(tag):
n += 1
v = (node.get(attribute) or "").strip()
if v:
ne += 1
values[v] = values.get(v, 0) + 1
total += n
non_empty += ne
per_file.append({"file": f.name, tag: n, "non_empty": ne})
return {
"directory": str(d), "files": len(files), "unreadable": unreadable,
"tag": tag, "attribute": attribute,
"total": total, "non_empty": non_empty,
"distinct_values": sorted(values.items(), key=lambda kv: -kv[1])[:25],
"per_file": per_file,
}
def tool_element_info(path: str) -> dict:
"""Introspect a .elmt: names, terminals, and which info fields it carries."""
root = _root(path)
names = {n.get("lang"): (n.text or "") for n in root.iter("name")}
terminals = [{"x": t.get("x"), "y": t.get("y"),
"orientation": t.get("orientation"),
"name": t.get("name", ""), "type": t.get("type", "")}
for t in root.iter("terminal")]
info_fields = sorted({(i.text or "").strip()
for i in root.iter("info_name") if (i.text or "").strip()})
parts = {}
desc = root.find("description")
for child in (desc if desc is not None else []):
parts[child.tag] = parts.get(child.tag, 0) + 1
return {
"file": str(Path(path).expanduser()),
"type": root.get("type", ""), "link_type": root.get("link_type", ""),
"width": root.get("width"), "height": root.get("height"),
"names": names,
"terminal_count": len(terminals), "terminals": terminals,
"info_fields": info_fields,
"parts": parts,
}
def _run_qet(binary: str, args: list[str], timeout: int = 180) -> dict:
"""Launch QElectroTech headlessly, carrying the known launch traps.
SingleApplication keys its socket on applicationFilePath(), so a second
launch of the same path forwards its request to an already-running
instance and returns THAT process's answer with no error. Copying the
binary to a unique path gives this run its own socket. A symlink will
not do: applicationFilePath() resolves it back.
"""
src = Path(binary).expanduser()
if not src.is_file() or not os.access(src, os.X_OK):
raise ValueError(f"not an executable: {src}")
with tempfile.TemporaryDirectory(prefix="qet-mcp-") as tmp:
sandbox = Path(tmp)
exe = sandbox / f"qet-mcp-{os.getpid()}"
shutil.copy2(src, exe)
home = sandbox / "home"
(home / ".config").mkdir(parents=True)
(home / ".local" / "share").mkdir(parents=True)
env = dict(os.environ,
HOME=str(home),
XDG_CONFIG_HOME=str(home / ".config"),
XDG_DATA_HOME=str(home / ".local" / "share"),
QT_QPA_PLATFORM="offscreen")
try:
p = subprocess.run([str(exe), *args], env=env, timeout=timeout,
capture_output=True, text=True)
except subprocess.TimeoutExpired:
return {"ok": False, "timed_out": True, "timeout_s": timeout,
"hint": "a modal dialog during load will hang a headless "
"run; check the project's format version"}
return {"ok": p.returncode == 0, "exit_code": p.returncode,
"stdout": p.stdout[-4000:], "stderr": p.stderr[-4000:]}
def tool_export(binary: str, project: str, format: str, output: str,
timeout: int = 180) -> dict:
if format not in EXPORT_FORMATS:
raise ValueError(f"unknown format {format!r}; "
f"expected one of {', '.join(sorted(EXPORT_FORMATS))}")
proj = Path(project).expanduser()
if not proj.is_file():
raise ValueError(f"no such project: {proj}")
# The CLI matches its flags by exact string (cli_export.cpp:828) and takes
# the project and output as the two positional arguments after the flag
# (:862, :882). A "--export-bom=out.csv" form is NOT recognised: it fails
# the flag test, so the run is not treated as an export at all and the
# application starts its GUI instead, which then hangs on an offscreen
# platform. Order matters here.
flag = EXPORT_FORMATS[format]
result = _run_qet(binary, [flag, str(proj), output], timeout)
out = Path(output).expanduser()
result["output"] = str(out)
result["output_exists"] = out.exists()
if out.exists() and out.is_file():
result["output_bytes"] = out.stat().st_size
return result
TOOLS = [
{
"name": "qet_project_info",
"description": "Summarise a .qet project: title, format version, folios, "
"and element/conductor counts per folio. Reads the file "
"directly; does not launch QElectroTech.",
"inputSchema": {
"type": "object",
"properties": {"path": {"type": "string", "description": "path to a .qet file"}},
"required": ["path"],
},
"handler": lambda a: tool_project_info(a["path"]),
},
{
"name": "qet_elements",
"description": "List placed elements with uuid, type, position, label and "
"their elementInformations bag. Optionally filter by folio "
"or by element name substring.",
"inputSchema": {
"type": "object",
"properties": {
"path": {"type": "string"},
"folio": {"type": "integer", "description": "1-based folio number"},
"name_contains": {"type": "string"},
"limit": {"type": "integer", "default": 200},
},
"required": ["path"],
},
"handler": lambda a: tool_elements(a["path"], a.get("folio"),
a.get("name_contains"),
a.get("limit", 200)),
},
{
"name": "qet_conductors",
"description": "List conductors with their documentation fields (num, "
"formula, cable, bus, function, colour, section). Set "
"attribute+non_empty to find only conductors that carry a "
"value for one attribute.",
"inputSchema": {
"type": "object",
"properties": {
"path": {"type": "string"},
"folio": {"type": "integer"},
"attribute": {"type": "string",
"description": "an XML attribute of <conductor>, e.g. cable"},
"non_empty": {"type": "boolean", "default": False},
"limit": {"type": "integer", "default": 200},
},
"required": ["path"],
},
"handler": lambda a: tool_conductors(a["path"], a.get("folio"),
a.get("attribute"),
a.get("non_empty", False),
a.get("limit", 200)),
},
{
"name": "qet_diff",
"description": "Structurally diff two .qet files: which elements moved and "
"by what delta, which were added, removed or relabelled, and "
"which conductor fields changed. Use this to verify what an "
"edit actually did, rather than reading a screenshot.",
"inputSchema": {
"type": "object",
"properties": {
"before": {"type": "string"},
"after": {"type": "string"},
},
"required": ["before", "after"],
},
"handler": lambda a: tool_diff(a["before"], a["after"]),
},
{
"name": "qet_scan",
"description": "Sweep every .qet in a directory and count how many nodes of "
"a given tag carry a non-empty attribute, with the distinct "
"values found. For corpus questions such as how many "
"conductors in the shipped examples have a cable value.",
"inputSchema": {
"type": "object",
"properties": {
"directory": {"type": "string"},
"tag": {"type": "string", "default": "conductor"},
"attribute": {"type": "string", "default": "cable"},
"recursive": {"type": "boolean", "default": True},
},
"required": ["directory"],
},
"handler": lambda a: tool_scan(a["directory"], a.get("tag", "conductor"),
a.get("attribute", "cable"),
a.get("recursive", True)),
},
{
"name": "qet_element_info",
"description": "Introspect a .elmt element definition: translated names, "
"terminals, which dynamic-text info fields it carries, and a "
"count of its drawing parts.",
"inputSchema": {
"type": "object",
"properties": {"path": {"type": "string", "description": "path to a .elmt file"}},
"required": ["path"],
},
"handler": lambda a: tool_element_info(a["path"]),
},
{
"name": "qet_export",
"description": "Run a QElectroTech export headlessly (pdf, png, svg, bom, "
"cables, wires, wiring, nets, links, info). Launches the "
"binary in an isolated sandbox so it cannot be captured by, "
"or capture, a running QElectroTech.",
"inputSchema": {
"type": "object",
"properties": {
"binary": {"type": "string", "description": "path to the qelectrotech executable"},
"project": {"type": "string"},
"format": {"type": "string", "enum": sorted(EXPORT_FORMATS)},
"output": {"type": "string"},
"timeout": {"type": "integer", "default": 180},
},
"required": ["binary", "project", "format", "output"],
},
"handler": lambda a: tool_export(a["binary"], a["project"], a["format"],
a["output"], a.get("timeout", 180)),
},
]
_BY_NAME = {t["name"]: t for t in TOOLS}
# --------------------------------------------------------------------------
# JSON-RPC / MCP plumbing
# --------------------------------------------------------------------------
def _public(tool: dict) -> dict:
return {k: v for k, v in tool.items() if k != "handler"}
def handle(msg: dict) -> dict | None:
method = msg.get("method")
mid = msg.get("id")
if method == "initialize":
want = (msg.get("params") or {}).get("protocolVersion")
return _ok(mid, {
"protocolVersion": want or DEFAULT_PROTOCOL,
"capabilities": {"tools": {}},
"serverInfo": {"name": SERVER_NAME, "version": SERVER_VERSION},
})
if method in ("notifications/initialized", "initialized"):
return None # notification: no reply
if method == "ping":
return _ok(mid, {})
if method == "tools/list":
return _ok(mid, {"tools": [_public(t) for t in TOOLS]})
if method == "tools/call":
params = msg.get("params") or {}
name = params.get("name")
tool = _BY_NAME.get(name)
if tool is None:
return _err(mid, -32602, f"unknown tool: {name}")
try:
result = tool["handler"](params.get("arguments") or {})
text = json.dumps(result, indent=2, ensure_ascii=False)
return _ok(mid, {"content": [{"type": "text", "text": text}]})
except Exception as exc: # surfaced to the model, not the transport
return _ok(mid, {
"isError": True,
"content": [{"type": "text",
"text": f"{type(exc).__name__}: {exc}"}],
})
if mid is None:
return None
return _err(mid, -32601, f"method not found: {method}")
def _ok(mid, result):
return {"jsonrpc": "2.0", "id": mid, "result": result}
def _err(mid, code, message):
return {"jsonrpc": "2.0", "id": mid, "error": {"code": code, "message": message}}
def serve(stdin=sys.stdin, stdout=sys.stdout) -> None:
for line in stdin:
line = line.strip()
if not line:
continue
try:
msg = json.loads(line)
except json.JSONDecodeError as exc:
print(json.dumps(_err(None, -32700, f"parse error: {exc}")),
file=stdout, flush=True)
continue
reply = handle(msg)
if reply is not None:
print(json.dumps(reply, ensure_ascii=False), file=stdout, flush=True)
def main() -> int:
if len(sys.argv) > 1 and sys.argv[1] in ("--list", "-l"):
for t in TOOLS:
print(f"{t['name']}\n {t['description']}\n")
return 0
serve()
return 0
if __name__ == "__main__":
raise SystemExit(main())
-6
View File
@@ -93,10 +93,7 @@
<file>ico/22x22/conductor2.png</file> <file>ico/22x22/conductor2.png</file>
<file>ico/22x22/configure.png</file> <file>ico/22x22/configure.png</file>
<file>ico/22x22/configure-toolbars.png</file> <file>ico/22x22/configure-toolbars.png</file>
<file>ico/22x22/diagram_add.png</file>
<file>ico/22x22/diagram_del.png</file>
<file>ico/22x22/dialog-cancel.png</file> <file>ico/22x22/dialog-cancel.png</file>
<file>ico/22x22/dialog-information.png</file>
<file>ico/22x22/dialog-ok.png</file> <file>ico/22x22/dialog-ok.png</file>
<file>ico/22x22/document-close.png</file> <file>ico/22x22/document-close.png</file>
<file>ico/22x22/document-export.png</file> <file>ico/22x22/document-export.png</file>
@@ -140,8 +137,6 @@
<file>ico/22x22/go-up.png</file> <file>ico/22x22/go-up.png</file>
<file>ico/22x22/hotspot.png</file> <file>ico/22x22/hotspot.png</file>
<file>ico/22x22/insert-image.png</file> <file>ico/22x22/insert-image.png</file>
<file>ico/22x22/pdf-import.png</file>
<file>ico/22x22/label.png</file>
<file>ico/22x22/landscape.png</file> <file>ico/22x22/landscape.png</file>
<file>ico/22x22/line.png</file> <file>ico/22x22/line.png</file>
<file>ico/22x22/list-add.png</file> <file>ico/22x22/list-add.png</file>
@@ -242,7 +237,6 @@
<file>ico/16x16/project.png</file> <file>ico/16x16/project.png</file>
<file>ico/16x16/qt.png</file> <file>ico/16x16/qt.png</file>
<file>ico/16x16/terminalstrip.png</file> <file>ico/16x16/terminalstrip.png</file>
<file>ico/22x22/diagram.png</file>
<file>ico/22x22/edit-clear-locationbar-rtl.png</file> <file>ico/22x22/edit-clear-locationbar-rtl.png</file>
<file>ico/22x22/export-csv.png</file> <file>ico/22x22/export-csv.png</file>
<file>ico/22x22/format-text-subscript.png</file> <file>ico/22x22/format-text-subscript.png</file>
@@ -0,0 +1,54 @@
/*
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 "elementpreviewdelegate.h"
#include <QWidget>
#include "../qetpalette.h"
/**
@brief ElementPreviewDelegate::initStyleOption
After the base class has filled the option from the model, replace
the icon with one that reads on a dark palette. The adapted icon is
built from the pixmap the view is about to draw, at the view's
decoration size and device pixel ratio.
*/
void ElementPreviewDelegate::initStyleOption(QStyleOptionViewItem *option,
const QModelIndex &index) const
{
QStyledItemDelegate::initStyleOption(option, index);
if (option->icon.isNull() || !QET::Palette::isDark(option->palette))
return;
const qint64 key = option->icon.cacheKey();
const auto it = m_dark_icons.constFind(key);
if (it != m_dark_icons.constEnd())
{
option->icon = *it;
return;
}
const qreal dpr = option->widget ? option->widget->devicePixelRatio() : 1.0;
const QPixmap source = option->icon.pixmap(option->decorationSize, dpr);
const QPixmap adapted = QET::Palette::forPalette(source, option->palette);
QIcon icon = option->icon;
if (adapted.cacheKey() != source.cacheKey())
icon = QIcon(adapted);
m_dark_icons.insert(key, icon);
option->icon = icon;
}
@@ -0,0 +1,50 @@
/*
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 ELEMENTPREVIEWDELEGATE_H
#define ELEMENTPREVIEWDELEGATE_H
#include <QHash>
#include <QIcon>
#include <QStyledItemDelegate>
/**
@brief The ElementPreviewDelegate class
Draws the items of an element collection tree with icons that read on
the current palette. Element previews are black line art drawn for a
white sheet; on a dark palette this delegate hands the view the same
picture with its lightness inverted (QET::Palette::forPalette), so the
ink is light on the dark row. Colored icons, such as folders, are left
alone, and nothing changes on a light palette. Adapted icons are kept
per source icon, so a repaint costs a hash lookup.
*/
class ElementPreviewDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
using QStyledItemDelegate::QStyledItemDelegate;
protected:
void initStyleOption(QStyleOptionViewItem *option,
const QModelIndex &index) const override;
private:
mutable QHash<qint64, QIcon> m_dark_icons;
};
#endif
@@ -725,7 +725,10 @@ void ElementsCollectionWidget::showThisDir()
ElementCollectionItem *eci = ElementCollectionItem *eci =
elementCollectionItemForIndex(m_showed_index); elementCollectionItemForIndex(m_showed_index);
if (eci) if (eci)
{
eci->setBackground(QBrush()); eci->setBackground(QBrush());
eci->setForeground(QBrush());
}
} }
m_showed_index = m_index_at_context_menu; m_showed_index = m_index_at_context_menu;
@@ -736,7 +739,11 @@ void ElementsCollectionWidget::showThisDir()
ElementCollectionItem *eci = ElementCollectionItem *eci =
elementCollectionItemForIndex(m_showed_index); elementCollectionItemForIndex(m_showed_index);
if (eci) if (eci)
{
// Amber under black, whatever the palette's text color.
eci->setBackground(QBrush(QColor(255, 204, 0, 255))); eci->setBackground(QBrush(QColor(255, 204, 0, 255)));
eci->setForeground(QBrush(Qt::black));
}
search(); search();
} }
else else
@@ -755,7 +762,10 @@ void ElementsCollectionWidget::resetShowThisDir()
ElementCollectionItem *eci = elementCollectionItemForIndex( ElementCollectionItem *eci = elementCollectionItemForIndex(
m_showed_index); m_showed_index);
if (eci) if (eci)
{
eci->setBackground(QBrush()); eci->setBackground(QBrush());
eci->setForeground(QBrush());
}
} }
m_showed_index = QModelIndex(); m_showed_index = QModelIndex();
@@ -16,6 +16,8 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>. along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "elementstreeview.h" #include "elementstreeview.h"
#include "elementpreviewdelegate.h"
#include "../qetpalette.h"
#include "../factory/elementfactory.h" #include "../factory/elementfactory.h"
#include "../qetgraphicsitem/element.h" #include "../qetgraphicsitem/element.h"
@@ -41,24 +43,11 @@ static int MAX_DND_PIXMAP_HEIGHT = 375;
ElementsTreeView::ElementsTreeView(QWidget *parent) : ElementsTreeView::ElementsTreeView(QWidget *parent) :
QTreeView(parent) QTreeView(parent)
{ {
// force du noir sur une alternance de blanc (comme le schema) et de gris // Rows follow the application palette. Element previews are black
// clair, avec du blanc sur bleu pas trop fonce pour la selection // line art drawn for a white sheet; ElementPreviewDelegate adapts them
// // to a dark palette, so this view no longer has to force a light one
// Element icons are rendered with colors read directly from each .elmt // (bugtracker #335).
// file (almost always black linework, matching printed-schematic setItemDelegate(new ElementPreviewDelegate(this));
// convention) onto a transparent background -- so this view must keep
// a light background regardless of the OS/desktop theme, or the icons
// become invisible on dark themes. QAbstractItemView paints its rows
// using the viewport's palette, not the view's own, so the palette
// must be applied to both to actually take effect under every style.
QPalette qp = palette();
qp.setColor(QPalette::Text, Qt::black);
qp.setColor(QPalette::Base, Qt::white);
qp.setColor(QPalette::AlternateBase, QColor("#e8e8e8"));
qp.setColor(QPalette::Highlight, QColor("#678db2"));
qp.setColor(QPalette::HighlightedText, Qt::black);
setPalette(qp);
viewport()->setPalette(qp);
} }
/** /**
@@ -220,7 +209,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(temp_elmt->pixmap()); QPixmap elmt_pixmap(QET::Palette::forPalette(temp_elmt->pixmap(), 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
@@ -35,13 +35,17 @@ AddTerminalToStripCommand::AddTerminalToStripCommand(QSharedPointer<RealTerminal
const auto t_label = terminal->label(); const auto t_label = terminal->label();
const auto ts_name = strip->name(); const auto ts_name = strip->name();
const auto str_1 = t_label.isEmpty() ? QObject::tr("Ajouter une borne") : QString text;
QObject::tr("Ajouter la borne %1").arg(t_label); if (ts_name.isEmpty()) {
text = t_label.isEmpty()
const auto str_2 = ts_name.isEmpty() ? QObject::tr("à un groupe de bornes") : ? QObject::tr("Ajouter une borne à un groupe de bornes")
QObject::tr("au groupe de bornes %1").arg(ts_name); : QObject::tr("Ajouter la borne %1 à un groupe de bornes").arg(t_label);
} else {
setText(str_1 % " " % str_2); text = t_label.isEmpty()
? QObject::tr("Ajouter une borne au groupe de bornes %1").arg(ts_name)
: QObject::tr("Ajouter la borne %1 au groupe de bornes %2").arg(t_label, ts_name);
}
setText(text);
} }
AddTerminalToStripCommand::AddTerminalToStripCommand(QVector<QSharedPointer<RealTerminal>> terminals, TerminalStrip *strip, QUndoCommand *parent) : AddTerminalToStripCommand::AddTerminalToStripCommand(QVector<QSharedPointer<RealTerminal>> terminals, TerminalStrip *strip, QUndoCommand *parent) :
@@ -50,14 +54,11 @@ AddTerminalToStripCommand::AddTerminalToStripCommand(QVector<QSharedPointer<Real
m_new_strip{strip} m_new_strip{strip}
{ {
const auto ts_name = strip->name(); const auto ts_name = strip->name();
const auto count = m_terminal.size();
const auto str_1 = m_terminal.size() > 1 ? QObject::tr("Ajouter %1 bornes").arg(m_terminal.size()) : setText(ts_name.isEmpty()
QObject::tr("Ajouter une borne"); ? QObject::tr("Ajouter %n borne(s) à un groupe de bornes", "", count)
: QObject::tr("Ajouter %n borne(s) au groupe de bornes %1", "", count).arg(ts_name));
const auto str_2 = ts_name.isEmpty() ? QObject::tr("à un groupe de bornes") :
QObject::tr("au groupe de bornes %1").arg(ts_name);
setText(str_1 % " " % str_2);
} }
@@ -137,13 +138,11 @@ void RemoveTerminalFromStripCommand::redo()
void RemoveTerminalFromStripCommand::setCommandTitle() void RemoveTerminalFromStripCommand::setCommandTitle()
{ {
const auto strip_name = m_strip->name(); const auto strip_name = m_strip->name();
const auto count = m_terminals.size();
const auto str_1 = m_terminals.size()>1 ? QObject::tr("Enlever %1 bornes").arg(m_terminals.size()): setText(strip_name.isEmpty()
QObject::tr("Enlever une borne"); ? QObject::tr("Enlever %n borne(s) d'un groupe de bornes", "", count)
: QObject::tr("Enlever %n borne(s) du groupe de bornes %1", "", count).arg(strip_name));
const auto str_2 = strip_name.isEmpty() ? QObject::tr("d'un groupe de bornes") :
QObject::tr("du groupe de bornes %1").arg(strip_name);
setText(str_1 % " " % str_2);
} }
/** /**
@@ -166,19 +165,30 @@ MoveTerminalCommand::MoveTerminalCommand(QSharedPointer<PhysicalTerminal> termin
t_label.append(", "); t_label.append(", ");
t_label.append(real_t->label()); t_label.append(real_t->label());
} }
const auto strip_name = old_strip->name();
const auto new_strip_name = new_strip->name();
auto strip_name = old_strip->name(); QString text;
auto new_strip_name = new_strip->name(); if (t_label.isEmpty()) {
if (strip_name.isEmpty() && new_strip_name.isEmpty())
auto str_1 = t_label.isEmpty() ? QObject::tr("Déplacer une borne") : text = QObject::tr("Déplacer une borne d'un groupe de bornes vers un groupe de bornes");
QObject::tr("Déplacer la borne %1").arg(t_label); else if (strip_name.isEmpty())
text = QObject::tr("Déplacer une borne d'un groupe de bornes vers le groupe de bornes %1").arg(new_strip_name);
auto str_2 = strip_name.isEmpty() ? QObject::tr(" d'un groupe de bornes") : else if (new_strip_name.isEmpty())
QObject::tr(" du groupe de bornes %1").arg(strip_name); text = QObject::tr("Déplacer une borne du groupe de bornes %1 vers un groupe de bornes").arg(strip_name);
else
auto str_3 = new_strip_name.isEmpty() ? QObject::tr("vers un groupe de bornes") : text = QObject::tr("Déplacer une borne du groupe de bornes %1 vers le groupe de bornes %2").arg(strip_name, new_strip_name);
QObject::tr("vers le groupe de bornes %1").arg(new_strip_name); } else {
setText(str_1 % " " % str_2 % " " % str_3); if (strip_name.isEmpty() && new_strip_name.isEmpty())
text = QObject::tr("Déplacer la borne %1 d'un groupe de bornes vers un groupe de bornes").arg(t_label);
else if (strip_name.isEmpty())
text = QObject::tr("Déplacer la borne %1 d'un groupe de bornes vers le groupe de bornes %2").arg(t_label, new_strip_name);
else if (new_strip_name.isEmpty())
text = QObject::tr("Déplacer la borne %1 du groupe de bornes %2 vers un groupe de bornes").arg(t_label, strip_name);
else
text = QObject::tr("Déplacer la borne %1 du groupe de bornes %2 vers le groupe de bornes %3").arg(t_label, strip_name, new_strip_name);
}
setText(text);
} }
MoveTerminalCommand::MoveTerminalCommand(QVector<QSharedPointer<PhysicalTerminal>> terminals, TerminalStrip *old_strip, MoveTerminalCommand::MoveTerminalCommand(QVector<QSharedPointer<PhysicalTerminal>> terminals, TerminalStrip *old_strip,
@@ -191,17 +201,18 @@ MoveTerminalCommand::MoveTerminalCommand(QVector<QSharedPointer<PhysicalTerminal
{ {
const auto strip_name = old_strip->name(); const auto strip_name = old_strip->name();
const auto new_strip_name = new_strip->name(); const auto new_strip_name = new_strip->name();
const auto count = m_terminal.size();
const auto str_1 = m_terminal.size() > 1 ? QObject::tr("Déplacer des bornes") : QString text;
QObject::tr("Déplacer une borne"); if (strip_name.isEmpty() && new_strip_name.isEmpty())
text = QObject::tr("Déplacer %n borne(s) d'un groupe de bornes vers un groupe de bornes", "", count);
const auto str_2 = strip_name.isEmpty() ? QObject::tr(" d'un groupe de bornes") : else if (strip_name.isEmpty())
QObject::tr(" du groupe de bornes %1").arg(strip_name); text = QObject::tr("Déplacer %n borne(s) d'un groupe de bornes vers le groupe de bornes %1", "", count).arg(new_strip_name);
else if (new_strip_name.isEmpty())
const auto str_3 = new_strip_name.isEmpty() ? QObject::tr("vers un groupe de bornes") : text = QObject::tr("Déplacer %n borne(s) du groupe de bornes %1 vers un groupe de bornes", "", count).arg(strip_name);
QObject::tr("vers le groupe de bornes %1").arg(new_strip_name); else
text = QObject::tr("Déplacer %n borne(s) du groupe de bornes %1 vers le groupe de bornes %2", "", count).arg(strip_name, new_strip_name);
setText(str_1 % " " % str_2 % " " % str_3); setText(text);
} }
void MoveTerminalCommand::undo() void MoveTerminalCommand::undo()
+77
View File
@@ -275,3 +275,80 @@ QString NumerotationContext::formatValue(const QStringList &item)
return QString("%1").arg(value.toInt(), 3, 10, QChar('0')); return QString("%1").arg(value.toInt(), 3, 10, QChar('0'));
return QString::number(value.toInt()); return QString::number(value.toInt());
} }
/**
@brief NumerotationContext::saveToSettings
Save a hash of named NumerotationContexts to QSettings.
@param contexts : the named rules to save
@param currentRule : the name of the currently active rule
@param settings : QSettings instance
@param prefix : settings key prefix (e.g. "autonum/conductor")
*/
void NumerotationContext::saveToSettings(
const QHash<QString, NumerotationContext> &contexts,
const QString &currentRule,
QSettings &settings,
const QString &prefix)
{
settings.setValue(prefix + "/current", currentRule);
// Clear stale array entries before writing (beginWriteArray does not
// remove entries beyond the new size).
settings.remove(prefix + "/rules");
QStringList names = contexts.keys();
settings.beginWriteArray(prefix + "/rules", names.size());
for (int i = 0; i < names.size(); ++i) {
settings.setArrayIndex(i);
const QString &name = names.at(i);
const NumerotationContext &nc = contexts.value(name);
settings.setValue("name", name);
// Serialize context to XML string
QDomDocument doc;
NumerotationContext nc_copy = nc;
QDomElement root = nc_copy.toXml(doc, "context");
doc.appendChild(root);
settings.setValue("xml", doc.toString());
}
settings.endArray();
}
/**
@brief NumerotationContext::loadFromSettings
Load named NumerotationContexts from QSettings.
@param settings : QSettings instance
@param prefix : settings key prefix (e.g. "autonum/conductor")
@return pair of (hash of named rules, name of current rule)
*/
QPair<QHash<QString, NumerotationContext>, QString> NumerotationContext::loadFromSettings(
QSettings &settings,
const QString &prefix)
{
QPair<QHash<QString, NumerotationContext>, QString> result;
QHash<QString, NumerotationContext> &contexts = result.first;
QString &currentRule = result.second;
currentRule = settings.value(prefix + "/current").toString();
int size = settings.beginReadArray(prefix + "/rules");
for (int i = 0; i < size; ++i) {
settings.setArrayIndex(i);
QString name = settings.value("name").toString();
QString xmlStr = settings.value("xml").toString();
if (name.isEmpty() || xmlStr.isEmpty()) continue;
QDomDocument doc;
if (!doc.setContent(xmlStr)) continue;
QDomElement root = doc.documentElement();
NumerotationContext nc;
nc.fromXml(root);
contexts.insert(name, nc);
}
settings.endArray();
return result;
}
+10
View File
@@ -21,6 +21,8 @@
#include <QStringList> #include <QStringList>
#include <QVariant> #include <QVariant>
#include <QDomElement> #include <QDomElement>
#include <QHash>
#include <QSettings>
/** /**
This class represents a numerotation context, i.e. the data (type, value, increase) This class represents a numerotation context, i.e. the data (type, value, increase)
@@ -60,6 +62,14 @@ class NumerotationContext
/// UI preview of a part's value matches what actually gets rendered. /// UI preview of a part's value matches what actually gets rendered.
static QString formatValue(const QStringList &item); static QString formatValue(const QStringList &item);
static void saveToSettings(const QHash<QString, NumerotationContext> &contexts,
const QString &currentRule,
QSettings &settings,
const QString &prefix);
static QPair<QHash<QString, NumerotationContext>, QString> loadFromSettings(
QSettings &settings,
const QString &prefix);
private: private:
QStringList content_; QStringList content_;
}; };
+4 -1
View File
@@ -505,7 +505,10 @@ void BorderTitleBlock::draw(QPainter *painter)
{ {
//Set the QPainter //Set the QPainter
painter -> save(); painter -> save();
QPen pen(Qt::black); //Use a pen color that contrasts with the background
QColor border_color = Diagram::background_color.lightness() < 128
? QColor(Qt::white) : QColor(Qt::black);
QPen pen(border_color);
painter -> setPen(pen); painter -> setPen(pen);
painter -> setBrush(Qt::NoBrush); painter -> setBrush(Qt::NoBrush);
+13
View File
@@ -499,6 +499,7 @@ void ConductorProperties::applyForEqualAttributes(QList<ConductorProperties> lis
horiz_rotate_text = cp.horiz_rotate_text; horiz_rotate_text = cp.horiz_rotate_text;
m_vertical_alignment = cp.m_vertical_alignment; m_vertical_alignment = cp.m_vertical_alignment;
m_horizontal_alignment = cp.m_horizontal_alignment; m_horizontal_alignment = cp.m_horizontal_alignment;
style = cp.style;
return; return;
} }
@@ -555,6 +556,18 @@ void ConductorProperties::applyForEqualAttributes(QList<ConductorProperties> lis
m_dash_size = i_value; m_dash_size = i_value;
equal = true; equal = true;
//style
Qt::PenStyle pen_style;
pen_style = clist.first().style;
for(ConductorProperties cp : clist)
{
if (cp.style != pen_style)
equal = false;
}
if (equal)
style = pen_style;
equal = true;
//text //text
s_value = clist.first().text; s_value = clist.first().text;
for(ConductorProperties cp : clist) for(ConductorProperties cp : clist)
+10 -7
View File
@@ -26,6 +26,7 @@
#include "diagramposition.h" #include "diagramposition.h"
#include "factory/elementfactory.h" #include "factory/elementfactory.h"
#include "qetapp.h" #include "qetapp.h"
#include "qetpalette.h"
#include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h" #include "qetgraphicsitem/ViewItem/qetgraphicstableitem.h"
#include "qetgraphicsitem/conductor.h" #include "qetgraphicsitem/conductor.h"
#include "qetgraphicsitem/conductortextitem.h" #include "qetgraphicsitem/conductortextitem.h"
@@ -283,10 +284,11 @@ void Diagram::drawBackground(QPainter *p, const QRectF &r) {
* if background color is black, * if background color is black,
* then grid spots shall be white, * then grid spots shall be white,
* else they shall be black in color. * else they shall be black in color.
* A view that shows the sheet with its lightness inverted
* gets softer dots, see QET::Palette::gridDotColor.
*/ */
QPen pen; QPen pen;
Diagram::background_color == Qt::black? pen.setColor(Qt::white) pen.setColor(QET::Palette::gridDotColor(Diagram::background_color, m_inverted_lightness));
: pen.setColor(Qt::black);
pen.setCosmetic(true); pen.setCosmetic(true);
p->setPen(pen); p->setPen(pen);
@@ -1707,10 +1709,7 @@ bool Diagram::fromXml(QDomElement &document,
//Get the top left corner of the rectangle that contain all added items //Get the top left corner of the rectangle that contain all added items
QRectF items_rect; QRectF items_rect;
for (auto item : added_items) { for (auto item : added_items) {
items_rect = items_rect.united( items_rect = items_rect.united(item->mapToScene(item->boundingRect()).boundingRect());
item->mapToScene(
item->boundingRect()
).boundingRect());
} }
QPointF point_ = items_rect.topLeft(); QPointF point_ = items_rect.topLeft();
@@ -1718,8 +1717,12 @@ bool Diagram::fromXml(QDomElement &document,
position.y() - point_.y())); position.y() - point_.y()));
//Translate all added items //Translate all added items
for (auto qgi : added_items) for (auto qgi : added_items) {
qgi->setPos(qgi->pos() += pos_); qgi->setPos(qgi->pos() += pos_);
}
}
else
{
} }
// Load conductor // Load conductor
+14
View File
@@ -123,6 +123,7 @@ class Diagram : public QGraphicsScene
qreal diagram_qet_version_; qreal diagram_qet_version_;
bool draw_grid_; bool draw_grid_;
bool m_inverted_lightness = false;
bool use_border_; bool use_border_;
bool draw_guides_; bool draw_guides_;
QList<Diagram::Guide> m_guides_list; QList<Diagram::Guide> m_guides_list;
@@ -222,6 +223,7 @@ class Diagram : public QGraphicsScene
ExportProperties applyProperties(const ExportProperties &); ExportProperties applyProperties(const ExportProperties &);
void setDisplayGrid(bool); void setDisplayGrid(bool);
bool displayGrid(); bool displayGrid();
void setInvertedLightness(bool);
void setDisplayGuides(bool); void setDisplayGuides(bool);
bool displayGuides(); bool displayGuides();
void updateProjectGuides(const QList<GuideProperties> &guides); void updateProjectGuides(const QList<GuideProperties> &guides);
@@ -355,6 +357,18 @@ inline void Diagram::setDisplayGrid(bool dg) {
draw_grid_ = dg; draw_grid_ = dg;
} }
/**
@brief Diagram::setInvertedLightness
Tell the diagram whether the view painting it will show the result
with its lightness inverted (PaletteGraphicsView on a dark palette).
drawBackground draws a softer grid in that case. Printing
and export never set this.
@param inverted
*/
inline void Diagram::setInvertedLightness(bool inverted) {
m_inverted_lightness = inverted;
}
/** /**
@brief Diagram::displayGrid @brief Diagram::displayGrid
@return draw_grid_ true if the grid is drawn, false otherwise. @return draw_grid_ true if the grid is drawn, false otherwise.
+21 -7
View File
@@ -67,7 +67,19 @@ m_preview_item(nullptr)
dummy_diagram->setDisplayGrid(false); dummy_diagram->setDisplayGrid(false);
dummy_diagram->fromXml(diagram_node, QPointF(0, 0), false, nullptr); dummy_diagram->fromXml(diagram_node, QPointF(0, 0), false, nullptr);
// Compute bounding rect of TOP-LEVEL items only (matching fromXml's added_items logic)
// Child items (DynamicElementTextItem, Terminal) are NOT included - they move with parents
QRectF top_level_rect;
for (auto *item : dummy_diagram->items()) {
if (!item->parentItem()) {
top_level_rect = top_level_rect.united(
item->mapToScene(item->boundingRect()).boundingRect());
}
}
m_items_top_left = top_level_rect.topLeft();
QRectF scene_rect = dummy_diagram->itemsBoundingRect(); QRectF scene_rect = dummy_diagram->itemsBoundingRect();
if (!scene_rect.isEmpty()) { if (!scene_rect.isEmpty()) {
QPixmap pixmap(scene_rect.toAlignedRect().size()); QPixmap pixmap(scene_rect.toAlignedRect().size());
pixmap.fill(Qt::transparent); pixmap.fill(Qt::transparent);
@@ -80,10 +92,11 @@ m_preview_item(nullptr)
} }
} }
if (m_preview_item) { if (m_preview_item) {
m_preview_item->setPos(Diagram::snapToGrid(pos)); QPointF snapped = Diagram::snapToGrid(pos);
m_preview_item->setOpacity(0.6); m_preview_item->setPos(snapped);
m_diagram->addItem(m_preview_item); m_preview_item->setOpacity(0.6);
m_diagram->addItem(m_preview_item);
m_running = true; m_running = true;
} }
@@ -117,6 +130,7 @@ void DiagramEventAddMacro::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{ {
if (m_preview_item) { if (m_preview_item) {
const auto pos_{Diagram::snapToGrid(event->scenePos())}; const auto pos_{Diagram::snapToGrid(event->scenePos())};
m_preview_item->setPos(pos_); m_preview_item->setPos(pos_);
if (m_status_bar) { if (m_status_bar) {
@@ -141,7 +155,8 @@ void DiagramEventAddMacro::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
emit finish(); emit finish();
} }
else if (event->button() == Qt::LeftButton) { else if (event->button() == Qt::LeftButton) {
addMacro(Diagram::snapToGrid(event->scenePos())); QPointF snapped = Diagram::snapToGrid(event->scenePos());
addMacro(snapped);
} }
} }
event->setAccepted(true); event->setAccepted(true);
@@ -238,10 +253,9 @@ void DiagramEventAddMacro::addMacro(QPointF final_pos)
if (!diagram_node.isNull()) { if (!diagram_node.isNull()) {
QDomElement cloned_node = diagram_node.cloneNode(true).toElement(); QDomElement cloned_node = diagram_node.cloneNode(true).toElement();
DiagramContent pasted_content; DiagramContent pasted_content;
m_diagram->fromXml(cloned_node, final_pos, false, &pasted_content); m_diagram->fromXml(cloned_node, final_pos + m_items_top_left, false, &pasted_content);
m_diagram->refreshContents(); m_diagram->refreshContents();
// Prevent PasteDiagramCommand from erasing labels (BMK) // Prevent PasteDiagramCommand from erasing labels (BMK)
@@ -40,6 +40,7 @@ private:
QDomDocument m_macro_doc; QDomDocument m_macro_doc;
QGraphicsPixmapItem *m_preview_item; QGraphicsPixmapItem *m_preview_item;
QPointer<QStatusBar> m_status_bar; QPointer<QStatusBar> m_status_bar;
QPointF m_items_top_left; // top-left of bounding rect of top-level items in the macro (for correct placement offset)
}; };
#endif // DIAGRAMEVENTADDMACRO_H #endif // DIAGRAMEVENTADDMACRO_H
+10 -1
View File
@@ -29,7 +29,14 @@
*/ */
DiagramEventAddText::DiagramEventAddText(Diagram *diagram) : DiagramEventAddText::DiagramEventAddText(Diagram *diagram) :
DiagramEventInterface(diagram) DiagramEventInterface(diagram)
{} {
//The tool is armed from the moment it is attached: the next left
//click places a text. DiagramView::keyPressEvent() asks
//Diagram::eventInterfaceIsRunning() -- which is isRunning(), i.e.
//m_running -- before letting Escape through to the tool, so without
//this the view swallows Escape and the tool cannot be cancelled.
m_running = true;
}
/** /**
@brief DiagramEventAddText::~DiagramEventAddText @brief DiagramEventAddText::~DiagramEventAddText
@@ -52,6 +59,8 @@ void DiagramEventAddText::mousePressEvent(QGraphicsSceneMouseEvent *event)
event->scenePos())); event->scenePos()));
text->setTextInteractionFlags(Qt::TextEditorInteraction); text->setTextInteractionFlags(Qt::TextEditorInteraction);
text->setFocus(Qt::MouseFocusReason); text->setFocus(Qt::MouseFocusReason);
//Placed: the tool is done before it announces it.
m_running = false;
emit finish(); emit finish();
event->setAccepted(true); event->setAccepted(true);
} }
+14 -2
View File
@@ -39,7 +39,9 @@
#include "ElementsCollection/xmlelementcollection.h" #include "ElementsCollection/xmlelementcollection.h"
#include "NameList/nameslist.h" #include "NameList/nameslist.h"
#include "elementdialog.h" #include "elementdialog.h"
#include <QApplication>
#include <QDropEvent> #include <QDropEvent>
#include <QPainter>
#include <QPointer> #include <QPointer>
/** /**
@@ -48,7 +50,7 @@
@param parent Le QWidget parent de cette vue de schema @param parent Le QWidget parent de cette vue de schema
*/ */
DiagramView::DiagramView(Diagram *diagram, QWidget *parent) : DiagramView::DiagramView(Diagram *diagram, QWidget *parent) :
QGraphicsView (parent), PaletteGraphicsView (parent),
m_diagram (diagram) m_diagram (diagram)
{ {
grabGesture(Qt::PinchGesture); grabGesture(Qt::PinchGesture);
@@ -1081,6 +1083,16 @@ bool DiagramView::event(QEvent *e) {
return(QGraphicsView::event(e)); return(QGraphicsView::event(e));
} }
/**
@brief DiagramView::paintingInverted
Reimplemented from PaletteGraphicsView: tell the diagram it is being
drawn for an inverted display, so it softens its grid.
*/
void DiagramView::paintingInverted(bool inverted)
{
m_diagram->setInvertedLightness(inverted);
}
/** /**
@brief DiagramView::paintEvent @brief DiagramView::paintEvent
Reimplemented from QGraphicsView Reimplemented from QGraphicsView
@@ -1088,7 +1100,7 @@ bool DiagramView::event(QEvent *e) {
*/ */
void DiagramView::paintEvent(QPaintEvent *event) void DiagramView::paintEvent(QPaintEvent *event)
{ {
QGraphicsView::paintEvent(event); PaletteGraphicsView::paintEvent(event);
if (m_free_rubberbanding && m_free_rubberband.count() >= 3) if (m_free_rubberbanding && m_free_rubberband.count() >= 3)
{ {
+3 -2
View File
@@ -22,7 +22,7 @@
#include "titleblock/templatelocation.h" #include "titleblock/templatelocation.h"
#include <QClipboard> #include <QClipboard>
#include <QGraphicsView> #include "palettegraphicsview.h"
class Conductor; class Conductor;
class Diagram; class Diagram;
@@ -35,7 +35,7 @@ class QGestureEvent;
This class provides a widget to render an electric diagram in an editable, This class provides a widget to render an electric diagram in an editable,
interactive way. interactive way.
*/ */
class DiagramView : public QGraphicsView class DiagramView : public PaletteGraphicsView
{ {
Q_OBJECT Q_OBJECT
@@ -84,6 +84,7 @@ class DiagramView : public QGraphicsView
///Set for one call only, by the Escape handler, to let focus leave the view. ///Set for one call only, by the Escape handler, to let focus leave the view.
bool m_releasing_focus = false; bool m_releasing_focus = false;
void paintEvent(QPaintEvent *event) override; void paintEvent(QPaintEvent *event) override;
void paintingInverted(bool inverted) override;
void mousePressEvent(QMouseEvent *) override; void mousePressEvent(QMouseEvent *) override;
void mouseMoveEvent(QMouseEvent *) override; void mouseMoveEvent(QMouseEvent *) override;
void mouseReleaseEvent(QMouseEvent *) override; void mouseReleaseEvent(QMouseEvent *) override;
+12
View File
@@ -52,6 +52,18 @@ ElementsCollectionCache::ElementsCollectionCache(const QString &database_path, Q
QSqlQuery(cache_db_).exec("PRAGMA locking_mode = EXCLUSIVE"); QSqlQuery(cache_db_).exec("PRAGMA locking_mode = EXCLUSIVE");
QSqlQuery(cache_db_).exec("PRAGMA synchronous = OFF"); QSqlQuery(cache_db_).exec("PRAGMA synchronous = OFF");
// Previews used to be stored on an opaque white sheet; they are
// transparent now, so the collection tree can adapt them to a
// dark palette. A cache written before that is dropped once.
QSqlQuery(cache_db_).exec("CREATE TABLE IF NOT EXISTS meta"
"(key VARCHAR(32) NOT NULL PRIMARY KEY, value VARCHAR(64));");
QSqlQuery meta(cache_db_);
meta.exec("SELECT value FROM meta WHERE key = 'pixmaps'");
if (!meta.next() || meta.value(0).toString() != QLatin1String("transparent")) {
QSqlQuery(cache_db_).exec("DROP TABLE IF EXISTS pixmaps");
QSqlQuery(cache_db_).exec("DROP TABLE IF EXISTS names");
QSqlQuery(cache_db_).exec("REPLACE INTO meta (key, value) VALUES ('pixmaps', 'transparent')");
}
#if TODO_LIST #if TODO_LIST
#pragma message("@TODO the tables could already exist, handle that case.") #pragma message("@TODO the tables could already exist, handle that case.")
#endif #endif
+2 -18
View File
@@ -54,24 +54,8 @@ ElementsPanel::ElementsPanel(QWidget *parent) :
setDropIndicatorShown(true); setDropIndicatorShown(true);
setAutoExpandDelay(1000); setAutoExpandDelay(1000);
// force du noir sur une alternance de blanc (comme le schema) et de gris // Rows follow the application palette; the icons shown here come from
// clair, avec du blanc sur bleu pas trop fonce pour la selection // the icon theme, which has a dark variant.
//
// Element icons are rendered with colors read directly from each .elmt
// file (almost always black linework, matching printed-schematic
// convention) onto a transparent background -- so this view must keep
// a light background regardless of the OS/desktop theme, or the icons
// become invisible on dark themes. QAbstractItemView paints its rows
// using the viewport's palette, not the view's own, so the palette
// must be applied to both to actually take effect under every style.
QPalette qp = palette();
qp.setColor(QPalette::Text, Qt::black);
qp.setColor(QPalette::Base, Qt::white);
qp.setColor(QPalette::AlternateBase, QColor("#e8e8e8"));
qp.setColor(QPalette::Highlight, QColor("#678db2"));
qp.setColor(QPalette::HighlightedText, Qt::black);
setPalette(qp);
viewport()->setPalette(qp);
// we handle double click on items ourselves // we handle double click on items ourselves
connect(this, &ElementsPanel::itemDoubleClicked, this, &ElementsPanel::slot_doubleClick); connect(this, &ElementsPanel::itemDoubleClicked, this, &ElementsPanel::slot_doubleClick);
+8 -19
View File
@@ -137,25 +137,14 @@ void ElementTextsMover::endMovement()
QString ElementTextsMover::undoText() const QString ElementTextsMover::undoText() const
{ {
QString undo_text; QStringList parts;
if (m_text_count)
parts << QObject::tr("%n texte(s) d'élément", "", m_text_count);
if (m_group_count)
parts << QObject::tr("%n groupe(s) de textes", "", m_group_count);
if(m_text_count == 1) if (parts.isEmpty())
undo_text.append(QObject::tr("Déplacer un texte d'élément")); return QString(); // should never occur
else if(m_text_count > 1)
undo_text.append(QObject::tr("Déplacer %1 textes d'élément").arg(m_items_hash.size()));
if(m_group_count >= 1) return QObject::tr("Déplacer %1").arg(QLocale().createSeparatedList(parts));
{
if(undo_text.isEmpty())
undo_text.append(QObject::tr("Déplacer"));
else
undo_text.append(QObject::tr(" et"));
if(m_group_count == 1)
undo_text.append(QObject::tr(" un groupe de texte"));
else
undo_text.append(QObject::tr((" %1 groupes de textes")).arg(m_group_count));
}
return undo_text;
} }
+7 -6
View File
@@ -146,12 +146,13 @@ QPixmap ElementPictureFactory::pixmap(const ElementsLocation &location)
QPixmap pix(w, h); QPixmap pix(w, h);
//Element definitions almost always draw with a hardcoded black //Element definitions almost always draw with a hardcoded black
//stroke color, on the assumption of the white diagram sheet they //stroke color, on the assumption of the white diagram sheet they
//are normally placed on. A transparent background here makes //are normally placed on. The pixmap is kept as drawn, on a
//that stroke disappear against a dark widget/tree-view background //transparent background: the places that show it (the
//(bugtracker #335). Give it an opaque white background instead - //collection tree through ElementPreviewDelegate, the drag icon)
//exactly what the element already assumes visually, in every //adapt it to the palette with QET::Palette::forPalette(), so a
//context this pixmap is used (tree icons, drag icon, previews). //dark palette gets light ink instead of black on black
pix.fill(Qt::white); //(bugtracker #335).
pix.fill(Qt::transparent);
QPainter painter(&pix); QPainter painter(&pix);
painter.setRenderHint(QPainter::Antialiasing, true); painter.setRenderHint(QPainter::Antialiasing, true);
+237
View File
@@ -0,0 +1,237 @@
/*
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 "palettegraphicsview.h"
#include "qetpalette.h"
#include <QApplication>
#include <QEvent>
#include <QPaintEvent>
#include <QStyleHintReturnMask>
#include <QStyleOptionRubberBand>
#include <QtMath>
namespace {
/**
QGraphicsScene::drawItems() is protected, and QGraphicsView::drawItems()
hands the scene the viewport only when the painter is on it. The
view paints into an image, and still needs the scene to get the
viewport: that is what makes the scene record where it painted
each item, which is where the item is erased from when it moves.
Naming the member through a derived class is the standard way to
a pointer to a protected member; a call through it dispatches to
the scene's own override, if any.
*/
struct SceneAccess : QGraphicsScene
{
using DrawItems = void (QGraphicsScene::*)(QPainter *, int, QGraphicsItem *[],
const QStyleOptionGraphicsItem[], QWidget *);
static DrawItems drawItemsPointer() { return &SceneAccess::drawItems; }
};
}
PaletteGraphicsView::PaletteGraphicsView(QWidget *parent) :
QGraphicsView(parent)
{
qApp->installEventFilter(this);
}
PaletteGraphicsView::PaletteGraphicsView(QGraphicsScene *scene, QWidget *parent) :
QGraphicsView(scene, parent)
{
qApp->installEventFilter(this);
}
/**
@brief PaletteGraphicsView::invertsLightness
@return true when the scene is shown with its lightness inverted,
i.e. when the application palette is dark. The application palette,
not the view's own: a style sheet on an ancestor (the folio tab
widget has one) makes QStyleSheetStyle pin the palette of every
widget under it to the application palette in force when the sheet
was applied, so after a live light/dark switch palette() is stale.
*/
bool PaletteGraphicsView::invertsLightness() const
{
return QET::Palette::isDark(QApplication::palette());
}
void PaletteGraphicsView::paintingInverted(bool inverted)
{
Q_UNUSED(inverted)
}
/**
@brief PaletteGraphicsView::eventFilter
Repaint the whole viewport when the application palette changes. Qt
sends that change to the application object and then repaints only
the widgets whose own palette changed with it, which under a style
sheet is not the case (see invertsLightness()): the scene would then
repaint only what it updates itself, and the viewport around the
sheet would keep the colors of the previous palette. The filter sits
on the application object, the one receiver Qt always notifies.
*/
bool PaletteGraphicsView::eventFilter(QObject *watched, QEvent *event)
{
if (watched == qApp && event->type() == QEvent::ApplicationPaletteChange)
viewport()->update();
return QGraphicsView::eventFilter(watched, event);
}
/**
@brief PaletteGraphicsView::paintEvent
Paints as QGraphicsView on a light palette, inverted on a dark one.
*/
void PaletteGraphicsView::paintEvent(QPaintEvent *event)
{
if (invertsLightness() && !customBackgroundColor())
{
paintInverted(event);
return;
}
m_buffer = QImage();
QGraphicsView::paintEvent(event);
}
/**
@brief PaletteGraphicsView::paintInverted
Run QGraphicsView::paintEvent() with the drawing hooks redirected to
an off-screen image of the viewport, then invert the lightness of the
exposed part of that image between the palette's Base and Text colors
and blit it to the viewport. Inverting the finished rendering turns
the white sheet dark and the black ink light in one pass, and keeps
the hue of colored strokes. The image is in viewport coordinates, so
the hooks paint with the view's own transform and the scene records
the items' places in the viewport, as it does on a light palette.
@param event the paint event, for the exposed area
*/
void PaletteGraphicsView::paintInverted(QPaintEvent *event)
{
const QRect exposed = event->rect().intersected(viewport()->rect());
if (exposed.isEmpty())
return;
const qreal ratio = viewport()->devicePixelRatioF();
const QSize size(qCeil(viewport()->width() * ratio), qCeil(viewport()->height() * ratio));
if (m_buffer.size() != size || m_buffer.devicePixelRatio() != ratio)
{
m_buffer = QImage(size, QImage::Format_RGB32);
m_buffer.setDevicePixelRatio(ratio);
}
m_buffer_painter.begin(&m_buffer);
// The hooks paint only what the scene draws; what they leave blank is
// the white sheet, which the inversion turns into the Base color.
m_buffer_painter.fillRect(exposed, Qt::white);
m_buffer_painter.setClipRect(exposed);
m_buffer_painter.setRenderHints(renderHints());
m_buffer_painter.setWorldTransform(viewportTransform());
m_inverting = true;
paintingInverted(true);
const OptimizationFlags flags = optimizationFlags();
setOptimizationFlag(QGraphicsView::IndirectPainting, true);
QGraphicsView::paintEvent(event);
setOptimizationFlags(flags);
paintingInverted(false);
m_inverting = false;
m_buffer_painter.end();
blitInverted(exposed);
}
/**
@brief PaletteGraphicsView::drawBackground
Into the off-screen image while painting inverted, else as
QGraphicsView.
*/
void PaletteGraphicsView::drawBackground(QPainter *painter, const QRectF &rect)
{
QGraphicsView::drawBackground(m_inverting ? &m_buffer_painter : painter, rect);
}
/**
@brief PaletteGraphicsView::drawItems
Into the off-screen image while painting inverted, with the viewport
as the scene's widget (see SceneAccess), else as QGraphicsView.
*/
void PaletteGraphicsView::drawItems(QPainter *painter, int count, QGraphicsItem *items[],
const QStyleOptionGraphicsItem options[])
{
if (m_inverting && scene())
(scene()->*SceneAccess::drawItemsPointer())(&m_buffer_painter, count, items, options, viewport());
else
QGraphicsView::drawItems(painter, count, items, options);
}
/**
@brief PaletteGraphicsView::drawForeground
Into the off-screen image while painting inverted, else as
QGraphicsView.
*/
void PaletteGraphicsView::drawForeground(QPainter *painter, const QRectF &rect)
{
QGraphicsView::drawForeground(m_inverting ? &m_buffer_painter : painter, rect);
}
/**
@brief PaletteGraphicsView::blitInverted
Invert the lightness of \a area of the off-screen image and draw it on
the viewport, then the selection rubber band on top: the one
QGraphicsView::paintEvent() drew went under the blit.
@param area the part of the viewport to blit, in viewport coordinates
*/
void PaletteGraphicsView::blitInverted(const QRect &area)
{
const qreal ratio = m_buffer.devicePixelRatio();
QImage part = m_buffer.copy(QRectF(area.topLeft() * ratio, area.size() * ratio).toAlignedRect());
part.setDevicePixelRatio(ratio);
// The application palette, for the reason given in invertsLightness().
const QPalette application_palette = QApplication::palette();
QET::Palette::invertLightness(part, application_palette.color(QPalette::Base),
application_palette.color(QPalette::Text));
QPainter painter(viewport());
painter.drawImage(area.topLeft(), part);
drawRubberBand(painter);
}
/**
@brief PaletteGraphicsView::drawRubberBand
Draw the selection rubber band the way QGraphicsView::paintEvent does,
after the inversion, in the palette colors.
@param painter a painter on the viewport
*/
void PaletteGraphicsView::drawRubberBand(QPainter &painter)
{
const QRect band = rubberBandRect();
if (band.isNull())
return;
QStyleOptionRubberBand option;
option.initFrom(viewport());
option.rect = band;
option.shape = QRubberBand::Rectangle;
QStyleHintReturnMask mask;
if (viewport()->style()->styleHint(QStyle::SH_RubberBand_Mask, &option,
viewport(), &mask))
painter.setClipRegion(mask.region, Qt::IntersectClip);
viewport()->style()->drawControl(QStyle::CE_RubberBand, &option,
&painter, viewport());
}
+99
View File
@@ -0,0 +1,99 @@
/*
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 PALETTE_GRAPHICS_VIEW_H
#define PALETTE_GRAPHICS_VIEW_H
#include <QGraphicsView>
#include <QImage>
#include <QPainter>
/**
A QGraphicsView that shows its scene with inverted lightness while the
application palette is dark: white becomes the palette's Base color,
black its Text color, and colored strokes keep their hue. The scene
itself is left as drawn, so printing and exporting it still give black
on white. On a light palette the view paints exactly as QGraphicsView
does.
On a dark palette the view still runs QGraphicsView::paintEvent(), with
the IndirectPainting flag set for the duration of that call, so that
the background, the items and the foreground come through the
drawBackground(), drawItems() and drawForeground() hooks. (That flag
selects Qt's older item-painting algorithm, which first builds a list
of the exposed items and their style options; it is set only while
the view paints inverted.) The hooks
paint into an off-screen image the size of the viewport, in viewport
coordinates; paintEvent() then inverts the lightness of the exposed
part of that image and blits it. Going through the real paint event,
and handing the scene the viewport when the items are drawn, keeps the
view on QGraphicsView's default update path, which erases a moved item
where it was last painted, children included, even a child whose
geometry is set while its parent is painted (a terminal's help lines).
The alternative, rendering with QGraphicsView::render(), needed a
receiver on QGraphicsScene::changed() to keep the scene's updates
flowing, and that receiver puts the scene on its Qt 4.4 compatibility
path, which erases only the moved item's own old rectangle: a moved
element then left its terminals' help lines, which span the whole
sheet, behind at every step (#954).
The CacheBackground cache mode is not supported on the inverted path.
*/
class PaletteGraphicsView : public QGraphicsView
{
Q_OBJECT
public:
explicit PaletteGraphicsView(QWidget *parent = nullptr);
explicit PaletteGraphicsView(QGraphicsScene *scene, QWidget *parent = nullptr);
bool invertsLightness() const;
static void setCustomBackgroundColor(bool custom) { s_custom_bg = custom; }
static bool customBackgroundColor() { return s_custom_bg; }
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
void paintEvent(QPaintEvent *event) override;
void drawBackground(QPainter *painter, const QRectF &rect) override;
void drawItems(QPainter *painter, int count, QGraphicsItem *items[],
const QStyleOptionGraphicsItem options[]) override;
void drawForeground(QPainter *painter, const QRectF &rect) override;
/**
Called with true right before the scene is painted for an
inverted display and with false right after, so a scene can
adapt what it draws (a softer grid, for instance). Does
nothing by default.
*/
virtual void paintingInverted(bool inverted);
private:
void paintInverted(QPaintEvent *event);
void blitInverted(const QRect &area);
void drawRubberBand(QPainter &painter);
/// The off-screen image the hooks paint into while m_inverting:
/// the viewport's size, in its coordinates. Kept between paints,
/// dropped when the view paints on a light palette again.
QImage m_buffer;
QPainter m_buffer_painter;
/// True while paintEvent() paints for an inverted display.
bool m_inverting = false;
static inline bool s_custom_bg = false;
};
#endif
+54 -50
View File
@@ -271,84 +271,88 @@ QString QET::ElementsAndConductorsSentence(
int tables_count, int tables_count,
int terminal_strip_count) int terminal_strip_count)
{ {
QString text; QStringList parts;
if (elements_count) { if (elements_count) {
text += QObject::tr( parts.append(
"%n élément(s)", QObject::tr(
"part of a sentence listing the content of a diagram", "%n élément(s)",
elements_count "part of a enumerative partial sentence listing the content of a diagram",
elements_count
)
); );
} }
if (conductors_count) { if (conductors_count) {
if (!text.isEmpty()) text += ", "; parts.append(
text += QObject::tr( QObject::tr(
"%n conducteur(s)", "%n conducteur(s)",
"part of a sentence listing the content of a diagram", "part of a enumerative partial sentence listing the content of a diagram",
conductors_count conductors_count
)
); );
} }
if (texts_count) { if (texts_count) {
if (!text.isEmpty()) text += ", "; parts.append(
text += QObject::tr( QObject::tr(
"%n champ(s) de texte", "%n champ(s) de texte",
"part of a sentence listing the content of a diagram", "part of a enumerative partial sentence listing the content of a diagram",
texts_count texts_count
)
); );
} }
if (images_count) { if (images_count) {
if (!text.isEmpty()) text += ", "; parts.append(
// Qt's %n only selects a grammatical singular/plural form (the QObject::tr(
// "(s)" convention used by every other count here) -- it never "%n image(s)",
// spells the number out as a word, so getting "une image" "part of a enumerative partial sentence listing the content of a diagram",
// instead of the literal "1 image" for the single-item case images_count
// means handling that count outside %n entirely, with its own )
// fixed string. );
text += images_count == 1
? QObject::tr("une image", "part of a sentence listing the content of a diagram")
: QObject::tr(
"%n images",
"part of a sentence listing the content of a diagram",
images_count
);
} }
if (shapes_count) { if (shapes_count) {
if (!text.isEmpty()) text += ", "; parts.append(
text += QObject::tr( QObject::tr(
"%n forme(s)", "%n forme(s)",
"part of a sentence listing the content of a diagram", "part of a enumerative partial sentence listing the content of a diagram",
shapes_count shapes_count
)
); );
} }
if (element_text_count) { if (element_text_count) {
if (!text.isEmpty()) text += ", "; parts.append(
text += QObject::tr( QObject::tr(
"%n texte(s) d'élément", "%n texte(s) d'élément",
"part of a sentence listing the content of a diagram", "part of a enumerative partial sentence listing the content of a diagram",
element_text_count); element_text_count
)
);
} }
if (tables_count) { if (tables_count) {
if (!text.isEmpty()) text += ", "; parts.append(
text += QObject::tr( QObject::tr(
"%n tableau(s)", "%n tableau(s)",
"part of a sentence listing the content of diagram", "part of a enumerative partial sentence listing the content of diagram",
tables_count); tables_count
)
);
} }
if (terminal_strip_count) { if (terminal_strip_count) {
if (!text.isEmpty()) text += ", "; parts.append(
text += QObject::tr( QObject::tr(
"%n plan de bornes", "%n plan(s) de bornes",
"part of a sentence listing the content of a diagram", "part of a enumerative partial sentence listing the content of a diagram",
terminal_strip_count); terminal_strip_count
)
);
} }
return(text); return QLocale().createSeparatedList(parts);
} }
/** /**
+14
View File
@@ -28,6 +28,7 @@
#include "qetdiagrameditor.h" #include "qetdiagrameditor.h"
#include "qeticons.h" #include "qeticons.h"
#include "qetpalette.h" #include "qetpalette.h"
#include "qetstyle.h"
#include "utils/qetutils.h" #include "utils/qetutils.h"
#include "qetmessagebox.h" #include "qetmessagebox.h"
#include "qetproject.h" #include "qetproject.h"
@@ -52,6 +53,7 @@
#include <QFontDatabase> #include <QFontDatabase>
#include <QProcessEnvironment> #include <QProcessEnvironment>
#include <QRegularExpression> #include <QRegularExpression>
#include <QStyleFactory>
#include <QStyleHints> #include <QStyleHints>
#ifdef BUILD_WITHOUT_KF #ifdef BUILD_WITHOUT_KF
# include "ui/nokde/kautosavefile.h" # include "ui/nokde/kautosavefile.h"
@@ -234,6 +236,8 @@ QString QETApp::loadedQtTranslationFile()
void QETApp::setLanguage(const QString &desired_language) { void QETApp::setLanguage(const QString &desired_language) {
QString languages_path = languagesPath(); QString languages_path = languagesPath();
QLocale::setDefault(QLocale(desired_language));
// load Qt library translations // load Qt library translations
QString qt_l10n_path = QLibraryInfo::path(QLibraryInfo::TranslationsPath); QString qt_l10n_path = QLibraryInfo::path(QLibraryInfo::TranslationsPath);
if (!qtTranslator.load("qt_" + desired_language, qt_l10n_path)) if (!qtTranslator.load("qt_" + desired_language, qt_l10n_path))
@@ -1806,6 +1810,10 @@ void QETApp::useSystemPalette(bool use) {
file.close(); file.close();
} }
} }
// Widgets with their own style sheet keep the palette they were
// polished with; after a live light/dark switch they would stay in
// the old colors (see QET::Palette::refreshStyleSheets).
QET::Palette::refreshStyleSheets();
} }
/** /**
@@ -2368,6 +2376,12 @@ void QETApp::applyIconTheme(const QPalette &palette)
*/ */
void QETApp::initStyle() void QETApp::initStyle()
{ {
// Wrap the running style so icons get a hover state (see qetstyle.h).
// The proxy keeps the base style's object name, so the Fusion checks
// below still see "fusion".
if (!qobject_cast<QETStyle *>(qApp->style()))
qApp->setStyle(new QETStyle(QStyleFactory::create(qApp->style()->objectName())));
initial_palette_ = qApp->palette(); initial_palette_ = qApp->palette();
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS
+22 -12
View File
@@ -50,6 +50,8 @@
#include "recentfiles.h" #include "recentfiles.h"
#include "shortcutmanager.h" #include "shortcutmanager.h"
#include "ui/bomexportdialog.h" #include "ui/bomexportdialog.h"
#include "ui/conductorcolortoolbutton.h"
#include "ui/diagrambgcolorbutton.h"
#include "ui/jumptoelementdialog.h" #include "ui/jumptoelementdialog.h"
#include "ui/diagrampropertieseditordockwidget.h" #include "ui/diagrampropertieseditordockwidget.h"
#include "ui/backupdialog.h" #include "ui/backupdialog.h"
@@ -389,6 +391,13 @@ void QETDiagramEditor::setUpActions()
if (ProjectView *pv = currentProjectView()) if (ProjectView *pv = currentProjectView())
pv->project()->setAutoConductor(ac); pv->project()->setAutoConductor(ac);
}); });
//Registered with no default sequence on purpose. This is a
//setting some people toggle constantly and others never touch,
//so it earns a place in the Shortcuts page rather than a key of
//its own taken from the ones still free. Asked for on the forum
//(viewtopic.php?pid=23296): "est il possible dans les raccourcis
//d'ajouter un pour création automatique de conducteur ?"
ShortcutManager::instance().registerAction(m_auto_conductor, "diagrameditor.auto_conductor", tr("Éditeur de schémas"), QKeySequence());
//AutoBreakConductor //AutoBreakConductor
m_auto_break_conductor = new QAction (QET::Icons::Conductor, tr("Coupure automatique de conducteur(s)","Tool tip of auto break conductor"), this); m_auto_break_conductor = new QAction (QET::Icons::Conductor, tr("Coupure automatique de conducteur(s)","Tool tip of auto break conductor"), this);
@@ -405,15 +414,8 @@ void QETDiagramEditor::setUpActions()
pv->project()->setAutoBreakConductor(abc); pv->project()->setAutoBreakConductor(abc);
}); });
//Switch background color //Diagram background color picker
m_grey_background = new QAction (QET::Icons::DiagramBg, tr("Couleur de fond blanc/gris","Tool tip of white/grey background button"), this); m_background_color_button = new DiagramBgColorToolButton(this, this);
m_grey_background -> setStatusTip (tr("Affiche la couleur de fond du folio en blanc ou en gris", "Status tip of white/grey background button"));
m_grey_background -> setCheckable (true);
connect (m_grey_background, &QAction::triggered, [this](bool checked) {
Diagram::background_color = checked ? Qt::darkGray : Qt::white;
if (this->currentDiagramView() && this->currentDiagramView()->diagram())
this->currentDiagramView()->diagram()->update();
});
//Draw or not the background grid //Draw or not the background grid
m_draw_grid = new QAction ( QET::Icons::Grid, tr("Afficher la grille"), this); m_draw_grid = new QAction ( QET::Icons::Grid, tr("Afficher la grille"), this);
@@ -904,7 +906,7 @@ void QETDiagramEditor::setUpToolBar()
view_tool_bar -> addSeparator(); view_tool_bar -> addSeparator();
view_tool_bar -> addAction(m_draw_grid); view_tool_bar -> addAction(m_draw_grid);
view_tool_bar -> addAction(m_draw_guides); view_tool_bar -> addAction(m_draw_guides);
view_tool_bar -> addAction (m_grey_background); view_tool_bar -> addWidget(m_background_color_button);
view_tool_bar -> addSeparator(); view_tool_bar -> addSeparator();
view_tool_bar -> addActions(m_zoom_action_toolBar); view_tool_bar -> addActions(m_zoom_action_toolBar);
@@ -912,6 +914,10 @@ void QETDiagramEditor::setUpToolBar()
diagram_tool_bar -> addAction (m_conductor_reset); diagram_tool_bar -> addAction (m_conductor_reset);
diagram_tool_bar -> addAction (m_auto_conductor); diagram_tool_bar -> addAction (m_auto_conductor);
diagram_tool_bar -> addAction (m_auto_break_conductor); diagram_tool_bar -> addAction (m_auto_break_conductor);
//Sits with the conductor actions it works alongside: it colours
//the selected conductors and sets the colour of the next one drawn.
m_conductor_color_button = new ConductorColorToolButton(this, this);
diagram_tool_bar -> addWidget (m_conductor_color_button);
m_add_item_tool_bar = new QToolBar(tr("Ajouter"), this); m_add_item_tool_bar = new QToolBar(tr("Ajouter"), this);
m_add_item_tool_bar->setObjectName("adding"); m_add_item_tool_bar->setObjectName("adding");
@@ -1051,7 +1057,7 @@ void QETDiagramEditor::setUpMenu()
menu_affichage -> addSeparator(); menu_affichage -> addSeparator();
menu_affichage -> addAction(m_draw_grid); menu_affichage -> addAction(m_draw_grid);
menu_affichage -> addAction(m_draw_guides); menu_affichage -> addAction(m_draw_guides);
menu_affichage -> addAction(m_grey_background); menu_affichage -> addMenu(m_background_color_button->menu());
menu_affichage -> addSeparator(); menu_affichage -> addSeparator();
menu_affichage -> addActions(m_zoom_actions_group.actions()); menu_affichage -> addActions(m_zoom_actions_group.actions());
@@ -1868,7 +1874,7 @@ void QETDiagramEditor::slot_updateActions()
m_select_actions_group. setEnabled(opened_diagram); m_select_actions_group. setEnabled(opened_diagram);
m_add_item_actions_group. setEnabled(editable_project); m_add_item_actions_group. setEnabled(editable_project);
m_row_column_actions_group. setEnabled(editable_project); m_row_column_actions_group. setEnabled(editable_project);
m_grey_background-> setEnabled(opened_diagram); m_background_color_button-> setEnabled(opened_diagram);
m_draw_grid-> setEnabled(opened_diagram); m_draw_grid-> setEnabled(opened_diagram);
m_draw_guides-> setEnabled(opened_diagram); m_draw_guides-> setEnabled(opened_diagram);
@@ -2110,6 +2116,10 @@ void QETDiagramEditor::slot_updateModeActions()
m_auto_conductor -> setDisabled(true); m_auto_conductor -> setDisabled(true);
m_auto_break_conductor -> setDisabled(true); m_auto_break_conductor -> setDisabled(true);
} }
if (m_conductor_color_button) {
m_conductor_color_button->updateEnabledState();
}
} }
/** /**
+7 -1
View File
@@ -32,7 +32,9 @@ class QMdiSubWindow;
class QETProject; class QETProject;
class QETResult; class QETResult;
class ProjectView; class ProjectView;
class ConductorColorToolButton;
class CustomElement; class CustomElement;
class DiagramBgColorToolButton;
class Diagram; class Diagram;
class DiagramView; class DiagramView;
class Element; class Element;
@@ -199,7 +201,6 @@ class QETDiagramEditor : public QETMainWindow
*m_paste, ///< Paste clipboard content on the current diagram *m_paste, ///< Paste clipboard content on the current diagram
*m_auto_conductor, ///< Enable/Disable the use of auto conductor *m_auto_conductor, ///< Enable/Disable the use of auto conductor
*m_auto_break_conductor, ///< Enable/Disable the use of auto break conductor *m_auto_break_conductor, ///< Enable/Disable the use of auto break conductor
*m_grey_background, ///< Switch the background color in white or grey
*m_draw_grid, ///< Switch the background grid display or not *m_draw_grid, ///< Switch the background grid display or not
*m_draw_guides = nullptr, ///< Switch the custom guides display or not *m_draw_guides = nullptr, ///< Switch the custom guides display or not
*m_project_edit_properties, ///< Edit the properties of the current project. *m_project_edit_properties, ///< Edit the properties of the current project.
@@ -237,6 +238,11 @@ class QETDiagramEditor : public QETMainWindow
*m_find = nullptr, *m_find = nullptr,
*m_jump_to_element = nullptr; ///< Open the "jump to element" quick-open popup *m_jump_to_element = nullptr; ///< Open the "jump to element" quick-open popup
///< One-click conductor colour, in the "Schéma" toolbar
ConductorColorToolButton *m_conductor_color_button = nullptr;
///< Diagram background color picker, in the "Affichage" toolbar
DiagramBgColorToolButton *m_background_color_button = nullptr;
QList <QAction *> m_zoom_action_toolBar; ///Only zoom action must displayed in the toolbar QList <QAction *> m_zoom_action_toolBar; ///Only zoom action must displayed in the toolbar
void removeDiagramSilent(Diagram *diagram); void removeDiagramSilent(Diagram *diagram);
@@ -24,6 +24,7 @@
#include "../qetgraphicsitem/terminal.h" #include "../qetgraphicsitem/terminal.h"
#include "../qetinformation.h" #include "../qetinformation.h"
#include "../utils/qetutils.h" #include "../utils/qetutils.h"
#include "../QetGraphicsItemModeler/qetgraphicshandleritem.h"
#include "crossrefitem.h" #include "crossrefitem.h"
#include "element.h" #include "element.h"
#include "elementtextitemgroup.h" #include "elementtextitemgroup.h"
@@ -67,7 +68,9 @@ DynamicElementTextItem::DynamicElementTextItem(Element *parent_element) :
} }
DynamicElementTextItem::~DynamicElementTextItem() DynamicElementTextItem::~DynamicElementTextItem()
{} {
removeResizeHandles();
}
/** /**
@brief DynamicElementTextItem::textFromMetaEnum @brief DynamicElementTextItem::textFromMetaEnum
@@ -629,7 +632,13 @@ void DynamicElementTextItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
int diffx = qRound(current_parent_pos.x() - button_down_parent_pos.x()); int diffx = qRound(current_parent_pos.x() - button_down_parent_pos.x());
int diffy = qRound(current_parent_pos.y() - button_down_parent_pos.y()); int diffy = qRound(current_parent_pos.y() - button_down_parent_pos.y());
QPointF new_pos = m_initial_position + QPointF(diffx, diffy); QPointF new_pos = m_initial_position + QPointF(diffx, diffy);
setPos(new_pos); //Snap to the grid, Ctrl to place freely -- the same line
//ElementTextItemGroup::mouseMoveEvent() and
//ElementTextsMover::continueMovement() already use, and
//DiagramTextItem::mouseMoveEvent() for independent texts.
//Without it this was the only text move in the editor that
//ignored the grid.
event->modifiers() == Qt::ControlModifier ? setPos(new_pos) : setPos(Diagram::snapToGrid(new_pos));
if(diagram()) if(diagram())
diagram()->elementTextsMover().continueMovement(event); diagram()->elementTextsMover().continueMovement(event);
@@ -723,6 +732,9 @@ void DynamicElementTextItem::paint(QPainter *painter, const QStyleOptionGraphics
{ {
DiagramTextItem::paint(painter, option, widget); DiagramTextItem::paint(painter, option, widget);
if (m_left_resize_handle || m_right_resize_handle)
updateResizeHandlesPos();
if (m_frame) if (m_frame)
{ {
painter->save(); painter->save();
@@ -812,12 +824,41 @@ QVariant DynamicElementTextItem::itemChange(QGraphicsItem::GraphicsItemChange ch
updateXref(); updateXref();
updateXref(); updateXref();
} }
else if (change == QGraphicsItem::ItemSelectedHasChanged)
{
if (value.toBool())
addResizeHandles();
else
removeResizeHandles();
}
else if (change == QGraphicsItem::ItemSceneHasChanged && !scene())
{
removeResizeHandles();
}
return QGraphicsObject::itemChange(change, value); return QGraphicsObject::itemChange(change, value);
} }
bool DynamicElementTextItem::sceneEventFilter(QGraphicsItem *watched, QEvent *event) bool DynamicElementTextItem::sceneEventFilter(QGraphicsItem *watched, QEvent *event)
{ {
if (watched == m_left_resize_handle || watched == m_right_resize_handle)
{
auto *handle = static_cast<QetGraphicsHandlerItem *>(watched);
if (event->type() == QEvent::GraphicsSceneMousePress) {
handlerMousePressEvent(handle, static_cast<QGraphicsSceneMouseEvent *>(event));
return true;
}
else if (event->type() == QEvent::GraphicsSceneMouseMove) {
handlerMouseMoveEvent(handle, static_cast<QGraphicsSceneMouseEvent *>(event));
return true;
}
else if (event->type() == QEvent::GraphicsSceneMouseRelease) {
handlerMouseReleaseEvent(handle, static_cast<QGraphicsSceneMouseEvent *>(event));
return true;
}
return false;
}
if(watched != m_slave_Xref_item) if(watched != m_slave_Xref_item)
return false; return false;
@@ -837,6 +878,125 @@ bool DynamicElementTextItem::sceneEventFilter(QGraphicsItem *watched, QEvent *ev
return false; return false;
} }
/**
@brief DynamicElementTextItem::addResizeHandles
Create and show the two width-resize handles (left/right edge of
frameRect()), reusing QetGraphicsHandlerItem the same way QetShapeItem
does for its own resize handles.
*/
void DynamicElementTextItem::addResizeHandles()
{
if (m_left_resize_handle || !scene())
return;
qreal size = QETUtils::graphicsHandlerSize(this);
m_left_resize_handle = new QetGraphicsHandlerItem(size);
m_right_resize_handle = new QetGraphicsHandlerItem(size);
for (QetGraphicsHandlerItem *handle : {m_left_resize_handle, m_right_resize_handle})
{
scene()->addItem(handle);
handle->setColor(Qt::darkGreen);
handle->setZValue(zValue() + 1);
handle->installSceneEventFilter(this);
}
updateResizeHandlesPos();
}
/**
@brief DynamicElementTextItem::removeResizeHandles
*/
void DynamicElementTextItem::removeResizeHandles()
{
delete m_left_resize_handle;
delete m_right_resize_handle;
m_left_resize_handle = nullptr;
m_right_resize_handle = nullptr;
}
/**
@brief DynamicElementTextItem::updateResizeHandlesPos
Keep the two resize handles at the vertical middle of frameRect()'s left
and right edges, in scene coordinates -- called on every paint() so it
stays correct across every kind of change that can move this item or
change its size (position, rotation, font, text, textWidth...) without
needing a dedicated hook for each one.
*/
void DynamicElementTextItem::updateResizeHandlesPos()
{
if (!m_left_resize_handle || !m_right_resize_handle)
return;
QRectF fr = frameRect();
m_left_resize_handle->setPos(mapToScene(QPointF(fr.left(), fr.center().y())));
m_right_resize_handle->setPos(mapToScene(QPointF(fr.right(), fr.center().y())));
}
/**
@brief DynamicElementTextItem::handlerMousePressEvent
@param handle
@param event
*/
void DynamicElementTextItem::handlerMousePressEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event)
{
Q_UNUSED(handle)
//The actual property value, kept as-is (possibly -1, meaning "auto")
//so a later undo restores the exact original state rather than a
//synthesized fixed width.
m_resize_original_width = textWidth();
//A concrete baseline for the live drag's delta math, which can't
//start from -1.
m_resize_baseline_width = (m_resize_original_width < 0) ? frameRect().width() : m_resize_original_width;
m_resize_start_local_x = mapFromScene(event->scenePos()).x();
}
/**
@brief DynamicElementTextItem::handlerMouseMoveEvent
Live-resize the text while dragging, exactly like the element editor's
resize handles live-update geometry during a drag (undo is only pushed
on release). The drag delta is resolved in this item's own local
coordinates (not scene coordinates) so a rotated text box still resizes
along its own baseline.
@param handle
@param event
*/
void DynamicElementTextItem::handlerMouseMoveEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event)
{
qreal local_x = mapFromScene(event->scenePos()).x();
qreal delta = local_x - m_resize_start_local_x;
if (handle == m_left_resize_handle)
delta = -delta;
qreal new_width = qMax(m_resize_baseline_width + delta, qreal(10));
setTextWidth(new_width);
updateResizeHandlesPos();
}
/**
@brief DynamicElementTextItem::handlerMouseReleaseEvent
Push the same QPropertyUndoCommand the properties-panel width spinbox
already pushes (sources/ui/dynamicelementtextmodel.cpp) -- the value is
already applied live from the drag, so this only makes it undoable.
@param handle
@param event
*/
void DynamicElementTextItem::handlerMouseReleaseEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event)
{
Q_UNUSED(handle)
Q_UNUSED(event)
qreal new_width = textWidth();
if (!qFuzzyCompare(m_resize_original_width, new_width) && m_parent_element && m_parent_element->diagram())
{
auto *undo = new QPropertyUndoCommand(this, "textWidth", QVariant(m_resize_original_width), QVariant(new_width));
undo->setAnimated(true, false);
undo->setText(tr("Redimensionner un texte d'élément"));
m_parent_element->diagram()->undoStack().push(undo);
}
}
void DynamicElementTextItem::elementInfoChanged() void DynamicElementTextItem::elementInfoChanged()
{ {
DiagramContext dc; DiagramContext dc;
@@ -29,6 +29,7 @@ class Element;
class Conductor; class Conductor;
class ElementTextItemGroup; class ElementTextItemGroup;
class CrossRefItem; class CrossRefItem;
class QetGraphicsHandlerItem;
/** /**
@brief The DynamicElementTextItem class @brief The DynamicElementTextItem class
@@ -151,6 +152,12 @@ class DynamicElementTextItem : public DiagramTextItem
void zoomToLinkedElement(); void zoomToLinkedElement();
void parentElementRotationChanged(); void parentElementRotationChanged();
void thisRotationChanged(); void thisRotationChanged();
void addResizeHandles();
void removeResizeHandles();
void updateResizeHandlesPos();
void handlerMousePressEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event);
void handlerMouseMoveEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event);
void handlerMouseReleaseEvent(QetGraphicsHandlerItem *handle, QGraphicsSceneMouseEvent *event);
private: private:
QPointer <Element> QPointer <Element>
@@ -182,6 +189,11 @@ class DynamicElementTextItem : public DiagramTextItem
bool m_rotation_point_center = false; bool m_rotation_point_center = false;
qreal m_visual_rotation_ref = 0; qreal m_visual_rotation_ref = 0;
bool m_move_parent = true; bool m_move_parent = true;
QetGraphicsHandlerItem *m_left_resize_handle = nullptr;
QetGraphicsHandlerItem *m_right_resize_handle = nullptr;
qreal m_resize_original_width = -1;
qreal m_resize_baseline_width = -1;
qreal m_resize_start_local_x = 0;
}; };
#endif // DYNAMICELEMENTTEXTITEM_H #endif // DYNAMICELEMENTTEXTITEM_H
+2
View File
@@ -37,6 +37,7 @@ namespace QET {
QIcon ConductorEdit; QIcon ConductorEdit;
QIcon ConductorSettings; QIcon ConductorSettings;
QIcon Configure; QIcon Configure;
QIcon ConfigureShortcuts;
QIcon ConfigureToolbars; QIcon ConfigureToolbars;
QIcon IC_CopyFile; QIcon IC_CopyFile;
QIcon DefaultConductor; QIcon DefaultConductor;
@@ -408,6 +409,7 @@ void QET::Icons::initIcons()
ConductorEdit = QIcon::fromTheme("conductor-edit"); ConductorEdit = QIcon::fromTheme("conductor-edit");
ConductorSettings = QIcon::fromTheme("conductor-reset"); ConductorSettings = QIcon::fromTheme("conductor-reset");
Configure = QIcon::fromTheme("configure"); Configure = QIcon::fromTheme("configure");
ConfigureShortcuts = QIcon::fromTheme("configure-shortcuts");
ConfigureToolbars = QIcon::fromTheme("configure-toolbars"); ConfigureToolbars = QIcon::fromTheme("configure-toolbars");
IC_CopyFile = QIcon::fromTheme("item-copy"); IC_CopyFile = QIcon::fromTheme("item-copy");
DiagramAdd = QIcon::fromTheme("folio-new"); DiagramAdd = QIcon::fromTheme("folio-new");
+1
View File
@@ -44,6 +44,7 @@ namespace QET {
extern QIcon ConductorEdit; extern QIcon ConductorEdit;
extern QIcon ConductorSettings; extern QIcon ConductorSettings;
extern QIcon Configure; extern QIcon Configure;
extern QIcon ConfigureShortcuts;
extern QIcon ConfigureToolbars; extern QIcon ConfigureToolbars;
extern QIcon IC_CopyFile; extern QIcon IC_CopyFile;
extern QIcon DefaultConductor; extern QIcon DefaultConductor;
+131
View File
@@ -17,7 +17,11 @@
*/ */
#include "qetpalette.h" #include "qetpalette.h"
#include <QApplication>
#include <QColor>
#include <QImage>
#include <QStyle> #include <QStyle>
#include <QWidget>
#include <cmath> #include <cmath>
namespace { namespace {
@@ -74,6 +78,57 @@ bool QET::Palette::isDark(const QPalette &palette)
return palette.color(QPalette::Active, QPalette::Window).lightness() < 128; return palette.color(QPalette::Active, QPalette::Window).lightness() < 128;
} }
void QET::Palette::invertLightness(QImage &image, const QColor &sheet,
const QColor &ink)
{
if (image.format() != QImage::Format_RGB32)
image.convertTo(QImage::Format_RGB32);
// One table per channel maps the inverted value (0 = was white,
// 255 = was black) onto the sheet..ink span.
uchar red_of[256], green_of[256], blue_of[256];
for (int v = 0; v < 256; ++v) {
red_of[v] = uchar(sheet.red() + (ink.red() - sheet.red()) * v / 255);
green_of[v] = uchar(sheet.green() + (ink.green() - sheet.green()) * v / 255);
blue_of[v] = uchar(sheet.blue() + (ink.blue() - sheet.blue()) * v / 255);
}
/* Inverting the lightness of an HSL color while keeping its hue and
saturation leaves the distance between the highest and the lowest
channel unchanged, so it comes down to one offset per pixel:
c + 255 - max - min. The offset turns the highest channel into
255 - min and the lowest into 255 - max, so no channel can leave
the 0..255 range and no clamping is needed. The loop runs on every
repaint of a folio, hence the plain integer arithmetic. */
for (int y = 0; y < image.height(); ++y) {
quint32 *line = reinterpret_cast<quint32 *>(image.scanLine(y));
for (int x = 0, width = image.width(); x < width; ++x) {
const quint32 pixel = line[x];
const int red = (pixel >> 16) & 0xff;
const int green = (pixel >> 8) & 0xff;
const int blue = pixel & 0xff;
int highest = red > green ? red : green;
int lowest = red < green ? red : green;
if (blue > highest) highest = blue;
if (blue < lowest) lowest = blue;
const int offset = 255 - highest - lowest;
line[x] = 0xff000000u
| (quint32(red_of[red + offset]) << 16)
| (quint32(green_of[green + offset]) << 8)
| quint32(blue_of[blue + offset]);
}
}
}
QColor QET::Palette::gridDotColor(const QColor &sheet, bool inverted)
{
if (sheet == QColor(Qt::black))
return Qt::white;
if (inverted)
return QColor(sheet.red() * 2 / 3, sheet.green() * 2 / 3, sheet.blue() * 2 / 3);
return Qt::black;
}
double QET::Palette::contrastRatio(const QColor &a, const QColor &b) double QET::Palette::contrastRatio(const QColor &a, const QColor &b)
{ {
double lighter = relativeLuminance(a); double lighter = relativeLuminance(a);
@@ -177,3 +232,79 @@ QPalette QET::Palette::forFusion(const QPalette &platform)
return withPlatformAccent(isDark(platform) ? fusionDark() : fusionLight(), return withPlatformAccent(isDark(platform) ? fusionDark() : fusionLight(),
platform); platform);
} }
bool QET::Palette::isLineArt(const QImage &image)
{
const QImage source = image.convertToFormat(QImage::Format_ARGB32);
int visible = 0;
int saturated = 0;
for (int y = 0; y < source.height(); ++y)
{
const QRgb *line = reinterpret_cast<const QRgb *>(source.constScanLine(y));
for (int x = 0; x < source.width(); ++x)
{
if (qAlpha(line[x]) <= 64)
continue;
++visible;
const QColor color(line[x]);
if (color.hslSaturationF() > 0.25 && color.value() > 60)
++saturated;
}
}
return visible > 0 && saturated < visible * 0.20;
}
QImage QET::Palette::invertedLightness(const QImage &image)
{
// Lightness of pure black after inversion: the dark palette's text.
const qreal ink = 220.0 / 255.0;
QImage result = image.convertToFormat(QImage::Format_ARGB32);
qreal darkest = 1.0;
for (int y = 0; y < result.height(); ++y)
{
const QRgb *line = reinterpret_cast<const QRgb *>(result.constScanLine(y));
for (int x = 0; x < result.width(); ++x)
if (qAlpha(line[x]) > 64)
darkest = qMin(darkest, QColor(line[x]).lightnessF());
}
const qreal span = qMax(1.0 - darkest, 1e-6);
for (int y = 0; y < result.height(); ++y)
{
QRgb *line = reinterpret_cast<QRgb *>(result.scanLine(y));
for (int x = 0; x < result.width(); ++x)
{
const int alpha = qAlpha(line[x]);
if (alpha == 0)
continue;
const QColor color(line[x]);
const qreal lightness = qBound(0.0, ink * (1.0 - (color.lightnessF() - darkest) / span), 1.0);
QColor out = QColor::fromHslF(qMax(color.hslHueF(), 0.0), color.hslSaturationF(), lightness);
out.setAlpha(alpha);
line[x] = out.rgba();
}
}
return result;
}
QPixmap QET::Palette::forPalette(const QPixmap &pixmap, const QPalette &palette)
{
if (pixmap.isNull() || !isDark(palette))
return pixmap;
const QImage image = pixmap.toImage();
if (!isLineArt(image))
return pixmap;
QPixmap result = QPixmap::fromImage(invertedLightness(image));
result.setDevicePixelRatio(pixmap.devicePixelRatio());
return result;
}
void QET::Palette::refreshStyleSheets()
{
// Setting the same sheet again is not a no-op: QWidget::setStyleSheet()
// asks QStyleSheetStyle to repolish the widget, which recomputes its
// palette from the application palette now in force.
const QWidgetList widgets = QApplication::allWidgets();
for (QWidget *widget : widgets)
if (!widget->styleSheet().isEmpty())
widget->setStyleSheet(widget->styleSheet());
}
+60
View File
@@ -18,8 +18,11 @@
#ifndef QET_PALETTE_H #ifndef QET_PALETTE_H
#define QET_PALETTE_H #define QET_PALETTE_H
#include <QImage>
#include <QPalette> #include <QPalette>
#include <QPixmap>
class QImage;
class QStyle; class QStyle;
/** /**
@@ -52,6 +55,28 @@ namespace QET {
*/ */
bool isDark(const QPalette &palette); bool isDark(const QPalette &palette);
/**
Invert the lightness of every pixel of \a image, keeping its hue
and saturation, then stretch the result between two colors: pure
white becomes \a sheet, pure black becomes \a ink, and a red
line stays red, only lighter. Made for a rendering of a white
sheet that has to read on a dark palette, with sheet = Base and
ink = Text. The image must be opaque; an image in another format
is converted to RGB32 first.
*/
void invertLightness(QImage &image, const QColor &sheet = Qt::black,
const QColor &ink = Qt::white);
/**
The color of the grid dots on a sheet of color \a sheet: black,
or white on a black sheet. With \a inverted the sheet is about to
be shown with its lightness inverted (PaletteGraphicsView), where
black dots would come out as bright as the ink; the dots are then
a third of the way from the sheet color to black, which shows as
a soft gray.
*/
QColor gridDotColor(const QColor &sheet, bool inverted);
/** /**
WCAG 2 contrast ratio between two opaque colors, from 1 (equal) WCAG 2 contrast ratio between two opaque colors, from 1 (equal)
to 21 (black on white). Normal text needs at least 4.5, large to 21 (black on white). Normal text needs at least 4.5, large
@@ -87,6 +112,41 @@ namespace QET {
platform's accent color kept when it is readable. platform's accent color kept when it is readable.
*/ */
QPalette forFusion(const QPalette &platform); QPalette forFusion(const QPalette &platform);
/**
True when fewer than a fifth of the visible pixels are
saturated: black or gray line art, which is what element
previews and most of QET's own icons are.
*/
bool isLineArt(const QImage &image);
/**
The image with its lightness inverted and hue, saturation and
alpha kept: the darkest ink becomes light gray (220), white
becomes black. The rule misc/make_icon_themes.py applies when
it builds the dark icon theme.
*/
QImage invertedLightness(const QImage &image);
/**
A picture drawn for a white sheet, made to read on palette:
returned as is on a light palette, and with its lightness
inverted on a dark one when it is line art. Colored art is
left alone either way.
*/
QPixmap forPalette(const QPixmap &pixmap, const QPalette &palette);
/**
Make every widget that carries a style sheet take the current
application palette. QApplication::setPalette() reaches plain
widgets, but a widget with a style sheet keeps the palette
QStyleSheetStyle resolved when the sheet was applied, so after a
live light/dark switch it is drawn in the old colors (the folio
tab bar and its buttons, the element info widgets, several
configuration pages). Re-applying each widget's own sheet makes
QStyleSheetStyle resolve it again. Call after setPalette().
*/
void refreshStyleSheets();
} }
} }
+24
View File
@@ -83,6 +83,30 @@ m_project_properties_handler{this}
m_default_guides.append(g); m_default_guides.append(g);
} }
settings.endArray(); settings.endArray();
//Load global auto-numbering defaults from QSettings
{
auto conductorData = NumerotationContext::loadFromSettings(settings, QStringLiteral("autonum/conductor"));
for (auto it = conductorData.first.constBegin(); it != conductorData.first.constEnd(); ++it) {
addConductorAutoNum(it.key(), it.value());
}
if (!conductorData.second.isEmpty()) {
setCurrentConductorAutoNum(conductorData.second);
}
auto elementData = NumerotationContext::loadFromSettings(settings, QStringLiteral("autonum/element"));
for (auto it = elementData.first.constBegin(); it != elementData.first.constEnd(); ++it) {
addElementAutoNum(it.key(), it.value());
}
if (!elementData.second.isEmpty()) {
setCurrrentElementAutonum(elementData.second);
}
auto folioData = NumerotationContext::loadFromSettings(settings, QStringLiteral("autonum/folio"));
for (auto it = folioData.first.constBegin(); it != folioData.first.constEnd(); ++it) {
addFolioAutoNum(it.key(), it.value());
}
}
} }
ProjectPropertiesHandler &QETProject::projectPropertiesHandler() ProjectPropertiesHandler &QETProject::projectPropertiesHandler()
+187
View File
@@ -0,0 +1,187 @@
/*
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 "qetstyle.h"
#include <QApplication>
#include <QImage>
#include <QPainter>
#include <QPixmap>
#include <QStyleOption>
#include <QWidget>
#include "qetpalette.h"
/**
@brief QETStyle::QETStyle
@param base the style to wrap; this object takes ownership of it.
*/
QETStyle::QETStyle(QStyle *base) :
QProxyStyle(base)
{
setObjectName(base ? base->objectName() : QString());
}
/**
@brief QETStyle::isLineArt
The same rule misc/make_icon_themes.py uses to pick the icons that
get a dark variant: an icon is line art when fewer than 20% of its
visible pixels are saturated.
*/
bool QETStyle::isLineArt(const QImage &image)
{
const QImage source = image.convertToFormat(QImage::Format_ARGB32);
int visible = 0;
int saturated = 0;
for (int y = 0; y < source.height(); ++y)
{
const QRgb *line = reinterpret_cast<const QRgb *>(source.constScanLine(y));
for (int x = 0; x < source.width(); ++x)
{
const QRgb pixel = line[x];
if (qAlpha(pixel) <= 64)
continue;
++visible;
const QColor color(pixel);
if (color.hslSaturationF() > 0.25 && color.value() > 60)
++saturated;
}
}
return visible > 0 && saturated < visible * 0.20;
}
/**
@brief QETStyle::hoverColor
The palette's highlight color is the accent users already know from
selections, moved away from the hovered button face until it reaches
3:1 (WCAG 1.4.11) against the Light role: Fusion paints a hovered
auto-raise button with a gradient that runs from Button up to about
that color, and the icon has to read on the lightest part of it. On a
dark face the accent is lightened, a step at a time; on a light face it
is darkened, which keeps a pale accent (macOS's green or yellow
selection color, which comes with black selection text) from being
pushed to white. Should twenty steps not get there, the button text
color serves, which reads on the face by construction.
*/
QColor QETStyle::hoverColor(const QPalette &palette)
{
const QColor face = palette.color(QPalette::Active, QPalette::Light);
const bool light_face = face.lightnessF() > 0.5;
QColor ink = palette.color(QPalette::Active, QPalette::Highlight);
// 3.5 rather than 3.0: the top of Fusion's hover gradient is a shade
// lighter than the Light role, so the icon needs some headroom there.
for (int step = 0; step < 20 && QET::Palette::contrastRatio(ink, face) < 3.5; ++step)
ink = light_face ? ink.darker(110) : ink.lighter(110);
if (QET::Palette::contrastRatio(ink, face) < 3.5)
ink = palette.color(QPalette::Active, QPalette::ButtonText);
return ink;
}
/**
@brief QETStyle::tinted
@return pixmap with every pixel set to color, alpha kept.
*/
QPixmap QETStyle::tinted(const QPixmap &pixmap, const QColor &color)
{
QImage image = pixmap.toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied);
QPainter painter(&image);
painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
painter.fillRect(image.rect(), color);
painter.end();
QPixmap result = QPixmap::fromImage(image);
result.setDevicePixelRatio(pixmap.devicePixelRatio());
return result;
}
/**
@brief QETStyle::menuIcon
An icon for a highlighted menu item: Normal as the original, Active
as the original in the HighlightedText color when it is line art.
Cached per source icon, size and device pixel ratio, since menus
repaint on every pointer move.
*/
QIcon QETStyle::menuIcon(const QIcon &icon, const QPalette &palette, int size, qreal dpr) const
{
const QString key = QString("%1/%2/%3/%4")
.arg(icon.cacheKey()).arg(size).arg(dpr)
.arg(palette.color(QPalette::Active, QPalette::HighlightedText).name());
auto it = menu_icons_.constFind(key);
if (it != menu_icons_.constEnd())
return *it;
const QPixmap normal = icon.pixmap(QSize(size, size), dpr, QIcon::Normal);
QIcon result;
result.addPixmap(normal, QIcon::Normal);
result.addPixmap(isLineArt(normal.toImage())
? tinted(normal, palette.color(QPalette::Active, QPalette::HighlightedText))
: normal,
QIcon::Active);
result.addPixmap(icon.pixmap(QSize(size, size), dpr, QIcon::Disabled), QIcon::Disabled);
menu_icons_.insert(key, result);
return result;
}
/**
@brief QETStyle::drawControl
CE_MenuItem with an icon and a highlight: swap the icon for menuIcon()
so the base style's QIcon::Active request gets an icon that reads on
the highlight bar. Everything else goes to the base style.
*/
void QETStyle::drawControl(ControlElement element,
const QStyleOption *option,
QPainter *painter,
const QWidget *widget) const
{
if (element == CE_MenuItem)
{
const auto *item = qstyleoption_cast<const QStyleOptionMenuItem *>(option);
if (item && !item->icon.isNull()
&& (item->state & State_Selected) && (item->state & State_Enabled))
{
QStyleOptionMenuItem copy = *item;
const int size = pixelMetric(PM_SmallIconSize, option, widget);
const qreal dpr = widget ? widget->devicePixelRatio()
: QApplication::instance() ? qApp->devicePixelRatio() : 1.0;
copy.icon = menuIcon(item->icon, item->palette, size, dpr);
QProxyStyle::drawControl(element, &copy, painter, widget);
return;
}
}
QProxyStyle::drawControl(element, option, painter, widget);
}
/**
@brief QETStyle::generatedIconPixmap
QIcon::Active: a line-art icon comes back tinted in hoverColor(),
alpha kept, so the hovered icon differs from the normal one on both
light and dark palettes. Colored icons and every
other mode go to the base style.
*/
QPixmap QETStyle::generatedIconPixmap(QIcon::Mode mode,
const QPixmap &pixmap,
const QStyleOption *option) const
{
if (mode != QIcon::Active || pixmap.isNull())
return QProxyStyle::generatedIconPixmap(mode, pixmap, option);
if (!isLineArt(pixmap.toImage()))
return QProxyStyle::generatedIconPixmap(mode, pixmap, option);
const QPalette palette = option ? option->palette : QApplication::palette();
return tinted(pixmap, hoverColor(palette));
}
+73
View File
@@ -0,0 +1,73 @@
/*
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 QET_STYLE_H
#define QET_STYLE_H
#include <QHash>
#include <QIcon>
#include <QProxyStyle>
/**
@brief The QETStyle class
A proxy over the running widget style that gives icons a hover state.
QToolButton, QTabBar and item views ask the application style for the
QIcon::Active pixmap of an icon while the mouse is over it. Every
built-in style returns the icon unchanged for that mode, so only the
button frame changes on hover, and on a dark palette Fusion's frame is
too faint to see (GitHub #870). This proxy answers QIcon::Active for
line-art icons with the icon tinted in the palette's highlight color,
lightened when needed to reach 3:1 on a button face, so the icon
itself changes on both light and dark palettes. Colored icons keep
their colors. Every other mode is left to the base style.
Fusion also asks for QIcon::Active for the icon of a highlighted menu
item and paints it on the highlight bar, where the hover tint would
vanish. The generated pixmap is cached per icon and cannot tell a
menu from a toolbar, so drawControl(CE_MenuItem) hands the base style
an icon whose Active pixmap is the icon in the highlighted-text color.
*/
class QETStyle : public QProxyStyle
{
Q_OBJECT
public:
explicit QETStyle(QStyle *base);
~QETStyle() override = default;
QPixmap generatedIconPixmap(QIcon::Mode mode,
const QPixmap &pixmap,
const QStyleOption *option) const override;
void drawControl(ControlElement element,
const QStyleOption *option,
QPainter *painter,
const QWidget *widget = nullptr) const override;
/// True when fewer than a fifth of the visible pixels are saturated.
static bool isLineArt(const QImage &image);
/// The highlight color, lightened until it reads at 3:1 on a button.
static QColor hoverColor(const QPalette &palette);
/// pixmap with every pixel set to color, alpha kept.
static QPixmap tinted(const QPixmap &pixmap, const QColor &color);
private:
QIcon menuIcon(const QIcon &icon, const QPalette &palette, int size, qreal dpr) const;
mutable QHash<QString, QIcon> menu_icons_;
};
#endif
+876
View File
@@ -27,11 +27,25 @@
#include "../qet.h" #include "../qet.h"
#include "../qetgraphicsitem/element.h" #include "../qetgraphicsitem/element.h"
#include "../qetmessagebox.h" #include "../qetmessagebox.h"
#include "../dataBase/projectdatabase.h"
#include "../qetproject.h" #include "../qetproject.h"
#include "../qetresult.h" #include "../qetresult.h"
#include "../qetgraphicsitem/conductor.h"
#include "../qetgraphicsitem/independenttextitem.h"
#include "../qetgraphicsitem/qetshapeitem.h"
#include "../qetgraphicsitem/terminal.h"
#include "../qetinformation.h"
#include "../titleblockproperties.h"
#include "../undocommand/addgraphicsobjectcommand.h" #include "../undocommand/addgraphicsobjectcommand.h"
#include "../undocommand/changeelementinformationcommand.h"
#include "../undocommand/changetitleblockcommand.h"
#include "../undocommand/deleteqgraphicsitemcommand.h" #include "../undocommand/deleteqgraphicsitemcommand.h"
#include "../undocommand/linkelementcommand.h"
#include "../utils/conductorcreator.h"
#include <QSqlError>
#include <QSqlQuery>
#include <QSqlRecord>
#include <QTextStream> #include <QTextStream>
#include <QUndoCommand> #include <QUndoCommand>
@@ -268,6 +282,137 @@ Element *QetScriptApi::findElement(int folioIndex, const QString &elementUuid) c
return nullptr; return nullptr;
} }
Terminal *QetScriptApi::findTerminal(int folioIndex, const QString &elementUuid,
int terminalIndex, const QString &caller)
{
Element *element = findElement(folioIndex, elementUuid);
if (!element) {
log(QStringLiteral("qet.%1: no element %2 on folio %3").arg(caller, elementUuid).arg(folioIndex));
return nullptr;
}
const QList<Terminal *> terminals = element->terminals();
if (terminalIndex < 0 || terminalIndex >= terminals.count()) {
log(QStringLiteral("qet.%1: %2 has %3 terminal(s), no index %4")
.arg(caller, element->name()).arg(terminals.count()).arg(terminalIndex));
return nullptr;
}
return terminals.at(terminalIndex);
}
/**
@brief QetScriptApi::findConductor
The single conductor attached to a terminal, or nullptr.
Conductors carry no persisted uuid, and the terminal1/terminal2 ids the
file uses for their ends are folio-scoped integers QElectroTech
renumbers on every save, so a conductor has no name that survives a
save/load cycle. Naming one by a terminal it is attached to does, and
it reads the way the question is usually asked ("the wire on A1 of
KM1"). A terminal with several conductors on it does not name one, so
refuse rather than silently take the first.
*/
Conductor *QetScriptApi::findConductor(int folioIndex, const QString &elementUuid,
int terminalIndex, const QString &caller)
{
Terminal *terminal = findTerminal(folioIndex, elementUuid, terminalIndex, caller);
if (!terminal) return nullptr;
const QList<Conductor *> conductors = terminal->conductors();
if (conductors.isEmpty()) {
log(QStringLiteral("qet.%1: terminal %2 of %3 has no conductor on it")
.arg(caller).arg(terminalIndex).arg(elementUuid));
return nullptr;
}
if (conductors.count() > 1) {
log(QStringLiteral("qet.%1: terminal %2 of %3 carries %4 conductors, so it does "
"not name one -- use a terminal with a single conductor")
.arg(caller).arg(terminalIndex).arg(elementUuid).arg(conductors.count()));
return nullptr;
}
return conductors.first();
}
namespace {
/**
Read or write one named conductor property. The names are the ones the
project file uses for the same fields (ConductorProperties::toXml), so
that what a script sets is what a reader of the .qet sees, rather than
a third spelling invented here.
*/
QString conductorPropertyValue(const ConductorProperties &p, const QString &name)
{
if (name == QLatin1String("num")) return p.text;
if (name == QLatin1String("formula")) return p.m_formula;
if (name == QLatin1String("function")) return p.m_function;
if (name == QLatin1String("bus")) return p.m_bus;
if (name == QLatin1String("cable")) return p.m_cable;
if (name == QLatin1String("tension_protocol")) return p.m_tension_protocol;
if (name == QLatin1String("conductor_color")) return p.m_wire_color;
if (name == QLatin1String("conductor_section")) return p.m_wire_section;
if (name == QLatin1String("color")) return p.color.name();
if (name == QLatin1String("text_color")) return p.text_color.name();
return QString();
}
bool setConductorPropertyValue(ConductorProperties &p, const QString &name, const QString &value)
{
if (name == QLatin1String("num")) { p.text = value; return true; }
if (name == QLatin1String("formula")) { p.m_formula = value; return true; }
if (name == QLatin1String("function")) { p.m_function = value; return true; }
if (name == QLatin1String("bus")) { p.m_bus = value; return true; }
if (name == QLatin1String("cable")) { p.m_cable = value; return true; }
if (name == QLatin1String("tension_protocol")) { p.m_tension_protocol = value; return true; }
if (name == QLatin1String("conductor_color")) { p.m_wire_color = value; return true; }
if (name == QLatin1String("conductor_section")) { p.m_wire_section = value; return true; }
// The two real colours are QColor, not free text: an unparseable name
// would otherwise be stored as an invalid colour and drawn as black.
if (name == QLatin1String("color") || name == QLatin1String("text_color"))
{
const QColor c(value);
if (!c.isValid()) return false;
if (name == QLatin1String("color")) p.color = c; else p.text_color = c;
return true;
}
return false;
}
const QStringList &conductorPropertyNames()
{
static const QStringList names {
QStringLiteral("num"), QStringLiteral("formula"), QStringLiteral("function"),
QStringLiteral("bus"), QStringLiteral("cable"), QStringLiteral("tension_protocol"),
QStringLiteral("conductor_color"), QStringLiteral("conductor_section"),
QStringLiteral("color"), QStringLiteral("text_color")};
return names;
}
} // namespace
bool QetScriptApi::setInfoKey(int folioIndex, const QString &elementUuid,
const QString &key, const QString &value, const QString &caller)
{
if (!m_project) return false;
if (m_project->isReadOnly()) {
log(QStringLiteral("qet.%1: project is read-only").arg(caller));
return false;
}
if (key.isEmpty()) {
log(QStringLiteral("qet.%1: empty information key").arg(caller));
return false;
}
Element *element = findElement(folioIndex, elementUuid);
if (!element) return false;
const DiagramContext old_info = element->elementInformations();
if (old_info.value(key).toString() == value) return true; // nothing to push
DiagramContext new_info = old_info;
new_info.addValue(key, value);
auto *cmd = new ChangeElementInformationCommand(element, old_info, new_info);
m_project->undoStack()->push(cmd);
return true;
}
/** /**
@brief QetScriptApi::addElement @brief QetScriptApi::addElement
Place a new element on a folio, through the same AddGraphicsObjectCommand Place a new element on a folio, through the same AddGraphicsObjectCommand
@@ -417,6 +562,737 @@ bool QetScriptApi::deleteElement(int folioIndex, const QString &elementUuid)
return true; return true;
} }
bool QetScriptApi::rotateElement(int folioIndex, const QString &elementUuid, double angle)
{
if (m_project && m_project->isReadOnly()) {
log(QStringLiteral("qet.rotateElement: project is read-only"));
return false;
}
Element *element = findElement(folioIndex, elementUuid);
if (!element) return false;
// The same property command RotateSelectionCommand pushes for an
// Element -- deliberately not RotateSelectionCommand itself, which
// works on diagram->selectedItems() and would mean quietly rewriting
// the user's selection to rotate one element by uuid. For a single
// element the two are mechanically identical: that class special-cases
// Element::Type to exactly this one command, and only adds a second,
// positional one when rotating a multi-item selection as a group.
auto *cmd = new QPropertyUndoCommand(element, "rotation",
QVariant(element->rotation()),
QVariant(element->rotation() + angle));
cmd->setText(QObject::tr("Pivoter %1").arg(element->name()));
m_project->undoStack()->push(cmd);
return true;
}
QStringList QetScriptApi::elementUuids(int folioIndex) const
{
QStringList uuids;
if (!m_project) return uuids;
const QList<Diagram *> diagrams = m_project->diagrams();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return uuids;
DiagramContent content(diagrams.at(folioIndex), false);
for (Element *elmt : std::as_const(content.m_elements)) {
uuids << elmt->uuid().toString();
}
return uuids;
}
QString QetScriptApi::elementName(int folioIndex, const QString &elementUuid) const
{
Element *element = findElement(folioIndex, elementUuid);
return element ? element->name() : QString();
}
/**
@brief QetScriptApi::elementTerminals
The element's terminals, in the order addConductor() indexes them: one
entry per terminal, "<index>: <name> (<n> conductor(s))". Descriptive
rather than structured because its only job is to let a script -- or a
human reading a script's output -- see which index is which before
wiring anything to it.
Indexes, not uuids, because a terminal uuid does not address a terminal
on a folio. Terminal::uuid() comes from the catalog .elmt definition
(see Terminal::stableUuid()), so it is empty for most of the installed
base, and where it is not, every instance of that same element carries
the same one -- two coils of one type placed side by side have
byte-identical terminal uuids, which is plainly visible in the saved
file of any project written through this API. The order of
Element::terminals() also comes from the definition, but it is at least
unambiguous within the element the caller has already named by uuid.
*/
QStringList QetScriptApi::elementTerminals(int folioIndex, const QString &elementUuid) const
{
QStringList list;
Element *element = findElement(folioIndex, elementUuid);
if (!element) return list;
const QList<Terminal *> terminals = element->terminals();
for (int i = 0 ; i < terminals.count() ; ++i)
{
Terminal *t = terminals.at(i);
list << QStringLiteral("%1: %2 (%3 conductor(s))")
.arg(i)
.arg(t->name().isEmpty() ? QStringLiteral("-") : t->name())
.arg(t->conductorsCount());
}
return list;
}
QString QetScriptApi::elementInfo(int folioIndex, const QString &elementUuid, const QString &key) const
{
Element *element = findElement(folioIndex, elementUuid);
if (!element) return QString();
return element->elementInformations().value(key).toString();
}
bool QetScriptApi::setElementInfo(int folioIndex, const QString &elementUuid,
const QString &key, const QString &value)
{
return setInfoKey(folioIndex, elementUuid, key, value, QStringLiteral("setElementInfo"));
}
QString QetScriptApi::elementLabel(int folioIndex, const QString &elementUuid) const
{
return elementInfo(folioIndex, elementUuid, QETInformation::ELMT_LABEL);
}
bool QetScriptApi::setElementLabel(int folioIndex, const QString &elementUuid, const QString &label)
{
return setInfoKey(folioIndex, elementUuid, QETInformation::ELMT_LABEL, label,
QStringLiteral("setElementLabel"));
}
/**
@brief QetScriptApi::addConductor
Wire terminal terminalIndexA of one element to terminalIndexB of
another, on the same folio, through ConductorCreator -- the same class
the "draw a selection rectangle over terminals" GUI path uses. Going
through it rather than constructing a Conductor directly is what makes
the new conductor inherit an existing potential's properties and take
part in conductor auto-numbering; a hand-built one would be silently
outside both.
Refuses, rather than creating anything, when the two terminals sit on
two different existing potentials: ConductorCreator then has to ask
which one's properties the new conductor should inherit, and it asks
with a plain modal QDialog that QET::QetMessageBox's non-interactive
mode does not cover -- so under headless --run there would be nobody to
answer it and the script would hang forever. Same reasoning, and the
same choice, as addElement() makes about the import-conflict dialog.
@return true if a conductor was created
*/
bool QetScriptApi::addConductor(int folioIndex,
const QString &elementUuidA, int terminalIndexA,
const QString &elementUuidB, int terminalIndexB)
{
if (!m_project) return false;
if (m_project->isReadOnly()) {
log(QStringLiteral("qet.addConductor: project is read-only"));
return false;
}
const QString caller = QStringLiteral("addConductor");
Terminal *t1 = findTerminal(folioIndex, elementUuidA, terminalIndexA, caller);
Terminal *t2 = findTerminal(folioIndex, elementUuidB, terminalIndexB, caller);
if (!t1 || !t2) return false;
if (t1 == t2) {
log(QStringLiteral("qet.addConductor: both ends are the same terminal"));
return false;
}
if (t1->isLinkedTo(t2)) {
log(QStringLiteral("qet.addConductor: those two terminals are already wired together"));
return false;
}
if (!t1->canBeLinkedTo(t2)) {
log(QStringLiteral("qet.addConductor: those two terminals cannot be linked"));
return false;
}
const QList<Terminal *> terminals {t1, t2};
if (ConductorCreator::needsPotentialChoice(terminals)) {
log(QStringLiteral("qet.addConductor: those terminals are on two different existing "
"potentials, so creating a conductor would ask which one to inherit "
"-- refusing rather than open a dialog no script can answer"));
return false;
}
Diagram *diagram = m_project->diagrams().at(folioIndex);
ConductorCreator creator(diagram, terminals);
Q_UNUSED(creator)
// ConductorCreator has no return value and several ways to decline
// quietly, so report what actually happened rather than that it ran.
return t1->isLinkedTo(t2);
}
/**
@brief QetScriptApi::conductors
One line per conductor on the folio: which terminals it joins and its
number, in the form setConductorProperty() addresses them. Descriptive
rather than structured for the same reason elementTerminals() is -- it
exists so a script, or a person reading its output, can see what is
there before changing it.
*/
QStringList QetScriptApi::conductors(int folioIndex) const
{
QStringList list;
if (!m_project) return list;
const QList<Diagram *> diagrams = m_project->diagrams();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return list;
auto describe = [](Terminal *t) -> QString {
if (!t || !t->parentElement()) return QStringLiteral("?");
return QStringLiteral("%1 terminal %2")
.arg(t->parentElement()->uuid().toString())
.arg(t->parentElement()->terminals().indexOf(t));
};
DiagramContent content(diagrams.at(folioIndex), false);
const QList<Conductor *> all = content.conductors(DiagramContent::AnyConductor);
for (Conductor *c : all)
{
list << QStringLiteral("%1 -- %2 : num='%3'")
.arg(describe(c->terminal1), describe(c->terminal2), c->properties().text);
}
return list;
}
QString QetScriptApi::conductorProperty(int folioIndex, const QString &elementUuid,
int terminalIndex, const QString &property) const
{
// const_cast: findConductor logs, and log() writes to stderr, which is
// not a const operation on this object. The lookup itself changes
// nothing.
auto *self = const_cast<QetScriptApi *>(this);
Conductor *conductor = self->findConductor(folioIndex, elementUuid, terminalIndex,
QStringLiteral("conductorProperty"));
if (!conductor) return QString();
return conductorPropertyValue(conductor->properties(), property);
}
/**
@brief QetScriptApi::setConductorProperty
Set one property on the conductor attached to a terminal -- and on
every other conductor of the same electrical potential.
That is not a convenience, it is the rule the application already
follows: SearchAndReplaceWorker does exactly this, pushing one
QPropertyUndoCommand per conductor of relatedPotentialConductors()
inside a single macro, because a wire number, colour or section
describes a potential and not one drawn segment. Setting it on one
conductor and leaving the rest of the potential disagreeing would
produce a file no GUI action could have produced.
@return true if anything was changed, or if it already held that value
*/
bool QetScriptApi::setConductorProperty(int folioIndex, const QString &elementUuid,
int terminalIndex, const QString &property,
const QString &value)
{
if (!m_project) return false;
const QString caller = QStringLiteral("setConductorProperty");
if (m_project->isReadOnly()) {
log(QStringLiteral("qet.%1: project is read-only").arg(caller));
return false;
}
if (!conductorPropertyNames().contains(property)) {
log(QStringLiteral("qet.%1: unknown property '%2'; expected one of %3")
.arg(caller, property, conductorPropertyNames().join(QStringLiteral(", "))));
return false;
}
Conductor *conductor = findConductor(folioIndex, elementUuid, terminalIndex, caller);
if (!conductor) return false;
ConductorProperties properties = conductor->properties();
if (!setConductorPropertyValue(properties, property, value)) {
log(QStringLiteral("qet.%1: '%2' is not a valid value for %3")
.arg(caller, value, property));
return false;
}
if (properties == conductor->properties()) return true; // already so
QSet<Conductor *> potential = conductor->relatedPotentialConductors(true);
potential << conductor;
m_project->undoStack()->beginMacro(QObject::tr("Modifier les propriétés du conducteur"));
for (Conductor *c : std::as_const(potential))
{
QVariant old_value, new_value;
old_value.setValue(c->properties());
new_value.setValue(properties);
m_project->undoStack()->push(new QPropertyUndoCommand(c, "properties", old_value, new_value));
}
m_project->undoStack()->endMacro();
return true;
}
QString QetScriptApi::elementLinkType(int folioIndex, const QString &elementUuid) const
{
Element *element = findElement(folioIndex, elementUuid);
if (!element) return QString();
switch (element->linkType())
{
case Element::Simple: return QStringLiteral("simple");
case Element::NextReport: return QStringLiteral("next_report");
case Element::PreviousReport: return QStringLiteral("previous_report");
case Element::Master: return QStringLiteral("master");
case Element::Slave: return QStringLiteral("slave");
case Element::Terminale: return QStringLiteral("terminal");
default: return QStringLiteral("unknown");
}
}
QStringList QetScriptApi::linkedElements(int folioIndex, const QString &elementUuid) const
{
QStringList list;
Element *element = findElement(folioIndex, elementUuid);
if (!element) return list;
const QList<Element *> linked = element->linkedElements();
for (Element *e : linked) {
list << e->uuid().toString();
}
return list;
}
/**
@brief QetScriptApi::linkElements
Link two elements -- a master to a slave, or one report to its
counterpart. Two folio indices because a master and its slave normally
sit on different folios; that is the usual case, not the exception.
Whether a given pair may be linked is not decided here.
LinkElementCommand::isLinkable() already holds those rules -- that a
master takes a slave and not another master, that a PLC master pairs
only with a PLC slave, that a next-report pairs only with a
previous-report, and that the target is free -- and asking it rather
than re-deriving them is what keeps a script from producing a link the
GUI would refuse to make.
*/
bool QetScriptApi::linkElements(int folioIndexA, const QString &elementUuidA,
int folioIndexB, const QString &elementUuidB)
{
if (!m_project) return false;
if (m_project->isReadOnly()) {
log(QStringLiteral("qet.linkElements: project is read-only"));
return false;
}
Element *a = findElement(folioIndexA, elementUuidA);
Element *b = findElement(folioIndexB, elementUuidB);
if (!a || !b) {
log(QStringLiteral("qet.linkElements: %1 does not resolve to an element")
.arg(a ? elementUuidB : elementUuidA));
return false;
}
if (a == b) {
log(QStringLiteral("qet.linkElements: an element cannot be linked to itself"));
return false;
}
if (!LinkElementCommand::isLinkable(a, b)) {
log(QStringLiteral("qet.linkElements: %1 (%2) cannot be linked to %3 (%4) -- "
"check the two link types, and that the target is still free")
.arg(elementUuidA, elementLinkType(folioIndexA, elementUuidA),
elementUuidB, elementLinkType(folioIndexB, elementUuidB)));
return false;
}
auto *cmd = new LinkElementCommand(a);
cmd->setLink(b);
m_project->undoStack()->push(cmd);
return a->linkedElements().contains(b);
}
bool QetScriptApi::unlinkElement(int folioIndex, const QString &elementUuid)
{
if (!m_project) return false;
if (m_project->isReadOnly()) {
log(QStringLiteral("qet.unlinkElement: project is read-only"));
return false;
}
Element *element = findElement(folioIndex, elementUuid);
if (!element) return false;
if (element->linkedElements().isEmpty()) return true; // nothing to undo
auto *cmd = new LinkElementCommand(element);
cmd->unlinkAll();
m_project->undoStack()->push(cmd);
return element->linkedElements().isEmpty();
}
namespace {
/**
Reading order for items that have no identity but their position:
top to bottom, then left to right.
On sceneBoundingRect(), not pos(): a QetShapeItem keeps its geometry in
its line/rect/polygon, and its pos() stays at the origin, so three
shapes drawn in different places all sort as (0, 0) and the ordering
collapses -- which is exactly what the first version of this did, and
it made every shape index refer to whichever one the set happened to
yield first. The scene bounding rect reflects where the item actually
is for both kinds.
*/
template <typename T>
QList<T *> sortedByPosition(const QSet<T *> &items)
{
QList<T *> list(items.cbegin(), items.cend());
std::sort(list.begin(), list.end(), [](T *a, T *b) {
const QPointF pa = a->sceneBoundingRect().topLeft();
const QPointF pb = b->sceneBoundingRect().topLeft();
if (pa.y() != pb.y()) return pa.y() < pb.y();
if (pa.x() != pb.x()) return pa.x() < pb.x();
// Two items genuinely at the same point still need a total order,
// or std::sort's result depends on the set's iteration order.
return a < b;
});
return list;
}
} // namespace
QList<IndependentTextItem *> QetScriptApi::sortedTexts(int folioIndex) const
{
if (!m_project) return {};
const QList<Diagram *> diagrams = m_project->diagrams();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return {};
DiagramContent content(diagrams.at(folioIndex), false);
return sortedByPosition(content.m_text_fields);
}
QList<QetShapeItem *> QetScriptApi::sortedShapes(int folioIndex) const
{
if (!m_project) return {};
const QList<Diagram *> diagrams = m_project->diagrams();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return {};
DiagramContent content(diagrams.at(folioIndex), false);
return sortedByPosition(content.m_shapes);
}
IndependentTextItem *QetScriptApi::findText(int folioIndex, int textIndex, const QString &caller)
{
const QList<IndependentTextItem *> list = sortedTexts(folioIndex);
if (textIndex < 0 || textIndex >= list.count()) {
log(QStringLiteral("qet.%1: folio %2 has %3 independent text(s), no index %4")
.arg(caller).arg(folioIndex).arg(list.count()).arg(textIndex));
return nullptr;
}
return list.at(textIndex);
}
QStringList QetScriptApi::texts(int folioIndex) const
{
QStringList out;
const QList<IndependentTextItem *> list = sortedTexts(folioIndex);
for (int i = 0 ; i < list.count() ; ++i)
{
IndependentTextItem *t = list.at(i);
const QPointF at = t->sceneBoundingRect().topLeft();
out << QStringLiteral("%1: '%2' at (%3, %4)")
.arg(i)
.arg(t->toPlainText())
.arg(at.x())
.arg(at.y());
}
return out;
}
/**
@brief QetScriptApi::addText
Place a free-standing text, as the "add text" tool does.
@return its index in texts(), or -1
*/
int QetScriptApi::addText(int folioIndex, const QString &text, double x, double y)
{
if (!m_project) return -1;
if (m_project->isReadOnly()) {
log(QStringLiteral("qet.addText: project is read-only"));
return -1;
}
const QList<Diagram *> diagrams = m_project->diagrams();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1;
Diagram *diagram = diagrams.at(folioIndex);
auto *item = new IndependentTextItem();
item->setPlainText(text);
diagram->undoStack().push(new AddGraphicsObjectCommand(item, diagram, QPointF(x, y)));
return sortedTexts(folioIndex).indexOf(item);
}
bool QetScriptApi::setTextContent(int folioIndex, int textIndex, const QString &text)
{
if (!m_project) return false;
if (m_project->isReadOnly()) {
log(QStringLiteral("qet.setTextContent: project is read-only"));
return false;
}
IndependentTextItem *item = findText(folioIndex, textIndex, QStringLiteral("setTextContent"));
if (!item) return false;
if (item->toPlainText() == text) return true;
auto *cmd = new QPropertyUndoCommand(item, "plainText",
QVariant(item->toPlainText()), QVariant(text));
cmd->setText(QObject::tr("Modifier un texte"));
m_project->undoStack()->push(cmd);
return true;
}
bool QetScriptApi::setTextColor(int folioIndex, int textIndex, const QString &color)
{
if (!m_project) return false;
if (m_project->isReadOnly()) {
log(QStringLiteral("qet.setTextColor: project is read-only"));
return false;
}
const QColor new_color(color);
if (!new_color.isValid()) {
log(QStringLiteral("qet.setTextColor: '%1' is not a valid colour").arg(color));
return false;
}
IndependentTextItem *item = findText(folioIndex, textIndex, QStringLiteral("setTextColor"));
if (!item) return false;
if (item->color() == new_color) return true;
auto *cmd = new QPropertyUndoCommand(item, "color",
QVariant(item->color()), QVariant(new_color));
cmd->setText(QObject::tr("Modifier la couleur d'un texte"));
m_project->undoStack()->push(cmd);
return true;
}
bool QetScriptApi::setTextRotation(int folioIndex, int textIndex, double angle)
{
if (!m_project) return false;
if (m_project->isReadOnly()) {
log(QStringLiteral("qet.setTextRotation: project is read-only"));
return false;
}
IndependentTextItem *item = findText(folioIndex, textIndex, QStringLiteral("setTextRotation"));
if (!item) return false;
auto *cmd = new QPropertyUndoCommand(item, "rotation",
QVariant(item->rotation()),
QVariant(item->rotation() + angle));
cmd->setText(QObject::tr("Pivoter un texte"));
m_project->undoStack()->push(cmd);
return true;
}
bool QetScriptApi::deleteText(int folioIndex, int textIndex)
{
if (!m_project) return false;
if (m_project->isReadOnly()) {
log(QStringLiteral("qet.deleteText: project is read-only"));
return false;
}
IndependentTextItem *item = findText(folioIndex, textIndex, QStringLiteral("deleteText"));
if (!item) return false;
Diagram *diagram = m_project->diagrams().at(folioIndex);
DiagramContent content;
content.m_text_fields << item;
diagram->undoStack().push(new DeleteQGraphicsItemCommand(diagram, content));
return true;
}
QStringList QetScriptApi::shapes(int folioIndex) const
{
QStringList out;
const QList<QetShapeItem *> list = sortedShapes(folioIndex);
for (int i = 0 ; i < list.count() ; ++i)
{
QetShapeItem *shape = list.at(i);
const QRectF r = shape->sceneBoundingRect();
out << QStringLiteral("%1: %2 (%3, %4) to (%5, %6)")
.arg(i)
.arg(shape->name())
.arg(r.left()).arg(r.top()).arg(r.right()).arg(r.bottom());
}
return out;
}
/**
@brief QetScriptApi::addShape
Draw a line, rectangle, ellipse or polygon, as the shape tools do.
Path is deliberately absent: it is built by successive clicks and has
no two-point form to give here.
@return the shape's index in shapes(), or -1
*/
int QetScriptApi::addShape(int folioIndex, const QString &type,
double x1, double y1, double x2, double y2)
{
if (!m_project) return -1;
if (m_project->isReadOnly()) {
log(QStringLiteral("qet.addShape: project is read-only"));
return -1;
}
const QList<Diagram *> diagrams = m_project->diagrams();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1;
QetShapeItem::ShapeType shape_type;
const QString t = type.toLower();
if (t == QLatin1String("line")) shape_type = QetShapeItem::Line;
else if (t == QLatin1String("rectangle")) shape_type = QetShapeItem::Rectangle;
else if (t == QLatin1String("ellipse")) shape_type = QetShapeItem::Ellipse;
else if (t == QLatin1String("polygon")) shape_type = QetShapeItem::Polygon;
else {
log(QStringLiteral("qet.addShape: unknown shape '%1'; expected line, "
"rectangle, ellipse or polygon").arg(type));
return -1;
}
Diagram *diagram = diagrams.at(folioIndex);
auto *shape = new QetShapeItem(QPointF(x1, y1), QPointF(x2, y2), shape_type);
diagram->undoStack().push(new AddGraphicsObjectCommand(shape, diagram, QPointF(0, 0)));
return sortedShapes(folioIndex).indexOf(shape);
}
bool QetScriptApi::deleteShape(int folioIndex, int shapeIndex)
{
if (!m_project) return false;
if (m_project->isReadOnly()) {
log(QStringLiteral("qet.deleteShape: project is read-only"));
return false;
}
const QList<QetShapeItem *> list = sortedShapes(folioIndex);
if (shapeIndex < 0 || shapeIndex >= list.count()) {
log(QStringLiteral("qet.deleteShape: folio %1 has %2 shape(s), no index %3")
.arg(folioIndex).arg(list.count()).arg(shapeIndex));
return false;
}
Diagram *diagram = m_project->diagrams().at(folioIndex);
DiagramContent content;
content.m_shapes << list.at(shapeIndex);
diagram->undoStack().push(new DeleteQGraphicsItemCommand(diagram, content));
return true;
}
/**
@brief QetScriptApi::tables
The tables and views the project database holds, as "name (type)".
Worth reading before writing a query against them: the three *_view
entries are the queryable surface and are named for it; the tables are
how the cache is arranged today.
*/
QStringList QetScriptApi::tables() const
{
QStringList list;
if (!m_project || !m_project->dataBase()) return list;
QSqlQuery q = m_project->dataBase()->newQuery(QStringLiteral(
"SELECT name, type FROM sqlite_master WHERE type IN ('table','view') "
"ORDER BY type, name"));
while (q.next()) {
list << QStringLiteral("%1 (%2)").arg(q.value(0).toString(), q.value(1).toString());
}
return list;
}
/**
@brief QetScriptApi::query
Run a read-only SELECT against the project database and return its rows
as objects, one property per column.
Goes through projectDataBase::newQuery(), which applies
isReadOnlySelect() itself -- the same rule, and the same rejection
message, that the "Requête SQL personnalisée" box in the element-query
dialog shows a user. Nothing here can write: a statement that is not a
single SELECT or WITH...SELECT is refused before it reaches SQLite.
No updateDB() first, deliberately. A script that has just edited
something is the expected caller, so querying a stale cache was the
obvious hazard -- but projectDataBase maintains itself incrementally
through addElement()/elementInfoChanged()/addConductor() and the rest,
which the undo commands behind every edit here already call. Tested
both ways on the cases most likely to be stale: an element added and
labelled, and a conductor property changed, each queried immediately
afterwards through both the table and the view. The counts are the
same with the rebuild and without it. Since updateDB() is a full
repopulation of every table, calling it per query would have been a
real cost for no observable benefit -- so it is not called, and this
note exists so it is not added back on the assumption that it must be
needed.
@return the rows; empty on refusal or SQL error, with queryError()
saying which. An empty result and a failure are not the same thing.
*/
QVariantList QetScriptApi::query(const QString &sql)
{
m_query_error.clear();
QVariantList rows;
if (!m_project || !m_project->dataBase()) {
m_query_error = QStringLiteral("no project database");
return rows;
}
QString rejection;
QSqlQuery q = m_project->dataBase()->newQuery(sql, &rejection);
if (!rejection.isEmpty()) {
m_query_error = rejection;
log(QStringLiteral("qet.query: %1").arg(rejection));
return rows;
}
if (q.lastError().isValid()) {
m_query_error = q.lastError().text();
log(QStringLiteral("qet.query: %1").arg(m_query_error));
return rows;
}
const QSqlRecord record = q.record();
while (q.next())
{
QVariantMap row;
for (int i = 0 ; i < record.count() ; ++i) {
row.insert(record.fieldName(i), q.value(i));
}
rows << row;
}
return rows;
}
QString QetScriptApi::queryError() const
{
return m_query_error;
}
int QetScriptApi::addFolio()
{
if (!m_project) return -1;
if (m_project->isReadOnly()) {
log(QStringLiteral("qet.addFolio: project is read-only"));
return -1;
}
Diagram *diagram = m_project->addNewDiagram();
if (!diagram) return -1;
return m_project->diagrams().indexOf(diagram);
}
bool QetScriptApi::setFolioTitle(int folioIndex, const QString &title)
{
if (!m_project) return false;
if (m_project->isReadOnly()) {
log(QStringLiteral("qet.setFolioTitle: project is read-only"));
return false;
}
const QList<Diagram *> diagrams = m_project->diagrams();
if (folioIndex < 0 || folioIndex >= diagrams.count()) return false;
Diagram *diagram = diagrams.at(folioIndex);
// The folio title is one field of the title block properties, so it
// changes the way the title block dialog changes it: read the whole
// struct, set one member, push the command with both versions.
const TitleBlockProperties old_properties = diagram->border_and_titleblock.exportTitleBlock();
if (old_properties.title == title) return true;
TitleBlockProperties new_properties = old_properties;
new_properties.title = title;
auto *cmd = new ChangeTitleBlockCommand(diagram, old_properties, new_properties);
m_project->undoStack()->push(cmd);
return true;
}
bool QetScriptApi::undo() bool QetScriptApi::undo()
{ {
if (!m_project || !m_project->undoStack()->canUndo()) return false; if (!m_project || !m_project->undoStack()->canUndo()) return false;
+153 -5
View File
@@ -21,10 +21,15 @@
#include <QObject> #include <QObject>
#include <QString> #include <QString>
#include <QStringList> #include <QStringList>
#include <QVariantList>
class QETProject; class QETProject;
class DiagramView; class DiagramView;
class Element; class Element;
class Terminal;
class Conductor;
class IndependentTextItem;
class QetShapeItem;
/** /**
@brief The QetScriptApi class @brief The QetScriptApi class
@@ -65,12 +70,86 @@ class Element;
QPropertyUndoCommand merges consecutive commands on the same QPropertyUndoCommand merges consecutive commands on the same
object+property when their text() also matches object+property when their text() also matches
(QPropertyUndoCommand::mergeWith(), pre-existing), and (QPropertyUndoCommand::mergeWith(), pre-existing), and
setElementPosition()/moveElement() always use the same text for a setElementPosition()/moveElement()/rotateElement() always use the
given element -- so several position changes to the same element in a same text for a given element -- so several position changes, or
row collapse into one undo step, the same way dragging an element several rotations, of the same element in a row collapse into one
does, not one step per call. Verified against exactly that: two undo step, the same way dragging or repeatedly rotating an element
does, not one step per call. setElementInfo()/setElementLabel()
behave the same way for the same reason, through
ChangeElementInformationCommand::mergeWith(). Verified against exactly that: two
consecutive calls on one element, then undo/undo/redo/redo, land consecutive calls on one element, then undo/undo/redo/redo, land
where a merge predicts, not where two independent steps would. where a merge predicts, not where two independent steps would.
- @b Wiring, @b labelling and @b folios: create a conductor between two
terminals (ConductorCreator, the same class the GUI's
drag-a-rectangle-over-terminals path uses, so the result inherits an
existing potential's properties and joins conductor auto-numbering),
change an element's label or any other information key
(ChangeElementInformationCommand, which also tells the project
database what changed), add a folio (QETProject::addNewDiagram(),
already undoable) and set its title (ChangeTitleBlockCommand). With
addElement() these are what make a script able to draw rather than
only rearrange: before them a script could place two symbols and had
no way to connect them.
Terminals are addressed by their @b index in Element::terminals(),
not by uuid, and elementTerminals() prints that indexing so a script
can see what it is about to wire. Terminal uuids look like the
obvious key and are not one: Terminal::uuid() is a property of the
catalog .elmt definition, empty for most of the installed base and,
where present, identical across every instance of that element -- so
it does not distinguish one placed coil's A1 from another's.
- @b Conductor properties and @b cross-references: set a conductor's
number, formula, colour or section, and link a master to a slave or
one report to another. Both follow the application's own rules rather
than writing the field: a conductor property is applied to every
conductor of the same electrical potential, which is what the GUI and
search-and-replace both do -- a wire number belongs to a potential,
not to one drawn segment -- and a link is refused unless
LinkElementCommand::isLinkable() allows it, which is where the
master/slave, PLC-pairing and report-direction rules already live.
linkElements() takes a folio index for each end because a master and
its slave are usually on different ones.
A conductor is addressed as "the conductor on terminal i of element
U", not by an identity of its own: conductors have no persisted uuid,
and the folio-scoped integer ids the file uses for their ends are
renumbered on every save, so there is nothing stable to name one by.
Since the change is potential-wide anyway, any terminal of the
potential names it equally well. A terminal carrying more than one
conductor is ambiguous and is refused rather than guessed at -- which
in practice means a potential is addressed from one of its leaf
terminals, not from the hub several conductors meet at.
- @b Text and @b shapes: the drawing furniture a folio carries beside
its circuit -- a free-standing note, a line, a rectangle, an ellipse
-- added with the same AddGraphicsObjectCommand the corresponding GUI
tools use, and changed through the plainText/color/rotation
properties those items already publish.
These are addressed by @b index into a listing sorted by position
(top to bottom, then left to right), because unlike an element they
carry no uuid and unlike a conductor they have no terminal to be
named by. Position is the only identity they have, and it persists,
so the ordering is the same after a save and reload -- verified
against exactly that. What it is @b not stable against is adding or
deleting one: indexes after the affected position shift, the way a
list's do. Call texts() or shapes() again rather than holding an
index across an edit that adds or removes one.
- @b Querying the project database: run a read-only SELECT against the
SQLite database QElectroTech builds from the project, and get rows
back as objects. This is not a new door. QET already ships a
"Requête SQL personnalisée" box in the element-query dialog where a
user types arbitrary SQL, and it is guarded by the same
projectDataBase::isReadOnlySelect() this calls through
projectDataBase::newQuery(). A script gets what a user already has,
under the same rule, and neither can write.
What is worth knowing is what the database @b is: a cache, rebuilt
from the XML on every load and never written to disk. The three
views -- element_nomenclature_view, project_summary_view and
wiring_list_view -- exist to be queried and are the surface to
depend on. The underlying tables are how the cache happens to be
arranged today, and a column may move. tables() lists both so a
script can see what it is querying rather than guess.
- @b Navigating and @b messaging: select an element, zoom the active - @b Navigating and @b messaging: select an element, zoom the active
view, and show the user a message. Deliberately narrow: selection and view, and show the user a message. Deliberately narrow: selection and
messaging work with no view at all (headless `--run`); zoom is a no-op messaging work with no view at all (headless `--run`); zoom is a no-op
@@ -87,7 +166,11 @@ class Element;
import-collision case that would otherwise reach import-collision case that would otherwise reach
QETProject::importElement()'s own ImportElementDialog::exec() and QETProject::importElement()'s own ImportElementDialog::exec() and
refuses instead, rather than let a plain QDialog (not routed through refuses instead, rather than let a plain QDialog (not routed through
QetMessageBox) block a script the same way. QetMessageBox) block a script the same way. addConductor() declines the
same way, for the same reason, when the two terminals belong to two
different existing potentials and ConductorCreator would therefore ask
which one's properties to inherit -- measured: with that check removed,
exactly that call never returns.
*/ */
class QetScriptApi : public QObject class QetScriptApi : public QObject
{ {
@@ -128,7 +211,62 @@ class QetScriptApi : public QObject
Q_INVOKABLE QString addElement(int folioIndex, const QString &locationPath, double x, double y); Q_INVOKABLE QString addElement(int folioIndex, const QString &locationPath, double x, double y);
Q_INVOKABLE bool setElementPosition(int folioIndex, const QString &elementUuid, double x, double y); Q_INVOKABLE bool setElementPosition(int folioIndex, const QString &elementUuid, double x, double y);
Q_INVOKABLE bool moveElement(int folioIndex, const QString &elementUuid, double dx, double dy); Q_INVOKABLE bool moveElement(int folioIndex, const QString &elementUuid, double dx, double dy);
Q_INVOKABLE bool rotateElement(int folioIndex, const QString &elementUuid, double angle);
Q_INVOKABLE bool deleteElement(int folioIndex, const QString &elementUuid); Q_INVOKABLE bool deleteElement(int folioIndex, const QString &elementUuid);
// -- address what is already there --
Q_INVOKABLE QStringList elementUuids(int folioIndex) const;
Q_INVOKABLE QString elementName(int folioIndex, const QString &elementUuid) const;
Q_INVOKABLE QStringList elementTerminals(int folioIndex, const QString &elementUuid) const;
// -- element information, through ChangeElementInformationCommand --
Q_INVOKABLE QString elementInfo(int folioIndex, const QString &elementUuid, const QString &key) const;
Q_INVOKABLE bool setElementInfo(int folioIndex, const QString &elementUuid, const QString &key, const QString &value);
Q_INVOKABLE QString elementLabel(int folioIndex, const QString &elementUuid) const;
Q_INVOKABLE bool setElementLabel(int folioIndex, const QString &elementUuid, const QString &label);
// -- wire two terminals together --
Q_INVOKABLE bool addConductor(int folioIndex,
const QString &elementUuidA, int terminalIndexA,
const QString &elementUuidB, int terminalIndexB);
// -- conductor properties, applied to the whole potential --
Q_INVOKABLE QStringList conductors(int folioIndex) const;
Q_INVOKABLE QString conductorProperty(int folioIndex, const QString &elementUuid,
int terminalIndex, const QString &property) const;
Q_INVOKABLE bool setConductorProperty(int folioIndex, const QString &elementUuid,
int terminalIndex, const QString &property,
const QString &value);
// -- cross-references: master/slave and report links --
Q_INVOKABLE QString elementLinkType(int folioIndex, const QString &elementUuid) const;
Q_INVOKABLE QStringList linkedElements(int folioIndex, const QString &elementUuid) const;
Q_INVOKABLE bool linkElements(int folioIndexA, const QString &elementUuidA,
int folioIndexB, const QString &elementUuidB);
Q_INVOKABLE bool unlinkElement(int folioIndex, const QString &elementUuid);
// -- independent text and drawing shapes --
Q_INVOKABLE QStringList texts(int folioIndex) const;
Q_INVOKABLE int addText(int folioIndex, const QString &text, double x, double y);
Q_INVOKABLE bool setTextContent(int folioIndex, int textIndex, const QString &text);
Q_INVOKABLE bool setTextColor(int folioIndex, int textIndex, const QString &color);
Q_INVOKABLE bool setTextRotation(int folioIndex, int textIndex, double angle);
Q_INVOKABLE bool deleteText(int folioIndex, int textIndex);
Q_INVOKABLE QStringList shapes(int folioIndex) const;
Q_INVOKABLE int addShape(int folioIndex, const QString &type,
double x1, double y1, double x2, double y2);
Q_INVOKABLE bool deleteShape(int folioIndex, int shapeIndex);
// -- query the project database --
Q_INVOKABLE QStringList tables() const;
Q_INVOKABLE QVariantList query(const QString &sql);
Q_INVOKABLE QString queryError() const;
// -- folios --
Q_INVOKABLE int addFolio();
Q_INVOKABLE bool setFolioTitle(int folioIndex, const QString &title);
Q_INVOKABLE bool undo(); Q_INVOKABLE bool undo();
Q_INVOKABLE bool redo(); Q_INVOKABLE bool redo();
Q_INVOKABLE bool canUndo() const; Q_INVOKABLE bool canUndo() const;
@@ -148,9 +286,19 @@ class QetScriptApi : public QObject
private: private:
bool runFlag(const QString &flag, const QStringList &args); bool runFlag(const QString &flag, const QStringList &args);
Element *findElement(int folioIndex, const QString &elementUuid) const; Element *findElement(int folioIndex, const QString &elementUuid) const;
Terminal *findTerminal(int folioIndex, const QString &elementUuid, int terminalIndex,
const QString &caller);
Conductor *findConductor(int folioIndex, const QString &elementUuid, int terminalIndex,
const QString &caller);
QList<IndependentTextItem *> sortedTexts(int folioIndex) const;
QList<QetShapeItem *> sortedShapes(int folioIndex) const;
IndependentTextItem *findText(int folioIndex, int textIndex, const QString &caller);
bool setInfoKey(int folioIndex, const QString &elementUuid,
const QString &key, const QString &value, const QString &caller);
QETProject *m_project; QETProject *m_project;
DiagramView *m_view; DiagramView *m_view;
QString m_query_error;
}; };
#endif // QET_SCRIPT_API_H #endif // QET_SCRIPT_API_H
+8 -3
View File
@@ -19,6 +19,7 @@
#include "NameList/nameslist.h" #include "NameList/nameslist.h"
#include "createdxf.h" #include "createdxf.h"
#include "diagram.h"
#include "qet.h" #include "qet.h"
#include "qetapp.h" #include "qetapp.h"
// uncomment the line below to get more debug information // uncomment the line below to get more debug information
@@ -1589,8 +1590,10 @@ void TitleBlockTemplate::render(QPainter &painter,
int titleblock_height = height(); int titleblock_height = height();
painter.save(); painter.save();
//Setup the QPainter //Setup the QPainter - use a color that contrasts with the background
QPen pen(Qt::black); QColor ink = Diagram::background_color.lightness() < 128
? QColor(Qt::white) : QColor(Qt::black);
QPen pen(ink);
painter.setPen(pen); painter.setPen(pen);
// draw the titleblock border // draw the titleblock border
@@ -1737,7 +1740,9 @@ void TitleBlockTemplate::renderCell(QPainter &painter,
{ {
// draw the border rect of the current cell // draw the border rect of the current cell
QPen pen(QBrush(), 1, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin); QPen pen(QBrush(), 1, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin);
pen.setColor(Qt::black); QColor ink = Diagram::background_color.lightness() < 128
? QColor(Qt::white) : QColor(Qt::black);
pen.setColor(ink);
painter.setPen(pen); painter.setPen(pen);
painter.drawRect(cell_rect); painter.drawRect(cell_rect);
+265
View File
@@ -0,0 +1,265 @@
/*
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 "conductorcolortoolbutton.h"
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
#include "../diagram.h"
#include "../diagramcontent.h"
#include "../diagramview.h"
#include "../lastusedstyle.h"
#include "../qetdiagrameditor.h"
#include "../projectview.h"
#include "../qetgraphicsitem/conductor.h"
#include "../conductorproperties.h"
#include <QColorDialog>
#include <QMenu>
#include <QPainter>
#include <QPixmap>
namespace {
/**
The colours an electrician reaches for, in the order the trade
names them: the three phases, neutral, earth, then the ones used
for control and extra-low-voltage circuits. Not a standard in
itself -- IEC 60445 only fixes blue for neutral and green/yellow
for protective earth -- but it covers the wiring a schematic
actually shows, which is what makes the toolbar worth a click.
*/
struct NamedColor { const char *context_name; QColor color; };
QList<NamedColor> standardColors()
{
return {
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Noir"), QColor(0x00, 0x00, 0x00)},
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Marron"), QColor(0x7B, 0x3F, 0x00)},
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Gris"), QColor(0x80, 0x80, 0x80)},
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Bleu"), QColor(0x00, 0x00, 0xFF)},
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Vert"), QColor(0x00, 0x80, 0x00)},
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Rouge"), QColor(0xFF, 0x00, 0x00)},
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Orange"), QColor(0xFF, 0x80, 0x00)},
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Violet"), QColor(0x80, 0x00, 0x80)},
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Blanc"), QColor(0xFF, 0xFF, 0xFF)},
};
}
const int MAX_RECENT = 6;
}
/**
@brief ConductorColorToolButton::ConductorColorToolButton
@param editor : the diagram editor this button acts on
@param parent
*/
ConductorColorToolButton::ConductorColorToolButton(QETDiagramEditor *editor, QWidget *parent) :
QToolButton(parent),
m_editor(editor)
{
setPopupMode(QToolButton::InstantPopup);
setToolTip(tr("Couleur de conducteur"));
setStatusTip(tr("Applique une couleur aux conducteurs sélectionnés, et l'utilise pour les prochains conducteurs tracés",
"status bar tip"));
m_current = LastUsedStyle::hasConductorColor() ? LastUsedStyle::conductorColor()
: QColor(Qt::black);
setMenu(new QMenu(this));
rebuildMenu();
setSwatch(m_current);
}
/**
@brief ConductorColorToolButton::currentView
@return the folio being edited, or nullptr when no project is open.
Goes through the project view because QETDiagramEditor keeps its own
currentDiagramView() private.
*/
DiagramView *ConductorColorToolButton::currentView() const
{
ProjectView *pv = m_editor ? m_editor->currentProjectView() : nullptr;
return pv ? pv->currentDiagram() : nullptr;
}
/**
@brief ConductorColorToolButton::updateEnabledState
Greyed out when there is no folio to act on, or when the project is
read-only -- the same rule the other conductor actions follow.
*/
void ConductorColorToolButton::updateEnabledState()
{
DiagramView *dv = currentView();
setEnabled(dv && dv->diagram() && !dv->diagram()->isReadOnly());
}
/**
@brief ConductorColorToolButton::rebuildMenu
The standard colours never change; the recent ones do, so the menu is
rebuilt rather than kept in sync entry by entry.
*/
void ConductorColorToolButton::rebuildMenu()
{
QMenu *m = menu();
m->clear();
for (const auto &nc : standardColors())
{
const QColor c = nc.color;
QAction *a = m->addAction(swatchIcon(c),
tr(nc.context_name));
connect(a, &QAction::triggered, this, [this, c]() { applyColor(c); });
}
if (!m_recent.isEmpty())
{
m->addSeparator();
QAction *title = m->addAction(tr("Récemment utilisées"));
title->setEnabled(false);
for (const QColor &c : std::as_const(m_recent))
{
QAction *a = m->addAction(swatchIcon(c), c.name());
connect(a, &QAction::triggered, this, [this, c]() { applyColor(c); });
}
}
m->addSeparator();
QAction *other = m->addAction(tr("Autre couleur…"));
connect(other, &QAction::triggered, this, &ConductorColorToolButton::chooseOtherColor);
}
/**
@brief ConductorColorToolButton::applyColor
Recolour the selected conductors, and remember the colour for the next
one drawn.
@param color
*/
void ConductorColorToolButton::applyColor(const QColor &color)
{
if (!color.isValid()) {
return;
}
//Always: this is the half that works with nothing selected.
LastUsedStyle::setConductorColor(color);
rememberRecent(color);
setSwatch(color);
DiagramView *dv = currentView();
if (!dv) {
return;
}
Diagram *diagram = dv->diagram();
if (!diagram || diagram->isReadOnly()) {
return;
}
DiagramContent dc(diagram);
const auto conductors = dc.conductors(DiagramContent::AnyConductor);
if (conductors.isEmpty()) {
return;
}
QUndoCommand *undo = new QUndoCommand(tr("Modifier la couleur de %n conducteur(s)",
"undo caption", conductors.count()));
int changed = 0;
for (Conductor *conductor : conductors)
{
ConductorProperties before = conductor->properties();
if (before.color == color) {
continue;
}
ConductorProperties after = before;
after.color = color;
QVariant old_value, new_value;
old_value.setValue(before);
new_value.setValue(after);
new QPropertyUndoCommand(conductor, "properties", old_value, new_value, undo);
++changed;
}
//Every selected conductor was already this colour: pushing an
//empty command would put a no-op step in the undo stack.
if (changed) {
diagram->undoStack().push(undo);
} else {
delete undo;
}
}
/**
@brief ConductorColorToolButton::chooseOtherColor
*/
void ConductorColorToolButton::chooseOtherColor()
{
const QColor c = QColorDialog::getColor(m_current, this,
tr("Choisir une couleur de conducteur"));
if (c.isValid()) {
applyColor(c);
}
}
/**
@brief ConductorColorToolButton::rememberRecent
Most recent first, no duplicates, capped.
@param color
*/
void ConductorColorToolButton::rememberRecent(const QColor &color)
{
//A colour that is already one row up in the standard list would
//only appear twice, under a hex name it does not need.
for (const auto &nc : standardColors()) {
if (nc.color == color) {
return;
}
}
m_recent.removeAll(color);
m_recent.prepend(color);
while (m_recent.size() > MAX_RECENT) {
m_recent.removeLast();
}
rebuildMenu();
}
/**
@brief ConductorColorToolButton::setSwatch
@param color
*/
void ConductorColorToolButton::setSwatch(const QColor &color)
{
m_current = color;
setIcon(swatchIcon(color));
}
/**
@brief ConductorColorToolButton::swatchIcon
@param color
@return a plain square of that colour, outlined so that white and very
light colours are still visible against the toolbar.
*/
QIcon ConductorColorToolButton::swatchIcon(const QColor &color)
{
QPixmap pix(16, 16);
pix.fill(Qt::transparent);
QPainter p(&pix);
p.setRenderHint(QPainter::Antialiasing, false);
p.setBrush(color);
p.setPen(QPen(QColor(0x40, 0x40, 0x40), 1));
p.drawRect(0, 0, 15, 15);
p.end();
return QIcon(pix);
}
+69
View File
@@ -0,0 +1,69 @@
/*
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 CONDUCTORCOLORTOOLBUTTON_H
#define CONDUCTORCOLORTOOLBUTTON_H
#include <QColor>
#include <QList>
#include <QToolButton>
class QETDiagramEditor;
class DiagramView;
/**
@brief The ConductorColorToolButton class
One-click conductor colour, from the "Schéma" toolbar.
Picking a colour does two things: it recolours every conductor
currently selected (one undo command for the lot), and it becomes the
colour of the next conductor drawn this session, through the existing
LastUsedStyle mechanism. Either half is useful on its own -- with
nothing selected it only sets the pen for what comes next.
This deliberately stores nothing in the project and nothing in
QSettings. It is the same session-scoped "what did I just use" idea
LastUsedStyle already implements; named presets that persist per
project are a separate, larger feature (upstream issue #461) that
needs a maintainer decision first.
*/
class ConductorColorToolButton : public QToolButton
{
Q_OBJECT
public:
explicit ConductorColorToolButton(QETDiagramEditor *editor,
QWidget *parent = nullptr);
public slots:
void updateEnabledState();
private:
DiagramView *currentView() const;
void rebuildMenu();
void applyColor(const QColor &color);
void chooseOtherColor();
void rememberRecent(const QColor &color);
void setSwatch(const QColor &color);
static QIcon swatchIcon(const QColor &color);
QETDiagramEditor *m_editor = nullptr;
QList<QColor> m_recent;
QColor m_current;
};
#endif // CONDUCTORCOLORTOOLBUTTON_H
+173
View File
@@ -31,6 +31,8 @@
#include "../titleblockpropertieswidget.h" #include "../titleblockpropertieswidget.h"
#include "../xrefpropertieswidget.h" #include "../xrefpropertieswidget.h"
#include "guidespropertieswidget.h" #include "guidespropertieswidget.h"
#include "../autoNum/numerotationcontext.h"
#include "../autoNum/ui/selectautonumw.h"
#include <QFont> #include <QFont>
#include <QFontDialog> #include <QFontDialog>
#include <QSizePolicy> #include <QSizePolicy>
@@ -101,6 +103,32 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
} }
m_gpw->setGuides(loaded_guides); m_gpw->setGuides(loaded_guides);
// global auto-numbering defaults (only when editing global settings, not a project)
if (!m_project) {
auto saw_conductor = new SelectAutonumW(1);
auto saw_element = new SelectAutonumW(0);
auto saw_folio = new SelectAutonumW(2);
initAutoNumTab(m_autonum_conductor, saw_conductor, QStringLiteral("autonum/conductor"));
initAutoNumTab(m_autonum_element, saw_element, QStringLiteral("autonum/element"));
initAutoNumTab(m_autonum_folio, saw_folio, QStringLiteral("autonum/folio"));
QSettings autonum_settings;
loadAutoNumTab(m_autonum_conductor, autonum_settings);
loadAutoNumTab(m_autonum_element, autonum_settings);
loadAutoNumTab(m_autonum_folio, autonum_settings);
// Intercept Return key in the combo line edits so it doesn't
// activate the dialog's default button (OK).
for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio}) {
if (QComboBox *combo = tab->widget->contextComboBox()) {
if (combo->lineEdit()) {
combo->lineEdit()->installEventFilter(this);
}
}
}
}
//If there is a project, we edit his properties //If there is a project, we edit his properties
if (m_project) { if (m_project) {
bpw -> setProperties (m_project -> defaultBorderProperties()); bpw -> setProperties (m_project -> defaultBorderProperties());
@@ -114,6 +142,7 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
// main tab widget // main tab widget
QTabWidget *tab_widget = new QTabWidget(this); QTabWidget *tab_widget = new QTabWidget(this);
m_tab_widget = tab_widget;
QWidget *diagram_widget = new QWidget(); QWidget *diagram_widget = new QWidget();
QVBoxLayout *diagram_layout = new QVBoxLayout(diagram_widget); QVBoxLayout *diagram_layout = new QVBoxLayout(diagram_widget);
@@ -127,6 +156,19 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
tab_widget -> addTab (xrefpw, tr("Références croisées")); tab_widget -> addTab (xrefpw, tr("Références croisées"));
tab_widget -> addTab (m_gpw, tr("Guides")); tab_widget -> addTab (m_gpw, tr("Guides"));
// add auto-numbering tab only for global settings (not per project)
if (!m_project) {
QWidget *autonum_widget = new QWidget();
QVBoxLayout *autonum_layout = new QVBoxLayout(autonum_widget);
autonum_layout->addWidget(new QLabel(tr("Définir les règles de numérotation automatique par défaut pour les nouveaux projets :")));
QTabWidget *autonum_inner_tab = new QTabWidget();
autonum_inner_tab->addTab(m_autonum_conductor.widget, tr("Conducteurs"));
autonum_inner_tab->addTab(m_autonum_element.widget, tr("Eléments"));
autonum_inner_tab->addTab(m_autonum_folio.widget, tr("Folios"));
autonum_layout->addWidget(autonum_inner_tab);
tab_widget -> addTab (autonum_widget, tr("Numérotation auto"));
}
QVBoxLayout *vlayout1 = new QVBoxLayout(); QVBoxLayout *vlayout1 = new QVBoxLayout();
vlayout1->addWidget(tab_widget); vlayout1->addWidget(tab_widget);
@@ -230,6 +272,9 @@ void NewDiagramPage::applyConf()
settings.setValue(QStringLiteral("color"), current_guides[i].color.name()); settings.setValue(QStringLiteral("color"), current_guides[i].color.name());
} }
settings.endArray(); settings.endArray();
// save global auto-numbering defaults
persistAutonumSettings();
} }
} }
@@ -295,6 +340,134 @@ void NewDiagramPage::loadSavedTbp()
applyConf(); applyConf();
} }
/**
@brief NewDiagramPage::isPlaceholder
Return true if @a name matches the combo box's built-in placeholder text
(first item). This is locale-independent because it reads the actual item text.
*/
bool NewDiagramPage::isPlaceholder(QComboBox *combo, const QString &name)
{
return !combo->count() || name == combo->itemText(0);
}
/**
@brief NewDiagramPage::initAutoNumTab
Initialise an AutoNumTab struct and connect its signals.
*/
void NewDiagramPage::initAutoNumTab(AutoNumTab &tab, SelectAutonumW *w, const QString &prefix)
{
tab.widget = w;
tab.prefix = prefix;
connect(w, &SelectAutonumW::applyPressed, this, [this, &tab]() { saveAutoNumContext(tab); });
connect(w, &SelectAutonumW::removeClicked, this, [this, &tab]() { removeAutoNumContext(tab); });
connect(w->contextComboBox(), &QComboBox::activated, this, [this, &tab](int index) {
if (index >= 0) {
QString name = tab.widget->contextComboBox()->itemText(index);
if (tab.contexts.contains(name)) {
tab.widget->setContext(tab.contexts.value(name));
}
}
});
}
/**
@brief NewDiagramPage::loadAutoNumTab
Load saved rules from QSettings into an AutoNumTab.
*/
void NewDiagramPage::loadAutoNumTab(AutoNumTab &tab, QSettings &settings)
{
auto data = NumerotationContext::loadFromSettings(settings, tab.prefix);
tab.contexts = data.first;
for (auto it = tab.contexts.constBegin(); it != tab.contexts.constEnd(); ++it) {
tab.widget->contextComboBox()->addItem(it.key());
}
if (!tab.contexts.isEmpty() && !data.second.isEmpty()
&& tab.contexts.contains(data.second)) {
tab.widget->contextComboBox()->setCurrentText(data.second);
tab.widget->setContext(tab.contexts.value(data.second));
}
}
/**
@brief NewDiagramPage::saveAutoNumContext
Save the current context from an AutoNumTab's widget into its hash and
persist to QSettings immediately.
*/
void NewDiagramPage::saveAutoNumContext(AutoNumTab &tab)
{
QString name = tab.widget->contextComboBox()->currentText().trimmed();
if (name.isEmpty() || isPlaceholder(tab.widget->contextComboBox(), name)) {
return;
}
tab.contexts.insert(name, tab.widget->toNumContext());
if (tab.widget->contextComboBox()->findText(name) == -1) {
tab.widget->contextComboBox()->addItem(name);
}
persistAutonumSettings();
}
/**
@brief NewDiagramPage::removeAutoNumContext
Remove the current context from an AutoNumTab's hash and persist.
*/
void NewDiagramPage::removeAutoNumContext(AutoNumTab &tab)
{
QString name = tab.widget->contextComboBox()->currentText().trimmed();
if (name.isEmpty() || isPlaceholder(tab.widget->contextComboBox(), name)) {
return;
}
int idx = tab.widget->contextComboBox()->findText(name);
if (idx == -1) return;
tab.contexts.remove(name);
tab.widget->contextComboBox()->removeItem(idx);
tab.widget->contextComboBox()->setCurrentText(QString());
tab.widget->setContext(NumerotationContext());
persistAutonumSettings();
}
/**
@brief NewDiagramPage::persistAutonumSettings
Save all autonum contexts to QSettings immediately.
*/
void NewDiagramPage::persistAutonumSettings()
{
QSettings settings;
for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio}) {
QString current;
QComboBox *combo = tab->widget->contextComboBox();
if (!isPlaceholder(combo, combo->currentText().trimmed())
&& tab->contexts.contains(combo->currentText().trimmed())) {
current = combo->currentText().trimmed();
}
NumerotationContext::saveToSettings(tab->contexts, current,
settings, tab->prefix);
}
}
/**
@brief NewDiagramPage::eventFilter
Intercept Return/Enter in combo box line edits so it doesn't close the
settings dialog.
*/
bool NewDiagramPage::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::KeyPress) {
auto *ke = static_cast<QKeyEvent *>(event);
if ((ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter)) {
// Check if this is a line edit inside one of our autonum combos
for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio}) {
if (QComboBox *combo = tab->widget->contextComboBox()) {
if (combo->lineEdit() && combo->lineEdit() == obj) {
return true; // eat the event
}
}
}
}
}
return ConfigPage::eventFilter(obj, event);
}
/** /**
Constructeur Constructeur
@param parent QWidget parent @param parent QWidget parent
+21
View File
@@ -20,6 +20,7 @@
#include "configpage.h" #include "configpage.h"
#include "../projectpropertiesdialog.h" #include "../projectpropertiesdialog.h"
#include "../titleblockpropertieswidget.h" #include "../titleblockpropertieswidget.h"
#include "../autoNum/numerotationcontext.h"
#include <QDialog> #include <QDialog>
#include <QtWidgets> #include <QtWidgets>
@@ -32,6 +33,7 @@ class XRefPropertiesWidget;
class GuidesPropertiesWidget; class GuidesPropertiesWidget;
class QETProject; class QETProject;
class TitleBlockProperties; class TitleBlockProperties;
class SelectAutonumW;
/** /**
@brief The NewDiagramPage class @brief The NewDiagramPage class
@@ -48,6 +50,7 @@ class NewDiagramPage : public ConfigPage {
~NewDiagramPage() override; ~NewDiagramPage() override;
private: private:
NewDiagramPage(const NewDiagramPage &); NewDiagramPage(const NewDiagramPage &);
bool eventFilter(QObject *obj, QEvent *event) override;
public slots: public slots:
void changeToAutoFolioTab(); void changeToAutoFolioTab();
void setFolioAutonum(QString); void setFolioAutonum(QString);
@@ -72,7 +75,25 @@ public slots:
XRefPropertiesWidget *xrefpw; ///< Widget to edit default xref properties XRefPropertiesWidget *xrefpw; ///< Widget to edit default xref properties
GuidesPropertiesWidget *m_gpw; ///< Widget to edit guides GuidesPropertiesWidget *m_gpw; ///< Widget to edit guides
TitleBlockProperties savedTbp; ///< Used to save current TBP and retrieve later TitleBlockProperties savedTbp; ///< Used to save current TBP and retrieve later
QTabWidget *m_tab_widget; ///< Main tab widget (stored for later access)
// auto-numbering tab data
struct AutoNumTab {
SelectAutonumW *widget = nullptr;
QHash<QString, NumerotationContext> contexts;
QString prefix;
};
AutoNumTab m_autonum_conductor;
AutoNumTab m_autonum_element;
AutoNumTab m_autonum_folio;
void initAutoNumTab(AutoNumTab &tab, SelectAutonumW *w, const QString &prefix);
void loadAutoNumTab(AutoNumTab &tab, QSettings &settings);
void saveAutoNumContext(AutoNumTab &tab);
void removeAutoNumContext(AutoNumTab &tab);
void persistAutonumSettings();
static bool isPlaceholder(QComboBox *combo, const QString &name);
}; };
/** /**
@@ -381,5 +381,5 @@ QString ShortcutsConfigPage::title() const
QIcon ShortcutsConfigPage::icon() const QIcon ShortcutsConfigPage::icon() const
{ {
return QET::Icons::ConfigureToolbars; return QET::Icons::ConfigureShortcuts;
} }
+242
View File
@@ -0,0 +1,242 @@
/*
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 "diagrambgcolorbutton.h"
#include "../diagram.h"
#include "../diagramview.h"
#include "../palettegraphicsview.h"
#include "../qetdiagrameditor.h"
#include "../projectview.h"
#include "../qetproject.h"
#include <QApplication>
#include <QColorDialog>
#include <QMenu>
#include <QPainter>
#include <QPixmap>
namespace {
struct NamedColor { const char *context_name; QColor color; };
QList<NamedColor> standardColors()
{
return {
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Blanc"), QColor(0xFF, 0xFF, 0xFF)},
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Blanc cassé"), QColor(0xFD, 0xFB, 0xF5)},
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Gris clair"), QColor(0xE0, 0xE0, 0xE0)},
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Gris"), QColor(0x80, 0x80, 0x80)},
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Gris foncé"), QColor(0x40, 0x40, 0x40)},
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Noir"), QColor(0x00, 0x00, 0x00)},
};
}
const int MAX_RECENT = 6;
}
/**
@brief DiagramBgColorToolButton::DiagramBgColorToolButton
@param editor : the diagram editor this button acts on
@param parent
*/
DiagramBgColorToolButton::DiagramBgColorToolButton(QETDiagramEditor *editor, QWidget *parent) :
QToolButton(parent),
m_editor(editor)
{
setPopupMode(QToolButton::InstantPopup);
setToolTip(tr("Couleur de fond du folio"));
setStatusTip(tr("Choisir la couleur de fond du folio",
"status bar tip"));
m_is_system_color = true;
m_current = QApplication::palette().color(QPalette::Base);
setMenu(new QMenu(this));
rebuildMenu();
setSwatch(m_current);
}
/**
@brief DiagramBgColorToolButton::rebuildMenu
*/
void DiagramBgColorToolButton::rebuildMenu()
{
QMenu *m = menu();
m->clear();
QAction *sys = m->addAction(tr("Couleur système"));
connect(sys, &QAction::triggered, this, &DiagramBgColorToolButton::applySystemColor);
m->addSeparator();
for (const auto &nc : standardColors())
{
const QColor c = nc.color;
QAction *a = m->addAction(swatchIcon(c),
tr(nc.context_name));
connect(a, &QAction::triggered, this, [this, c]() { applyColor(c); });
}
if (!m_recent.isEmpty())
{
m->addSeparator();
QAction *title = m->addAction(tr("Récemment utilisées"));
title->setEnabled(false);
for (const QColor &c : std::as_const(m_recent))
{
QAction *a = m->addAction(swatchIcon(c), c.name());
connect(a, &QAction::triggered, this, [this, c]() { applyColor(c); });
}
}
m->addSeparator();
QAction *other = m->addAction(tr("Autre couleur…"));
connect(other, &QAction::triggered, this, &DiagramBgColorToolButton::chooseOtherColor);
}
/**
@brief DiagramBgColorToolButton::applyColor
Set a custom background color on all open diagrams.
@param color
*/
void DiagramBgColorToolButton::applyColor(const QColor &color)
{
if (!color.isValid()) {
return;
}
m_is_system_color = false;
rememberRecent(color);
setSwatch(color);
PaletteGraphicsView::setCustomBackgroundColor(true);
Diagram::background_color = color;
QETDiagramEditor *editor = m_editor;
if (!editor) {
return;
}
for (ProjectView *pv : editor->openedProjects())
for (Diagram *d : pv->project()->diagrams())
d->update();
}
/**
@brief DiagramBgColorToolButton::applySystemColor
Restore the system-default background and re-enable dark-mode
inversion.
*/
void DiagramBgColorToolButton::applySystemColor()
{
m_is_system_color = true;
m_current = QApplication::palette().color(QPalette::Base);
setSwatch(m_current);
PaletteGraphicsView::setCustomBackgroundColor(false);
Diagram::background_color = Qt::white;
QETDiagramEditor *editor = m_editor;
if (!editor) {
return;
}
for (ProjectView *pv : editor->openedProjects())
for (Diagram *d : pv->project()->diagrams())
d->update();
}
/**
@brief DiagramBgColorToolButton::chooseOtherColor
*/
void DiagramBgColorToolButton::chooseOtherColor()
{
const QColor c = QColorDialog::getColor(m_current, this,
tr("Choisir une couleur de fond"));
if (c.isValid()) {
applyColor(c);
}
}
/**
@brief DiagramBgColorToolButton::rememberRecent
Most recent first, no duplicates, capped.
@param color
*/
void DiagramBgColorToolButton::rememberRecent(const QColor &color)
{
for (const auto &nc : standardColors()) {
if (nc.color == color) {
return;
}
}
m_recent.removeAll(color);
m_recent.prepend(color);
while (m_recent.size() > MAX_RECENT) {
m_recent.removeLast();
}
rebuildMenu();
}
/**
@brief DiagramBgColorToolButton::setSwatch
@param color
*/
void DiagramBgColorToolButton::setSwatch(const QColor &color)
{
m_current = color;
setIcon(swatchIcon(color));
}
/**
@brief DiagramBgColorToolButton::syncFromDiagram
Sync the button swatch to whatever Diagram::background_color is
currently set to. Called when the active folio changes.
*/
void DiagramBgColorToolButton::syncFromDiagram()
{
if (m_is_system_color) {
m_current = QApplication::palette().color(QPalette::Base);
} else {
m_current = Diagram::background_color;
}
setSwatch(m_current);
}
/**
@brief DiagramBgColorToolButton::updateEnabledState
*/
void DiagramBgColorToolButton::updateEnabledState()
{
setEnabled(m_editor && m_editor->currentProjectView());
}
/**
@brief DiagramBgColorToolButton::swatchIcon
@param color
@return a plain square of that colour, outlined so that white and very
light colours are still visible against the toolbar.
*/
QIcon DiagramBgColorToolButton::swatchIcon(const QColor &color)
{
QPixmap pix(16, 16);
pix.fill(Qt::transparent);
QPainter p(&pix);
p.setRenderHint(QPainter::Antialiasing, false);
p.setBrush(color);
p.setPen(QPen(QColor(0x40, 0x40, 0x40), 1));
p.drawRect(0, 0, 15, 15);
p.end();
return QIcon(pix);
}
+64
View File
@@ -0,0 +1,64 @@
/*
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 DIAGRAMBGCOLORTOOLBUTTON_H
#define DIAGRAMBGCOLORTOOLBUTTON_H
#include <QColor>
#include <QList>
#include <QToolButton>
class QETDiagramEditor;
/**
@brief The DiagramBgColorToolButton class
Color picker for the diagram sheet background, placed in the
"Affichage" toolbar. Mirrors the ConductorColorToolButton UX:
preset colors in the dropdown, "Autre couleur..." at the bottom,
and a swatch icon on the button itself.
Picking "Couleur système" clears any custom colour and lets the
dark-mode inversion handle the background as before.
*/
class DiagramBgColorToolButton : public QToolButton
{
Q_OBJECT
public:
explicit DiagramBgColorToolButton(QETDiagramEditor *editor,
QWidget *parent = nullptr);
public slots:
void updateEnabledState();
void syncFromDiagram();
private:
void rebuildMenu();
void applyColor(const QColor &color);
void applySystemColor();
void chooseOtherColor();
void rememberRecent(const QColor &color);
void setSwatch(const QColor &color);
static QIcon swatchIcon(const QColor &color);
QETDiagramEditor *m_editor = nullptr;
QList<QColor> m_recent;
QColor m_current;
bool m_is_system_color = false;
};
#endif // DIAGRAMBGCOLORTOOLBUTTON_H
+6 -15
View File
@@ -55,20 +55,12 @@ m_diagram(diagram)
{ {
openDialog(); openDialog();
QString text; QStringList parts;
if(texts_list.count()) if (texts_list.count())
text.append(QObject::tr("Pivoter %1 textes").arg(texts_list.count())); parts << QObject::tr("%n texte(s)", "", texts_list.count());
if(groups_list.count()) if (groups_list.count())
{ parts << QObject::tr("%n groupe(s) de textes", "", groups_list.count());
if(text.isEmpty()) setText(QObject::tr("Pivoter %1").arg(QLocale().createSeparatedList(parts)));
text.append(QObject::tr("Pivoter"));
else
text.append(QObject::tr(" et"));
text.append(QObject::tr(" %1 groupes de textes").arg(groups_list.count()));
}
if(!text.isNull())
setText(text);
for(DiagramTextItem *dti : texts_list) for(DiagramTextItem *dti : texts_list)
setupAnimation(dti, "rotation", dti->rotation(), m_rotation); setupAnimation(dti, "rotation", dti->rotation(), m_rotation);
@@ -77,7 +69,6 @@ m_diagram(diagram)
} }
else else
setObsolete(true); setObsolete(true);
} }
void RotateTextsCommand::undo() void RotateTextsCommand::undo()
+29 -5
View File
@@ -95,6 +95,29 @@ void ConductorCreator::create(Diagram *d, const QPolygonF &polygon)
} }
} }
/**
@brief ConductorCreator::needsPotentialChoice
Whether creating a potential between these terminals would ask the user
to choose which of several existing potentials to inherit from -- that
is, whether the constructor would reach PotentialSelectorDialog.
This exists for callers with nobody there to answer: the dialog is a
plain QDialog::exec(), not routed through QET::QetMessageBox, so its
non-interactive mode does not cover it and a headless caller would hang
on it indefinitely. Such a caller can check this first and decline.
Exposed here, rather than reimplemented by the caller, so the condition
cannot drift away from the one setUpPropertieToUse() actually applies.
@param terminals_list the terminals a potential would be created between
@return true if the constructor would open the dialog
*/
bool ConductorCreator::needsPotentialChoice(const QList<Terminal *> &terminals_list)
{
if (terminals_list.size() <= 1) {
return false;
}
return existingPotential(terminals_list).size() >= 2;
}
/** /**
@brief ConductorCreator::propertieToUse @brief ConductorCreator::propertieToUse
@return true if the caller should proceed with conductor creation, @return true if the caller should proceed with conductor creation,
@@ -104,7 +127,7 @@ void ConductorCreator::create(Diagram *d, const QPolygonF &polygon)
*/ */
bool ConductorCreator::setUpPropertieToUse() bool ConductorCreator::setUpPropertieToUse()
{ {
QList<Conductor *> potentials = existingPotential(); QList<Conductor *> potentials = existingPotential(m_terminals_list);
//There is an existing potential //There is an existing potential
//we get one of them //we get one of them
@@ -145,14 +168,15 @@ bool ConductorCreator::setUpPropertieToUse()
@brief ConductorCreator::existingPotential @brief ConductorCreator::existingPotential
Return the list of existing potential of Return the list of existing potential of
the terminal list the terminal list
@param terminals_list the terminals to inspect
@return c_list QList<Conductor *> @return c_list QList<Conductor *>
*/ */
QList<Conductor *> ConductorCreator::existingPotential() QList<Conductor *> ConductorCreator::existingPotential(const QList<Terminal *> &terminals_list)
{ {
QList<Conductor *> c_list; QList<Conductor *> c_list;
QList<Terminal *> t_exclude; QList<Terminal *> t_exclude;
for (Terminal *t : m_terminals_list) for (Terminal *t : terminals_list)
{ {
if (t_exclude.contains(t)) { if (t_exclude.contains(t)) {
continue; continue;
@@ -166,9 +190,9 @@ QList<Conductor *> ConductorCreator::existingPotential()
//in the same potential of c, and if true, exclude this terminal from the search. //in the same potential of c, and if true, exclude this terminal from the search.
for (Conductor *c : t->conductors().first()->relatedPotentialConductors(false)) for (Conductor *c : t->conductors().first()->relatedPotentialConductors(false))
{ {
if (m_terminals_list.contains(c->terminal1)) { if (terminals_list.contains(c->terminal1)) {
t_exclude.append(c->terminal1); t_exclude.append(c->terminal1);
} else if (m_terminals_list.contains(c->terminal2)) { } else if (terminals_list.contains(c->terminal2)) {
t_exclude.append(c->terminal2); t_exclude.append(c->terminal2);
} }
} }

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