Compare commits

...

19 Commits

Author SHA1 Message Date
Laurent Trinques 451f8c3296 Merge pull request #892 from jp2images/fix-terminal-destructor-snapshot
Delete a terminal's conductors from a snapshot of its conductor list
2026-09-16 11:49:40 +02:00
Laurent Trinques 43a1f6a1a9 Merge pull request #856 from Kellermorph/fix-copy-page
Fix text position shift when duplicating diagram pages
2026-09-16 11:44:18 +02:00
Jeff Patterson edf483d88f Delete a terminal's conductors from a snapshot of its conductor list
Terminal::~Terminal() called qDeleteAll(m_conductors_list) on the live
member. Each Conductor destructor calls removeConductor() on both of its
terminals, and that removes the conductor from the same list qDeleteAll
is iterating. Mutating a QList while iterating it is undefined
behaviour; with two or more conductors on one terminal (terminal strips,
bridged terminals) it can skip a delete or delete one conductor twice,
which leaves another conductor's terminal1/terminal2 pointing at freed
memory.

The pattern dates from a00404bc9 (2021), which replaced a foreach loop
(iterating an implicit copy) with a direct qDeleteAll. It went unnoticed
until the deterministic sort keys added to Diagram::toXml() in #844
started reading pos() on both terminals of every conductor on every
save, including the periodic backup, which turned the stale pointer into
an EXC_BAD_ACCESS in QGraphicsItem::pos() while deleting an element.

