Compare commits

...

21 Commits

Author SHA1 Message Date
Laurent Trinques 1212f48c6d Merge pull request #979 from Kellermorph/colour-programm
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m48s
Add custom application color picker in global settings
2026-09-22 13:49:42 +02:00
Laurent Trinques e01f46c5b0 Merge pull request #981 from ispyisail/fix-974-report-link-colour
Fix report-link colour/style mismatch detection (#974)
2026-09-22 13:38:40 +02:00
ispyisail 3cec02b3f3 Fix report-link colour/style mismatch detection (bugtracker #974)
LinkElementCommand::redo() already had a check meant to catch exactly
this -- two report-linked conductors whose properties disagree -- and
ask the user which to keep via PotentialSelectorDialog. It never
worked: it built ONE combined list from three unrelated fields
(tension_protocol, wire_color, wire_section) and tested that whole
list for string equality, so a tension-protocol value could never
equal a wire-colour value even when every field individually matched
across every conductor. Worse, "wire_color"/"wire_section" are
ConductorProperties::m_wire_color/m_wire_section, a separate free-text
documentation pair that says nothing about how the wire is actually
drawn -- that's "color"/"style" -- so the one field #974 is actually
about was never compared at all.

Fixed by comparing each relevant field (text/num, function, tension
protocol, colour, line style) separately. Downloaded the reporter's
actual project, confirmed the mismatched wire reads color="#0000ff" on
one side of a "Folio suivant"/"Folio precedent" link and
color="#55aa00" on the other, with the link's other four conductors
matching correctly (ruling out a rendering artifact) -- see PR #980's
checkContinuity() extension, which now flags this class of mismatch on
sight.

Extracted the comparison into its own static
reportLinkNeedsPotentialChoice(), for the same reason
ConductorCreator::needsPotentialChoice() already exists as its own
method: a caller with nobody there to answer a modal dialog needs to
check first and decline, and the condition must not drift away from
the one redo() actually applies.

Fixing the comparison surfaced a real, previously-latent hang in this
session's own qet.linkElements(): PotentialSelectorDialog::exec() is a
plain QDialog::exec(), not routed through QET::QetMessageBox, so
headless --run has nobody to answer it. Measured directly -- hung
until killed with the property-comparison fix alone, clean refusal
after adding the guard. linkElements() now calls
reportLinkNeedsPotentialChoice() before constructing the command and
declines with a clear reason, the same choice addConductor() already
makes about ConductorCreator's own equivalent dialog.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 23:00:05 +12:00
Laurent Trinques 56f60be60a CI: try to fix 404 error on downlad page 2026-09-22 10:16:49 +02:00
Laurent Trinques 852f581206 Merge pull request #971 from arummler/fix-translation-syntax-02
Fix translation syntax follow-up
2026-09-22 09:57:30 +02:00
Laurent Trinques 3b626a7d1a CI: try to fix 404 error on downlad page 2026-09-22 09:50:14 +02: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
Andre Rummler e1f887a036 Standarize pugixml header path to recommended variant with target INTERFACE. 2026-09-22 09:07:27 +02:00
Laurent Trinques c5edd0a54b CI: try to fix 404 error on downlad page
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m11s
2026-09-22 08:19:23 +02:00
Laurent Trinques 3a13287ba9 CI: delete auto-doxygen.yml 2026-09-22 08:05:53 +02:00
Andre Rummler 24776bfcf6 Avoid setting globally QLocale which was introduced in a recent MR as it will change number formats, etc. which is not necessarily what a user switching language wants. Beside that
small fix of a trasnlation comment.
2026-09-22 08:03:07 +02:00
Andre Rummler 3202c145e8 Fix French origin strings (language improvement). 2026-09-22 08:03:07 +02:00
Laurent Trinques 4888ae87f3 Merge pull request #978 from Kellermorph/feature/refresh-all
fix: update composite text %{label} when folio changes
2026-09-22 07:53:36 +02:00
Laurent Trinques 59344eb565 Merge pull request #977 from ispyisail/fix/591-text-resize-handles
Fix #591's resize handles: unreachable via plain click, wrong position
2026-09-22 07:51:32 +02:00
Kellermorph 5d033bf1b2 Add custom application color picker in global settings
Add a KColorButton next to the 'Utiliser les couleurs du système'
checkbox in the Apparence settings tab:

- When the checkbox is active (default), system colors are used
  and the color button is disabled
- When the checkbox is inactive, the color button becomes active
  and lets the user pick any color for the entire application
- useCustomPalette() builds a full QPalette from the chosen color
  with proper light/dark text contrast, button shading, and icon
  theme switching
- The chosen color is persisted in QSettings as
  'customapplicationcolor' and restored on next startup

Files changed:
- sources/ui/configpage/generalconfigurationpage.ui: HBoxLayout
  with checkbox + KColorButton, customwidget declaration
- sources/ui/configpage/generalconfigurationpage.h: new slot
- sources/ui/configpage/generalconfigurationpage.cpp: load/save
  custom color, enable/disable logic, toggled slot
- sources/qetapp.h: useCustomPalette() declaration
- sources/qetapp.cpp: useCustomPalette() implementation,
  startup restore of custom color
2026-09-21 22:05:31 +02:00
Kellermorph 8985babfe7 fix: update composite text %{label} when folio changes
DynamicElementTextItem::updateLabel() resolved %{label} in composite
text using the stale value from elementInformations()["label"], which
is only set once at load time and never updated when the folio/page
number changes.

Use element->actualLabel() instead, which resolves the label formula
(including %F, %f, %id) against the current folio at call time.
2026-09-21 21:45:03 +02:00
ispyisail 29d16c3337 Fix #591's resize handles: reachable only via Shift/right-click, wrong position
Two bugs reported by @arummler on #591 after merge:

  "It works but to select the text field one has to right click on
  it...I think there are competing handlers or something."
  "the drag elements should be on the border of the box. In the
  moment they appear directly left and right from the text."

Both reproduced headlessly (scripts/qet-gui-dialog.sh) against a fresh
build of current master and root-caused before touching anything.

Selection: DynamicElementTextItem::mousePressEvent() forwards a plain
click (no Shift) straight to parentElement()->mousePressEvent(), by
design and pre-existing -- it's what lets dragging a symbol by its own
label move the whole symbol rather than just the label. That's correct
and untouched here. But it means a plain click leaves the *parent*
selected, not the text, and #591's handles were wired only to the
text's own ItemSelectedHasChanged -- so they were only reachable via
Shift+click or a right-click's context menu (which happens to select
the item under the cursor for its own context menu, unrelated to the
Shift path), neither of which anyone reaches for to resize a text.
Confirmed with screenshots at each step, including that Shift+click
already reached the existing (if misplaced) handles correctly.

Fix: DynamicElementTextItem::refreshResizeHandlesVisibility() shows the
handles when either the text itself or its parent element is selected,
and Element gets an itemChange() override (it had none) that calls it
on each of its own texts when the element's own selection changes. Both
sides driven from itemChange(), Qt's own hook for exactly this and the
same one already used for the text's own selection.

First attempt drove this from paint() instead, since the PR's own
updateResizeHandlesPos() already runs there. That crashed reproducibly
(SIGABRT) on deselecting a text: paint() runs while QGraphicsScene
iterates its item list to draw it, and addResizeHandles()/
removeResizeHandles() mutate that list via QGraphicsScene::addItem()/
removeItem(), which cannot safely happen mid-iteration. Caught it with
the same headless repro before it went anywhere near a PR, moved the
logic to itemChange(), and re-ran the full sequence -- select, resize,
undo, deselect, twice through -- clean.

Position: updateResizeHandlesPos() placed the handles on frameRect(),
which is a box sized to the text's natural (idealWidth()) content and
then re-centred inside boundingRect() -- it does not grow with
textWidth(). Once a text has been widened, frameRect() stays tight
around the glyphs while boundingRect() -- the box QGraphicsView actually
outlines as the selection, and the box a user drags relative to -- grows
around it, leaving the handles stranded well inside the visible
selection border. Fix: position them on boundingRect() instead, which
does track textWidth(); confirmed by widening a text and checking the
handle lands exactly on the new edge rather than partway across it.

Verified headlessly end to end on the original report's own element
("motor off" on grafcet.qet, folio 1): a single plain left-click (no
Shift, no right-click) now shows both handles at the true box border;
dragging resizes correctly and the handle tracks the growing edge;
Ctrl+Z restores the -1 auto-width sentinel and the handles stay at the
reverted position; clicking away removes them; repeated twice with no
crash. Qt 6.10.2, ctest 12/12, no new warnings in either changed file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 07:30:39 +12:00
Laurent Trinques 61f5e5e502 Merge pull request #972 from arummler/fix-pugixml-linking
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 1m42s
fix PUGIXML linking
2026-09-21 19:36:28 +02:00
Laurent Trinques 77bc9ed8e4 Merge pull request #976 from arummler/untrack-qch
Stop tracking *.qch via LFS.
2026-09-21 19:35:48 +02:00
Andre Rummler 9b26d5dc6a Stop tracking *.qch via LFS. 2026-09-21 17:57:59 +02:00
Andre Rummler b76d8ce8a1 Fix: remove PUGIXML files from compilation fileset. This is redundant using the target approach and leads sometimes to failures. 2026-09-21 13:29:40 +02:00
26 changed files with 392 additions and 151 deletions
-1
View File
@@ -1 +0,0 @@
*.qch filter=lfs diff=lfs merge=lfs -text
-67
View File
@@ -1,67 +0,0 @@
name: Auto-build doxygen docs
on:
push:
tags:
- '**'
jobs:
doxygen:
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: 'recursive'
show-progress: ''
- name: Setup and run doxygen
run: sudo apt update && sudo apt install doxygen graphviz qhelpgenerator-qt5 -y
- name: Set up Git LFS
run: |
git lfs install
git lfs track "*.qch"
- name: Run doxygen
run: doxygen Doxyfile
- name: Create Pull Request
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.MR_TOKEN }}
commit-message: update QCH file
committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
title: Update QCH Help file
body: |
- Updating QT Help file following commit ${{ github.sha }}.
- Auto-generated by [create-pull-request][1]
[1]: https://github.com/peter-evans/create-pull-request
branch: update-qch
labels: |
qch
cicd
delete-branch: true
add-paths: doc/*.qch
- uses: actions/upload-pages-artifact@v3
with:
path: ${{ github.workspace }}/doc/html/
deploy:
# Add a dependency to the build job
needs: doxygen
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
permissions:
pages: write # to deploy to Pages
id-token: write # to verify the deployment originates from an appropriate source
# Deploy to the github-pages environment
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
# Specify runner + deployment step
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4 # or specific "vX.X.X" version tag for this action
+53 -10
View File
@@ -59,17 +59,41 @@ jobs:
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# 2. Download the portable artifact for this flavor # 2. Download the portable artifact for this flavor
#
# Wrapped in nick-fields/retry: actions/download-artifact@v8 has
# shown repeated "Artifact download failed after 5 retries"
# failures on this cross-workflow download (via run-id) — not a
# real content/digest problem, just flaky Azure blob delivery.
# Two separate occurrences observed within days of each other
# (different artifact IDs/digests, same failure signature), each
# one enough to fail build-msi outright and block the rest of the
# pipeline. Retrying the whole download 3x is cheap insurance.
# Switched to `gh run download` here because nick-fields/retry
# can only retry a shell command, not re-invoke a `uses:` step.
# ---------------------------------------------------------------- # ----------------------------------------------------------------
- name: Download portable artifact - name: Download portable artifact
uses: actions/download-artifact@v8 uses: nick-fields/retry@v3
with: with:
name: ${{ matrix.portable_artifact }} timeout_minutes: 10
path: artifact\files max_attempts: 3
# workflow_run => use the triggering run's ID retry_wait_seconds: 30
# workflow_dispatch => use input run_id if provided, otherwise current run shell: pwsh
run-id: ${{ github.event.workflow_run.id || github.event.inputs.run_id || github.run_id }} command: |
github-token: ${{ secrets.GITHUB_TOKEN }} if (Test-Path "artifact\files") { Remove-Item -Recurse -Force "artifact\files" }
repository: ${{ github.repository }} New-Item -ItemType Directory -Force -Path "artifact\files" | Out-Null
$runId = "${{ github.event.workflow_run.id || github.event.inputs.run_id || github.run_id }}"
gh run download $runId `
--repo "${{ github.repository }}" `
--name "${{ matrix.portable_artifact }}" `
--dir "artifact\files"
if ($LASTEXITCODE -ne 0) {
Write-Error "gh run download failed (exit $LASTEXITCODE)"
exit $LASTEXITCODE
}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# 3. Extract version # 3. Extract version
@@ -336,7 +360,13 @@ jobs:
if-no-files-found: error if-no-files-found: error
- name: Delete old nightly .msi asset - name: Delete old nightly .msi asset
if: always() # Only run if a new MSI actually exists in dist/ — otherwise the old
# (still working) nightly .msi would be deleted without anything to
# replace it, leaving the nightly release with no MSI at all until
# the next successful build (see windows-msi-pipeline notes,
# run 35694391567: an upstream artifact-download failure meant
# dist\*.msi never existed for that run).
if: always() && hashFiles('dist/*.msi') != ''
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }} REPO: ${{ github.repository }}
@@ -381,7 +411,20 @@ jobs:
deploy-pages: deploy-pages:
needs: build-msi needs: build-msi
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: always() && needs.build-msi.result == 'success' # Regenerate the page whenever the MSI job actually ran (success OR
# packaging/signing failure) — not only on full success.
# generate-page.py queries the published assets on the "nightly"
# release itself (line 407: `gh release view nightly --json assets`),
# so it already handles a missing MSI natively (empty MSI_NAME -> no
# MSI button on the page). The page only needs the exe/zip already
# published by windows-build.yml, independent of the MSI's fate.
# Only "skipped"/"cancelled" are excluded: if build-msi never ran at
# all (e.g. Windows Build itself failed), there is nothing new to
# publish and regenerating the page would be pointless.
if: >
always() &&
needs.build-msi.result != 'skipped' &&
needs.build-msi.result != 'cancelled'
permissions: permissions:
contents: write contents: write
pages: write pages: write
-1
View File
@@ -318,7 +318,6 @@ target_include_directories(
${QET_DIR}/sources/NameList ${QET_DIR}/sources/NameList
${QET_DIR}/sources/NameList/ui ${QET_DIR}/sources/NameList/ui
${QET_DIR}/sources/utils ${QET_DIR}/sources/utils
${QET_DIR}/pugixml/src
${QET_DIR}/sources/dataBase ${QET_DIR}/sources/dataBase
${QET_DIR}/sources/dataBase/ui ${QET_DIR}/sources/dataBase/ui
${QET_DIR}/sources/factory/ui ${QET_DIR}/sources/factory/ui
-4
View File
@@ -507,10 +507,6 @@ set(QET_SRC_FILES
${QET_DIR}/sources/PropertiesEditor/propertieseditorwidget.cpp ${QET_DIR}/sources/PropertiesEditor/propertieseditorwidget.cpp
${QET_DIR}/sources/PropertiesEditor/propertieseditorwidget.h ${QET_DIR}/sources/PropertiesEditor/propertieseditorwidget.h
${QET_DIR}/pugixml/src/pugiconfig.hpp
${QET_DIR}/pugixml/src/pugixml.cpp
${QET_DIR}/pugixml/src/pugixml.hpp
${QET_DIR}/sources/qetgraphicsitem/conductor.cpp ${QET_DIR}/sources/qetgraphicsitem/conductor.cpp
${QET_DIR}/sources/qetgraphicsitem/conductor.h ${QET_DIR}/sources/qetgraphicsitem/conductor.h
${QET_DIR}/sources/qetgraphicsitem/conductortextitem.cpp ${QET_DIR}/sources/qetgraphicsitem/conductortextitem.cpp
@@ -20,7 +20,7 @@
#include "../NameList/nameslist.h" #include "../NameList/nameslist.h"
#include "../diagramcontext.h" #include "../diagramcontext.h"
#include "pugixml/src/pugixml.hpp" #include "pugixml.hpp"
#include <QIcon> #include <QIcon>
#include <QString> #include <QString>
+1 -1
View File
@@ -17,7 +17,7 @@
*/ */
#ifndef NAMES_LIST_H #ifndef NAMES_LIST_H
#define NAMES_LIST_H #define NAMES_LIST_H
#include "pugixml/src/pugixml.hpp" #include "pugixml.hpp"
#include <QtXml> #include <QtXml>
/** /**
@@ -171,20 +171,20 @@ MoveTerminalCommand::MoveTerminalCommand(QSharedPointer<PhysicalTerminal> termin
QString text; QString text;
if (t_label.isEmpty()) { if (t_label.isEmpty()) {
if (strip_name.isEmpty() && new_strip_name.isEmpty()) if (strip_name.isEmpty() && new_strip_name.isEmpty())
text = QObject::tr("Déplacer une borne d'un groupe de bornes vers un groupe de bornes"); text = QObject::tr("Déplacer une borne d'un groupe de bornes vers un autre groupe de bornes");
else if (strip_name.isEmpty()) 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); text = QObject::tr("Déplacer une borne d'un groupe de bornes vers le groupe de bornes %1").arg(new_strip_name);
else if (new_strip_name.isEmpty()) else if (new_strip_name.isEmpty())
text = QObject::tr("Déplacer une borne du groupe de bornes %1 vers un groupe de bornes").arg(strip_name); text = QObject::tr("Déplacer une borne du groupe de bornes %1 vers un autre groupe de bornes").arg(strip_name);
else else
text = QObject::tr("Déplacer une borne du groupe de bornes %1 vers le groupe de bornes %2").arg(strip_name, new_strip_name); text = QObject::tr("Déplacer une borne du groupe de bornes %1 vers le groupe de bornes %2").arg(strip_name, new_strip_name);
} else { } else {
if (strip_name.isEmpty() && new_strip_name.isEmpty()) 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); text = QObject::tr("Déplacer la borne %1 d'un groupe de bornes vers un autre groupe de bornes").arg(t_label);
else if (strip_name.isEmpty()) 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); 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()) 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); text = QObject::tr("Déplacer la borne %1 du groupe de bornes %2 vers un autre groupe de bornes").arg(t_label, strip_name);
else 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); 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);
} }
@@ -205,11 +205,11 @@ MoveTerminalCommand::MoveTerminalCommand(QVector<QSharedPointer<PhysicalTerminal
QString text; QString text;
if (strip_name.isEmpty() && new_strip_name.isEmpty()) 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); text = QObject::tr("Déplacer %n borne(s) d'un groupe de bornes vers un autre groupe de bornes", "", count);
else if (strip_name.isEmpty()) else if (strip_name.isEmpty())
text = QObject::tr("Déplacer %n borne(s) d'un groupe de bornes vers le groupe de bornes %1", "", count).arg(new_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()) else if (new_strip_name.isEmpty())
text = QObject::tr("Déplacer %n borne(s) du groupe de bornes %1 vers un groupe de bornes", "", count).arg(strip_name); text = QObject::tr("Déplacer %n borne(s) du groupe de bornes %1 vers un autre groupe de bornes", "", count).arg(strip_name);
else 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); 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(text); setText(text);
+1 -1
View File
@@ -17,7 +17,7 @@
*/ */
#ifndef DIAGRAM_CONTEXT_H #ifndef DIAGRAM_CONTEXT_H
#define DIAGRAM_CONTEXT_H #define DIAGRAM_CONTEXT_H
#include "pugixml/src/pugixml.hpp" #include "pugixml.hpp"
#include <QDomElement> #include <QDomElement>
#include <QHash> #include <QHash>
+2 -1
View File
@@ -19,6 +19,7 @@
#include "QPropertyUndoCommand/qpropertyundocommand.h" #include "QPropertyUndoCommand/qpropertyundocommand.h"
#include "diagram.h" #include "diagram.h"
#include "qetapp.h"
#include "qetgraphicsitem/dynamicelementtextitem.h" #include "qetgraphicsitem/dynamicelementtextitem.h"
#include "qetgraphicsitem/elementtextitemgroup.h" #include "qetgraphicsitem/elementtextitemgroup.h"
@@ -146,5 +147,5 @@ QString ElementTextsMover::undoText() const
if (parts.isEmpty()) if (parts.isEmpty())
return QString(); // should never occur return QString(); // should never occur
return QObject::tr("Déplacer %1").arg(QLocale().createSeparatedList(parts)); return QObject::tr("Déplacer %1").arg(QLocale(QETApp::interfaceLanguage()).createSeparatedList(parts));
} }
+10 -9
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>. along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "qet.h" #include "qet.h"
#include "qetapp.h"
#include "qeticons.h" #include "qeticons.h"
#include "shortcutmanager.h" #include "shortcutmanager.h"
@@ -276,7 +277,7 @@ QString QET::ElementsAndConductorsSentence(
parts.append( parts.append(
QObject::tr( QObject::tr(
"%n élément(s)", "%n élément(s)",
"part of a enumerative partial sentence listing the content of a diagram", "Sentence fragment used in an automatically generated list of different objects, e.g. objects moved at the same time, which will be combined into a sentence.",
elements_count elements_count
) )
); );
@@ -286,7 +287,7 @@ QString QET::ElementsAndConductorsSentence(
parts.append( parts.append(
QObject::tr( QObject::tr(
"%n conducteur(s)", "%n conducteur(s)",
"part of a enumerative partial sentence listing the content of a diagram", "Sentence fragment used in an automatically generated list of different objects, e.g. objects moved at the same time, which will be combined into a sentence.",
conductors_count conductors_count
) )
); );
@@ -296,7 +297,7 @@ QString QET::ElementsAndConductorsSentence(
parts.append( parts.append(
QObject::tr( QObject::tr(
"%n champ(s) de texte", "%n champ(s) de texte",
"part of a enumerative partial sentence listing the content of a diagram", "Sentence fragment used in an automatically generated list of different objects, e.g. objects moved at the same time, which will be combined into a sentence.",
texts_count texts_count
) )
); );
@@ -306,7 +307,7 @@ QString QET::ElementsAndConductorsSentence(
parts.append( parts.append(
QObject::tr( QObject::tr(
"%n image(s)", "%n image(s)",
"part of a enumerative partial sentence listing the content of a diagram", "Sentence fragment used in an automatically generated list of different objects, e.g. objects moved at the same time, which will be combined into a sentence.",
images_count images_count
) )
); );
@@ -316,7 +317,7 @@ QString QET::ElementsAndConductorsSentence(
parts.append( parts.append(
QObject::tr( QObject::tr(
"%n forme(s)", "%n forme(s)",
"part of a enumerative partial sentence listing the content of a diagram", "Sentence fragment used in an automatically generated list of different objects, e.g. objects moved at the same time, which will be combined into a sentence.",
shapes_count shapes_count
) )
); );
@@ -326,7 +327,7 @@ QString QET::ElementsAndConductorsSentence(
parts.append( parts.append(
QObject::tr( QObject::tr(
"%n texte(s) d'élément", "%n texte(s) d'élément",
"part of a enumerative partial sentence listing the content of a diagram", "Sentence fragment used in an automatically generated list of different objects, e.g. objects moved at the same time, which will be combined into a sentence.",
element_text_count element_text_count
) )
); );
@@ -336,7 +337,7 @@ QString QET::ElementsAndConductorsSentence(
parts.append( parts.append(
QObject::tr( QObject::tr(
"%n tableau(s)", "%n tableau(s)",
"part of a enumerative partial sentence listing the content of diagram", "Sentence fragment used in an automatically generated list of different objects, e.g. objects moved at the same time, which will be combined into a sentence.",
tables_count tables_count
) )
); );
@@ -346,13 +347,13 @@ QString QET::ElementsAndConductorsSentence(
parts.append( parts.append(
QObject::tr( QObject::tr(
"%n plan(s) de bornes", "%n plan(s) de bornes",
"part of a enumerative partial sentence listing the content of a diagram", "Sentence fragment used in an automatically generated list of different objects, e.g. objects moved at the same time, which will be combined into a sentence.",
terminal_strip_count terminal_strip_count
) )
); );
} }
return QLocale().createSeparatedList(parts); return QLocale(QETApp::interfaceLanguage()).createSeparatedList(parts);
} }
/** /**
+85 -2
View File
@@ -236,7 +236,7 @@ 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)); m_interface_language = 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);
@@ -1816,6 +1816,81 @@ void QETApp::useSystemPalette(bool use) {
QET::Palette::refreshStyleSheets(); QET::Palette::refreshStyleSheets();
} }
/**
@brief QETApp::useCustomPalette
Apply a user-chosen color as the application-wide palette.
Builds a full QPalette from \a color, keeping the system palette
as a fallback for roles we don't touch.
@param color the user-chosen base color
*/
void QETApp::useCustomPalette(const QColor &color) {
if (!color.isValid())
return;
// Derive readable text colors from the chosen color.
const bool dark = color.lightness() < 128;
const QColor text = dark ? QColor(220, 220, 220) : QColor(30, 30, 30);
const QColor disabled_text = dark ? QColor(175, 175, 175) : QColor(128, 128, 128);
// Slightly lighter/darker for button and window shading.
QColor button = color;
button = QColor::fromHslF(color.hslHueF(),
color.hslSaturationF(),
dark ? qMin(color.lightnessF() + 0.08, 1.0)
: qMax(color.lightnessF() - 0.08, 0.0));
QColor light = QColor::fromHslF(color.hslHueF(),
color.hslSaturationF(),
dark ? qMin(color.lightnessF() + 0.15, 1.0)
: qMax(color.lightnessF() - 0.15, 0.0));
QColor mid = QColor::fromHslF(color.hslHueF(),
color.hslSaturationF(),
dark ? qMin(color.lightnessF() + 0.04, 1.0)
: qMax(color.lightnessF() - 0.04, 0.0));
QColor dark_c = QColor::fromHslF(color.hslHueF(),
color.hslSaturationF(),
dark ? qMin(color.lightnessF() - 0.04, 1.0)
: qMax(color.lightnessF() - 0.12, 0.0));
QColor shadow = QColor::fromHslF(color.hslHueF(),
color.hslSaturationF(),
dark ? qMin(color.lightnessF() - 0.10, 1.0)
: qMax(color.lightnessF() - 0.20, 0.0));
QPalette p;
// Active and Inactive get the same colors; only Disabled differs.
for (auto group : {QPalette::Active, QPalette::Inactive}) {
p.setColor(group, QPalette::Window, color);
p.setColor(group, QPalette::WindowText, text);
p.setColor(group, QPalette::Base, color);
p.setColor(group, QPalette::AlternateBase, button);
p.setColor(group, QPalette::Text, text);
p.setColor(group, QPalette::Button, button);
p.setColor(group, QPalette::ButtonText, text);
p.setColor(group, QPalette::BrightText, dark ? QColor(255,90,90) : Qt::white);
p.setColor(group, QPalette::Highlight, QColor(30, 96, 176));
p.setColor(group, QPalette::HighlightedText, Qt::white);
p.setColor(group, QPalette::ToolTipBase, button);
p.setColor(group, QPalette::ToolTipText, text);
p.setColor(group, QPalette::Light, light);
p.setColor(group, QPalette::Midlight, mid);
p.setColor(group, QPalette::Mid, mid);
p.setColor(group, QPalette::Dark, dark_c);
p.setColor(group, QPalette::Shadow, shadow);
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
p.setColor(group, QPalette::Accent, QColor(30, 96, 176));
#endif
}
p.setColor(QPalette::Disabled, QPalette::WindowText, disabled_text);
p.setColor(QPalette::Disabled, QPalette::Text, disabled_text);
p.setColor(QPalette::Disabled, QPalette::ButtonText, disabled_text);
qApp->setPalette(p);
qApp->setStyleSheet(QString());
// Switch icon theme to match light/dark.
applyIconTheme(p);
QET::Palette::refreshStyleSheets();
}
/** /**
@brief QETApp::quitQET @brief QETApp::quitQET
Request the closing of all windows; Request the closing of all windows;
@@ -2404,7 +2479,13 @@ void QETApp::initStyle()
//Apply or not the system style //Apply or not the system style
QSettings settings; QSettings settings;
useSystemPalette(settings.value("usesystemcolors", true).toBool()); if (settings.value("usesystemcolors", true).toBool()) {
useSystemPalette(true);
} else if (settings.contains("customapplicationcolor")) {
useCustomPalette(QColor(settings.value("customapplicationcolor").toString()));
} else {
useSystemPalette(false);
}
#if defined(Q_OS_MACOS) && QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) #if defined(Q_OS_MACOS) && QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
// Setting an application palette stops Qt from following the OS // Setting an application palette stops Qt from following the OS
@@ -3045,3 +3126,5 @@ int QETApp::projectId(const QETProject *project) {
} }
return(-1); return(-1);
} }
QString QETApp::m_interface_language;
+4
View File
@@ -68,6 +68,7 @@ class QETApp : public QObject
public: public:
static QETApp *instance(); static QETApp *instance();
void setLanguage(const QString &); void setLanguage(const QString &);
static QString interfaceLanguage() { return m_interface_language; }
static QString langFromSetting (); static QString langFromSetting ();
void switchLayout(Qt::LayoutDirection); void switchLayout(Qt::LayoutDirection);
static void printHelp(); static void printHelp();
@@ -247,6 +248,8 @@ class QETApp : public QObject
static QString m_user_custom_tbt_dir; static QString m_user_custom_tbt_dir;
static QString m_user_macros_dir; static QString m_user_macros_dir;
static QString m_interface_language;
public slots: public slots:
void systray(QSystemTrayIcon::ActivationReason); void systray(QSystemTrayIcon::ActivationReason);
void reduceEveryEditor(); void reduceEveryEditor();
@@ -263,6 +266,7 @@ class QETApp : public QObject
void setMainWindowVisible(QMainWindow *, bool); void setMainWindowVisible(QMainWindow *, bool);
void invertMainWindowVisibility(QWidget *); void invertMainWindowVisibility(QWidget *);
void useSystemPalette(bool); void useSystemPalette(bool);
void useCustomPalette(const QColor &color);
void quitQET(); void quitQET();
void checkRemainingWindows(); void checkRemainingWindows();
void openFiles(const QETArguments &); void openFiles(const QETArguments &);
@@ -732,6 +732,16 @@ void DynamicElementTextItem::paint(QPainter *painter, const QStyleOptionGraphics
{ {
DiagramTextItem::paint(painter, option, widget); DiagramTextItem::paint(painter, option, widget);
//Only ever repositions already-existing sibling items here --
//never adds or removes one. paint() runs while QGraphicsScene is
//iterating its item list to draw it, and mutating that list mid
//-iteration (which addResizeHandles()/removeResizeHandles() do,
//through QGraphicsScene::addItem()/removeItem()) crashes. An
//earlier version of this fix called them from here and crashed
//qelectrotech reproducibly on deselecting a text (SIGABRT); see
//refreshResizeHandlesVisibility() for where that now happens
//instead -- itemChange(), Qt's own safe hook for exactly this,
//already used below for this item's own selection.
if (m_left_resize_handle || m_right_resize_handle) if (m_left_resize_handle || m_right_resize_handle)
updateResizeHandlesPos(); updateResizeHandlesPos();
@@ -826,10 +836,7 @@ QVariant DynamicElementTextItem::itemChange(QGraphicsItem::GraphicsItemChange ch
} }
else if (change == QGraphicsItem::ItemSelectedHasChanged) else if (change == QGraphicsItem::ItemSelectedHasChanged)
{ {
if (value.toBool()) refreshResizeHandlesVisibility();
addResizeHandles();
else
removeResizeHandles();
} }
else if (change == QGraphicsItem::ItemSceneHasChanged && !scene()) else if (change == QGraphicsItem::ItemSceneHasChanged && !scene())
{ {
@@ -878,6 +885,31 @@ bool DynamicElementTextItem::sceneEventFilter(QGraphicsItem *watched, QEvent *ev
return false; return false;
} }
/**
@brief DynamicElementTextItem::refreshResizeHandlesVisibility
Show the resize handles when this text is selected directly, OR when its
parent element is -- which is what an ordinary click without Shift
selects (DynamicElementTextItem::mousePressEvent() forwards a plain
click to the parent, so dragging a symbol by its label moves the whole
symbol; a pre-existing, unrelated behaviour, left untouched here).
Without this, the handles were reachable only via Shift+click or a
right-click's context menu, neither of which a user reaches for to
resize a text field (qelectrotech#591, reported by @arummler).
Called from itemChange() -- both this item's own ItemSelectedHasChanged,
below, and Element::itemChange() on the parent's, which calls this on
every one of its texts. Not from paint(): see the comment there for why
that crashed.
*/
void DynamicElementTextItem::refreshResizeHandlesVisibility()
{
const bool handles_wanted = isSelected() || (m_parent_element && m_parent_element->isSelected());
if (handles_wanted && !m_left_resize_handle)
addResizeHandles();
else if (!handles_wanted && m_left_resize_handle)
removeResizeHandles();
}
/** /**
@brief DynamicElementTextItem::addResizeHandles @brief DynamicElementTextItem::addResizeHandles
Create and show the two width-resize handles (left/right edge of Create and show the two width-resize handles (left/right edge of
@@ -917,20 +949,32 @@ void DynamicElementTextItem::removeResizeHandles()
/** /**
@brief DynamicElementTextItem::updateResizeHandlesPos @brief DynamicElementTextItem::updateResizeHandlesPos
Keep the two resize handles at the vertical middle of frameRect()'s left Keep the two resize handles at the vertical middle of boundingRect()'s
and right edges, in scene coordinates -- called on every paint() so it left and right edges, in scene coordinates -- called on every paint() so
stays correct across every kind of change that can move this item or it stays correct across every kind of change that can move this item or
change its size (position, rotation, font, text, textWidth...) without change its size (position, rotation, font, text, textWidth...) without
needing a dedicated hook for each one. needing a dedicated hook for each one.
Deliberately boundingRect(), not frameRect(): frameRect() is a tight box
around the text's own natural (idealWidth()) size, re-centred inside
boundingRect() -- it does not grow with textWidth(). Once a text has
been widened, that leaves a growing gap between the tight frame and the
dashed selection outline QGraphicsView draws at boundingRect(), which is
the box a user actually sees and expects a resize handle to sit on
(qelectrotech#591, reported by @arummler: "the drag elements should be
on the border of the box"). boundingRect() reflects the full
textWidth() (it is QGraphicsTextItem's own, driven by the document's
laid-out size), so the handles now track the box that is visibly
resized rather than the text glyphs inside it.
*/ */
void DynamicElementTextItem::updateResizeHandlesPos() void DynamicElementTextItem::updateResizeHandlesPos()
{ {
if (!m_left_resize_handle || !m_right_resize_handle) if (!m_left_resize_handle || !m_right_resize_handle)
return; return;
QRectF fr = frameRect(); QRectF br = boundingRect();
m_left_resize_handle->setPos(mapToScene(QPointF(fr.left(), fr.center().y()))); m_left_resize_handle->setPos(mapToScene(QPointF(br.left(), br.center().y())));
m_right_resize_handle->setPos(mapToScene(QPointF(fr.right(), fr.center().y()))); m_right_resize_handle->setPos(mapToScene(QPointF(br.right(), br.center().y())));
} }
/** /**
@@ -1289,9 +1333,15 @@ void DynamicElementTextItem::updateLabel()
if(m_text_from == ElementInfo && element) { if(m_text_from == ElementInfo && element) {
setPlainText(element->actualLabel()); QString new_label = element->actualLabel();
if (toPlainText() != new_label) {
setPlainText(new_label);
}
} }
else if (m_text_from == CompositeText) { else if (m_text_from == CompositeText) {
// Use actualLabel() to ensure %{label} reflects the current
// resolved label (e.g. after a folio/page-number change)
dc.addValue(QStringLiteral("label"), element->actualLabel());
setPlainText(autonum::AssignVariables::replaceVariable(m_composite_text, dc)); setPlainText(autonum::AssignVariables::replaceVariable(m_composite_text, dc));
} }
} }
@@ -121,6 +121,12 @@ class DynamicElementTextItem : public DiagramTextItem
void setRotationPointCenter(bool set); void setRotationPointCenter(bool set);
bool rotationPointCenter() const; bool rotationPointCenter() const;
//Called by Element::itemChange() when the PARENT's selection
//changes, so the parent can keep each of its texts' resize
//handles in sync with its own selection state. Public for that;
//see the .cpp for why it exists.
void refreshResizeHandlesVisibility();
protected: protected:
void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
+25
View File
@@ -1664,6 +1664,31 @@ void Element::hoverLeaveEvent(QGraphicsSceneHoverEvent *e)
update(); update();
} }
/**
@brief Element::itemChange
On ItemSelectedHasChanged, tell each of this element's own dynamic texts
to re-check whether its resize handles should be showing --
DynamicElementTextItem::refreshResizeHandlesVisibility() shows them when
either the text itself or its parent (this) is selected. An ordinary
click with no Shift selects the parent, not the text
(DynamicElementTextItem::mousePressEvent() forwards it), so without this
a plain click on a symbol never showed the resize handles this PR adds
to its texts (qelectrotech#591, reported by @arummler) -- only
Shift+click or a right-click's context menu did, since those are the
paths that leave the text itself selected.
*/
QVariant Element::itemChange(GraphicsItemChange change, const QVariant &value)
{
if (change == QGraphicsItem::ItemSelectedHasChanged)
{
const QList<DynamicElementTextItem *> texts = dynamicTextItems();
for (DynamicElementTextItem *deti : texts) {
deti->refreshResizeHandlesVisibility();
}
}
return QetGraphicsItem::itemChange(change, value);
}
/** /**
@brief Element::setUpFormula @brief Element::setUpFormula
Set up the formula used to create the label of this element Set up the formula used to create the label of this element
+1
View File
@@ -255,6 +255,7 @@ class Element : public QetGraphicsItem
QGraphicsSceneMouseEvent *event) override; QGraphicsSceneMouseEvent *event) override;
void hoverEnterEvent(QGraphicsSceneHoverEvent *) override; void hoverEnterEvent(QGraphicsSceneHoverEvent *) override;
void hoverLeaveEvent(QGraphicsSceneHoverEvent *) override; void hoverLeaveEvent(QGraphicsSceneHoverEvent *) override;
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
protected: protected:
//ATTRIBUTES related to linked element //ATTRIBUTES related to linked element
+8
View File
@@ -20,7 +20,9 @@
#include <QApplication> #include <QApplication>
#include <QColor> #include <QColor>
#include <QImage> #include <QImage>
#include <QMdiArea>
#include <QStyle> #include <QStyle>
#include <QTabBar>
#include <QWidget> #include <QWidget>
#include <cmath> #include <cmath>
@@ -307,4 +309,10 @@ void QET::Palette::refreshStyleSheets()
for (QWidget *widget : widgets) for (QWidget *widget : widgets)
if (!widget->styleSheet().isEmpty()) if (!widget->styleSheet().isEmpty())
widget->setStyleSheet(widget->styleSheet()); widget->setStyleSheet(widget->styleSheet());
// Force an immediate repaint on tab bars and MDI areas so their text
// updates together with the rest of the UI, not one event loop later.
for (QWidget *widget : widgets)
if (qobject_cast<QTabBar *>(widget) || qobject_cast<QMdiArea *>(widget))
widget->update();
} }
@@ -23,6 +23,7 @@
#include "../../utils/qetsettings.h" #include "../../utils/qetsettings.h"
#include "../../utils/qetutils.h" #include "../../utils/qetutils.h"
#include "../../qetmessagebox.h" #include "../../qetmessagebox.h"
#include "../nokde/kcolorbutton.h"
#include <QFileDialog> #include <QFileDialog>
#include <QFontDialog> #include <QFontDialog>
#include <QSettings> #include <QSettings>
@@ -74,6 +75,12 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
ui->DiagramEditor_Grid_PointSize_min_sb->setValue(settings.value("diagrameditor/grid_pointsize_min", 1).toInt()); ui->DiagramEditor_Grid_PointSize_min_sb->setValue(settings.value("diagrameditor/grid_pointsize_min", 1).toInt());
ui->DiagramEditor_Grid_PointSize_max_sb->setValue(settings.value("diagrameditor/grid_pointsize_max", 1).toInt()); ui->DiagramEditor_Grid_PointSize_max_sb->setValue(settings.value("diagrameditor/grid_pointsize_max", 1).toInt());
ui->m_use_system_color_cb->setChecked(settings.value("usesystemcolors", "true").toBool()); ui->m_use_system_color_cb->setChecked(settings.value("usesystemcolors", "true").toBool());
bool sysColors = ui->m_use_system_color_cb->isChecked();
ui->m_custom_app_color_kpb->setEnabled(!sysColors);
if (settings.contains("customapplicationcolor"))
ui->m_custom_app_color_kpb->setColor(QColor(settings.value("customapplicationcolor").toString()));
else
ui->m_custom_app_color_kpb->setColor(QApplication::palette().color(QPalette::Window));
bool tabbed = settings.value("diagrameditor/viewmode", "tabbed") == "tabbed"; bool tabbed = settings.value("diagrameditor/viewmode", "tabbed") == "tabbed";
if(tabbed) if(tabbed)
ui->m_use_tab_mode_rb->setChecked(true); ui->m_use_tab_mode_rb->setChecked(true);
@@ -206,7 +213,17 @@ void GeneralConfigurationPage::applyConf()
bool must_use_system_colors = ui->m_use_system_color_cb->isChecked(); bool must_use_system_colors = ui->m_use_system_color_cb->isChecked();
settings.setValue("usesystemcolors", must_use_system_colors); settings.setValue("usesystemcolors", must_use_system_colors);
if (was_using_system_colors != must_use_system_colors) { if (was_using_system_colors != must_use_system_colors) {
QETApp::instance()->useSystemPalette(must_use_system_colors); if (must_use_system_colors) {
QETApp::instance()->useSystemPalette(true);
} else {
QColor custom_color = ui->m_custom_app_color_kpb->color();
settings.setValue("customapplicationcolor", custom_color.name());
QETApp::instance()->useCustomPalette(custom_color);
}
} else if (!must_use_system_colors) {
QColor custom_color = ui->m_custom_app_color_kpb->color();
settings.setValue("customapplicationcolor", custom_color.name());
QETApp::instance()->useCustomPalette(custom_color);
} }
settings.setValue("border-columns_0",ui->m_border_0->isChecked()); settings.setValue("border-columns_0",ui->m_border_0->isChecked());
settings.setValue("lang", ui->m_lang_cb->itemData(ui->m_lang_cb->currentIndex()).toString()); settings.setValue("lang", ui->m_lang_cb->itemData(ui->m_lang_cb->currentIndex()).toString());
@@ -625,3 +642,14 @@ void GeneralConfigurationPage::on_m_hdpi_round_cb_clicked(bool checked)
ui->m_hdpi_round_policy_cb->setEnabled(checked); ui->m_hdpi_round_policy_cb->setEnabled(checked);
} }
/**
@brief GeneralConfigurationPage::on_m_use_system_color_cb_toggled
Enable/disable the custom color picker when the system color
checkbox is toggled.
@param checked
*/
void GeneralConfigurationPage::on_m_use_system_color_cb_toggled(bool checked)
{
ui->m_custom_app_color_kpb->setEnabled(!checked);
}
@@ -53,6 +53,7 @@ class GeneralConfigurationPage : public ConfigPage
void on_ElementEditor_Grid_PointSize_min_sb_valueChanged(int value); void on_ElementEditor_Grid_PointSize_min_sb_valueChanged(int value);
void on_m_hdpi_round_cb_clicked(bool checked); void on_m_hdpi_round_cb_clicked(bool checked);
void on_m_use_system_color_cb_toggled(bool checked);
private: private:
void fillLang(); void fillLang();
@@ -24,6 +24,8 @@
<string>Apparence</string> <string>Apparence</string>
</attribute> </attribute>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="m_system_color_layout">
<item> <item>
<widget class="QCheckBox" name="m_use_system_color_cb"> <widget class="QCheckBox" name="m_use_system_color_cb">
<property name="text"> <property name="text">
@@ -31,6 +33,15 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="KColorButton" name="m_custom_app_color_kpb">
<property name="toolTip">
<string>Couleur de l'application</string>
</property>
</widget>
</item>
</layout>
</item>
<item> <item>
<widget class="Line" name="line"> <widget class="Line" name="line">
<property name="orientation"> <property name="orientation">
@@ -1161,4 +1172,11 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
</tabstops> </tabstops>
<resources/> <resources/>
<connections/> <connections/>
<customwidgets>
<customwidget>
<class>KColorButton</class>
<extends>QPushButton</extends>
<header>nokde/kcolorbutton.h</header>
</customwidget>
</customwidgets>
</ui> </ui>
+63 -21
View File
@@ -314,31 +314,15 @@ void LinkElementCommand::redo()
if(m_element->diagram()) m_element->diagram()->showMe(); if(m_element->diagram()) m_element->diagram()->showMe();
makeLink(m_linked_after); makeLink(m_linked_after);
//If the action is to link two reports together, we check if the conductors //If the action is to link two reports together, and the conductors
//of the new potential have the same text, function, and protocol. //of the new potential disagree on a property that matters, a
//if not, a dialog ask what do to. //dialog asks what to do. See reportLinkNeedsPotentialChoice() for
//what "disagree" checks and the bug fixed there (bugtracker #974).
if (m_first_redo && (m_element->linkType() & Element::AllReport) \ if (m_first_redo && (m_element->linkType() & Element::AllReport) \
&& m_element->conductors().size() \ && m_element->conductors().size() \
&& m_linked_after.size() && m_linked_after.first()->conductors().size()) && m_linked_after.size() && m_linked_after.first()->conductors().size())
{ {
//fill list of potential if (reportLinkNeedsPotentialChoice(m_element, m_linked_after.first()))
QSet <Conductor *> c_list = m_element->conductors().first()->relatedPotentialConductors();
c_list << m_element->conductors().first();
//fill list of text
QStringList str_txt;
QStringList str_funct;
QStringList str_tens;
for (const Conductor *c : c_list)
{
str_txt << c->properties().text;
str_funct << c->properties().m_function;
str_tens << c->properties().m_tension_protocol;
str_tens << c->properties().m_wire_color;
str_tens << c->properties().m_wire_section;
}
//check text list, isn't same in potential, ask user what to do
if (!QET::eachStrIsEqual(str_txt) || !QET::eachStrIsEqual(str_funct) || !QET::eachStrIsEqual(str_tens))
{ {
PotentialSelectorDialog psd(m_element, this); PotentialSelectorDialog psd(m_element, this);
psd.exec(); psd.exec();
@@ -348,6 +332,64 @@ void LinkElementCommand::redo()
QUndoCommand::redo(); QUndoCommand::redo();
} }
/**
@brief LinkElementCommand::reportLinkNeedsPotentialChoice
Whether linking these two report elements (next_report/previous_report)
would pop PotentialSelectorDialog -- i.e. whether their conductors (if
any exist yet, on either side) disagree on a property redo() cares
about. Exposed as its own static method, rather than left inline in
redo(), for the same reason ConductorCreator::needsPotentialChoice()
is: a caller with nobody there to answer a modal dialog (the scripting
API) can check first and decline, and the condition cannot drift away
from the one redo() actually applies.
Bug fixed here (bugtracker #974): the original check built ONE
combined list from three unrelated fields (tension_protocol,
wire_color, wire_section) and tested that whole list for equality --
comparing a tension-protocol string against a wire-colour string is
never equal even when each field individually matches across every
conductor, and wire_color/wire_section are ConductorProperties::
m_wire_color/m_wire_section, a separate free-text documentation pair
that says nothing about how the wire is actually drawn (that is
"color"/"style"). Net effect: the dialog could not reliably detect a
real mismatch, including the exact case #974 reported -- two
report-linked conductors drawn in different colours -- and could just
as easily fire on conductors that matched in every way that mattered.
Comparing each relevant field (text/num, function, tension protocol,
colour, line style) on its own fixes both.
@param element_a @param element_b the two elements about to be (or
already) linked; order does not matter
@return true if the dialog would (or does) open
*/
bool LinkElementCommand::reportLinkNeedsPotentialChoice(Element *element_a, Element *element_b)
{
if (!element_a || !element_b) return false;
if (element_a->conductors().isEmpty() || element_b->conductors().isEmpty()) return false;
QSet<Conductor *> c_list;
for (Element *e : {element_a, element_b})
{
if (e->conductors().isEmpty()) continue;
c_list << e->conductors().first();
c_list += e->conductors().first()->relatedPotentialConductors();
}
if (c_list.size() < 2) return false;
QStringList str_txt, str_funct, str_tens, str_color, str_style;
for (const Conductor *c : std::as_const(c_list))
{
str_txt << c->properties().text;
str_funct << c->properties().m_function;
str_tens << c->properties().m_tension_protocol;
str_color << c->properties().color.name();
str_style << QString::number(int(c->properties().style));
}
return !QET::eachStrIsEqual(str_txt) || !QET::eachStrIsEqual(str_funct)
|| !QET::eachStrIsEqual(str_tens) || !QET::eachStrIsEqual(str_color)
|| !QET::eachStrIsEqual(str_style);
}
/** /**
@brief LinkElementCommand::setUpNewLink @brief LinkElementCommand::setUpNewLink
Update the content of m_link_after with the content of element_list. Update the content of m_link_after with the content of element_list.
+1
View File
@@ -38,6 +38,7 @@ class LinkElementCommand : public QUndoCommand
bool mergeWith(const QUndoCommand *other) override; bool mergeWith(const QUndoCommand *other) override;
static bool isLinkable (Element *element_a, Element *element_b, bool already_linked = false); static bool isLinkable (Element *element_a, Element *element_b, bool already_linked = false);
static bool reportLinkNeedsPotentialChoice(Element *element_a, Element *element_b);
void setLink (const QList<Element *>& element_list); void setLink (const QList<Element *>& element_list);
void setLink (Element *element_); void setLink (Element *element_);
+1 -1
View File
@@ -60,7 +60,7 @@ m_diagram(diagram)
parts << QObject::tr("%n texte(s)", "", 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()); parts << QObject::tr("%n groupe(s) de textes", "", groups_list.count());
setText(QObject::tr("Pivoter %1").arg(QLocale().createSeparatedList(parts))); setText(QObject::tr("Pivoter %1").arg(QLocale(QETApp::interfaceLanguage()).createSeparatedList(parts)));
for(DiagramTextItem *dti : texts_list) for(DiagramTextItem *dti : texts_list)
setupAnimation(dti, "rotation", dti->rotation(), m_rotation); setupAnimation(dti, "rotation", dti->rotation(), m_rotation);
+3 -4
View File
@@ -141,7 +141,7 @@ add_executable(
${QET_DIR}/sources/shortcutmanager.cpp) ${QET_DIR}/sources/shortcutmanager.cpp)
add_test(NAME tst_qetstrings COMMAND tst_qetstrings) add_test(NAME tst_qetstrings COMMAND tst_qetstrings)
target_include_directories(tst_qetstrings PRIVATE ${QET_DIR}/sources) target_include_directories(tst_qetstrings PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_qetstrings PRIVATE Qt::Test Qt::Widgets Qt::Xml) target_link_libraries(tst_qetstrings PRIVATE Qt::Test Qt::Widgets Qt::Xml pugixml::pugixml)
# CrashHandler::formatInt() -- the async-signal-safe decimal formatter the # CrashHandler::formatInt() -- the async-signal-safe decimal formatter the
# signal handler uses for the "Signal: N" line of a crash dump. Compiles # signal handler uses for the "Signal: N" line of a crash dump. Compiles
@@ -179,11 +179,10 @@ add_test(NAME tst_crashdumps COMMAND tst_crashdumps)
target_include_directories(tst_crashdumps PRIVATE target_include_directories(tst_crashdumps PRIVATE
${QET_DIR} ${QET_DIR}
${QET_DIR}/sources ${QET_DIR}/sources
${QET_DIR}/sources/NameList ${QET_DIR}/sources/NameList)
${QET_DIR}/pugixml/src)
if(Backtrace_FOUND) if(Backtrace_FOUND)
target_link_libraries(tst_crashdumps PRIVATE target_link_libraries(tst_crashdumps PRIVATE
Qt::Test Qt::Widgets Qt::Xml ${Backtrace_LIBRARIES}) Qt::Test Qt::Widgets Qt::Xml pugixml::pugixml ${Backtrace_LIBRARIES})
else() else()
target_link_libraries(tst_crashdumps PRIVATE Qt::Test Qt::Widgets Qt::Xml) target_link_libraries(tst_crashdumps PRIVATE Qt::Test Qt::Widgets Qt::Xml)
endif() endif()
+3
View File
@@ -1,6 +1,9 @@
#include <QtTest> #include <QtTest>
#include "qet.h" #include "qet.h"
#include "qetapp.h"
QString QETApp::m_interface_language;
/** /**
QET::joinWithSpaces() / QET::splitWithSpaces() are the wire format for the QET::joinWithSpaces() / QET::splitWithSpaces() are the wire format for the