Commit Graph

4172 Commits

Author SHA1 Message Date
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 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
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 242f134e0f Revert "Revert "Draw the folio with inverted lightness on a dark palette"" 2026-09-20 13:05:13 +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 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
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
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 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
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
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
Laurent Trinques 2da516667e Merge pull request #918 from Kellermorph/fix-colour-selection
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m41s
Persist QColorDialog custom colors across application restarts
2026-09-18 19:36:25 +02:00
Laurent Trinques b62b118d45 Merge pull request #919 from jp2images/feature-icon-themes
Add a "qet-dark" icon theme for dark palettes
2026-09-18 19:00:47 +02:00
Laurent Trinques b0b7814015 Merge pull request #925 from arummler/fix-undo-stack
Fix issues with undo stack
2026-09-18 18:54:11 +02:00
Andre Rummler 059d179d9b Fix:
a) Project stayed in modified state even after undoing all changes.
b) Saving the project deleted the undo stack.
2026-09-18 18:28:48 +02:00
Kellermorph fa52aab149 Persist QColorDialog custom colors across application restarts
Qt's QColorDialog loads custom colors from QSettings on startup but
never writes them back, so user-defined colors in the color picker
are lost when the application exits.

Save and load the 16 custom color slots explicitly via QSettings
in QETApp's constructor (after initStyle) and destructor (before
other settings are flushed). This covers every QColorDialog usage
in the application transparently.