Copy the list first and delete from the copy, restoring the pre-2021
behaviour. An isolated regression test (a hub terminal with 2 to 8
conductors, under AddressSanitizer) did not trigger the failure with the
old code, so none is included; the crash analysis and that attempt are
recorded in jp2images/qelectrotech-source-mirror#1.
2026-09-16 04:27:27 -05:00
ispyisail 2eed3acd3d Merge pull request #723 from IBSYSLevi/feature/cabinet-layout
Added width/height/depth properties to elements
2026-09-16 18:51:34 +12:00
Laurent Trinques d3424db23d Merge pull request #883 from bhangart/persist-diagram-uuid
Persist the folio uuid, derived deterministically for legacy folios
2026-09-16 05:58:19 +02:00
Laurent Trinques 0d722b6033 Merge pull request #888 from ispyisail/fix/879-remember-conductor-color
Remember the F2 conductor color for the rest of the session
2026-09-16 05:52:29 +02:00
Laurent Trinques 96fac96b89 Merge pull request #884 from bhangart/persist-project-uuid
Persist the project uuid, derived from the file content for legacy files
2026-09-16 05:47:12 +02:00
ispyisail 1ec4f56a50 Remember the F2 conductor color for the rest of the session (#879)
The F2 color editor recolors one conductor, but the next one drawn
falls straight back to defaultConductorProperties -- the choice made
via F2 is lost the moment you place another wire, and lost again on
restart. LastUsedStyle already solves the same problem for shapes
(pen/brush) and free text (font), session-scoped and deliberately not
QSettings-backed; this extends it with a conductor color, following
the identical has/get/set shape.

F2's handler records the color after pushing its undo command.
Conductor's constructor -- the one place a new conductor's properties
are set from defaultConductorProperties -- overrides just the color
field when a session color has been recorded, leaving every other
default (style, thickness, text) alone.

Verified: build clean, ctest 6/6. Could not get a reliable headless
GUI trace of "F2 one wire, draw a new one, see it inherit the color"
-- drag-and-drop element placement under Xvfb was unreliable in this
environment (one attempt did nothing, another drew an unintended long
conductor undo didn't fully clear). The code path is otherwise
identical to the already-shipped shape/text mechanism this mirrors.

Refs #461.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 15:40:41 +12:00
Laurent Trinques 839324f231 Merge pull request #887 from ispyisail/fix/885-summary-custom-sql-mode
Fix bugtracker #885: preserve custom summary-table SQL on accept
2026-09-16 05:37:43 +02:00
ispyisail a756bc0722 Fix bugtracker #885: preserve custom summary-table SQL on accept
setQuery() restored the loaded SQL text into the line edit but never
restored the "Edit SQL query" checkbox, so a custom query (a join, a
subquery, a view other than project_summary_view) displayed correctly
while the widget stayed in built-in mode. queryStr() only consults that
checkbox, so accepting the dialog without touching anything silently
replaced the custom query with a freshly generated one.

Detect it instead of trusting a flag that was never set: after parsing
columns from the loaded query, rebuild the query those columns would
produce and compare it against what was loaded. A mismatch means the
widget cannot reconstruct it, so it must be user-written -- check the
box and keep the literal text.

Verified against both cases this has to get right, not just the one
in the report: a genuinely custom query (join) now round-trips through
an untouched accept+save byte-for-byte, and a plain built-in query
written before the #238 pos-ordering fix (examples/industrial.qet, no
ORDER BY clause) is classified as custom rather than silently gaining
an ORDER BY it didn't have -- it round-trips unchanged rather than
being corrupted, though its column picker is now disabled until a user
rebuilds it by hand.

Based on the patch attached to #885.
2026-09-16 13:09:48 +12:00
Beat Hangartner 2269a4641b Persist the project uuid, derived from the file content for legacy files
QETProject::m_uuid was created in the constructor and never written, so
a project got a new uuid every time it was opened. Inside a running
instance it is only used to name the SQLite connection, but nothing
outside the instance could tell which project a file belongs to.

Motivation

A .qet file is increasingly handled by tools outside QElectroTech: Git
repositories on GitHub or GitLab, cloud storage, key-value stores,
per-project locks. All of them need a stable key for "this project":

- The file name and path are not stable: files get renamed, moved,
  checked out in different places.
- The project title is user-editable and not unique.
- Folio uuids (persisted separately) are only unique within their
  project; keying folios globally needs a project identifier as well,
  e.g. projects/{projectUuid}/folios/{folioUuid}.

Change

Write the uuid as an attribute of <project> and restore it in
QETProject::openFile(), right after parsing and before the project is
built from the XML. Older versions ignore the attribute, so files stay
readable in both directions.

The project database is not affected: it takes its connection name
from the uuid created at construction (m_uuid is declared before
m_data_base), before the file is read. Two open projects carrying the
same persisted uuid therefore still get distinct connections.

Files without a uuid: why not a random one

Keeping the random uuid created by the constructor and saving it
conflicts with #754 / #779: saving an unmodified project must give the
same bytes every time. Every example project predates the attribute.
Measured on the 24 example projects (resaved 3x each from the same
original, QT_HASH_SEED=0 so that QDom's attribute order is stable,
isolated HOME per run):

  upstream master              23/24 byte-identical
  persist, random uuid          0/24
  persist, derived uuid (this) 23/24

The remaining project, schema_indus.qet, differs only in element uuids,
the known residual #779 leaves for elements; its project uuid is
stable.

Instead, a project file without a uuid gets a name-based (version 5)
uuid derived from the raw content of the file:

  QUuid::createUuidV5(<fixed QET project namespace>,
                      "qet-project-legacy\n" + file content without CR)

- The same file always yields the same uuid, so resaving an unmodified
  legacy project stays reproducible.
- Different projects practically never share a uuid, because any
  difference in content gives a different one. This is unlike folios,
  where only data such as title and position could be used; the raw
  file bytes are stable input for the whole project.
- Carriage returns are dropped before hashing. QFile's Text mode already
  strips them on Windows but not elsewhere, and git's autocrlf can
  change them on checkout; either way the uuid is the same on every
  platform.
- The uuid is derived once, at load time, and saved from then on. After
  that it is read, never recomputed: renaming the project, editing it
  or changing it in the same session as the migration does not change
  it.
- Two people opening the same legacy file on different branches get the
  same project uuid.

The namespace uuid is fixed in the code and must never change, or every
legacy project would get a different uuid.

Known limitations, open for discussion

- Copies share the uuid. Two byte-identical legacy files get the same
  uuid (examples/cablage-eclairages_sikli-v5.qet and
  câblage-éclairages-sikli-v5.qet are such a pair), and so does a
  migrated file copied in the file manager or saved with "Save as".
  That is what identity means for a copy, and the same happens with Git,
  but a tool that treats the uuid as globally unique has to cope with
  it. Regenerating the uuid on "Save as" could be a follow-up, if that
  is the preferred behaviour.
- A legacy file that differs from another only in formatting (e.g.
  re-indented) gets a different uuid. The two sides of a merge only
  agree if they started from the same bytes, which is the normal case.

Tests (Qt 6.4, offscreen, qelectrotech --resave / --set-titleblock /
--info)

- 24 example projects, 3 resaves each from the same original: results
  above; the project uuid is identical across runs. All 24 uuids are
  distinct, except the byte-identical pair mentioned above.
- Resaving an already migrated file is byte-identical to the first
  output.
- The same legacy file with CRLF line endings gets the same uuid as
  with LF.
- Changing the project title in a migrated file keeps its uuid.
- Migrating and modifying in the same run (--set-titleblock on a legacy
  file) gives the same uuid as a plain resave.
- Re-indenting a legacy file gives a different uuid (expected).
- A migrated file opened with upstream master loads normally; the
  attribute is ignored and dropped on save.
- --info on a migrated file still works.

Refs #754, #779

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDyt4txaott5JyPNGQaeVp
2026-09-15 23:34:24 +00:00
Beat Hangartner 7cd04f2a14 Persist the folio uuid, derived deterministically for legacy folios
Diagram::m_uuid was created in the constructor and never written, so a
folio got a new uuid on every load. Inside a running instance that is
enough (the project database keys on it), but nothing outside it could
tell which folio is which: in the file, folios were only identified by
their position.

Motivation

More and more .qet projects live in version control -- a Git repository
on GitHub or GitLab, reviewed through pull requests, sometimes edited
by several people -- or are synchronised through a cloud or key-value
store. A .qet file is plain XML, so in principle it can be diffed,
merged and split up, but only if the same folio can be recognised in
two versions of the file. Today it cannot:

- Inserting, deleting or reordering a folio shifts every following
  <diagram> element. A line-based diff, and GitHub's review view, then
  pair up unrelated folios and show far more change than was made.
- A three-way merge of two branches that both touched the project has
  no way to match "folio 3" on one side with "folio 3" on the other if
  either side reordered folios.
- Any tool that wants to say "folio X changed in this commit", keep
  per-folio history, lock a single folio, or store folios as separate
  objects has nothing stable to key on. The title and the folio number
  are user-editable and not unique.

Element uuids are already persisted and used for cross-folio links, so
the file format already relies on uuids for identity; the folio itself
was the missing piece. A stable folio uuid is the prerequisite for
later work towards better version control support: per-folio diffs and
locks (check-out / check-in), and possibly storing a project as a
directory with one file per folio.

Change

Write the uuid as an attribute of <diagram> when the whole content is
saved, and restore it first thing when the project is loaded, before any
item is created. Older versions ignore the attribute, so files stay
readable in both directions.

Folios without a uuid: why not a random one

The obvious migration -- keep the random uuid created by the
constructor and save it -- conflicts with #754 / #779: saving an
unmodified project must give the same bytes every time. Every example
project predates the attribute, so each load would invent different
uuids and write them out. Measured on the 24 example projects (resaved
3-4x each from the same original, QT_HASH_SEED=0 so that QDom's
attribute order is stable, isolated HOME per run):

  upstream master              23/24 byte-identical
  persist, random uuid          0/24
  persist, derived uuid (this) 23/24

The remaining project, schema_indus.qet, differs only in element uuids,
the known residual #779 leaves for elements; its folio uuid is stable.

This is the same problem #779 solved for conductors by not writing an
invented uuid back at all. That is not an option here: legacy folios
would never get a persistent uuid, which is the whole point of the
change. Instead, a folio without a uuid gets a name-based (version 5)
uuid, derived only from data read from the file:

  QUuid::createUuidV5(<fixed QET folio namespace>,
                      "legacy" + project title
                               + position of the folio in the file
                               + folio title)

- The same input file always yields the same uuids, so resaving an
  unmodified legacy project stays reproducible.
- The uuid is derived once, at load time, and saved from then on. After
  that it is read, never recomputed: renaming, reordering or editing
  the folio later does not change it. Renaming in the same session as
  the migration does not change it either, since it was derived from
  the title as loaded.
- Two people opening the same legacy file on different branches get
  the same uuid for each folio, even if one of them reorders or renames
  folios before saving. With random uuids the two branches would
  disagree about every folio and a later merge could not match them.
- The folio content is deliberately not part of the name: QDom keeps
  attributes in a hash whose iteration order changes between runs, so
  hashing the content would need a canonical form for no real gain.

Folios are only guaranteed unique within their project. Two unrelated
legacy projects with the same title and the same first folio title get
the same uuid for that folio; anything keying folios globally has to
combine the folio uuid with a project identifier. (The project uuid is
not persisted yet; that is a separate change.)

Duplicated uuids

A hand-edited or merged file can contain the same uuid twice, e.g. a
folio copied by duplicating its XML block. Since the uuid is used as a
key, the second folio gets a derived uuid as well ("duplicate" + the
clashing uuid + the same inputs as above), so this case is
reproducible too. Should a derived uuid ever be taken already, which
takes a hand-crafted file, the name is salted with a counter until it
is free.

The namespace uuid is fixed in the code and must never change, or every
legacy folio would get a different uuid.

Tests (Qt 6.4, offscreen, qelectrotech --resave / --set-titleblock)

- 24 example projects, 3-4 resaves each from the same original: results
  above; all folio uuids identical across runs, no duplicates within
  any project.
- Resaving an already migrated file is byte-identical to the first
  output.
- Renaming a folio in a migrated file keeps its uuid.
- Swapping two <diagram> blocks in a migrated file: each uuid moves
  with its folio.
- Migrating and renaming in the same run (--set-titleblock title=...
  on a legacy file) gives the same uuids as a plain resave.
- A file with a duplicated uuid: the second folio gets a new uuid, the
  same one on every run.
- A migrated file opened with upstream master loads normally; the
  attribute is ignored and dropped on save.

Refs #754, #779

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BDyt4txaott5JyPNGQaeVp
2026-09-15 22:09:10 +00:00
Kellermorph a5fe544415 Fix text position shift when duplicating diagram pages
duplicateDiagram() called restoreText() on every newly loaded
element.  Each setPlainText() inside restoreText() is wrapped in
m_block_alignment except the last one, so finishAlignment() ran on
elements whose positions came straight from the XML — shifting
center and right-aligned texts.

Fix: use toXml(true, true) which handles correctTextPos/restoreText
internally for Slave and Report elements only, and call restoreText()
on the target for those same element types to recalculate their text
positions for the actual resolved text.
2026-09-14 20:38:15 +02:00
Levi Jetzer 02e6eceb65 Reject trailing-dot input like "12." in numeric elementInformation fields
numericInfoPattern()'s integer branch no longer allows an optional
trailing ".", so "12." is now Intermediate rather than Acceptable --
already-built hasAcceptableInput() guard then keeps it from being
stored, same as a lone ".". Previously it validated fine and got
saved verbatim, silently freezing in that form since nothing ever
normalized it afterward.
2026-08-21 15:22:41 +02:00
Levi Jetzer 7c941e4697 Guard numeric elementInformation fields against "." and normalize decimal comma
- ElementInfoWidget::currentInfo() now skips a field whose validator
  hasn't accepted its text (e.g. a lone "." mid-typing), which
  previously stored and later parsed to 0.
- New QETInformation::NumericInfoValidator rewrites "," to "." before
  validating, so 80,5 on a German/French keyboard no longer silently
  becomes 805. Used at both existing call sites.
- Restored the header's #1/#2/#3 doc comment (was reflowed into a
  run-on paragraph by a previous edit).
