Commit Graph

88 Commits

Author SHA1 Message Date
Laurent Trinques 6dd260a7b2 Merge pull request #1038 from ispyisail/feature/cell-rulers
Add an option to keep the folio row and column headers visible (#1034)
2026-09-26 08:43:42 +02:00
ispyisail 95f3ea2f6d Share the text grid choices; add a preference, a status hint and a unit test
Drop 1:2.5: on a grid of 10 it steps by 4, which misses 10, so texts on
two elements 10 apart could never line up. Every remaining divisor is a
whole number, which tst_textgrid checks.

The divisor list and the snapping arithmetic move to the header-only
textgrid.h so the toolbar menu, the preferences page and the test share
them. The preferences page gets the same choice under Grille + Clavier;
QETApp::textGridChanged keeps every editor's toolbar button in step.
While element texts are dragged, the status bar names the text grid and
says to release Shift and hold Ctrl for free placement -- Ctrl+Shift
together is the pan shortcut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-26 10:22:50 +12:00
ispyisail 9738345b45 Add an option to keep the folio row and column headers visible (#1034)
Affichage > "Garder les en-têtes visibles" adds a bar along the top and
the left of the diagram view that repeats the folio's column numbers and
row letters, aligned with the cells at any zoom, so they stay in sight
when the folio's own headers are scrolled away. Off by default; the
choice is stored as diagrameditor/cell_rulers.

The bars are CellRuler widgets in the view's margins
(setViewportMargins), not scene items, so printing and PDF/PNG/DXF
export never see them, and they paint with the application palette
outside of the dark-palette inversion. They keep a constant thickness;
when cells get narrower than their labels, only every 2nd, 5th, 10th...
label is written. A bar is hidden when the folio hides that header.
Showing or hiding them keeps the centre of the view where it was.

The labels come from BorderCellLabels, now also used by
BorderTitleBlock::draw(), so the bars and the border cannot disagree.
PNG export of all 133 folios of the examples is pixel-identical to
master, with border-columns_0 true and false.

Known limits: changing border-columns_0 repaints the bars at the next
scroll or zoom; the menu toggle updates the views of its own editor
window only, like the grid toggle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-26 08:22:12 +12:00
ispyisail 4c9a307cb9 Say what realLibrary() can and cannot check
Measured on a macOS 15 runner with 3DxWare 10.8.13: an ad-hoc signed
test binary with the hardened runtime loads 3DconnexionClient with or
without the entitlement, so ad-hoc signing does not enforce library
validation and CI cannot prove the entitlement is needed.

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:55:30 +12:00
ispyisail 17ebbffbca Add 3D mouse speed, direction, dead zone and twist-to-zoom settings
The 3D mouse's pan and zoom speeds were fixed guesses, and each sample
was applied as it came, so the speed on screen depended on how often
the driver sends samples -- different for every platform and device.

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 09:56:04 +12:00
Kellermorph 9af562fcbd Remember the sheet background colour between runs
Picking a sheet (folio) background colour in the diagram editor was lost
on every restart. Diagram::background_color is a static initialised to
white and PaletteGraphicsView's s_custom_bg a static bool, and neither
was ever written anywhere -- Diagram::toXml() carries no colour attribute
either -- so closing and reopening a project always came back on the
default and the choice had to be made again.

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

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

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

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

Covered by tst_sheetbackgroundsetting, which pins the custom flag and the
dropped alpha -- the two rules a single stored colour would lose.
2026-09-24 10:30:01 +02:00
ispyisail 28acbadaf0 Merge remote-tracking branch 'upstream/master' into pr983-rebase-check
# Conflicts:
#	sources/dataBase/projectdatabase.cpp
#	tests/qttest/CMakeLists.txt
2026-09-23 13:16:48 +12:00
Laurent Trinques 4456d08b91 Merge pull request #988 from elevatormind/chore/remove-obsolete-google-tests
Chore/remove obsolete google tests
2026-09-23 02:55:16 +02:00
Laurent Trinques 4bcf5834e4 Merge pull request #984 from ispyisail/feature/scripting-opt-in
Scripting off by default, with a prompt and a setting to turn it on
2026-09-23 02:40:18 +02:00
Magnus Hellströmer a7ae17a1b4 chore: remove obsolete Google test projects 2026-09-22 20:34:40 +02:00
Magnus Hellströmer a1675d7dea fix(tests): link pugixml to crash dump test 2026-09-22 20:02:01 +02:00
ispyisail 8334a9a27f Scripting is off until asked for, and says how to turn it on
A script reaches the whole project and, through the export calls, the
filesystem. That is a capability most people installing an electrical CAD
program never asked for, and leaving it on by default hands it to them
anyway. So QET_HAS_SCRIPTING builds now ship with it switched off.

QetSettings::scriptingEnabled() is the single answer, read by all three
places that need it, with QET_ENABLE_SCRIPTING=1 overriding the stored
value. The override is not decoration: a CI job or a batch run has no
dialog to tick, and a machine whose HOME is created fresh for each run has
nowhere to keep the setting either. It beats a stored "false" on purpose,
so a box unticked once cannot lock a build server out of --run for good.
Only the exact value "1" counts.

  --run          refuses with exit 3 and a message naming both ways in.
  Projet > Exécuter un script...  asks once, and turns the setting on if
                 the answer is yes. Asking beats grey: a disabled menu
                 entry says something exists and nothing about how to have
                 it, and this is the pattern people already know from
                 macro security in office software.
  Configurer QElectroTech > Général > Projets  has the checkbox, for
                 turning it back off. While the environment forces
                 scripting on, the box is disabled and says why, and
                 applyConf() then leaves the stored value alone rather
                 than quietly overwriting it.

runOnProject() checks as well, after both callers have. It is the one
function that actually evaluates JavaScript, so it is the one place a
future caller cannot forget to ask; the callers check first only to give a
better answer than it can.

Verified on the built binary, all four states, with an isolated HOME:

  stored   env  result
  absent    -   refused, exit 3
  true      -   script runs, exit 0
  false     -   refused, exit 3
  false     1   script runs, exit 0

tst_scriptingsetting covers the same matrix hermetically, in its own
QSettings scope, and was mutation-checked: flipping the default to true
turns defaultsToOff() red.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 04:06:33 +12:00
ispyisail f777be05b4 Enforce read-only SQL with SQLite, not with a first-word check
projectDataBase::isReadOnlySelect() decides whether a query only reads
by looking at its first keyword and rejecting internal semicolons.
SQLite has allowed a CTE prefix in front of a data-modifying statement
since 3.8.3, so

    WITH x AS (SELECT 1) DELETE FROM element

begins with WITH, contains no semicolon, passes the check, and deletes
every row. UPDATE and INSERT go through the same way.

This is not only reachable from the custom-query box. ProjectDBModel::
fromXml() reads a <graphics_table>'s saved <query> straight out of the
.qet and fillValue() executes it, so a project file can carry the
statement. Reproduced against a build of this branch's parent, with no
scripting and no CLI flag beyond the export itself: a project whose
stored table query was replaced with the DELETE above exported a bill
of materials of 0 rows instead of 14, exit code 0, nothing logged. A
silently empty or -- with UPDATE -- silently altered BOM is the kind of
output someone orders parts from.

Fixed by asking SQLite about the statement it actually compiled.
sqlite3_prepare_v2() compiles without running, sqlite3_stmt_readonly()
reports on the compiled statement rather than on how it was spelled,
and the prepare tail catches a second statement structurally. The same
project now exports its 14 rows again and logs a reason for the
refusal, while an ordinary WITH ... SELECT in a project file still runs
untouched -- the fix is not "ban CTEs".

isReadOnlySelect() stays in front of it rather than being replaced:
SQLite considers ATTACH, BEGIN and several PRAGMAs read-only too, since
none of them change the contents of the database, so dropping the
statement-type allowlist would have widened what is accepted while
fixing what is executed.

The check lives in its own translation unit depending on nothing but
QString and SQLite, so tests/qttest/tst_sqlreadonly.cpp can link it
alone and exercise the security property without standing up a
QETProject: 18 assertions covering the three CTE-prefixed writes named
in the review, bare writes, trailing statements, comment-only input
(which compiles to a null statement sqlite3_stmt_readonly() must not be
handed) and a null connection (refused, not waved through). Confirmed
the suite discriminates by deliberately disabling the new check and
watching exactly the nine write-refusal assertions go red while the
accept cases stayed green.

ctest 13/13, qet-coherence-check and qet-pdflink-check clean on the
example corpus.

Reported in PR #980's review thread by @elevatormind and confirmed
against this code by @scorpio810; fixed here on its own because the
flaw is in already-released code and needs none of that branch to
reach.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 03:23:40 +12:00
Andre Rummler 069a75d44b Use pugixml target in test removing the old directly including approach. Fix a static variable which is now missing as the correspondign source file is not used by the particular test. 2026-09-22 09:08:03 +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
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 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
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
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 242f134e0f Revert "Revert "Draw the folio with inverted lightness on a dark palette"" 2026-09-20 13:05:13 +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 75fafd5acc Revert "Draw the folio with inverted lightness on a dark palette" 2026-09-20 06:57:56 +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 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
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
Jeff Patterson 800ddfbcbe Add a "qet-dark" icon theme for dark palettes
189 of QET's 266 fixed-size icons are black line art with no dark
variant, so on a dark palette they were black on a dark toolbar, and
Fusion's disabled rendering lightened them into something more readable
than the enabled state (GitHub #466, #870; bugtracker 335 for the
element panels, which keep their own fix).

The theme "qet-dark" holds light-ink copies of the line-art icons in
ico/themes/qet-dark, generated by misc/make_icon_themes.py. Colored
icons are not copied; the theme inherits them from "qet". An icon counts
as line art when fewer than 20% of its visible pixels are saturated. The
copies keep hue and alpha and invert lightness, scaled so each icon's
darkest ink becomes (220,220,220), the dark palette's text color. The
eight SVG icons get their color replaced the same way.

QETApp::applyIconTheme() picks "qet-dark" for a dark palette and "qet"
otherwise. It runs from initIconTheme(), again from initStyle() once the
palette is final, and on the OS color scheme switch. Icons created with
QIcon::fromTheme() re-resolve on their next paint, so nothing else
changes.

With light-ink files, Fusion's own disabled rendering comes out dimmer
than enabled with no extra code.

tests/qttest/tst_qeticons: every name resolves in both themes, every
dark file has light ink, and a Fusion tool button shows its icon at 3:1
in both themes with disabled weaker than enabled.
2026-09-18 05:06:51 -05:00
Laurent Trinques 0067ba1dca Merge pull request #905 from ispyisail/harden/crash-reporter
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m1s
Harden the crash reporter: keep every dump, and say what crashed
2026-09-18 04:59:45 +02:00
ispyisail 16220af012 Merge remote-tracking branch 'upstream/master' into harden/crash-reporter 2026-09-18 14:31:54 +12:00
ispyisail f8c1b5206a Address review on #905: offered-list semantics, dump ordering, FreeBSD, legacy dumps
Five things raised in review, plus tests for the parts that were only
described in prose.

clearPendingCrashDump() did not do what its comment said. It called
pendingCrashDumpFiles() again at clear time, so it deleted whatever was
in the directory then, not what had been offered. The offer sits inside a
modal dialog that stays open as long as the user reads it, and
SingleApplication keys its socket on the binary path, so a second
QElectroTech build running alongside is a separate process that can crash
and write a dump in that window. Re-listing deleted that dump unseen --
the exact failure this change exists to fix. The list is now taken once
in QETApp::checkCrashDump() and passed to both
pendingCrashDumpContents() and clearPendingCrashDump().

The ring is now written before the backtrace. backtrace() unwinds through
libgcc, which calls dl_iterate_phdr and takes the loader lock; warming it
in install() removes the allocation but not the lock. Crashing inside
dlopen() (Qt plugin loading), or on a corrupted stack, could therefore
hang or re-fault the handler at the backtrace and lose the ring with it.
Order is now header, signal, ring, backtrace, so the cheapest and most
valuable part is already on disk before anything that can block. The
class comment claimed the handler takes no locks; that was not strictly
true and now says so.

QET_CRASH_BACKTRACE comes from find_package(Backtrace) rather than
__has_include(<execinfo.h>). The header exists on FreeBSD but backtrace()
lives in libexecinfo there, so the probe compiled and the link failed.

A crash_dump.log left by a pre-#905 version is migrated into crashes/ at
startup, named from its own mtime. Otherwise upgrading stranded it: the
new code never looks at that path, so the dump from the crash that
prompted the upgrade would sit there unoffered forever.

Also from the review: dumps are capped at the 10 newest, so a crash loop
cannot fill the log directory before any dialog is shown; crashDumpDir()
no longer creates the directory as a side effect of a const getter
(ensureCrashDumpDir() does that for the callers that write); and redact()
now masks an AppImage's per-run /tmp/.mount_XXXXXX prefix, which
backtrace_symbols_fd() writes into every frame.

Two test executables, both of which were checked to fail against the
behaviour they replace:

- tst_crashhandler covers CrashHandler::formatInt(), which had no
  coverage at all despite running only inside a signal handler, where
  nothing can assert: zero, negatives, INT_MIN (negated through unsigned,
  since -INT_MIN is UB), INT_MAX, truncation and a zero-sized buffer,
  each checked against a sentinel-filled buffer so a write past the
  reported length fails.
- tst_crashdumps covers the bookkeeping: ordering, empty dumps, the
  exclusion of this run's own path, the cap, concatenation of every
  offered dump, that clearing deletes only what was offered, and what
  redact() masks. qetlogger.cpp needs exactly one symbol from the
  application, QETApp::dataDir(), which the test supplies itself.

Not addressed here: the timestamp in crash_<timestamp>_<pid> is the
launch time, not the crash time -- correct as observed, and the commit
message that implied otherwise was the thing that was wrong. Resolvable
QET frames for AppImage/Flatpak/Snap/Debian need -rdynamic and archived
debug symbols, which is a packaging discussion, not this change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 14:31:49 +12:00
ispyisail d3c8544fd9 Stop the quit-during-modal test taking its full watchdog period every run
The test bounded gdb with a backgrounded watchdog:

    ( sleep 120; kill -9 "$GDB_PID" 2>/dev/null ) &
    WATCHDOG_PID=$!
    wait "$GDB_PID"
    kill "$WATCHDOG_PID" 2>/dev/null

sleep runs as a child of the subshell, so killing the subshell leaves the
sleep orphaned -- and the orphan still holds the write end of whatever this
script's stdout is. Run from a terminal that costs nothing. Run through a
pipe, which is how CTest invokes it, the reader sees no EOF until the sleep
expires, so every run lasted the full 120 seconds regardless of how fast
gdb finished. gdb itself takes ten.

That made modal_quit_regression 92% of the runtime of the entire ctest
suite (122 s of 133 s) and left it 60 s short of its own 180 s CTest
timeout -- near enough that a loaded CI machine could have turned it into
a flaky failure in somebody else's build.

Use timeout(1) instead, which leaves nothing behind, with a plain gdb call
as a fallback where it is unavailable. Suite time drops to 12 s.

Verified on the merged branch: fixed build passes both the .qet and the
read-only .elmt scenario, and with the fix commit reverted both still fail
with signal 6, under ~QETDiagramEditor() and ~QETElementEditor()
respectively.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 13:53:16 +12:00
ispyisail eea939c31c Merge remote-tracking branch 'upstream/master' into fix/904-quit-during-modal 2026-09-18 13:34:58 +12:00