Also fixes a QColorDialog memory leak in DiagramView.
2026-09-18 13:53:14 +02:00
Andre Rummler 54e61d482d Fix: "group rotate" of elements detached the conductors as those were not rotated. 2026-09-18 12:35:08 +02: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 0258d1a74f Merge pull request #917 from ispyisail/fix/913-paste-under-cursor
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m43s
Paste under the cursor, and restore the folio context menu after a paste (#913)
2026-09-18 10:26:00 +02:00
ispyisail dd0c194a3c Paste under the cursor, and give the context menu back (#913)
Two fixes in the placement tool Ctrl+V starts.

Paste appeared on top of the original, not under the cursor. 55c2c0df9
added the placement tool precisely so the copy would not land invisibly
on top of what was copied, but 53a0f07ca then warped the pointer to the
group's grid-snapped origin -- which is the original's position -- so the
copy reappeared exactly over the original until the mouse was moved. The
start_pos the caller computes from the cursor was left unread: three
mentions in the file, all declaration or comment. Reported on #913,
where it reads as Ctrl+V pasting in place.

Move the group to the cursor instead of the cursor to the group. Both put
the copy under the pointer; only one of them takes the pointer away from
where the user put it. start_pos is honoured, the items are translated
once at construction, their conductors re-routed before anything is
drawn, and the movement baseline is set there too rather than waiting for
the first mouse move.

That made the baseline sentinel matter, so it is now the
m_baseline_captured flag the header already declared and nothing used,
rather than m_initial_cursor.isNull() -- which cannot tell "not set yet"
from a baseline that is legitimately scene (0,0).

Separately: one Ctrl+V killed the folio's right-click menu for the rest
of the session. init() sets Qt::NoContextMenu so a right click cancels
the placement instead of opening a menu over it, and nothing ever set it
back, taking "Coller ici", "Collage multiple", the folio properties and
add/remove column/row with it. Every other DiagramEvent* class restores
the policy in its destructor; this one did not. Confirmed against an
unmodified master build: the menu opens before a paste and not after one.
It matters here because "use the right-click menu instead" is the answer
people are given when Ctrl+V does not place where they wanted.

Verified on a built binary driven through a virtual X display, against
examples/741.qet and convertisseur.qet:

- the pointer does not move across Ctrl+V (1300,870 before and after;
  on master it jumps to the original at 798,455), and the copy is under it
- a multi-element selection keeps its layout and its conductors re-route
- Escape and right-click both cancel, leaving nothing behind and nothing
  on the undo stack; click and Return both commit; one undo removes the
  whole paste and redo restores it
- with the pointer outside the viewport the copy lands at the viewport
  centre, visible, and follows correctly once the mouse enters
- pasting into a different folio from the one copied from works
- the context menu opens before a paste, after a cancelled one, and after
  a committed one, with "Coller ici" present

ctest 9/9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 19:57:59 +12: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 eea939c31c Merge remote-tracking branch 'upstream/master' into fix/904-quit-during-modal 2026-09-18 13:34:58 +12:00
Kellermorph 520f4245b3 Fix PDF export checkbox state not being remembered
- Save and restore the state of m_component_info_cb, m_fit_in_page_cb,
  and m_use_full_page_cb via QSettings, in addition to the existing
  ExportProperties-based checkboxes (border, titleblock, terminals, etc.)
- Add savePrintProperties() to persist all checkbox states after
  print/export under the "print/default" settings prefix
- Remove unused QPrinter(HighResolution) local variable in launchDialog()
  that caused an unnecessary ~2s CUPS round-trip on Linux
2026-09-17 21:33:05 +02:00
Kellermorph 7a73e8075f Merge remote-tracking branch 'upstream/master' into copying-fix
# Conflicts:
#	sources/diagramevent/diagrameventaddpaste.cpp
2026-09-17 17:59:12 +02:00
Kellermorph 53a0f07ca3 Fix copy-paste bugs in DiagramEventAddPaste and PasteDiagramCommand
Fix multiple issues introduced by commit 55c2c0df9 (interactive paste):

Paste placement (diagrameventaddpaste):
- Items now load at their original XML coordinates instead of being
  snapped to the cursor position, preventing jumps on initial placement
- Use delta-based movement: record the actual grid-snapped cursor
  position on first mouseMoveEvent as baseline, then compute
  grid-snapped deltas from there
- Bypass Diagram::snapToGrid() in moveTo() to avoid Ctrl modifier
  causing pixel-snapping instead of grid-snapping
- Remove moveTo() from mouseReleaseEvent to prevent a final jump
  on click
- Warp cursor to group bounding rect origin for visual feedback

Paste command (diagramcommands):
- Always clear PLC slave data (type, address, function, comment,
  cross-ref, label, TC, T1-T4) on paste regardless of the
  erase-label-on-copy user preference
- Block alignment (m_block_alignment / blockAlignmentUpdate) before
  setElementInformations() for both slaves and non-slaves, preventing
  finishAlignment() from shifting right/center-aligned text items
- Clear non-UserText items directly after setElementInformations()
  for slaves as a safety net
2026-09-17 17:54:57 +02:00
Andre Rummler 26285373a3 Fix alignment when pasting an element. 2026-09-17 17:05:15 +02:00
Laurent Trinques 8c135a009d Clear the tools dock when an editor refuses the selection 2026-09-17 13:59:32 +02:00
Laurent Trinques 261b0f9e12 Merge pull request #675 from IBSYSLevi/improvement/avoid-needless-dock-churn
Avoid needless dock churn in QETElementEditor::updateInformations()
2026-09-17 13:49:52 +02:00
Laurent Trinques 1183849ce2 Merge pull request #895 from Kellermorph/fix-copy-page-plc-cleanup
Clean up stale data when duplicating diagram pages
2026-09-17 13:38:20 +02:00
Laurent Trinques 2b30083193 Merge pull request #909 from arummler/fix-move-insert
additional fix requested in #899
2026-09-17 13:29:07 +02:00
Andre Rummler 863ac5f0ae avoid that another action triggers while aborting operation 2026-09-17 13:25:17 +02:00
ispyisail 8623dd4c6f Refuse to close an editor while a modal dialog is running (#904)
openAndAddProject() shows BackupDialog as a stack object parented to the
editor and exec()s it; every QET::QetMessageBox does the same. exec() runs a
nested event loop, and closing the editor during it turns WA_DeleteOnClose
into a deleteLater() that the nested loop processes: ~QWidget() deletes the
editor's children, the stack-allocated dialog among them, and the process
aborts. Reported on macOS, where File > Quit lives in the application menu
and stays usable while the backup question is up.

The close is now refused while any modal widget is active, and the dialog is
raised so the refused quit is not silent. It is done in QETMainWindow::event()
rather than in closeEvent(), because QETDiagramEditor::closeEvent() starts
closing projects before it decides whether to accept. That covers the diagram
and title-block editors; QETElementEditor is a plain QMainWindow, so its
closeEvent() calls the same helper before canClose(), which itself opens a
modal. QETApp::quitQET() needs nothing: closeEveryEditor() goes through each
editor's close(), and quitQET() already only quits when every close succeeded.

Rejected alternatives, both suggested on the issue:

- Giving the dialog no parent stops the abort but not the deletion. One
  caller of openAndAddProject() is the editor's own constructor, which goes
  on to open the next file and call slot_updateActions() on this -- a loud
  abort would become a silent use-after-free.
- Guarding only QETApp::closeEveryEditor(), which I first recommended on the
  issue, misses the reported route entirely: File > Quit is connected to
  QETDiagramEditor::close(), not to quitQET().

Verified on Linux, where there is nothing to click (the menu bar belongs to
the blocked window, and Qt ignores window-manager close requests for it), by
calling close() from gdb while the dialog's loop was running -- both
QETApp::quitQET() and QWidget::close() on the editor. Unfixed, both abort
with "free(): invalid size" in QObjectPrivate::deleteChildren() under
~QETDiagramEditor(), matching the report frame for frame; fixed, close()
returns false, the editor and the dialog stay up, and after answering the
dialog Ctrl+Q exits normally. The element-editor guard is the same helper
but was not exercised separately.

tests/modal-quit-regression/ turns that into a gate: it breaks on
QDialog::exec(), interrupts inside the nested loop, calls quitQET() and
checks the process survives. It matches no window titles (translated) and no
window ids, runs on the offscreen platform, and needs only gdb with Python.
Checked both ways: exit 1 with the backtrace above on a build without this
change, exit 0 with it.

ctest 8/8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 23:19:59 +12:00
Kellermorph b7f516d485 Clean up stale data when duplicating diagram pages
When duplicating a diagram page, copied slave elements retain stale
data from the source: labels, descriptions, link references, and PLC
master information (type, address, function, comment, cross-ref, timer
values) remain in the copies.

Fix by adding clearPendingLinks() to Element to prevent copies from
linking back to source elements via stale UUIDs, and by cleaning up
copied element data after fromXml():

- Slaves always lose their label, formula, comment, location, and
  PLC master data, since their text comes from a master element which
  is not available on the copy. The displayed text on slave elements
  is cleared directly via setPlainText(), but UserText items (free
  text typed by the user) are preserved.

- For PLC slaves, setElementInformations() is called without
  m_block_alignment so that elementInfoChanged() can run
  finishAlignment() to correctly adjust text positions for the
  cleared content.

- Non-slave elements respect the existing erase-label-on-copy
  preference, same as PasteDiagramCommand::redo().

- Conductor labels are also reset when erase-label-on-copy is
  active, matching PasteDiagramCommand::redo() (issue #413).

- Text alignment is preserved by wrapping setElementInformations()
  with m_block_alignment for non-slave elements, same as
  Element::fromXml().
2026-09-17 13:12:15 +02:00
ispyisail 0646f9ca4f Harden the crash reporter: keep every dump, and say what crashed
Three weaknesses, all visible in ChuckNr11's report on #898 -- "the report
appeared only once despite there being 10 or more crashes".

One dump per run instead of one per install
-------------------------------------------
crashDumpPath() was a single fixed crash_dump.log, and the handler opens it
O_TRUNC, so each crash destroyed the evidence from the one before. Ten
crashes left one dump. Dumps now go to a crashes/ directory named
crash_<timestamp>_<pid>.log, and every pending one is offered together,
newest first, with a banner saying how many there are. A crash that repeats
is exactly the case where the earlier dumps matter, because the difference
between them is the evidence.

The name is built in normal context and handed to CrashHandler::install(),
which copies it into a preallocated buffer as before -- the handler still
writes to one fixed path, so its no-allocation invariant is untouched.

The dump now says which signal fired
------------------------------------
The header is built once at install(), so every dump looked identical no
matter what killed the process -- and SIGSEGV and SIGABRT point at very
different bugs. Written with an async-signal-safe integer formatter into a
stack buffer, since snprintf is not on the POSIX safe list.

...and where it was
-------------------
The ring said what the program was doing; nothing said where it died. The
dump now carries a backtrace. backtrace() is warmed once in install() so
its first-call lazy resolution cannot allocate inside the handler, and
backtrace_symbols_fd() writes straight to the fd -- unlike
backtrace_symbols(), which mallocs and must never be used here. Guarded on
__has_include(<execinfo.h>) so platforms without it are unaffected.

QET's own frames currently resolve as offsets rather than names, since the
binary does not export its dynamic symbols. They are still resolvable
offline: the header records the exact git SHA. Building with -rdynamic
would give names directly, but that is a build-flag decision for its own
change.

Deliberately unchanged: the four invariants in crashhandler.h. Nothing
added here allocates, blocks, takes a lock, or swallows the crash.

Verified: three consecutive SIGSEGVs now leave three separate dumps, each
carrying "Signal: 11" and a backtrace with resolved Qt frames; launching
afterwards offers all three in one dialog, newest first, and clears them
once shown. ctest 8/8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 22:44:59 +12:00
Jeff Patterson 8ef130a59d Add a "qet" icon theme and load icons by name
QET had no icon theme: the 446 entries of the icon table and the 116
iconsets in .ui files each named a resource path, so an icon could only
ever be one file, and a variant for another palette or a vector source
had nowhere to go (GitHub #466, #690, #870). This adds the theme layout
without changing a single pixel; a dark variant comes in a follow-up.

The theme "qet" follows the freedesktop layout Qt's icon loader
understands. misc/make_icon_themes.py generates ico/icon-themes.qrc,
which aliases the existing ico/<size>/<name>.png files into
themes/qet/<size>/<name>.png, and ico/themes/qet/index.theme. No file
moves. The four table entries that paired a 16 pixel file with a 22
pixel file of another name (ConductorSettings, DiagramAdd,
DiagramDelete, DialogInformation) get the 22 pixel file aliased under
the 16 pixel name.

QETApp::initIconTheme() registers the theme before initIcons() and makes
it current on every platform, so a desktop icon theme cannot replace
QET's icons. Icons are then looked up by name: QIcon::fromTheme() in
qeticons.cpp and in the few places that built a QIcon from a resource
path directly, and theme="..." on the iconsets in .ui files, with the
resource path kept as fallback. Flags, color swatches, application and
MIME icons stay on their paths.

One entry does not go through the theme. The elements panel draws the
project root with ProjectFileGP in the 50 pixel slot it reserves for
element previews, and the name "project" also carries the 128 pixel
file the configuration dialog uses. On a 2x display Qt's loader picks
that file for a 50 pixel request and fills the slot. ProjectFileGP
loads the 16 and 22 pixel files directly, as before.

tests/qttest/tst_qeticons: every name in the theme resolves, the four
aliases resolve at 22 pixels, a Fusion tool button shows its icon at
3:1 with disabled weaker than enabled, and the project root icon stays
at 22 pixels or less when asked for 50 at 2x while the configuration
dialog still gets its 128 pixel file. The rendering helpers shared
with tst_qetpalette moved to tests/qttest/inkcontrast.h.
2026-09-17 05:41:50 -05:00
Laurent Trinques c3b0e6725e Merge pull request #896 from ispyisail/feature/886-custom-sql-reports
Add read-only enforcement, preview and import/export to custom SQL reports
2026-09-17 12:38:17 +02:00
Laurent Trinques 3f8e6418d0 Merge pull request #881 from Kellermorph/checkbox-master-slave
Add Hide full masters checkbox to slave linking widget
2026-09-17 12:30:42 +02:00
Laurent Trinques a86fe2a46b Merge pull request #902 from ispyisail/fix/901-crash-report-never-offered
Offer the crash report after the recovery prompt, not instead of it
2026-09-17 12:27:10 +02:00
ispyisail 52713704bc Offer the crash report after the recovery prompt, not instead of it (#901)
QETApp::checkBackupFiles() only reached checkCrashDump() when there was
nothing to recover:

    if (stale_files.isEmpty()) {
        checkCrashDump();
        return;
    }

A crash with a project open always leaves a stale KAutoSaveFile, so on the
next launch the recovery prompt won every time and the dump sat on disk
unoffered -- the report was unreachable in exactly the case it is most
wanted. Reported by ChuckNr11 as a side note in #898: "the report appeared
only once despite there being 10 or more crashes". It appears on the runs
that happen to have nothing to recover.

Discussion #644 step 5 asks that the two prompts never show at the same
time, which this keeps: the recovery prompt is answered first, then the
report. The recovery half moves into offerBackupFiles() so both paths fall
through to the same place.

Verified under Xvfb, from a real crash state (SIGABRT with a project open,
leaving both a stale file and a 5.4 KB crash_dump.log): the recovery prompt
appears, and dismissing it now brings up "Rapport de plantage" carrying the
version, git SHA, OS, Qt version and the log ring. Before this change the
report never appeared -- that half rests on the four lines above rather
than on a captured before/after, since re-creating the crash state for a
clean baseline run kept consuming it.

Not addressed: the dump is a single fixed path opened O_TRUNC, so
consecutive crashes still overwrite one another.

ctest 8/8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 22:10:03 +12:00
Andre Rummler 923723a9de Fix the following issues:
a) paste
- conductor does not move
- pressing escape does not abort
- pressing anything during the process crashes the program instead of aborting
b) similar issue with the escape not working fixed for graphics items, texxt fields (and probably others)
2026-09-17 11:34:25 +02:00