2026-08-21 15:16:36 +02:00
Levi Jetzer 5dbb9f28de Fix numeric field validation gaps found in review
Address feedback on the width/height/depth elementInformation fields:

- The regex accepted "." alone as a complete value ([0-9]* permits
  zero digits on both sides), meaning a field could be committed with
  a literal "." saved to the XML. Require at least one digit either
  before or after the separator.
- The same pattern was duplicated verbatim in elementinfopartwidget.cpp
  and elementpropertieseditorwidget.cpp's EditorDelegate. Factored into
  QETInformation::numericInfoPattern(), a single shared definition both
  call sites now use.
- Tightened the pattern to at most 2 decimal places (down from 4) to
  match the precision actually meaningful for these fields.

Not changed, by design:
- Storage stays a plain string, consistent with every other numeric
  elementInformation field (quantity etc.) in this codebase -- values
  round-trip through XML text regardless, so a long/micron
  representation wouldn't avoid the string<->number conversion, only
  relocate it.
- No decimal-comma normalization needed: with the fixed pattern, only
  digits and "." are ever accepted at the keystroke level, so an
  alternate separator can't enter the field in the first place.
2026-08-12 08:43:42 +02:00
Levi Jetzer c6e7c5d371 Fixed issue of taking wrong variable (key instead of key_) in elementinfopartwidget.cpp for regex checker and helping info (tooltip, placeholder) 2026-08-11 18:28:29 +02:00
Levi Jetzer 7347322221 Replace QDoubleValidator with QRegularExpressionValidator on width/height/depth fields
The selection properties dock (elementinfopartwidget.cpp) and the
element editor's information tree (elementpropertieseditorwidget.cpp's
EditorDelegate) both restrict the width/height/depth fields added for
cabinet layout support to numeric input via a validator.

QDoubleValidator follows the system/UI locale for its decimal
separator, which meant a comma was accepted as an intermediate state
and left the field impossible to leave in some contexts, even though
it was never a valid final value.

Switch both to a QRegularExpressionValidator matching
^[0-9]*\.?[0-9]{0,4}$: "." is a literal character in the pattern, not
locale-dependent, and [0-9] (rather than \d) excludes non-ASCII
digits. This guarantees a value entered this way can always be read
back with QString::toDouble() without locale handling. The check for
which keys are numeric (QETInformation::isNumericInfoKey) is shared
between both call sites; the validator setup itself stays local to
each, since QETInformation intentionally has no Qt Widgets dependency.

Also adds a placeholder ("ex. 80.5") and tooltip explaining the
expected format.
2026-08-11 18:01:10 +02:00
Levi Jetzer c23999acd1 Added width/height/depth properties to element definitions
Adds three new elementInformation keys — width, height, depth (in mm)
— alongside the existing manufacturer/manufacturer_reference fields.
These describe the physical dimensions of the device a symbol
represents, set once per element definition.

Values are restricted to plain decimal numbers via a QDoubleValidator
on the information tree's item delegate (fixed-point, "." as decimal
separator via QLocale::c(), independent of the UI language), so a
later consumer can always parse them with toDouble() without
additional sanitization.
2026-08-11 16:09:34 +02:00
17 changed files with 354 additions and 39 deletions
@@ -109,6 +109,7 @@ void SummaryQueryWidget::setQuery(const QString &query)
if (query.startsWith("SELECT"))
{
reset();
ui->m_edit_sql_query_cb->setChecked(false);
ui->m_user_query_le->setText(query);
QString select = query;
@@ -129,6 +130,17 @@ void SummaryQueryWidget::setQuery(const QString &query)
}
}
}
//If the query this widget would build from the columns just
//parsed above does not match the query as loaded byte-for-byte,
//the user wrote it by hand (a join, a subquery, a different
//view) and accepting the dialog unmodified must not silently
//replace it with a generated one (bugtracker #885).
const bool custom_query = query != queryStr();
m_custom_query = custom_query ? query : QString();
ui->m_edit_sql_query_cb->setChecked(custom_query);
ui->m_user_query_le->setEnabled(custom_query);
ui->m_info_widget->setDisabled(custom_query);
}
}
+93
View File
@@ -691,6 +691,79 @@ QUuid Diagram::uuid()
return m_uuid;
}
/**
@brief Diagram::uuidUsedByOtherDiagram
A hand-edited or merged project file can contain two folios with the same
uuid. The uuid is used as a key (e.g. in the project database), so the
second one must get another one.
@param uuid
@return true if another diagram of the parent project already uses @p uuid
*/
bool Diagram::uuidUsedByOtherDiagram(const QUuid &uuid) const
{
if (!m_project) {
return false;
}
const auto diagrams = m_project->diagrams();
for (const Diagram *diagram : diagrams) {
if (diagram != this && diagram->m_uuid == uuid) {
return true;
}
}
return false;
}
/**
@brief Diagram::derivedUuid
Name-based (version 5) uuid for a folio that has no usable uuid in the
file it is loaded from : either the file predates the uuid attribute, or
the uuid it carries is already taken by another folio of the project.
A random uuid would do as an identity, but it would make saving an
unmodified legacy project non-reproducible : every load would invent a
different one and write it out (see #754). The name is therefore built
only from data read from the file itself -- the project title, the
position of the folio in the file and its title -- so the same input file
always yields the same uuid. It is computed once, when the folio is
loaded, and saved from then on : renaming or moving the folio later does
not change it.
@param root : the <diagram> element being loaded
@param reason : distinguishes the two cases above, so that they can not
produce the same name
@return a uuid not used by any other diagram of the project
*/
QUuid Diagram::derivedUuid(const QDomElement &root, const QString &reason) const
{
//Fixed namespace for QElectroTech folio uuids, never change it :
//doing so would change the uuid given to every legacy folio.
static const QUuid folio_namespace(
QStringLiteral("{d5951240-154d-44d6-8277-0092a31d1920}"));
const int index = m_project
? m_project->diagrams().indexOf(const_cast<Diagram *>(this))
: -1;
const QString project_title = root.ownerDocument()
.documentElement()
.attribute(QStringLiteral("title"));
const QString base = QStringLiteral("qet-folio\n%1\n%2\n%3\n%4")
.arg(reason,
project_title,
QString::number(index),
root.attribute(QStringLiteral("title")));
//A clash is only possible with a hand-crafted file, but the uuid is a
//key : salt the name until it is free. Still deterministic.
QUuid uuid = QUuid::createUuidV5(folio_namespace, base);
for (int salt = 1 ; uuidUsedByOtherDiagram(uuid) ; ++salt) {
uuid = QUuid::createUuidV5(folio_namespace,
base + QStringLiteral("\n")
+ QString::number(salt));
}
return uuid;
}
/**
@brief Diagram::setEventInterface
Set event_interface has current interface.
@@ -906,6 +979,11 @@ QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) {
// schema properties
// proprietes du schema
if (whole_content) {
//Persist the folio identity, so that a folio keeps the same uuid
//across save/load. Without it every load invents a new one, and
//nothing outside the running instance (version control, a lock,
//a diff tool...) can tell which folio is which.
dom_root.setAttribute(QStringLiteral("uuid"), m_uuid.toString());
border_and_titleblock.titleBlockToXml(dom_root);
border_and_titleblock.borderToXml(dom_root);
@@ -1406,6 +1484,21 @@ bool Diagram::fromXml(QDomElement &document,
// Read attributes of this diagram
if (consider_informations)
{
// Restore the persisted folio uuid. Done first, before any item is
// loaded, so that everything created below sees the final uuid.
// A folio without a usable one (file written before the uuid was
// persisted, or uuid already taken by another folio) gets a
// deterministic one instead, see derivedUuid().
const QUuid persisted_uuid(root.attribute(QStringLiteral("uuid")));
if (persisted_uuid.isNull()) {
m_uuid = derivedUuid(root, QStringLiteral("legacy"));
} else if (uuidUsedByOtherDiagram(persisted_uuid)) {
m_uuid = derivedUuid(root, QStringLiteral("duplicate ")
+ persisted_uuid.toString());
} else {
m_uuid = persisted_uuid;
}
// Load border and titleblock
border_and_titleblock.titleBlockFromXml(root);
border_and_titleblock.borderFromXml(root);
+3
View File
@@ -136,6 +136,9 @@ class Diagram : public QGraphicsScene
bool m_freeze_new_elements;
bool m_freeze_new_conductors_;
QUuid m_uuid = QUuid::createUuid();
bool uuidUsedByOtherDiagram(const QUuid &uuid) const;
QUuid derivedUuid(const QDomElement &root, const QString &reason) const;
// METHODS
protected:
+5
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "diagramview.h"
#include "lastusedstyle.h"
#include "qetproject.h"
#include "QPropertyUndoCommand/qpropertyundocommand.h"
#include "diagramcommands.h"
@@ -143,6 +144,10 @@ DiagramView::DiagramView(Diagram *diagram, QWidget *parent) :
QPropertyUndoCommand *undo = new QPropertyUndoCommand(edited_conductor, "properties", old_value, new_value);
undo->setText(tr("Modifier les propriétés d'un conducteur", "undo caption"));
m_diagram->undoStack().push(undo);
// remember it for the next conductor drawn this session,
// the way LastUsedStyle already does for shapes (#879)
LastUsedStyle::setConductorColor(new_color);
}
}
});
@@ -20,7 +20,6 @@
#include "../../qetapp.h"
#include "../../qetinformation.h"
#include "ui_elementpropertieseditorwidget.h"
#include "../../qetinformation.h"
#include <QItemDelegate>
#include <QComboBox>
@@ -45,6 +44,7 @@
#include <QSplitter>
#include <QShortcut>
#include <QMenu>
#include <QRegularExpressionValidator>
/**
@brief The EditorDelegate class
@@ -64,9 +64,20 @@ class EditorDelegate : public QItemDelegate
{
if(index.column() == 1)
{
return QItemDelegate::createEditor(parent,
option,
index);
const QString key = index.sibling(index.row(), 0)
.data(Qt::UserRole).toString();
if (key == QETInformation::ELMT_WIDTH || key == QETInformation::ELMT_HEIGHT || key == QETInformation::ELMT_DEPTH)
{
auto *line_edit = new QLineEdit(parent);
auto *validator = new QETInformation::NumericInfoValidator(line_edit);
line_edit->setValidator(validator);
line_edit->setPlaceholderText(tr("ex. 80.5"));
line_edit->setToolTip(tr("Nombre décimal avec un point comme séparateur (ex. 80.5)"));
return line_edit;
}
return QItemDelegate::createEditor(parent, option, index);
}
return nullptr;
}
+18 -14
View File
@@ -658,21 +658,14 @@ void ElementsPanelWidget::duplicateDiagram()
BorderProperties bp = source_diagram->border_and_titleblock.exportBorder();
new_diagram->border_and_titleblock.importBorder(bp);
for (QGraphicsItem *item : source_diagram->items()) {
if (Element *elmt = dynamic_cast<Element *>(item)) {
source_diagram->correctTextPos(elmt);
}
}
QDomDocument doc = source_diagram->toXml();
// Serialize the whole diagram with is_copy_command=true.
// This is the same mechanism as Ctrl+C: toXml(true, true)
// internally calls correctTextPos/restoreText for Slave and
// Report elements only — producing correct text positions
// in the XML. No manual correctTextPos/restoreText needed.
QDomDocument doc = source_diagram->toXml(true, true);
QDomElement diagram_elmt = doc.documentElement();
for (QGraphicsItem *item : source_diagram->items()) {
if (Element *elmt = dynamic_cast<Element *>(item)) {
source_diagram->restoreText(elmt);
}
}
new_diagram->fromXml(diagram_elmt, QPointF(0, 0), false, nullptr);
for (QGraphicsItem *item : new_diagram->items()) {
@@ -683,7 +676,18 @@ void ElementsPanelWidget::duplicateDiagram()
// of the project database, so duplicates fail to insert and
// silently vanish from nomenclature/summary tables.
elmt->newUuid();
new_diagram->restoreText(elmt);
// toXml(true, true) applied correctTextPos to Slave and
// Report elements, which shifted their text positions to
// match the stripped composite text. restoreText()
// recalculates the position for the actual resolved text.
// Only Slave and Report need this — other element types
// were not affected by correctTextPos.
if (elmt->linkType() == Element::Slave ||
elmt->linkType() & Element::AllReport)
{
new_diagram->restoreText(elmt);
}
}
else if (Conductor *cond = dynamic_cast<Conductor *>(item)) {
// Same reasoning for conductors: conductor.uuid is the PRIMARY
+29
View File
@@ -23,6 +23,8 @@ QBrush LastUsedStyle::m_shape_brush;
bool LastUsedStyle::m_has_shape_brush = false;
QFont LastUsedStyle::m_text_font;
bool LastUsedStyle::m_has_text_font = false;
QColor LastUsedStyle::m_conductor_color;
bool LastUsedStyle::m_has_conductor_color = false;
/**
@return true if a shape pen was set this session
@@ -104,3 +106,30 @@ void LastUsedStyle::setTextFont(const QFont &font)
m_text_font = font;
m_has_text_font = true;
}
/**
@return true if a conductor color was set this session
*/
bool LastUsedStyle::hasConductorColor()
{
return m_has_conductor_color;
}
/**
@return the last color applied to a conductor this session
*/
QColor LastUsedStyle::conductorColor()
{
return m_conductor_color;
}
/**
@brief LastUsedStyle::setConductorColor
Record @a color as the last-used conductor color for this session
@param color
*/
void LastUsedStyle::setConductorColor(const QColor &color)
{
m_conductor_color = color;
m_has_conductor_color = true;
}
+14 -5
View File
@@ -19,20 +19,23 @@
#define LAST_USED_STYLE_H
#include <QBrush>
#include <QColor>
#include <QFont>
#include <QPen>
/**
@brief The LastUsedStyle class
Session-scoped "last used" style for new shapes and free text created
on the diagram canvas: whatever pen/brush/font was last applied through
the properties editors becomes the starting point for the next new
item of that type, the way most drawing tools behave.
Session-scoped "last used" style for new shapes, free text and
conductors created on the diagram canvas: whatever pen/brush/font/color
was last applied through the properties editors becomes the starting
point for the next new item of that type, the way most drawing tools
behave.
Deliberately in-memory only, not QSettings-backed: this is a live
"what did I just use" value for the current editing session, not an
app-wide default (that's already covered by the Preferences dialog's
font setting, read as the fallback when nothing has been set yet).
font setting, read as the fallback when nothing has been set yet, and
by the project's default conductor color, read the same way).
*/
class LastUsedStyle
{
@@ -49,6 +52,10 @@ class LastUsedStyle
static QFont textFont();
static void setTextFont(const QFont &font);
static bool hasConductorColor();
static QColor conductorColor();
static void setConductorColor(const QColor &color);
private:
LastUsedStyle() = delete;
@@ -58,6 +65,8 @@ class LastUsedStyle
static bool m_has_shape_brush;
static QFont m_text_font;
static bool m_has_text_font;
static QColor m_conductor_color;
static bool m_has_conductor_color;
};
#endif // LAST_USED_STYLE_H
+12 -5
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "../qetgraphicsitem/conductor.h"
#include "../lastusedstyle.h"
#include "../qetproject.h"
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
#include "../autoNum/numerotationcontextcommands.h"
@@ -127,11 +128,17 @@ Conductor::Conductor(Terminal *p1, Terminal* p2) :
m_text_item = new ConductorTextItem(m_properties.text, this);
connect(m_text_item, &ConductorTextItem::textEdited, this, &Conductor::displayedTextChanged);
//Set the default conductor properties.
if (p1->diagram())
setProperties(p1->diagram()->defaultConductorProperties);
else if (p2->diagram())
setProperties(p2->diagram()->defaultConductorProperties);
//Set the default conductor properties. The color, specifically, is
//overridden by the last one applied via the F2 color editor this
//session (#879), the same way LastUsedStyle already does for shapes.
Diagram *dia = p1->diagram() ? p1->diagram() : p2->diagram();
if (dia)
{
ConductorProperties properties = dia->defaultConductorProperties;
if (LastUsedStyle::hasConductorColor())
properties.color = LastUsedStyle::conductorColor();
setProperties(properties);
}
}
/**
+6 -1
View File
@@ -86,7 +86,12 @@ Terminal::Terminal(TerminalData* data, Element* e) :
* Destruction of the terminal, and also docked conductor
*/
Terminal::~Terminal() {
qDeleteAll(m_conductors_list);
// Each conductor's destructor calls removeConductor() on both its
// terminals, which mutates m_conductors_list while qDeleteAll() is
// still iterating it. Delete from a snapshot so the live list can
// change underneath without affecting the iteration.
const QList<Conductor *> conductors_to_delete = m_conductors_list;
qDeleteAll(conductors_to_delete);
delete d;
}
+49 -1
View File
@@ -158,7 +158,10 @@ QStringList QETInformation::elementInfoKeys()
ELMT_MACHINE_MANUFACTURER_REF,
ELMT_SUPPLIER,
ELMT_QUANTITY,
ELMT_UNITY,
ELMT_UNITY,
ELMT_WIDTH,
ELMT_HEIGHT,
ELMT_DEPTH,
ELMT_AUX1,
ELMT_DESCRIPTION_AUX1,
ELMT_DESIGNATION_AUX1,
@@ -219,6 +222,45 @@ QString QETInformation::elementInfoToVar(const QString &info)
return (QString ("%{void}"));
}
/**
* @brief QETInformation::numericInfoPattern
* @return the pattern used to validate numeric elementInformation
* fields (currently width/height/depth): digits with an optional "."
* as decimal separator, requiring at least one digit overall so a
* lone "." can never be a complete, acceptable value on its own.
*/
QRegularExpression QETInformation::numericInfoPattern()
{
return QRegularExpression(QStringLiteral(R"(^[0-9]+$|^[0-9]*\.[0-9]{1,2}$)"));
}
/**
@brief QETInformation::NumericInfoValidator::NumericInfoValidator
@param parent
*/
QETInformation::NumericInfoValidator::NumericInfoValidator(QObject *parent) :
QRegularExpressionValidator(numericInfoPattern(), parent)
{
}
/**
@brief QETInformation::NumericInfoValidator::validate
Rewrites any "," in @a input to "." in place, then delegates to
the base class for the actual numericInfoPattern() check. @a pos
is left untouched by the rewrite itself -- replacing "," with "."
never changes the string's length, so the cursor position the
caller already tracked stays correct.
@param input the text being validated; may be rewritten
@param pos the cursor position within @a input
@return the resulting validation state
*/
QValidator::State QETInformation::NumericInfoValidator::validate(QString &input, int &pos) const
{
if (input.contains(QLatin1Char(',')))
input.replace(QLatin1Char(','), QLatin1Char('.'));
return QRegularExpressionValidator::validate(input, pos);
}
/**
* @brief QETInformation::infoToVar
* @param info
@@ -279,6 +321,9 @@ QString QETInformation::translatedInfoKey(const QString &info)
else if (info == ELMT_SUPPLIER) return QObject::tr("Fournisseur");
else if (info == ELMT_QUANTITY) return QObject::tr("Quantité");
else if (info == ELMT_UNITY) return QObject::tr("Unité");
else if (info == ELMT_WIDTH) return QObject::tr("Largeur [mm]");
else if (info == ELMT_HEIGHT) return QObject::tr("Hauteur [mm]");
else if (info == ELMT_DEPTH) return QObject::tr("Profondeur [mm]");
else if (info == ELMT_LOCATION) return QObject::tr("Localisation (+)");
else if (info == COND_FUNCTION) return QObject::tr("Fonction");
else if (info == COND_TENSION_PROTOCOL) return QObject::tr("Tension / Protocole");
@@ -351,6 +396,9 @@ QStringList QETInformation::elementEditorElementInfoKeys()
ELMT_SUPPLIER,
ELMT_QUANTITY,
ELMT_UNITY,
ELMT_WIDTH,
ELMT_HEIGHT,
ELMT_DEPTH,
ELMT_AUX1,
ELMT_DESCRIPTION_AUX1,
ELMT_DESIGNATION_AUX1,
+21 -5
View File
@@ -18,17 +18,22 @@
#ifndef QETINFORMATION_H
#define QETINFORMATION_H
#include <QStringList>
#include <QHash>
#include <QRegularExpression>
#include <QRegularExpressionValidator>
#include <QStringList>
/**
* Inside this namespace you will find all information used in QElectrotech for
* element, conductor and diagram.
* Each information have 3 values :
* #1 the info key = the key of an information as a QString used in the code (example : label)
* #2 the info key to variable = the key in form of a variable.
* This is used by the user to replace a variable by the string of this variable (example : %{label})
* #3 the info key translated to the current local (example label in dutch = Betriebsmittelkennzeichen)
* #1 the info key = the key of an information as a QString used in the code
* (example : label)
* #2 the info key to variable = the key in form of a variable. This is used
* by the user to replace a variable by the string of this variable
* (example : %{label})
* #3 the info key translated to the current local (example label in dutch =
* Betriebsmittelkennzeichen)
*/
namespace QETInformation
{
@@ -50,6 +55,9 @@ namespace QETInformation
static QString ELMT_CURRENT_RATING = "current_rating";
static QString ELMT_NOTES = "notes";
static QString ELMT_UNITY = "unity";
static QString ELMT_WIDTH = "width";
static QString ELMT_HEIGHT = "height";
static QString ELMT_DEPTH = "depth";
static QString ELMT_PLANT = "plant";
static QString ELMT_LOCATION = "location";
static QString ELMT_AUX1 = "auxiliary1";
@@ -161,6 +169,14 @@ namespace QETInformation
QStringList elementEditorElementInfoKeys();
QString elementInfoToVar(const QString &info);
QRegularExpression numericInfoPattern();
class NumericInfoValidator : public QRegularExpressionValidator
{
public:
explicit NumericInfoValidator(QObject *parent = nullptr);
State validate(QString &input, int &pos) const override;
};
QStringList terminalElementInfoKeys();
QString infoToVar(const QString &info);
+52 -2
View File
@@ -255,6 +255,38 @@ void QETProject::init()
}
/**
@brief QETProject::derivedUuid
Name-based (version 5) uuid for a project file that has no uuid yet,
because it was written before the uuid was persisted.
A random uuid would do as an identity, but it would make saving an
unmodified legacy project non-reproducible : every load would invent a
different one and write it out (see #754). The uuid is therefore derived
from the content of the file, so the same file always yields the same
uuid, while two different projects practically never share one.
Carriage returns are dropped first, so that a checkout with CRLF line
endings (Windows, git autocrlf) gives the same uuid as one with LF.
It is computed once, when the file is loaded, and saved from then on :
editing, renaming or moving the project later does not change it.
@param content : the raw content of the project file
@return the derived uuid
*/
QUuid QETProject::derivedUuid(const QByteArray &content)
{
//Fixed namespace for QElectroTech project uuids, never change it :
//doing so would change the uuid given to every legacy project.
static const QUuid project_namespace(
QStringLiteral("{c8c75719-0fea-4b1c-9f4b-2dd179fb2f0c}"));
QByteArray normalized(content);
normalized.replace('\r', QByteArray());
return QUuid::createUuidV5(project_namespace,
QByteArrayLiteral("qet-project-legacy\n")
+ normalized);
}
/**
@brief QETProject::openFile
@param file
@@ -274,9 +306,11 @@ QETProject::ProjectState QETProject::openFile(QFile *file)
QFileInfo fi(*file);
setFilePath(fi.absoluteFilePath());
//Extract the content of the xml
//Extract the content of the xml. The raw bytes are kept : a project
//file without a persisted uuid derives its uuid from them.
const QByteArray content = file->readAll();
QDomDocument xml_project;
if (!xml_project.setContent(file))
if (!xml_project.setContent(content))
{
if(opened_here) {
file->close();
@@ -285,6 +319,17 @@ QETProject::ProjectState QETProject::openFile(QFile *file)
}
const qint64 xml_parse_ms = load_timer.elapsed();
//Restore the persisted project uuid before anything else is built
//from the file. The project database already got its connection name
//from the uuid created at construction, it does not depend on this.
const QDomElement root_elmt = xml_project.documentElement();
if (root_elmt.tagName() == QLatin1String("project"))
{
const QUuid persisted_uuid(root_elmt.attribute(QStringLiteral("uuid")));
m_uuid = persisted_uuid.isNull() ? derivedUuid(content)
: persisted_uuid;
}
//Build the project from the xml
readProjectXml(xml_project);
@@ -1038,6 +1083,11 @@ QDomDocument QETProject::toXml()
setTitle(QFileInfo(m_file_path).completeBaseName());
}
project_root.setAttribute("title", project_title_);
//Persist the project identity, so that the project keeps the same
//uuid across save/load. Without it every load invents a new one, and
//nothing outside the running instance (version control, a cloud or
//key-value store, a lock...) can tell which project a file belongs to.
project_root.setAttribute(QStringLiteral("uuid"), m_uuid.toString());
xml_doc.appendChild(project_root);
// titleblock templates, if any
+1
View File
@@ -271,6 +271,7 @@ class QETProject : public QObject
void writeBackup();
void init();
ProjectState openFile(QFile *file);
static QUuid derivedUuid(const QByteArray &content);
void refresh();
// attributes
+19
View File
@@ -18,7 +18,9 @@
#include "elementinfopartwidget.h"
#include "../SearchAndReplace/searchandreplaceworker.h"
#include "../qetinformation.h"
#include "ui_elementinfopartwidget.h"
#include <QRegularExpressionValidator>
#include <utility>
@@ -43,6 +45,14 @@ ElementInfoPartWidget::ElementInfoPartWidget(
ui->label_->setText(translated_key);
ui->m_erase_text->setVisible(false);
if (key_ == QETInformation::ELMT_WIDTH || key_ == QETInformation::ELMT_HEIGHT || key_ == QETInformation::ELMT_DEPTH)
{
auto *validator = new QETInformation::NumericInfoValidator(ui->line_edit);
ui->line_edit->setValidator(validator);
ui->line_edit->setPlaceholderText(tr("ex. 80.5"));
ui->line_edit->setToolTip(tr("Nombre décimal avec un point comme séparateur (ex. 80.5)"));
}
connect(ui->line_edit, &QLineEdit::textEdited,
this, &ElementInfoPartWidget::textEdited);
connect(ui->line_edit, &QLineEdit::textChanged,
@@ -67,6 +77,15 @@ QString ElementInfoPartWidget::text() const
return (ui->line_edit->text());
}
/**
@brief ElementInfoPartWidget::hasAcceptableInput
@return whether the line edit's current text satisfies its validator
*/
bool ElementInfoPartWidget::hasAcceptableInput() const
{
return ui->line_edit->hasAcceptableInput();
}
/**
@brief ElementInfoPartWidget::setText
Set text to line edit
+2 -2
View File
@@ -41,9 +41,9 @@ class ElementInfoPartWidget : public QWidget
QWidget *parent = nullptr);
~ElementInfoPartWidget() override;
QString key () const
{return key_;}
QString key () const {return key_;}
QString text () const;
bool hasAcceptableInput() const;
void setText (const QString &);
void setPlaceHolderText (const QString &text);
void setFocusTolineEdit();
+3
View File
@@ -395,6 +395,9 @@ DiagramContext ElementInfoWidget::currentInfo() const
for (const auto &eipw : std::as_const(m_eipw_list))
{
if (!eipw->hasAcceptableInput())
continue;
//add value only if they're something to store
if (!eipw->text().isEmpty())
{