diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake
index a523a52da..39225e099 100644
--- a/cmake/qet_compilation_vars.cmake
+++ b/cmake/qet_compilation_vars.cmake
@@ -324,6 +324,18 @@ set(QET_SRC_FILES
${QET_DIR}/sources/autoNum/numerotationcontext.h
${QET_DIR}/sources/autoNum/renumberelementscommand.cpp
${QET_DIR}/sources/autoNum/renumberelementscommand.h
+ ${QET_DIR}/sources/autoNum/autonumschemecommand.cpp
+ ${QET_DIR}/sources/autoNum/autonumschemecommand.h
+ ${QET_DIR}/sources/autoNum/elementautonumschemecommand.cpp
+ ${QET_DIR}/sources/autoNum/elementautonumschemecommand.h
+ ${QET_DIR}/sources/autoNum/ui/counterwarning.cpp
+ ${QET_DIR}/sources/autoNum/ui/counterwarning.h
+ ${QET_DIR}/sources/autoNum/ui/pastenumberingimport.cpp
+ ${QET_DIR}/sources/autoNum/ui/pastenumberingimport.h
+ ${QET_DIR}/sources/autoNum/ui/renumberpreviewdialog.cpp
+ ${QET_DIR}/sources/autoNum/ui/renumberpreviewdialog.h
+ ${QET_DIR}/sources/undocommand/freezeelementlabelcommand.cpp
+ ${QET_DIR}/sources/undocommand/freezeelementlabelcommand.h
${QET_DIR}/sources/autoNum/ui/autonumberingdockwidget.cpp
${QET_DIR}/sources/autoNum/ui/autonumberingdockwidget.h
${QET_DIR}/sources/autoNum/ui/autonumberingmanagementw.cpp
diff --git a/misc/qet-mcp/qet_mcp.py b/misc/qet-mcp/qet_mcp.py
index 969e8a501..947a708a6 100755
--- a/misc/qet-mcp/qet_mcp.py
+++ b/misc/qet-mcp/qet_mcp.py
@@ -1444,9 +1444,17 @@ OPS = {
"add_autonum": ("addAutoNum", [("kind", "str"), ("name", "str"),
("parts", "list")]),
"remove_autonum": ("removeAutoNum", [("kind", "str"), ("name", "str")]),
+ "rename_autonum": ("renameAutoNum", [("kind", "str"), ("name", "str"),
+ ("new_name", "str")]),
"use_conductor_autonum": ("useConductorAutoNum", [("folio", "folio"), ("name", "str")]),
"use_element_autonum": ("useElementAutoNum", [("name", "str")]),
"number_element": ("numberElement", [("folio", "folio"), ("element", "elmt")]),
+ "renumber_element_autonum": ("renumberElementAutoNum", [("name", "str")]),
+ "free_element_numbers": ("freeElementNumbers", [("folio", "folio"), ("element", "elmt")]),
+ "assign_element_number": ("assignElementNumber", [("folio", "folio"), ("element", "elmt"),
+ ("number", "num")]),
+ "assign_element_autonum": ("assignElementAutoNum", [("name", "str"), ("folio", "folio"),
+ ("element", "elmt"), ("overwrite", "bool")]),
# The text fields drawn on a symbol. Indexed within the element's own
# list, which follows its definition and shifts on delete (and undo of a
# delete puts the field back at the end).
@@ -1959,7 +1967,7 @@ def _build_script(operations: list, output: str) -> str:
raise ValueError(f"operation {i}: unknown folio property "
f"{op.get('property')!r}; expected one of "
f"{', '.join(FOLIO_PROPERTIES)}")
- if name in ("add_autonum", "remove_autonum") and op.get("kind") not in AUTONUM_KINDS:
+ if name in ("add_autonum", "remove_autonum", "rename_autonum") and op.get("kind") not in AUTONUM_KINDS:
raise ValueError(f"operation {i}: unknown kind {op.get('kind')!r}; "
f"expected one of {', '.join(AUTONUM_KINDS)}")
if name == "search_and_replace":
@@ -4500,6 +4508,18 @@ TOOLS = [
"use_conductor_autonum then makes new conductors on a folio "
"take their number from it, so define and select it BEFORE the "
"add_conductor ops it should number. For elements, "
+ "renumber_element_autonum numbers an element context's elements again "
+ "(an element with a frozen label keeps its label, and nobody else gets it); "
+ "free_element_numbers lists the numbers an element of an element context may "
+ "be given by hand, assign_element_number gives it one of them "
+ "(it keeps following the context, its counter moves past the number); "
+ "assign_element_autonum makes one element follow a named element context "
+ "(refused when it holds a formula, frozen label or follows "
+ "another one, unless overwrite is true); "
+ "rename_autonum renames a context of any kind, what follows it (elements, or "
+ "the folios of a conductor or folio context) keeps following it; "
+ "remove_autonum refuses a context which elements (element kind) or "
+ "folios (conductor and folio kinds) still follow. "
"use_element_autonum selects the context and number_element applies "
"it to one element AFTER it is placed (add_element does not number "
"what it places); slaves and reports are refused, since they take "
diff --git a/misc/qet-mcp/test_qet_mcp.py b/misc/qet-mcp/test_qet_mcp.py
index f20351f7f..e7cef7c75 100644
--- a/misc/qet-mcp/test_qet_mcp.py
+++ b/misc/qet-mcp/test_qet_mcp.py
@@ -441,6 +441,7 @@ class EditValidation(unittest.TestCase):
"sort_terminal_strip": [{"op": "sort_terminal_strip", "strip": 0}],
"add_autonum": [{"op": "add_autonum", "kind": "conductor", "name": "W", "parts": ["string:W"]}],
"remove_autonum": [{"op": "remove_autonum", "kind": "conductor", "name": "W"}],
+ "rename_autonum": [{"op": "rename_autonum", "kind": "element", "name": "EL", "new_name": "EL2"}],
"use_conductor_autonum": [f, {"op": "use_conductor_autonum", "folio": "$f", "name": "W"}],
"use_element_autonum": [{"op": "use_element_autonum", "name": "EL"}],
"insert_folio": [{"op": "insert_folio", "id": "i", "position": 0}],
@@ -458,6 +459,12 @@ class EditValidation(unittest.TestCase):
"duplicate_elements": el + [{"op": "duplicate_elements", "id": "d", "folio": "$f",
"elements": ["$e"], "to_folio": "$f", "x": 50, "y": 50}],
"number_element": el + [{"op": "number_element", "folio": "$f", "element": "$e"}],
+ "renumber_element_autonum": [{"op": "renumber_element_autonum", "name": "EL"}],
+ "free_element_numbers": el + [{"op": "free_element_numbers", "folio": "$f", "element": "$e"}],
+ "assign_element_number": el + [{"op": "assign_element_number", "folio": "$f",
+ "element": "$e", "number": 3}],
+ "assign_element_autonum": el + [{"op": "assign_element_autonum", "name": "EL", "folio": "$f",
+ "element": "$e", "overwrite": False}],
"add_table": [f, {"op": "add_table", "id": "t", "folio": "$f", "kind": "nomenclature",
"name": "BOM", "query": "SELECT label FROM element_nomenclature_view"}],
"set_table_position": [f, {"op": "add_table", "id": "t", "folio": "$f", "kind": "nomenclature",
diff --git a/sources/autoNum/autonumschemecommand.cpp b/sources/autoNum/autonumschemecommand.cpp
new file mode 100644
index 000000000..db20f3ec8
--- /dev/null
+++ b/sources/autoNum/autonumschemecommand.cpp
@@ -0,0 +1,297 @@
+/*
+ Copyright 2006-2026 The QElectroTech Team
+ This file is part of QElectroTech.
+
+ QElectroTech is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ QElectroTech is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with QElectroTech. If not, see .
+*/
+#include "autonumschemecommand.h"
+
+#include "../diagram.h"
+#include "../qetproject.h"
+
+#include
+
+AutoNumSchemeCommand::AutoNumSchemeCommand(QETProject *project, Kind kind) :
+ m_project(project),
+ m_kind(kind)
+{}
+
+/// @return the titles of the schemes of @p kind, sorted
+QStringList AutoNumSchemeCommand::titles(const QETProject *project, Kind kind)
+{
+ if (!project) {
+ return {};
+ }
+ QStringList list = kind == Kind::Conductor ? project->conductorAutoNum().keys()
+ : project->folioAutoNum().keys();
+ list.sort(Qt::CaseInsensitive);
+ return list;
+}
+
+bool AutoNumSchemeCommand::contains(const QETProject *project, Kind kind, const QString &title)
+{
+ if (!project) {
+ return false;
+ }
+ return kind == Kind::Conductor ? project->conductorAutoNum().contains(title)
+ : project->folioAutoNum().contains(title);
+}
+
+NumerotationContext AutoNumSchemeCommand::contextOf(const QETProject *project, Kind kind, const QString &title)
+{
+ if (!project) {
+ return NumerotationContext();
+ }
+ return kind == Kind::Conductor ? project->conductorAutoNum(title)
+ : project->folioAutoNum(title);
+}
+
+/**
+ @return the title of the scheme @p name clashes with, empty if none:
+ the same name, whatever the case or the white space
+ @param ignored_title : the scheme being renamed
+*/
+QString AutoNumSchemeCommand::nameClash(const QETProject *project, Kind kind,
+ const QString &name, const QString &ignored_title)
+{
+ const QString wanted = QETProject::normalizedAutoNumName(name);
+ for (const QString &title : titles(project, kind)) {
+ if (title == ignored_title) {
+ continue;
+ }
+ if (QString::compare(QETProject::normalizedAutoNumName(title), wanted,
+ Qt::CaseInsensitive) == 0) {
+ return title;
+ }
+ }
+ return QString();
+}
+
+/// @return why @p name cannot be the name of a scheme, translated, empty if it can
+QString AutoNumSchemeCommand::nameProblem(const QETProject *project, Kind kind,
+ const QString &name, const QString &ignored_title)
+{
+ if (QETProject::normalizedAutoNumName(name).isEmpty()) {
+ return tr("Le nom de la numérotation ne peut pas être vide.");
+ }
+ const QString clash = nameClash(project, kind, name, ignored_title);
+ if (!clash.isEmpty()) {
+ return tr("Une numérotation nommée « %1 » existe déjà.").arg(clash);
+ }
+ return QString();
+}
+
+/**
+ @return the folios which follow the scheme @p title
+ @param references : every folio which names it, for a folio numbering,
+ even if its number has been written into the folio field already (see
+ the class description)
+*/
+QList AutoNumSchemeCommand::usersOf(const QETProject *project, Kind kind,
+ const QString &title, bool references)
+{
+ QList users;
+ if (!project || title.isEmpty()) {
+ return users;
+ }
+ for (Diagram *diagram : project->diagrams()) {
+ if (kind == Kind::Conductor) {
+ if (diagram->conductorsAutonumName() == title) {
+ users << diagram;
+ }
+ } else if (diagram->border_and_titleblock.autoPageNum() == title
+ && (references || diagram->border_and_titleblock.folio().contains(QLatin1String("%autonum")))) {
+ users << diagram;
+ }
+ }
+ return users;
+}
+
+AutoNumSchemeCommand *AutoNumSchemeCommand::create(QETProject *project, Kind kind,
+ const QString &title,
+ const NumerotationContext &context,
+ bool make_current)
+{
+ if (!project || !nameProblem(project, kind, title).isEmpty()) {
+ return nullptr;
+ }
+ auto *cmd = new AutoNumSchemeCommand(project, kind);
+ cmd->m_after = Scheme{QETProject::normalizedAutoNumName(title), context};
+ cmd->m_current_before = project->conductorCurrentAutoNum();
+ cmd->m_current_after = (kind == Kind::Conductor && make_current)
+ ? cmd->m_after->title
+ : cmd->m_current_before;
+ cmd->setText(tr("Créer la numérotation %1").arg(cmd->m_after->title));
+ return cmd;
+}
+
+/**
+ Rename the scheme @p old_title to @p new_title and give it @p context.
+ @param make_current : for conductors, make it the scheme new conductors
+ take; otherwise it stays so if it was
+ @return nullptr if there is no such scheme, the new name is not
+ acceptable, or nothing changes
+*/
+AutoNumSchemeCommand *AutoNumSchemeCommand::edit(QETProject *project, Kind kind,
+ const QString &old_title,
+ const QString &new_title,
+ const NumerotationContext &context,
+ bool make_current)
+{
+ if (!project || !contains(project, kind, old_title)) {
+ return nullptr;
+ }
+ if (!nameProblem(project, kind, new_title, old_title).isEmpty()) {
+ return nullptr;
+ }
+
+ auto *cmd = new AutoNumSchemeCommand(project, kind);
+ cmd->m_before = Scheme{old_title, contextOf(project, kind, old_title)};
+ cmd->m_after = Scheme{QETProject::normalizedAutoNumName(new_title), context};
+ cmd->m_current_before = project->conductorCurrentAutoNum();
+ cmd->m_current_after = kind == Kind::Conductor
+ ? ((make_current || cmd->m_current_before == old_title)
+ ? cmd->m_after->title : cmd->m_current_before)
+ : cmd->m_current_before;
+ if (cmd->m_before->title != cmd->m_after->title) {
+ cmd->m_users = [&] {
+ QList> list;
+ for (Diagram *d : usersOf(project, kind, old_title, true)) list << d;
+ return list;
+ }();
+ }
+
+ bool same_definition = cmd->m_before->context.size() == cmd->m_after->context.size();
+ for (int i = 0 ; same_definition && i < cmd->m_before->context.size() ; ++i) {
+ same_definition = cmd->m_before->context[i] == cmd->m_after->context[i];
+ }
+ if (cmd->m_before->title == cmd->m_after->title && same_definition
+ && cmd->m_current_before == cmd->m_current_after) {
+ delete cmd;
+ return nullptr;
+ }
+ if (cmd->m_before->title != cmd->m_after->title && same_definition) {
+ cmd->setText(tr("Renommer la numérotation %1 en %2")
+ .arg(cmd->m_before->title, cmd->m_after->title));
+ } else {
+ cmd->setText(tr("Modifier la numérotation %1").arg(cmd->m_after->title));
+ }
+ return cmd;
+}
+
+/**
+ Remove the scheme @p title.
+ @return nullptr if there is no such scheme or a folio still follows it
+*/
+AutoNumSchemeCommand *AutoNumSchemeCommand::remove(QETProject *project, Kind kind,
+ const QString &title)
+{
+ if (!project || !contains(project, kind, title)) {
+ return nullptr;
+ }
+ if (!usersOf(project, kind, title).isEmpty()) {
+ return nullptr;
+ }
+ auto *cmd = new AutoNumSchemeCommand(project, kind);
+ cmd->m_before = Scheme{title, contextOf(project, kind, title)};
+ cmd->m_current_before = project->conductorCurrentAutoNum();
+ cmd->m_current_after = (kind == Kind::Conductor && cmd->m_current_before == title)
+ ? QString() : cmd->m_current_before;
+ cmd->setText(tr("Supprimer la numérotation %1").arg(title));
+ return cmd;
+}
+
+/// Let the windows which list the schemes read them again
+void AutoNumSchemeCommand::announce(bool added, bool removed)
+{
+ if (m_kind == Kind::Conductor) {
+ if (removed) m_project->conductorAutoNumRemoved();
+ if (added) m_project->conductorAutoNumAdded();
+ } else {
+ if (removed) m_project->folioAutoNumRemoved();
+ if (added) m_project->folioAutoNumAdded();
+ }
+}
+
+/// Give the scheme @p from the title @p to, and the folios which follow it
+void AutoNumSchemeCommand::rename(const QString &from, const QString &to)
+{
+ const NumerotationContext context = contextOf(m_project, m_kind, from);
+ if (m_kind == Kind::Conductor) {
+ m_project->removeConductorAutoNum(from);
+ m_project->addConductorAutoNum(to, context);
+ if (m_project->conductorCurrentAutoNum() == from) {
+ m_project->setCurrentConductorAutoNum(to);
+ }
+ for (Diagram *d : m_project->diagrams()) {
+ //The highest folio number given, kept by scheme title
+ for (auto *hash : {&d->m_cnd_unitfolio_max, &d->m_cnd_tenfolio_max, &d->m_cnd_hundredfolio_max}) {
+ if (hash->contains(from)) {
+ hash->insert(to, hash->take(from));
+ }
+ }
+ }
+ } else {
+ m_project->removeFolioAutoNum(from);
+ m_project->addFolioAutoNum(to, context);
+ }
+ for (const QPointer &d : std::as_const(m_users)) {
+ if (!d) continue;
+ if (m_kind == Kind::Conductor) {
+ d->setConductorsAutonumName(to);
+ } else {
+ d->border_and_titleblock.setAutoPageNum(to);
+ }
+ }
+}
+
+void AutoNumSchemeCommand::redo()
+{
+ if (!m_project) return;
+
+ if (m_before && m_after && m_before->title != m_after->title) {
+ rename(m_before->title, m_after->title);
+ } else if (m_before && !m_after) {
+ if (m_kind == Kind::Conductor) m_project->removeConductorAutoNum(m_before->title);
+ else m_project->removeFolioAutoNum(m_before->title);
+ }
+ if (m_after) {
+ if (m_kind == Kind::Conductor) m_project->addConductorAutoNum(m_after->title, m_after->context);
+ else m_project->addFolioAutoNum(m_after->title, m_after->context);
+ }
+ if (m_kind == Kind::Conductor) {
+ m_project->setCurrentConductorAutoNum(m_current_after);
+ }
+ announce(m_after.has_value(), m_before.has_value() && (!m_after || m_after->title != m_before->title));
+}
+
+void AutoNumSchemeCommand::undo()
+{
+ if (!m_project) return;
+
+ if (m_before && m_after && m_before->title != m_after->title) {
+ rename(m_after->title, m_before->title);
+ } else if (!m_before && m_after) {
+ if (m_kind == Kind::Conductor) m_project->removeConductorAutoNum(m_after->title);
+ else m_project->removeFolioAutoNum(m_after->title);
+ }
+ if (m_before) {
+ if (m_kind == Kind::Conductor) m_project->addConductorAutoNum(m_before->title, m_before->context);
+ else m_project->addFolioAutoNum(m_before->title, m_before->context);
+ }
+ if (m_kind == Kind::Conductor) {
+ m_project->setCurrentConductorAutoNum(m_current_before);
+ }
+ announce(m_before.has_value(), m_after.has_value() && (!m_before || m_after->title != m_before->title));
+}
diff --git a/sources/autoNum/autonumschemecommand.h b/sources/autoNum/autonumschemecommand.h
new file mode 100644
index 000000000..52167859f
--- /dev/null
+++ b/sources/autoNum/autonumschemecommand.h
@@ -0,0 +1,111 @@
+/*
+ Copyright 2006-2026 The QElectroTech Team
+ This file is part of QElectroTech.
+
+ QElectroTech is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ QElectroTech is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with QElectroTech. If not, see .
+*/
+#ifndef AUTONUMSCHEMECOMMAND_H
+#define AUTONUMSCHEMECOMMAND_H
+
+#include "numerotationcontext.h"
+
+#include
+#include
+#include
+#include
+
+#include
+
+class Diagram;
+class QETProject;
+
+/**
+ @brief The AutoNumSchemeCommand class
+ Undoable creation, edition (rename and/or new definition) and removal of
+ one conductor or folio numbering scheme of a project.
+
+ A folio refers to the scheme it follows by its title, as the project file
+ has always had it (the title of the conductor numbering a folio reads,
+ the title of the folio numbering its title block follows). So a rename
+ moves those references with it, in the same undo step, and a scheme which
+ a folio still follows cannot be removed: the folio would be left reading
+ a numbering which does not exist.
+
+ A folio numbering is applied once: the number is written into the folio
+ field of the title block, in place of %autonum, and the title block keeps
+ the name of the numbering it came from. So a folio really depends on a
+ folio numbering only while its folio field still holds %autonum, and
+ only then can the numbering not be removed; every title block which
+ names it has its name moved by a rename.
+
+ Conductors keep the formula they were numbered with, and the numbers
+ folios show are worked out from the scheme when they are drawn: a new
+ definition changes what is numbered from then on, nothing is renumbered.
+
+ Counter changes made by numbering something are not edits of the scheme
+ and do not come here.
+*/
+class AutoNumSchemeCommand : public QUndoCommand
+{
+ Q_DECLARE_TR_FUNCTIONS(AutoNumSchemeCommand)
+
+ public:
+ enum class Kind { Conductor, Folio };
+
+ struct Scheme
+ {
+ QString title;
+ NumerotationContext context;
+ };
+
+ static QStringList titles(const QETProject *project, Kind kind);
+ static bool contains(const QETProject *project, Kind kind, const QString &title);
+ static NumerotationContext contextOf(const QETProject *project, Kind kind, const QString &title);
+ static QString nameClash(const QETProject *project, Kind kind,
+ const QString &name, const QString &ignored_title = QString());
+ static QString nameProblem(const QETProject *project, Kind kind,
+ const QString &name, const QString &ignored_title = QString());
+ static QList usersOf(const QETProject *project, Kind kind,
+ const QString &title, bool references = false);
+
+ static AutoNumSchemeCommand *create(QETProject *project, Kind kind,
+ const QString &title,
+ const NumerotationContext &context,
+ bool make_current = false);
+ static AutoNumSchemeCommand *edit(QETProject *project, Kind kind,
+ const QString &old_title,
+ const QString &new_title,
+ const NumerotationContext &context,
+ bool make_current = false);
+ static AutoNumSchemeCommand *remove(QETProject *project, Kind kind,
+ const QString &title);
+
+ void undo() override;
+ void redo() override;
+
+ private:
+ AutoNumSchemeCommand(QETProject *project, Kind kind);
+ void rename(const QString &from, const QString &to);
+ void announce(bool added, bool removed);
+
+ QETProject *m_project = nullptr;
+ Kind m_kind;
+ std::optional m_before;
+ std::optional m_after;
+ QString m_current_before;
+ QString m_current_after;
+ QList> m_users; ///< the folios which follow the scheme being renamed
+};
+
+#endif // AUTONUMSCHEMECOMMAND_H
diff --git a/sources/autoNum/elementautonumschemecommand.cpp b/sources/autoNum/elementautonumschemecommand.cpp
new file mode 100644
index 000000000..9e7ffd518
--- /dev/null
+++ b/sources/autoNum/elementautonumschemecommand.cpp
@@ -0,0 +1,1263 @@
+/*
+ Copyright 2006-2026 The QElectroTech Team
+ This file is part of QElectroTech.
+
+ QElectroTech is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ QElectroTech is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with QElectroTech. If not, see .
+*/
+#include "elementautonumschemecommand.h"
+
+#include "assignvariables.h"
+#include "numerotationcontextcommands.h"
+#include "../diagram.h"
+#include "../qetgraphicsitem/element.h"
+#include "../qetinformation.h"
+#include "../qetproject.h"
+
+#include
+
+#include
+#include
+
+namespace {
+bool sameContext(const NumerotationContext &a, const NumerotationContext &b)
+{
+ if (a.size() != b.size()) return false;
+ for (int i = 0; i < a.size(); ++i) {
+ if (a[i] != b[i]) return false;
+ }
+ return true;
+}
+} // namespace
+
+ElementAutoNumSchemeCommand::ElementAutoNumSchemeCommand(QETProject *project) :
+ m_project(project)
+{}
+
+/**
+ @brief ElementAutoNumSchemeCommand::nameProblem
+ @return why @p name cannot be the name of an element numbering scheme
+ of @p project, translated, or an empty string if it can
+ @param ignored_title : the scheme being renamed, if any
+*/
+QString ElementAutoNumSchemeCommand::nameProblem(const QETProject *project,
+ const QString &name,
+ const QString &ignored_title)
+{
+ if (QETProject::normalizedAutoNumName(name).isEmpty()) {
+ return tr("Le nom de la numérotation ne peut pas être vide.");
+ }
+ if (!project) {
+ return QString();
+ }
+ const QString clash = project->elementAutoNumNameClash(name, ignored_title);
+ if (!clash.isEmpty()) {
+ return tr("Une numérotation nommée « %1 » existe déjà.").arg(clash);
+ }
+ return QString();
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::sameSequentialParts
+ @return true if both formulas use the same sequential numbers
+ (%sequ_1, %seqtf_2...), so the numbers an element holds for one still
+ fit the other
+*/
+bool ElementAutoNumSchemeCommand::sameSequentialParts(const QString &formula_a,
+ const QString &formula_b)
+{
+ static const QRegularExpression rx(QStringLiteral("%seq[a-z]+_\\d+"));
+ const auto tokens = [](const QString &formula) {
+ QStringList list;
+ auto it = rx.globalMatch(formula);
+ while (it.hasNext()) {
+ list << it.next().captured(0);
+ }
+ list.sort();
+ list.removeDuplicates();
+ return list;
+ };
+ return tokens(formula_a) == tokens(formula_b);
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::resetForRenumber
+ @return @p context with every number part set back to 1, the other
+ parts unchanged, to number elements again from the start
+*/
+NumerotationContext ElementAutoNumSchemeCommand::resetForRenumber(const NumerotationContext &context)
+{
+ NumerotationContext out = context;
+ for (int i = 0; i < out.size(); ++i) {
+ const QStringList parts = out.itemAt(i);
+ if (parts.isEmpty()) continue;
+ if (out.keyIsNumber(parts.at(0))) {
+ out.replaceValue(i, QStringLiteral("1"));
+ }
+ }
+ return out;
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::frozenFollowers
+ @return the elements with a frozen label which follow the element
+ numbering scheme @p title
+*/
+QVector ElementAutoNumSchemeCommand::frozenFollowers(
+ const QETProject *project,
+ const QString &title)
+{
+ QVector list;
+ if (!project) {
+ return list;
+ }
+ const auto followers = project->elementsUsingElementAutoNum(title);
+ for (Element *el : followers) {
+ if (el->isFreezeLabel()) {
+ list << el;
+ }
+ }
+ return list;
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::editBlockedBy
+ @return the elements with a frozen label which stop the element
+ numbering scheme @p title from being given the definition @p context:
+ none when its formula stays the same (a rename, a change of the
+ counter), otherwise every frozen follower
+*/
+QVector ElementAutoNumSchemeCommand::editBlockedBy(
+ const QETProject *project,
+ const QString &title,
+ const NumerotationContext &context)
+{
+ if (!project || !project->elementAutoNum().contains(title)) {
+ return {};
+ }
+ if (project->elementAutoNumFormula(title)
+ == autonum::numerotationContextToFormula(context)) {
+ return {};
+ }
+ return frozenFollowers(project, title);
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::renumberChanges
+ Number @p elements again, in folio and position order, with @p context
+ started from 1, and give them @p formula.
+ @param hash_key : key of the scheme in the folios' maxima of folio
+ sequential numbers (its title)
+ @param final_context : if not null, receives the context after the
+ last element, to store as the scheme's counter
+ @return one change per element
+*/
+QVector ElementAutoNumSchemeCommand::renumberChanges(
+ const QString &hash_key,
+ const NumerotationContext &context,
+ QVector elements,
+ const QString &formula,
+ NumerotationContext *final_context,
+ bool from_start,
+ const QUuid &scheme_id,
+ const QSet &reserved)
+{
+ QVector changes;
+ std::sort(elements.begin(), elements.end(),
+ [](Element *a, Element *b){ return comparPos(a, b); });
+
+ NumerotationContext nc = from_start ? resetForRenumber(context) : context;
+ for (Element *el : std::as_const(elements))
+ {
+ RenumberElementsCommand::ElementChange ch;
+ ch.element = el;
+ ch.old_infos = el->elementInformations();
+ ch.old_seq = el->sequenceStruct();
+ ch.old_frozen = el->isFreezeLabel();
+ ch.new_frozen = ch.old_frozen;
+
+ //The next number which does not give the element a label an
+ //element left as it is already has. A numbering whose number
+ //parts cannot move on (fixed text only) has nothing to skip
+ //to: it keeps its label.
+ autonum::sequentialNumbers new_seq;
+ QString label;
+ for (int attempt = 0 ; ; ++attempt)
+ {
+ new_seq = autonum::sequentialNumbers();
+ autonum::setSequential(formula, new_seq, nc, el->diagram(), hash_key);
+ label = autonum::AssignVariables::formulaToLabel(
+ formula, new_seq, el->diagram(), el, nullptr);
+ if (label.isEmpty() || !reserved.contains(label) || attempt >= 100000) {
+ break;
+ }
+ NumerotationContextCommands skip(nc);
+ const NumerotationContext advanced = skip.next();
+ if (sameContext(advanced, nc)) {
+ break;
+ }
+ nc = advanced;
+ }
+
+ DiagramContext new_infos = ch.old_infos;
+ new_infos.addValue(QETInformation::ELMT_FORMULA, formula);
+ if (!scheme_id.isNull()) {
+ new_infos.addValue(QETInformation::ELMT_FORMULA_ID,
+ scheme_id.toString(), false);
+ }
+ new_infos.addValue(QETInformation::ELMT_LABEL, label);
+ ch.new_infos = new_infos;
+ ch.new_seq = new_seq;
+ changes << ch;
+
+ NumerotationContextCommands ncc(nc);
+ nc = ncc.next();
+ }
+ if (final_context) {
+ *final_context = nc;
+ }
+ return changes;
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::reservedLabels
+ @return the labels the elements following the scheme @p title keep
+ while @p changing change: numbers an operation on @p changing must not
+ give again, or two elements would carry the same label
+*/
+QSet ElementAutoNumSchemeCommand::reservedLabels(
+ const QETProject *project,
+ const QString &title,
+ const QVector &changing)
+{
+ QSet labels;
+ if (!project) {
+ return labels;
+ }
+ const QSet moving(changing.constBegin(), changing.constEnd());
+ const auto followers = project->elementsUsingElementAutoNum(title);
+ for (const Element *el : followers)
+ {
+ if (moving.contains(el)
+ || el->linkType() == Element::Slave
+ || (el->linkType() & Element::AllReport)) {
+ continue;
+ }
+ const QString label = el->elementInformations().value(QETInformation::ELMT_LABEL).toString();
+ if (!label.isEmpty()) {
+ labels << label;
+ }
+ }
+ return labels;
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::renumber
+ Number again, from the first number, the elements following the
+ element numbering scheme @p title, or every scheme if @p title is
+ empty, in folio and position order. One undo step.
+
+ An element whose label is frozen is left as it is, and its label is not
+ given to another element: the numbering goes past it.
+ @param frozen : if not null, receives the elements left as they are
+ because their label is frozen
+ @param text : caption of the undo step
+ @return the command, nullptr if there is nothing to number
+*/
+RenumberElementsCommand *ElementAutoNumSchemeCommand::renumber(
+ QETProject *project,
+ const QString &title,
+ QVector *frozen,
+ const QString &text)
+{
+ if (frozen) frozen->clear();
+ if (!project || project->isReadOnly()) {
+ return nullptr;
+ }
+
+ QVector changes;
+ QHash old_ctx;
+ QHash new_ctx;
+
+ QStringList titles = title.isEmpty() ? QStringList(project->elementAutoNum().keys())
+ : QStringList{title};
+ titles.sort(Qt::CaseInsensitive);
+ for (const QString &key : std::as_const(titles))
+ {
+ if (!project->elementAutoNum().contains(key)) {
+ continue;
+ }
+
+ QVector elements;
+ const auto followers = project->elementsUsingElementAutoNum(key);
+ for (Element *el : followers)
+ {
+ if (el->linkType() == Element::Slave || (el->linkType() & Element::AllReport)) {
+ continue;
+ }
+ if (el->isFreezeLabel()) {
+ if (frozen) *frozen << el;
+ continue;
+ }
+ elements << el;
+ }
+ if (elements.isEmpty()) {
+ continue;
+ }
+
+ const NumerotationContext tmpl = project->elementAutoNum(key);
+ NumerotationContext final_ctx;
+ changes << renumberChanges(key, tmpl, elements,
+ autonum::numerotationContextToFormula(tmpl),
+ &final_ctx, true, QUuid(),
+ reservedLabels(project, key, elements));
+ old_ctx.insert(key, tmpl);
+ new_ctx.insert(key, final_ctx);
+ }
+ if (changes.isEmpty()) {
+ return nullptr;
+ }
+ return new RenumberElementsCommand(project, changes, old_ctx, new_ctx, text);
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::numberSupport
+ Giving an element a number by hand makes sense when the numbers of a
+ scheme are one sequence: a definition with exactly one number part, a
+ "unit", "ten" or "hundred" (a folio number, a cycle or letters would make
+ "which number is free" depend on more than one value).
+*/
+ElementAutoNumSchemeCommand::NumberSupport ElementAutoNumSchemeCommand::numberSupport(
+ const NumerotationContext &context)
+{
+ NumberSupport support;
+ int numbers = 0;
+ for (int i = 0 ; i < context.size() ; ++i)
+ {
+ const QStringList part = context.itemAt(i);
+ const QString type = part.value(0);
+ if (type == QLatin1String("unit") || type == QLatin1String("ten")
+ || type == QLatin1String("hundred")) {
+ ++numbers;
+ support.partIndex = i;
+ support.type = type;
+ support.increase = std::max(1, part.value(2).toInt());
+ } else if (type == QLatin1String("unitfolio") || type == QLatin1String("tenfolio")
+ || type == QLatin1String("hundredfolio") || type == QLatin1String("alpha")
+ || type == QLatin1String("wrap")) {
+ return NumberSupport();
+ }
+ }
+ support.supported = numbers == 1;
+ if (!support.supported) {
+ return NumberSupport();
+ }
+ return support;
+}
+
+/// @return the number @p element was given by the scheme, if it knows it
+std::optional ElementAutoNumSchemeCommand::numberOf(const NumberSupport &support,
+ const Element *element)
+{
+ if (!support.supported || !element) {
+ return std::nullopt;
+ }
+ const autonum::sequentialNumbers seq = element->sequenceStruct();
+ const QStringList &list = support.type == QLatin1String("unit") ? seq.unit
+ : support.type == QLatin1String("ten") ? seq.ten
+ : seq.hundred;
+ bool ok = false;
+ const int number = list.value(0).toInt(&ok);
+ if (!ok) {
+ return std::nullopt;
+ }
+ return number;
+}
+
+namespace {
+/// The elements of @p project which follow the scheme @p title and are not slaves or reports
+QVector numberedFollowers(const QETProject *project, const QString &title)
+{
+ QVector list;
+ const auto followers = project->elementsUsingElementAutoNum(title);
+ for (Element *el : followers) {
+ if (el->linkType() != Element::Slave && !(el->linkType() & Element::AllReport)) {
+ list << el;
+ }
+ }
+ return list;
+}
+} // namespace
+
+/**
+ @brief ElementAutoNumSchemeCommand::labelsHeldBesides
+ @return the labels the elements of @p project carry, @p element apart:
+ those a number given to @p element must not turn into
+*/
+QSet ElementAutoNumSchemeCommand::labelsHeldBesides(const QETProject *project,
+ const Element *element)
+{
+ QSet labels;
+ if (!project) {
+ return labels;
+ }
+ for (Diagram *d : project->diagrams()) {
+ for (QGraphicsItem *item : d->items()) {
+ if (auto *el = qgraphicsitem_cast(item)) {
+ if (el != element) {
+ labels << el->elementInformations().value(QETInformation::ELMT_LABEL).toString();
+ }
+ }
+ }
+ }
+ labels.remove(QString());
+ return labels;
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::counterConflict
+ Giving the numbering @p title the counter of @p proposed, with the same
+ formula it has, makes its next element meet numbers in use when elements
+ of the numbering have a number of at least that counter. Those numbers
+ are skipped when the element is placed, but the next number is not the
+ one asked for, which is worth telling.
+ @return what the counter would meet, nothing if the counter does not
+ change, the formula does, or the numbers are not a single sequence
+*/
+std::optional ElementAutoNumSchemeCommand::counterConflict(
+ const QETProject *project, const QString &title, const NumerotationContext &proposed)
+{
+ if (!project || !project->elementAutoNum().contains(title)) {
+ return std::nullopt;
+ }
+ const NumerotationContext stored = project->elementAutoNum().value(title);
+ const NumberSupport support = numberSupport(stored);
+ if (!support.supported || proposed.size() != stored.size()
+ || autonum::numerotationContextToFormula(proposed) != autonum::numerotationContextToFormula(stored)) {
+ return std::nullopt;
+ }
+ const int counter = proposed.itemAt(support.partIndex).value(1).toInt();
+ if (counter == stored.itemAt(support.partIndex).value(1).toInt()) {
+ return std::nullopt;
+ }
+
+ CounterConflict conflict;
+ conflict.counter = counter;
+ for (const Element *el : numberedFollowers(project, title)) {
+ if (const auto number = numberOf(support, el)) {
+ if (*number >= counter) {
+ ++conflict.count;
+ conflict.highest = std::max(conflict.highest, *number);
+ }
+ }
+ }
+ if (!conflict.count) {
+ return std::nullopt;
+ }
+ return conflict;
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::gapRanges
+ @return the numbers between the lowest and the highest which the elements
+ following the scheme @p title carry, that none of them carries: where an
+ element was deleted or given another number. Empty for a scheme whose
+ elements cannot be given a number by hand (see numberSupport()).
+*/
+QList ElementAutoNumSchemeCommand::gapRanges(
+ const QETProject *project, const QString &title)
+{
+ QList ranges;
+ if (!project || !project->elementAutoNum().contains(title)) {
+ return ranges;
+ }
+ const NumberSupport support = numberSupport(project->elementAutoNum().value(title));
+ if (!support.supported) {
+ return ranges;
+ }
+ QSet used;
+ for (const Element *el : numberedFollowers(project, title)) {
+ if (const auto number = numberOf(support, el)) {
+ used << *number;
+ }
+ }
+ if (used.size() < 2) {
+ return ranges;
+ }
+ const int low = *std::min_element(used.cbegin(), used.cend());
+ const int high = *std::max_element(used.cbegin(), used.cend());
+ for (int n = low + 1 ; n < high ; ++n)
+ {
+ if (used.contains(n)) {
+ continue;
+ }
+ if (!ranges.isEmpty() && ranges.last().to == n - 1) {
+ ranges.last().to = n;
+ } else {
+ ranges << GapRange{n, n};
+ }
+ }
+ return ranges;
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::labelForNumber
+ @return the label @p element would have with the number @p number of the
+ scheme @p title, empty if the scheme cannot give a number by hand
+*/
+QString ElementAutoNumSchemeCommand::labelForNumber(const QETProject *project,
+ const QString &title,
+ Element *element,
+ int number)
+{
+ if (!project || !element || !element->diagram() || !project->elementAutoNum().contains(title)) {
+ return QString();
+ }
+ NumerotationContext context = project->elementAutoNum().value(title);
+ const NumberSupport support = numberSupport(context);
+ if (!support.supported) {
+ return QString();
+ }
+ context.replaceValue(support.partIndex, QString::number(number));
+ const QString formula = autonum::numerotationContextToFormula(context);
+ autonum::sequentialNumbers seq;
+ autonum::setSequential(formula, seq, context, element->diagram(), title);
+ return autonum::AssignVariables::formulaToLabel(formula, seq, element->diagram(), element, nullptr);
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::freeNumbers
+ @return the numbers @p element of the scheme @p title may be given by
+ hand, in order: from 1 to a few past the highest in use or the counter,
+ those no other element of the scheme carries and which would not give it
+ the label of any other element. At most @p limit.
+*/
+QList ElementAutoNumSchemeCommand::freeNumbers(const QETProject *project,
+ const QString &title,
+ const Element *element,
+ int limit)
+{
+ QList numbers;
+ if (!project || !element || !project->elementAutoNum().contains(title)) {
+ return numbers;
+ }
+ const NumerotationContext context = project->elementAutoNum().value(title);
+ const NumberSupport support = numberSupport(context);
+ if (!support.supported) {
+ return numbers;
+ }
+
+ QSet used;
+ int highest = context.itemAt(support.partIndex).value(1).toInt();
+ for (const Element *el : numberedFollowers(project, title)) {
+ if (el == element) continue;
+ if (const auto number = numberOf(support, el)) {
+ used << *number;
+ highest = std::max(highest, *number);
+ }
+ }
+
+ QSet labels;
+ for (Diagram *d : project->diagrams()) {
+ for (QGraphicsItem *item : d->items()) {
+ if (auto *el = qgraphicsitem_cast(item)) {
+ if (el != element) {
+ labels << el->elementInformations().value(QETInformation::ELMT_LABEL).toString();
+ }
+ }
+ }
+ }
+ labels.remove(QString());
+
+ for (int n = 1 ; n <= highest + 10 && numbers.size() < limit ; ++n)
+ {
+ if (used.contains(n)) {
+ continue;
+ }
+ const QString label = labelForNumber(project, title, const_cast(element), n);
+ if (!label.isEmpty() && labels.contains(label)) {
+ continue;
+ }
+ numbers << n;
+ }
+ return numbers;
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::assignNumber
+ Give @p element, which follows an element numbering scheme, the number
+ @p number of it, which must be free (see freeNumbers()): its label is the
+ formula worked out with that number and it keeps following the scheme.
+ When the scheme's counter is at or below @p number it moves to the next
+ number after it, so that the next element is not given the same label.
+ One undo step.
+ @param problem : if not null, receives why nothing is done, translated
+ @return the command, nullptr if @p number cannot be given
+*/
+RenumberElementsCommand *ElementAutoNumSchemeCommand::assignNumber(QETProject *project,
+ Element *element,
+ int number,
+ QString *problem,
+ QUndoCommand *parent)
+{
+ const auto fail = [&](const QString &text) -> RenumberElementsCommand * {
+ if (problem) *problem = text;
+ return nullptr;
+ };
+ if (!project || !element || !element->diagram()) {
+ return fail(tr("Cet élément n'est dans aucun folio."));
+ }
+ const DiagramContext info = element->elementInformations();
+ const QString title = project->elementAutoNumTitle(
+ QUuid(info.value(QETInformation::ELMT_FORMULA_ID).toString()));
+ if (title.isEmpty()) {
+ return fail(tr("Cet élément ne suit aucune numérotation."));
+ }
+ const NumerotationContext context = project->elementAutoNum().value(title);
+ const NumberSupport support = numberSupport(context);
+ if (!support.supported) {
+ return fail(tr("La numérotation « %1 » n'a pas un seul numéro : on ne peut pas en choisir un à la main.").arg(title));
+ }
+ if (number < 1) {
+ return fail(tr("Le numéro doit être au moins 1."));
+ }
+ if (!freeNumbers(project, title, element, 100000).contains(number)) {
+ return fail(tr("Le numéro %1 n'est pas libre.").arg(number));
+ }
+
+ const QString formula = autonum::numerotationContextToFormula(context);
+ NumerotationContext at = context;
+ at.replaceValue(support.partIndex, QString::number(number));
+ autonum::sequentialNumbers seq;
+ autonum::setSequential(formula, seq, at, element->diagram(), title);
+
+ RenumberElementsCommand::ElementChange change;
+ change.element = element;
+ change.old_infos = info;
+ change.old_seq = element->sequenceStruct();
+ change.old_frozen = element->isFreezeLabel();
+ change.new_frozen = change.old_frozen;
+ change.new_infos = info;
+ change.new_infos.addValue(QETInformation::ELMT_FORMULA, formula);
+ change.new_infos.addValue(QETInformation::ELMT_FORMULA_ID,
+ project->elementAutoNumId(title).toString(), false);
+ change.new_infos.addValue(QETInformation::ELMT_LABEL,
+ autonum::AssignVariables::formulaToLabel(
+ formula, seq, element->diagram(), element, nullptr));
+ change.new_seq = seq;
+
+ NumerotationContext counter = context;
+ if (counter.itemAt(support.partIndex).value(1).toInt() <= number) {
+ counter.replaceValue(support.partIndex, QString::number(number + support.increase));
+ }
+ return new RenumberElementsCommand(project, {change},
+ {{title, context}}, {{title, counter}},
+ tr("Attribuer le numéro %1").arg(number), parent);
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::skipReason
+ @return why assign() would leave @p element alone, None if it would
+ give it the scheme @p title
+*/
+ElementAutoNumSchemeCommand::Skipped ElementAutoNumSchemeCommand::skipReason(
+ const QETProject *project,
+ const QString &title,
+ const Element *element)
+{
+ if (!project || !element) {
+ return Skipped::Linked;
+ }
+ if (element->linkType() == Element::Slave
+ || (element->linkType() & Element::AllReport)) {
+ return Skipped::Linked;
+ }
+ const DiagramContext &info = element->elementInformations();
+ const QString formula = info.value(QETInformation::ELMT_FORMULA).toString();
+ const QUuid wanted = project->elementAutoNumId(title);
+ if (!formula.isEmpty()) {
+ if (!wanted.isNull()
+ && QUuid(info.value(QETInformation::ELMT_FORMULA_ID).toString()) == wanted) {
+ return Skipped::FollowsAlready;
+ }
+ return Skipped::OtherFormula;
+ }
+ if (element->isFreezeLabel()) {
+ return Skipped::Frozen;
+ }
+ return Skipped::None;
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::assignPlan
+ Sort @p elements by what assign() would do with them. A frozen label
+ is told apart from a formula held already: both are left alone unless
+ the user agrees to replace them.
+*/
+ElementAutoNumSchemeCommand::AssignPlan ElementAutoNumSchemeCommand::assignPlan(
+ const QETProject *project,
+ const QString &title,
+ const QVector &elements)
+{
+ AssignPlan plan;
+ for (Element *el : elements)
+ {
+ switch (skipReason(project, title, el)) {
+ case Skipped::None:
+ plan.todo << el;
+ break;
+ case Skipped::Frozen:
+ plan.frozen << el;
+ break;
+ case Skipped::OtherFormula:
+ plan.otherFormula << el;
+ break;
+ case Skipped::FollowsAlready:
+ ++plan.followsAlready;
+ break;
+ case Skipped::Linked:
+ ++plan.linked;
+ break;
+ }
+ }
+ return plan;
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::numberElements
+ Give every one of @p elements (a slave or a report excepted) the
+ element numbering scheme @p title: its formula and the next number of
+ the scheme, in folio and position order, the scheme's counter moving
+ on. The numbering goes past the labels the other elements which follow
+ the scheme keep. One undo step.
+ @param freeze : what the frozen state of the elements becomes
+ @param parent : parent undo command, if the numbering is a part of one
+ @return the command, nullptr if there is nothing to number
+*/
+RenumberElementsCommand *ElementAutoNumSchemeCommand::numberElements(
+ QETProject *project,
+ const QString &title,
+ const QVector &elements,
+ const QString &text,
+ FreezeRule freeze,
+ QUndoCommand *parent)
+{
+ if (!project || !project->elementAutoNum().contains(title)) {
+ return nullptr;
+ }
+ QVector todo;
+ for (Element *el : elements) {
+ if (el && el->linkType() != Element::Slave && !(el->linkType() & Element::AllReport)) {
+ todo << el;
+ }
+ }
+ if (todo.isEmpty()) {
+ return nullptr;
+ }
+
+ const NumerotationContext context = project->elementAutoNum().value(title);
+ NumerotationContext final_context;
+ QVector changes = renumberChanges(
+ title, context, todo,
+ autonum::numerotationContextToFormula(context),
+ &final_context, false, project->elementAutoNumId(title),
+ reservedLabels(project, title, todo));
+ for (auto &change : changes) {
+ change.new_frozen = freeze == FreezeRule::NewElementPolicy
+ && change.element && change.element->diagram()
+ && (change.element->diagram()->freezeNewElements()
+ || project->isFreezeNewElements());
+ }
+
+ return new RenumberElementsCommand(
+ project, changes,
+ {{title, context}}, {{title, final_context}},
+ text, parent);
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::assign
+ Make @p elements follow the element numbering scheme @p title: each
+ takes its formula and the next number of the scheme, in folio and
+ position order, and the scheme's counter moves on. One undo step.
+
+ An element is left alone (see assignPlan()) when it follows the scheme
+ already, is a slave or a report, or when something would be lost: a
+ frozen label, or a formula it follows or holds already, unless
+ @p overwrite. An element with a label typed by hand and no formula is
+ given the scheme; its label is replaced by the number.
+ @param skipped : if not null, receives how many elements were left alone
+ @return the command, nullptr if there is nothing to give
+*/
+RenumberElementsCommand *ElementAutoNumSchemeCommand::assign(
+ QETProject *project,
+ const QString &title,
+ const QVector &elements,
+ bool overwrite,
+ int *skipped,
+ QUndoCommand *parent)
+{
+ if (skipped) *skipped = 0;
+ if (!project || !project->elementAutoNum().contains(title)) {
+ return nullptr;
+ }
+
+ const AssignPlan plan = assignPlan(project, title, elements);
+ QVector todo = plan.todo;
+ int left = plan.followsAlready + plan.linked;
+ if (overwrite) {
+ todo += plan.frozen;
+ todo += plan.otherFormula;
+ } else {
+ left += plan.frozen.size() + plan.otherFormula.size();
+ }
+ if (skipped) *skipped = left;
+ if (todo.isEmpty()) {
+ return nullptr;
+ }
+ return numberElements(project, title, todo,
+ tr("Appliquer la numérotation %1").arg(title),
+ FreezeRule::Unfrozen, parent);
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::schemeForFormula
+ @return the title of the element numbering scheme of @p project which
+ gives @p formula, empty if there is none. When several schemes have the
+ same formula (the same parts, other counters), the project's current one
+ if it is among them, otherwise the first by name: the label is worked
+ out afresh in either case, only the counter which goes on differs.
+*/
+QString ElementAutoNumSchemeCommand::schemeForFormula(const QETProject *project,
+ const QString &formula)
+{
+ if (!project || formula.isEmpty()) {
+ return QString();
+ }
+ QStringList matches;
+ const auto titles = project->elementAutoNum().keys();
+ for (const QString &title : titles) {
+ if (project->elementAutoNumFormula(title) == formula) {
+ matches << title;
+ }
+ }
+ if (matches.isEmpty()) {
+ return QString();
+ }
+ if (matches.contains(project->elementCurrentAutoNum())) {
+ return project->elementCurrentAutoNum();
+ }
+ matches.sort(Qt::CaseInsensitive);
+ return matches.first();
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::writeCopiedSchemes
+ Add to the copy @p root the definition of every element numbering scheme
+ of @p project which the @p copied elements follow, so that a paste into
+ another project, which knows nothing of it, can offer to import it.
+*/
+void ElementAutoNumSchemeCommand::writeCopiedSchemes(QDomDocument &document,
+ QDomElement &root,
+ const QETProject *project,
+ const QVector &copied)
+{
+ if (!project) {
+ return;
+ }
+ QSet done;
+ QDomElement list;
+ for (const Element *el : copied)
+ {
+ const QUuid id(el->elementInformations().value(QETInformation::ELMT_FORMULA_ID).toString());
+ const QString title = project->elementAutoNumTitle(id);
+ if (title.isEmpty() || done.contains(id)) {
+ continue;
+ }
+ done << id;
+ if (list.isNull()) {
+ list = document.createElement(QStringLiteral("copied_element_autonums"));
+ root.appendChild(list);
+ }
+ NumerotationContext context = project->elementAutoNum().value(title);
+ QDomElement scheme = context.toXml(document, QStringLiteral("element_autonum"));
+ scheme.setAttribute(QStringLiteral("title"), title);
+ scheme.setAttribute(QStringLiteral("id"), id.toString());
+ scheme.setAttribute(QStringLiteral("formula"), autonum::numerotationContextToFormula(context));
+ list.appendChild(scheme);
+ }
+}
+
+/// @return the numbering schemes a copy carries (see writeCopiedSchemes())
+QList ElementAutoNumSchemeCommand::copiedSchemes(
+ const QDomElement &root)
+{
+ QList schemes;
+ const QDomElement list = root.firstChildElement(QStringLiteral("copied_element_autonums"));
+ for (QDomElement e = list.firstChildElement(QStringLiteral("element_autonum"));
+ !e.isNull();
+ e = e.nextSiblingElement(QStringLiteral("element_autonum")))
+ {
+ Scheme scheme;
+ scheme.title = e.attribute(QStringLiteral("title"));
+ scheme.id = QUuid(e.attribute(QStringLiteral("id")));
+ scheme.context.fromXml(e);
+ if (!scheme.title.isEmpty() && !scheme.id.isNull() && !scheme.context.isEmpty()) {
+ schemes << scheme;
+ }
+ }
+ return schemes;
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::missingForPaste
+ @return those of the @p copied schemes which the @p pasted elements
+ follow and which @p project has no use for: it has neither their id nor
+ a scheme with their formula (see schemeForFormula()), so the pasted
+ elements would follow no numbering. Slaves and reports follow none.
+*/
+QList ElementAutoNumSchemeCommand::missingForPaste(
+ const QETProject *project,
+ const QList &copied,
+ const QList &pasted)
+{
+ QList missing;
+ if (!project) {
+ return missing;
+ }
+ QSet followed;
+ for (const Element *el : pasted) {
+ if (el->linkType() == Element::Slave || (el->linkType() & Element::AllReport)) {
+ continue;
+ }
+ followed << QUuid(el->elementInformations().value(QETInformation::ELMT_FORMULA_ID).toString());
+ }
+ for (const Scheme &scheme : copied)
+ {
+ if (!followed.contains(scheme.id)
+ || !project->elementAutoNumTitle(scheme.id).isEmpty()
+ || !schemeForFormula(project, autonum::numerotationContextToFormula(scheme.context)).isEmpty()) {
+ continue;
+ }
+ missing << scheme;
+ }
+ return missing;
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::pastedSchemes
+ @return the element numbering schemes of @p project which freshly
+ pasted @p elements follow, by title, each with its elements.
+
+ An element follows a scheme when its formula_id names one, or, when
+ it names none (the element comes from another project, or from a file
+ written before the ids), when one has its formula (see
+ schemeForFormula()). A slave or a report follows none: it takes its
+ label from its master.
+*/
+QMap> ElementAutoNumSchemeCommand::pastedSchemes(
+ const QETProject *project,
+ const QList &elements)
+{
+ QMap> schemes;
+ if (!project || project->elementAutoNum().isEmpty()) {
+ return schemes;
+ }
+
+ for (Element *el : elements)
+ {
+ if (!el || el->linkType() == Element::Slave || (el->linkType() & Element::AllReport)) {
+ continue;
+ }
+ const DiagramContext &info = el->elementInformations();
+ const QString formula = info.value(QETInformation::ELMT_FORMULA).toString();
+ if (formula.isEmpty()) {
+ continue;
+ }
+ QString title = project->elementAutoNumTitle(
+ QUuid(info.value(QETInformation::ELMT_FORMULA_ID).toString()));
+ if (title.isEmpty()) {
+ title = schemeForFormula(project, formula);
+ }
+ if (!title.isEmpty()) {
+ schemes[title] << el;
+ }
+ }
+ return schemes;
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::linkPasted
+ Make the formula_id of pasted @p elements name a scheme of @p project,
+ as QETProject does for the elements of a file it loads: an id which
+ names one is kept; otherwise the scheme with the element's formula (see
+ schemeForFormula()), else none. Labels are not touched.
+*/
+void ElementAutoNumSchemeCommand::linkPasted(const QETProject *project,
+ const QList &elements)
+{
+ if (!project) {
+ return;
+ }
+ for (Element *el : elements)
+ {
+ if (!el) continue;
+ const DiagramContext &info = el->elementInformations();
+ const QString formula = info.value(QETInformation::ELMT_FORMULA).toString();
+ if (formula.isEmpty()) {
+ if (info.contains(QETInformation::ELMT_FORMULA_ID)) {
+ el->setFormulaSchemeId(QUuid());
+ }
+ continue;
+ }
+ const QUuid id(info.value(QETInformation::ELMT_FORMULA_ID).toString());
+ if (!project->elementAutoNumTitle(id).isEmpty()) {
+ continue;
+ }
+ const QString title = schemeForFormula(project, formula);
+ el->setFormulaSchemeId(title.isEmpty() ? QUuid() : project->elementAutoNumId(title));
+ }
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::numberPasted
+ Number the pasted elements of @p schemes (see pastedSchemes()) the way
+ elements placed one after the other are: each scheme gives its next
+ numbers, in folio and position order, and its counter moves on. One
+ numbering command per scheme, children of @p parent, which runs them
+ with its own redo(), so that undoing the paste gives the numbers back.
+ @return how many elements are numbered
+*/
+int ElementAutoNumSchemeCommand::numberPasted(
+ QETProject *project,
+ const QMap> &schemes,
+ QUndoCommand *parent)
+{
+ int count = 0;
+ for (auto it = schemes.constBegin() ; it != schemes.constEnd() ; ++it)
+ {
+ if (numberElements(project, it.key(), it.value(),
+ tr("Numéroter les éléments collés (%1)").arg(it.key()),
+ FreezeRule::NewElementPolicy, parent)) {
+ count += it.value().size();
+ }
+ }
+ return count;
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::create
+ A new element numbering scheme.
+ @param id : its uuid; a new one if null or already used
+ @param make_current : make it the scheme given to new elements
+ @return nullptr if @p title is not an acceptable name
+*/
+ElementAutoNumSchemeCommand *ElementAutoNumSchemeCommand::create(
+ QETProject *project,
+ const QString &title,
+ const NumerotationContext &context,
+ const QUuid &id,
+ bool make_current)
+{
+ if (!project || !nameProblem(project, title).isEmpty()) {
+ return nullptr;
+ }
+
+ auto *cmd = new ElementAutoNumSchemeCommand(project);
+ Scheme after;
+ after.title = QETProject::normalizedAutoNumName(title);
+ after.id = (id.isNull() || !project->elementAutoNumTitle(id).isEmpty())
+ ? QUuid::createUuid()
+ : id;
+ after.context = context;
+ cmd->m_after = after;
+ cmd->m_current_before = project->elementCurrentAutoNum();
+ cmd->m_current_after = make_current ? after.title : cmd->m_current_before;
+ cmd->setText(tr("Créer la numérotation d'éléments %1").arg(after.title));
+ return cmd;
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::edit
+ Rename the element numbering scheme @p old_title to @p new_title and
+ give it @p context. The elements following it follow it still (see
+ the class description).
+ @param make_current : make it the scheme given to new elements; if
+ false, it stays so if it was
+ @return nullptr if there is no scheme @p old_title, @p new_title is not
+ an acceptable name, an element with a frozen label follows the scheme
+ and its formula would change (see editBlockedBy()), or nothing changes
+*/
+ElementAutoNumSchemeCommand *ElementAutoNumSchemeCommand::edit(
+ QETProject *project,
+ const QString &old_title,
+ const QString &new_title,
+ const NumerotationContext &context,
+ bool make_current)
+{
+ if (!project || !project->elementAutoNum().contains(old_title)) {
+ return nullptr;
+ }
+ if (!nameProblem(project, new_title, old_title).isEmpty()) {
+ return nullptr;
+ }
+ if (!editBlockedBy(project, old_title, context).isEmpty()) {
+ return nullptr;
+ }
+
+ Scheme before;
+ before.title = old_title;
+ before.id = project->elementAutoNumId(old_title);
+ before.context = project->elementAutoNum(old_title);
+
+ Scheme after;
+ after.title = QETProject::normalizedAutoNumName(new_title);
+ after.id = before.id;
+ after.context = context;
+
+ auto *cmd = new ElementAutoNumSchemeCommand(project);
+ cmd->m_before = before;
+ cmd->m_current_before = project->elementCurrentAutoNum();
+ cmd->m_current_after = (make_current || cmd->m_current_before == old_title)
+ ? after.title
+ : cmd->m_current_before;
+
+ const QString old_formula = autonum::numerotationContextToFormula(before.context);
+ const QString new_formula = autonum::numerotationContextToFormula(after.context);
+ if (old_formula != new_formula)
+ {
+ QVector elements;
+ const auto followers = project->elementsUsingElementAutoNum(old_title);
+ for (Element *el : followers)
+ {
+ if (el->linkType() == Element::Slave || (el->linkType() & Element::AllReport)) {
+ continue;
+ }
+ elements << el;
+ }
+ if (sameSequentialParts(old_formula, new_formula))
+ {
+ for (Element *el : elements)
+ {
+ RenumberElementsCommand::ElementChange ch;
+ ch.element = el;
+ ch.old_infos = el->elementInformations();
+ ch.old_seq = el->sequenceStruct();
+ ch.new_seq = ch.old_seq;
+ ch.old_frozen = el->isFreezeLabel();
+ ch.new_frozen = ch.old_frozen;
+
+ autonum::sequentialNumbers seq = ch.old_seq;
+ ch.new_infos = ch.old_infos;
+ ch.new_infos.addValue(QETInformation::ELMT_FORMULA, new_formula);
+ ch.new_infos.addValue(QETInformation::ELMT_LABEL,
+ autonum::AssignVariables::formulaToLabel(
+ new_formula, seq, el->diagram(), el, nullptr));
+ cmd->m_changes << ch;
+ }
+ }
+ else if (!elements.isEmpty())
+ {
+ cmd->m_changes = renumberChanges(old_title, after.context,
+ elements, new_formula,
+ &after.context, true, QUuid(),
+ reservedLabels(project, old_title, elements));
+ }
+ }
+ cmd->m_after = after;
+
+ const bool renamed = before.title != after.title;
+ const bool redefined = !sameContext(before.context, after.context);
+ if (!renamed && !redefined && cmd->m_changes.isEmpty()
+ && cmd->m_current_before == cmd->m_current_after) {
+ delete cmd;
+ return nullptr;
+ }
+
+ if (renamed && !redefined) {
+ cmd->setText(tr("Renommer la numérotation d'éléments %1 en %2")
+ .arg(before.title, after.title));
+ } else {
+ cmd->setText(tr("Modifier la numérotation d'éléments %1").arg(after.title));
+ }
+ return cmd;
+}
+
+/**
+ @brief ElementAutoNumSchemeCommand::remove
+ Remove the element numbering scheme @p title.
+ @return nullptr if there is no such scheme or elements still follow
+ it: removing it would leave them with a formula nothing defines
+*/
+ElementAutoNumSchemeCommand *ElementAutoNumSchemeCommand::remove(
+ QETProject *project,
+ const QString &title)
+{
+ if (!project || !project->elementAutoNum().contains(title)) {
+ return nullptr;
+ }
+ if (!project->elementsUsingElementAutoNum(title).isEmpty()) {
+ return nullptr;
+ }
+
+ auto *cmd = new ElementAutoNumSchemeCommand(project);
+ Scheme before;
+ before.title = title;
+ before.id = project->elementAutoNumId(title);
+ before.context = project->elementAutoNum(title);
+ cmd->m_before = before;
+ cmd->m_current_before = project->elementCurrentAutoNum();
+ cmd->m_current_after = cmd->m_current_before == title
+ ? QString()
+ : cmd->m_current_before;
+ cmd->setText(tr("Supprimer la numérotation d'éléments %1").arg(title));
+ return cmd;
+}
+
+void ElementAutoNumSchemeCommand::redo()
+{
+ if (!m_project) return;
+
+ if (m_before && m_after) {
+ m_project->renameElementAutoNum(m_before->title, m_after->title);
+ } else if (m_before) {
+ m_project->removeElementAutoNum(m_before->title);
+ }
+ if (m_after) {
+ m_project->addElementAutoNum(m_after->title, m_after->context, m_after->id);
+ }
+ m_project->setCurrrentElementAutonum(m_current_after);
+
+ for (const auto &change : std::as_const(m_changes)) {
+ RenumberElementsCommand::applyChange(change, true);
+ }
+}
+
+void ElementAutoNumSchemeCommand::undo()
+{
+ if (!m_project) return;
+
+ for (const auto &change : std::as_const(m_changes)) {
+ RenumberElementsCommand::applyChange(change, false);
+ }
+
+ if (m_before && m_after) {
+ m_project->renameElementAutoNum(m_after->title, m_before->title);
+ } else if (m_after) {
+ m_project->removeElementAutoNum(m_after->title);
+ }
+ if (m_before) {
+ m_project->addElementAutoNum(m_before->title, m_before->context, m_before->id);
+ }
+ m_project->setCurrrentElementAutonum(m_current_before);
+}
diff --git a/sources/autoNum/elementautonumschemecommand.h b/sources/autoNum/elementautonumschemecommand.h
new file mode 100644
index 000000000..8ca0c1baa
--- /dev/null
+++ b/sources/autoNum/elementautonumschemecommand.h
@@ -0,0 +1,243 @@
+/*
+ Copyright 2006-2026 The QElectroTech Team
+ This file is part of QElectroTech.
+
+ QElectroTech is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ QElectroTech is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with QElectroTech. If not, see .
+*/
+#ifndef ELEMENTAUTONUMSCHEMECOMMAND_H
+#define ELEMENTAUTONUMSCHEMECOMMAND_H
+
+#include "numerotationcontext.h"
+#include "renumberelementscommand.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+class Element;
+class QETProject;
+
+/**
+ @brief The ElementAutoNumSchemeCommand class
+ Undoable creation, edition (rename and/or new definition) and removal
+ of one element numbering scheme of a project, together with what it
+ does to the elements following the scheme.
+
+ Elements follow a scheme by its uuid (QETInformation::ELMT_FORMULA_ID),
+ so a rename touches no element. A new definition with a different
+ formula rewrites the formula of every element following the scheme:
+ when its sequential parts are the same (only the fixed text or the
+ folio parts changed), each element keeps its numbers; otherwise the
+ elements are numbered again, in folio and position order, as
+ QETProject::renumberElementsBySchemeTitle() does.
+
+ A scheme whose formula would change cannot be edited while an element
+ with a frozen label follows it: that element would keep a label and a
+ formula the scheme no longer defines (see editBlockedBy(); edit()
+ refuses). A rename, or a change of the counter, touches no formula and
+ is allowed. renumber() leaves a frozen element exactly as it is,
+ label, formula and numbers, and does not give its label to another
+ element: the numbering goes past it (see reservedLabels()).
+
+ Counter changes made by placing elements are not edits of the scheme:
+ they stay with SetAutoNumContextCommand.
+
+ Build one with create(), edit() or remove(); each returns nullptr when
+ the change is refused or changes nothing. Show changedElementCount()
+ to the user before pushing it when it is not zero.
+*/
+class ElementAutoNumSchemeCommand : public QUndoCommand
+{
+ Q_DECLARE_TR_FUNCTIONS(ElementAutoNumSchemeCommand)
+
+ public:
+ struct Scheme
+ {
+ QString title;
+ QUuid id;
+ NumerotationContext context;
+ };
+
+ static ElementAutoNumSchemeCommand *create(
+ QETProject *project,
+ const QString &title,
+ const NumerotationContext &context,
+ const QUuid &id = QUuid(),
+ bool make_current = true);
+ static ElementAutoNumSchemeCommand *edit(
+ QETProject *project,
+ const QString &old_title,
+ const QString &new_title,
+ const NumerotationContext &context,
+ bool make_current = true);
+ static ElementAutoNumSchemeCommand *remove(
+ QETProject *project,
+ const QString &title);
+
+ static QString nameProblem(const QETProject *project,
+ const QString &name,
+ const QString &ignored_title = QString());
+ static bool sameSequentialParts(const QString &formula_a,
+ const QString &formula_b);
+ static NumerotationContext resetForRenumber(const NumerotationContext &context);
+ static QVector renumberChanges(
+ const QString &hash_key,
+ const NumerotationContext &context,
+ QVector elements,
+ const QString &formula,
+ NumerotationContext *final_context,
+ bool from_start = true,
+ const QUuid &scheme_id = QUuid(),
+ const QSet &reserved = QSet());
+ static QSet reservedLabels(const QETProject *project,
+ const QString &title,
+ const QVector &changing);
+ static RenumberElementsCommand *renumber(QETProject *project,
+ const QString &title,
+ QVector *frozen,
+ const QString &text);
+ static QVector frozenFollowers(const QETProject *project,
+ const QString &title);
+ static QVector editBlockedBy(const QETProject *project,
+ const QString &title,
+ const NumerotationContext &context);
+
+ /// Why assign() leaves an element as it is
+ enum class Skipped {
+ None,
+ FollowsAlready, ///< already follows this scheme
+ Frozen, ///< its label is frozen
+ OtherFormula, ///< follows another scheme or has a formula of its own
+ Linked ///< slave or report: takes its label from its master
+ };
+ static Skipped skipReason(const QETProject *project,
+ const QString &title,
+ const Element *element);
+ /// What assign() would do with each of the given elements
+ struct AssignPlan
+ {
+ QVector todo; ///< given the scheme
+ QVector frozen; ///< a frozen label: left alone unless overwrite
+ QVector otherFormula; ///< follow or hold another formula: left alone unless overwrite
+ int followsAlready = 0; ///< follow this scheme already
+ int linked = 0; ///< slave or report: take their label from their master
+ };
+ static AssignPlan assignPlan(const QETProject *project,
+ const QString &title,
+ const QVector &elements);
+ static RenumberElementsCommand *assign(
+ QETProject *project,
+ const QString &title,
+ const QVector &elements,
+ bool overwrite,
+ int *skipped = nullptr,
+ QUndoCommand *parent = nullptr);
+
+ /// What a numbered element's frozen state becomes
+ enum class FreezeRule {
+ Unfrozen, ///< free: the user asked for this numbering
+ NewElementPolicy ///< as an element just placed: frozen if the folio or project freezes new ones
+ };
+ static RenumberElementsCommand *numberElements(
+ QETProject *project,
+ const QString &title,
+ const QVector &elements,
+ const QString &text,
+ FreezeRule freeze = FreezeRule::Unfrozen,
+ QUndoCommand *parent = nullptr);
+
+ static QString schemeForFormula(const QETProject *project, const QString &formula);
+ static void writeCopiedSchemes(QDomDocument &document,
+ QDomElement &root,
+ const QETProject *project,
+ const QVector &copied);
+ static QList copiedSchemes(const QDomElement &root);
+ static QList missingForPaste(const QETProject *project,
+ const QList &copied,
+ const QList &pasted);
+ /// How the elements of a scheme are numbered by hand (see numberSupport())
+ struct NumberSupport
+ {
+ bool supported = false;
+ int partIndex = -1; ///< the one number part of the definition
+ QString type; ///< "unit", "ten" or "hundred"
+ int increase = 1;
+ };
+ struct GapRange
+ {
+ int from = 0;
+ int to = 0;
+ };
+ /// A counter which would meet numbers already in use
+ struct CounterConflict
+ {
+ int counter = 0; ///< the next number it would hand out
+ int highest = 0; ///< the highest number in use
+ int count = 0; ///< how many elements have a number of at least counter
+ };
+ static std::optional counterConflict(const QETProject *project,
+ const QString &title,
+ const NumerotationContext &proposed);
+ static QSet labelsHeldBesides(const QETProject *project, const Element *element);
+ static NumberSupport numberSupport(const NumerotationContext &context);
+ static std::optional numberOf(const NumberSupport &support, const Element *element);
+ static QList gapRanges(const QETProject *project, const QString &title);
+ static QList freeNumbers(const QETProject *project, const QString &title,
+ const Element *element, int limit = 500);
+ static QString labelForNumber(const QETProject *project, const QString &title,
+ Element *element, int number);
+ static RenumberElementsCommand *assignNumber(QETProject *project,
+ Element *element,
+ int number,
+ QString *problem = nullptr,
+ QUndoCommand *parent = nullptr);
+
+ static QMap> pastedSchemes(
+ const QETProject *project,
+ const QList &elements);
+ static void linkPasted(const QETProject *project,
+ const QList &elements);
+ static int numberPasted(QETProject *project,
+ const QMap> &schemes,
+ QUndoCommand *parent);
+
+ /// Elements whose label and formula change
+ int changedElementCount() const {return m_changes.size();}
+ const QVector &changes() const {return m_changes;}
+ std::optional before() const {return m_before;}
+ std::optional after() const {return m_after;}
+
+ void undo() override;
+ void redo() override;
+
+ private:
+ explicit ElementAutoNumSchemeCommand(QETProject *project);
+
+ QETProject *m_project = nullptr;
+ std::optional m_before;
+ std::optional m_after;
+ QString m_current_before;
+ QString m_current_after;
+ QVector m_changes;
+};
+
+#endif // ELEMENTAUTONUMSCHEMECOMMAND_H
diff --git a/sources/autoNum/renumberelementscommand.cpp b/sources/autoNum/renumberelementscommand.cpp
index 27f2c5912..66c436e14 100644
--- a/sources/autoNum/renumberelementscommand.cpp
+++ b/sources/autoNum/renumberelementscommand.cpp
@@ -26,8 +26,9 @@ RenumberElementsCommand::RenumberElementsCommand(
QVector changes,
QHash old_ctx,
QHash new_ctx,
- const QString &text)
- : QUndoCommand(text)
+ const QString &text,
+ QUndoCommand *parent)
+ : QUndoCommand(text, parent)
, project_(project)
, changes_(std::move(changes))
, old_ctx_(std::move(old_ctx))
@@ -61,18 +62,23 @@ void RenumberElementsCommand::apply(bool use_new)
// Apply per-element changes
for (const ElementChange &c : changes_) {
- if (!c.element) continue;
-
- const bool frozen = use_new ? c.new_frozen : c.old_frozen;
- const auto &infos = use_new ? c.new_infos : c.old_infos;
- const auto &seq = use_new ? c.new_seq : c.old_seq;
-
- // Temporarily unfreeze so that label/infos update correctly.
- const bool was_frozen = c.element->isFreezeLabel();
- if (was_frozen) c.element->freezeLabel(false);
-
- c.element->rSequenceStruct() = seq;
- c.element->setElementInformations(infos);
- c.element->freezeLabel(frozen);
+ applyChange(c, use_new);
}
}
+
+void RenumberElementsCommand::applyChange(const ElementChange &c, bool use_new)
+{
+ if (!c.element) return;
+
+ const bool frozen = use_new ? c.new_frozen : c.old_frozen;
+ const auto &infos = use_new ? c.new_infos : c.old_infos;
+ const auto &seq = use_new ? c.new_seq : c.old_seq;
+
+ // Temporarily unfreeze so that label/infos update correctly.
+ const bool was_frozen = c.element->isFreezeLabel();
+ if (was_frozen) c.element->freezeLabel(false);
+
+ c.element->rSequenceStruct() = seq;
+ c.element->setElementInformations(infos);
+ c.element->freezeLabel(frozen);
+}
diff --git a/sources/autoNum/renumberelementscommand.h b/sources/autoNum/renumberelementscommand.h
index 4c3d1c871..cf3bff21e 100644
--- a/sources/autoNum/renumberelementscommand.h
+++ b/sources/autoNum/renumberelementscommand.h
@@ -51,11 +51,17 @@ public:
QVector changes,
QHash old_ctx,
QHash new_ctx,
- const QString &text);
+ const QString &text,
+ QUndoCommand *parent = nullptr);
+
+ const QVector &changes() const { return changes_; }
void undo() override;
void redo() override;
+ /// Apply one change's old (@p use_new false) or new state.
+ static void applyChange(const ElementChange &change, bool use_new);
+
private:
void apply(bool use_new);
diff --git a/sources/autoNum/ui/autonumberingdockwidget.cpp b/sources/autoNum/ui/autonumberingdockwidget.cpp
index 39f0aa985..61450f7ac 100644
--- a/sources/autoNum/ui/autonumberingdockwidget.cpp
+++ b/sources/autoNum/ui/autonumberingdockwidget.cpp
@@ -19,6 +19,11 @@
#include "../../diagram.h"
#include "../../diagramview.h"
+#include "../../qetgraphicsitem/element.h"
+#include "../../qeticons.h"
+#include "../elementautonumschemecommand.h"
+#include "counterwarning.h"
+#include "renumberpreviewdialog.h"
#include "../../qetapp.h"
#include "../../shortcutmanager.h"
#include "../../titleblockproperties.h"
@@ -29,7 +34,10 @@
#include "../../undocommand/changetitleblockcommand.h"
#include
+#include
#include
+#include
+#include
#include
#include
@@ -43,9 +51,127 @@ AutoNumberingDockWidget::AutoNumberingDockWidget(QWidget *parent) :
ui(new Ui::AutoNumberingDockWidget)
{
ui->setupUi(this);
+
+ //The element row gets a button to the left of its reset button
+ //which gives the numbering shown to the selected elements
+ QPushButton *reset = ui->m_element_reset_start_pb;
+ ui->gridLayout->removeWidget(reset);
+ auto *actions = new QWidget(this);
+ auto *actions_layout = new QHBoxLayout(actions);
+ actions_layout->setContentsMargins(0, 0, 0, 0);
+ actions_layout->setSpacing(2);
+ m_element_apply_pb = new QPushButton(QET::Icons::DialogOk, QString(), actions);
+ m_element_apply_pb->setObjectName(QStringLiteral("m_element_apply_pb"));
+ m_element_apply_pb->setMaximumWidth(24);
+ m_element_apply_pb->setToolTip(tr("Appliquer cette numérotation aux éléments sélectionnés"));
+ reset->setParent(actions);
+ actions_layout->addWidget(m_element_apply_pb);
+ actions_layout->addWidget(reset);
+ ui->gridLayout->addWidget(actions, 3, 2);
+ connect(m_element_apply_pb, &QPushButton::clicked,
+ this, &AutoNumberingDockWidget::applyElementSchemeToSelection);
+ connect(ui->m_element_cb, qOverload(&QComboBox::currentIndexChanged),
+ this, &AutoNumberingDockWidget::updateApplyEnabled);
+ m_element_apply_pb->setEnabled(false);
+
this->setDisabled(true);
}
+/**
+ @brief AutoNumberingDockWidget::applyElementSchemeToSelection
+ Give the element numbering shown in the list to the elements selected
+ in the current folio: each takes its formula and the next number, in
+ folio and position order, and the counter moves on.
+
+ What would change is shown first. An element which holds another
+ numbering, or whose label is frozen, is left as it is, unless the user
+ asks to replace them too (a frozen one is then given back). One undo
+ step.
+*/
+void AutoNumberingDockWidget::applyElementSchemeToSelection()
+{
+ if (!m_project || m_project->isReadOnly()) {
+ return;
+ }
+ const QString caption = tr("Appliquer la numérotation");
+ const QString title = ui->m_element_cb->currentText();
+ DiagramView *view = m_project_view ? m_project_view->currentDiagram() : nullptr;
+ Diagram *diagram = view ? view->diagram() : nullptr;
+ if (!diagram || !m_project->elementAutoNum().contains(title)) {
+ QMessageBox::information(this, caption,
+ tr("Choisissez une numérotation d'éléments du projet."));
+ return;
+ }
+
+ QVector selected;
+ const auto items = diagram->selectedItems();
+ for (QGraphicsItem *item : items) {
+ if (auto *el = qgraphicsitem_cast(item)) {
+ selected << el;
+ }
+ }
+ if (selected.isEmpty()) {
+ QMessageBox::information(this, caption,
+ tr("Sélectionnez d'abord des éléments dans le folio."));
+ return;
+ }
+
+ const auto plan = ElementAutoNumSchemeCommand::assignPlan(m_project, title, selected);
+ QVector left;
+ for (Element *el : plan.frozen) {
+ left.append({el, tr("(figé)")});
+ }
+ for (Element *el : plan.otherFormula) {
+ left.append({el, tr("(autre numérotation)")});
+ }
+ const int replaceable = static_cast(left.size());
+
+ auto *cmd = ElementAutoNumSchemeCommand::assign(m_project, title, selected, false);
+ if (!cmd && !replaceable) {
+ QMessageBox::information(
+ this, caption,
+ tr("Rien à appliquer : les éléments sélectionnés suivent déjà "
+ "la numérotation « %1 », ou prennent leur nom d'un autre élément.").arg(title));
+ return;
+ }
+
+ const QString intro = cmd
+ ? tr("%n élément(s) vont recevoir la numérotation « %1 », dans l'ordre des folios "
+ "et des positions.", "", static_cast(plan.todo.size())).arg(title)
+ : tr("Aucun élément ne peut recevoir la numérotation « %1 » sans remplacer "
+ "ce qu'il a.").arg(title);
+ const auto answer = RenumberPreviewDialog::ask(
+ this, caption, intro,
+ cmd ? cmd->changes() : QVector(),
+ left,
+ replaceable
+ ? tr("Remplacer aussi les %n élément(s) laissés comme ils sont", "", replaceable)
+ : QString());
+
+ if (answer == RenumberPreviewDialog::Answer::Cancel) {
+ delete cmd;
+ return;
+ }
+ int applied = cmd ? static_cast(cmd->changes().size()) : 0;
+ if (answer == RenumberPreviewDialog::Answer::GoAndReplace) {
+ delete cmd;
+ cmd = ElementAutoNumSchemeCommand::assign(m_project, title, selected, true);
+ applied = cmd ? static_cast(cmd->changes().size()) : 0;
+ }
+ if (cmd) {
+ m_project->undoStack()->push(cmd);
+ }
+
+ const int not_given = static_cast(selected.size()) - applied;
+ if (not_given > 0) {
+ QMessageBox::information(
+ this, caption,
+ tr("%n élément(s) numérotés.", "", applied)
+ + QLatin1Char('\n')
+ + tr("%n élément(s) sélectionnés sont restés comme ils sont.", "", not_given));
+ }
+}
+
/**
@brief AutoNumberingDockWidget::~AutoNumberingDockWidget
Destructor
@@ -117,6 +243,8 @@ void AutoNumberingDockWidget::setProject(QETProject *project,
disconnect(m_project, &QETProject::conductorAutoNumRemoved, this, &AutoNumberingDockWidget::conductorAutoNumChanged);
disconnect(m_project, &QETProject::conductorAutoNumAdded, this, &AutoNumberingDockWidget::conductorAutoNumChanged);
disconnect(m_project_view, &ProjectView::diagramActivated, this, &AutoNumberingDockWidget::setConductorActive);
+ disconnect(m_project_view, &ProjectView::diagramActivated, this, &AutoNumberingDockWidget::followSelectionOf);
+ followSelectionOf(nullptr);
//Element Signals
disconnect(m_project, &QETProject::elementAutoNumRemoved, this, &AutoNumberingDockWidget::elementAutoNumChanged);
@@ -142,6 +270,8 @@ void AutoNumberingDockWidget::setProject(QETProject *project,
connect(m_project, &QETProject::conductorAutoNumRemoved, this, &AutoNumberingDockWidget::conductorAutoNumChanged);
connect(m_project, &QETProject::conductorAutoNumAdded, this, &AutoNumberingDockWidget::conductorAutoNumChanged);
connect(m_project_view, &ProjectView::diagramActivated, this, &AutoNumberingDockWidget::setConductorActive);
+ connect(m_project_view, &ProjectView::diagramActivated, this, &AutoNumberingDockWidget::followSelectionOf);
+ followSelectionOf(m_project_view->currentDiagram());
//Element Signals
connect(m_project, &QETProject::elementAutoNumRemoved, this, &AutoNumberingDockWidget::elementAutoNumChanged);
@@ -207,6 +337,56 @@ void AutoNumberingDockWidget::setContext()
this->setActive();
}
+/**
+ @brief AutoNumberingDockWidget::followSelectionOf
+ Enable the apply button only while elements are selected in the folio
+ shown: it gives a numbering to the selected elements, and with none
+ selected there is nothing for it to do.
+ @param view : the folio now shown, nullptr for none
+*/
+void AutoNumberingDockWidget::followSelectionOf(DiagramView *view)
+{
+ Diagram *diagram = view ? view->diagram() : nullptr;
+ if (m_selection_diagram) {
+ disconnect(m_selection_diagram, &QGraphicsScene::selectionChanged,
+ this, &AutoNumberingDockWidget::updateApplyEnabled);
+ }
+ m_selection_diagram = diagram;
+ if (m_selection_diagram) {
+ connect(m_selection_diagram, &QGraphicsScene::selectionChanged,
+ this, &AutoNumberingDockWidget::updateApplyEnabled);
+ }
+ updateApplyEnabled();
+}
+
+/**
+ @brief AutoNumberingDockWidget::updateApplyEnabled
+ The apply button needs a numbering to give and elements selected.
+*/
+void AutoNumberingDockWidget::updateApplyEnabled()
+{
+ if (!m_element_apply_pb) {
+ return;
+ }
+ bool elements_selected = false;
+ if (m_selection_diagram) {
+ const auto items = m_selection_diagram->selectedItems();
+ for (QGraphicsItem *item : items) {
+ if (qgraphicsitem_cast(item)) {
+ elements_selected = true;
+ break;
+ }
+ }
+ }
+ m_element_apply_pb->setEnabled(
+ elements_selected && m_project && !m_project->isReadOnly()
+ && ui->m_element_cb->count() > 0);
+ m_element_apply_pb->setToolTip(
+ elements_selected
+ ? tr("Appliquer cette numérotation aux éléments sélectionnés")
+ : tr("Sélectionnez d'abord des éléments dans le folio"));
+}
+
/**
@brief AutoNumberingDockWidget::setConductorActive
@param dv: activated diagramview
@@ -539,6 +719,12 @@ void AutoNumberingDockWidget::applyValueField(QComboBox *combo_box, QLineEdit *l
}
context.replaceValue(index, typed);
+ //An element counter put back on numbers in use: ask first
+ if (category == AutoNumCategory::Element
+ && !CounterWarning::confirm(this, m_project, combo_box->currentText(), context)) {
+ refreshRow(category);
+ return;
+ }
storeContext(combo_box, category, context);
refreshRow(category);
}
@@ -684,6 +870,12 @@ void AutoNumberingDockWidget::resetAutoNum(QComboBox *combo_box, AutoNumCategory
context.replaceValue(i, QStringLiteral("a"));
}
+ //Back to the first number, over numbers in use: ask first
+ if (category == AutoNumCategory::Element
+ && !CounterWarning::confirm(this, m_project, combo_box->currentText(), context)) {
+ return;
+ }
+
storeContext(combo_box, category, context);
refreshRow(category);
}
diff --git a/sources/autoNum/ui/autonumberingdockwidget.h b/sources/autoNum/ui/autonumberingdockwidget.h
index 8472439bf..e0ac49eb0 100644
--- a/sources/autoNum/ui/autonumberingdockwidget.h
+++ b/sources/autoNum/ui/autonumberingdockwidget.h
@@ -22,9 +22,11 @@
#include "../../qetproject.h"
#include
+#include
class QComboBox;
class QLineEdit;
+class QPushButton;
class QSpinBox;
namespace Ui {
@@ -58,6 +60,9 @@ class AutoNumberingDockWidget : public QDockWidget
void refreshValueFields();
void on_m_configure_pb_clicked();
+ void applyElementSchemeToSelection();
+ void updateApplyEnabled();
+ void followSelectionOf(DiagramView *view);
void on_m_conductor_reset_start_pb_clicked();
void on_m_element_reset_start_pb_clicked();
@@ -126,6 +131,8 @@ class AutoNumberingDockWidget : public QDockWidget
/// this replaces refreshValueField() at each of them.
void refreshRow(AutoNumCategory category);
+ QPushButton *m_element_apply_pb = nullptr;
+ QPointer m_selection_diagram; ///< the folio whose selection enables the apply button
Ui::AutoNumberingDockWidget *ui;
QETProject* m_project = nullptr;
ProjectView* m_project_view = nullptr;
diff --git a/sources/autoNum/ui/autonumberingmanagementw.cpp b/sources/autoNum/ui/autonumberingmanagementw.cpp
index 4e6499548..c50926c9a 100644
--- a/sources/autoNum/ui/autonumberingmanagementw.cpp
+++ b/sources/autoNum/ui/autonumberingmanagementw.cpp
@@ -16,8 +16,11 @@
along with QElectroTech. If not, see .
*/
#include "autonumberingmanagementw.h"
+#include "../elementautonumschemecommand.h"
+#include "renumberpreviewdialog.h"
#include "../../diagram.h"
+#include "../../qetgraphicsitem/element.h"
#include "../../qetproject.h"
#include "../numerotationcontextcommands.h"
#include "formulaautonumberingw.h"
@@ -122,7 +125,42 @@ void AutoNumberingManagementW::on_m_renumber_elements_pb_clicked()
RenumberElementsDialog dlg(titles, this);
if (dlg.exec() != QDialog::Accepted) return;
- project_->renumberElementsBySchemeTitle(dlg.selectedSchemeTitle());
+ const QString title = dlg.selectedSchemeTitle();
+ QVector frozen;
+ auto *cmd = ElementAutoNumSchemeCommand::renumber(
+ project_, title, &frozen,
+ title.isEmpty() ? tr("Renuméroter les éléments")
+ : tr("Renuméroter les éléments (%1)").arg(title));
+ if (!cmd)
+ {
+ QMessageBox::information(
+ this, tr("Renuméroter les éléments"),
+ frozen.isEmpty()
+ ? tr("Aucun élément ne suit cette numérotation.")
+ : tr("Les %n élément(s) qui suivent cette numérotation ont un nom "
+ "figé : rien n'est renuméroté.", "", frozen.size()));
+ return;
+ }
+
+ const int renumbered = static_cast(cmd->changes().size());
+ const int changed = RenumberPreviewDialog::changedLabelCount(cmd->changes());
+ if (!RenumberPreviewDialog::confirm(
+ this, tr("Renuméroter les éléments"),
+ tr("%n élément(s) vont être renumérotés, à partir du premier numéro, "
+ "dans l'ordre des folios et des positions.", "", renumbered),
+ cmd->changes(), frozen)) {
+ delete cmd;
+ return;
+ }
+ project_->undoStack()->push(cmd);
+
+ QString summary = tr("%n élément(s) renumérotés, dont %1 avec un nouveau nom.", "", renumbered)
+ .arg(changed);
+ if (!frozen.isEmpty()) {
+ summary += QLatin1Char('\n') + tr("%n élément(s) au nom figé n'ont pas été touchés, "
+ "et leur numéro n'a pas été redonné.", "", frozen.size());
+ }
+ QMessageBox::information(this, tr("Renuméroter les éléments"), summary);
}
/**
diff --git a/sources/autoNum/ui/counterwarning.cpp b/sources/autoNum/ui/counterwarning.cpp
new file mode 100644
index 000000000..caa8ba9be
--- /dev/null
+++ b/sources/autoNum/ui/counterwarning.cpp
@@ -0,0 +1,50 @@
+/*
+ Copyright 2006-2026 The QElectroTech Team
+ This file is part of QElectroTech.
+
+ QElectroTech is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ QElectroTech is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with QElectroTech. If not, see .
+*/
+#include "counterwarning.h"
+
+#include "../elementautonumschemecommand.h"
+#include "../../qetmessagebox.h"
+
+/**
+ @brief CounterWarning::confirm
+ @param title : the element numbering
+ @param proposed : its definition, with the counter to give it
+ @return true if the counter meets no number in use, or the user goes
+ on; false if the user gives up (and, with nobody to ask, always)
+*/
+bool CounterWarning::confirm(QWidget *parent,
+ const QETProject *project,
+ const QString &title,
+ const NumerotationContext &proposed)
+{
+ const auto conflict = ElementAutoNumSchemeCommand::counterConflict(project, title, proposed);
+ if (!conflict) {
+ return true;
+ }
+ const auto answer = QET::QetMessageBox::question(
+ parent,
+ tr("Compteur de la numérotation"),
+ tr("Le prochain numéro serait %1, mais %n élément(s) de la numérotation « %2 » "
+ "ont déjà un numéro égal ou supérieur (jusqu'à %3).", "", conflict->count)
+ .arg(conflict->counter).arg(title).arg(conflict->highest)
+ + tr("\nLes nouveaux éléments sauteront les numéros déjà pris : ils ne recevront "
+ "pas forcément les numéros à partir de %1.\n\nContinuer ?").arg(conflict->counter),
+ QMessageBox::Yes | QMessageBox::Cancel,
+ QMessageBox::Cancel);
+ return answer == QMessageBox::Yes;
+}
diff --git a/sources/autoNum/ui/counterwarning.h b/sources/autoNum/ui/counterwarning.h
new file mode 100644
index 000000000..58f5dbced
--- /dev/null
+++ b/sources/autoNum/ui/counterwarning.h
@@ -0,0 +1,46 @@
+/*
+ Copyright 2006-2026 The QElectroTech Team
+ This file is part of QElectroTech.
+
+ QElectroTech is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ QElectroTech is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with QElectroTech. If not, see .
+*/
+#ifndef COUNTERWARNING_H
+#define COUNTERWARNING_H
+
+#include "../numerotationcontext.h"
+
+#include
+
+class QETProject;
+class QWidget;
+
+/**
+ @brief The CounterWarning class
+ Asks before the counter of an element numbering is set to a number which
+ elements of it have already: the next elements skip the numbers in use,
+ so the next number is not the one asked for. The user may go on or give
+ up.
+*/
+class CounterWarning
+{
+ Q_DECLARE_TR_FUNCTIONS(CounterWarning)
+
+ public:
+ static bool confirm(QWidget *parent,
+ const QETProject *project,
+ const QString &title,
+ const NumerotationContext &proposed);
+};
+
+#endif // COUNTERWARNING_H
diff --git a/sources/autoNum/ui/pastenumberingimport.cpp b/sources/autoNum/ui/pastenumberingimport.cpp
new file mode 100644
index 000000000..71378af49
--- /dev/null
+++ b/sources/autoNum/ui/pastenumberingimport.cpp
@@ -0,0 +1,94 @@
+/*
+ Copyright 2006-2026 The QElectroTech Team
+ This file is part of QElectroTech.
+
+ QElectroTech is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ QElectroTech is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with QElectroTech. If not, see .
+*/
+#include "pastenumberingimport.h"
+
+#include "../../diagram.h"
+#include "../../diagramcommands.h"
+#include "../../qetmessagebox.h"
+#include "../../qetproject.h"
+
+#include
+
+/// @return the numberings a copy carries, which pasted elements follow
+QList PasteNumberingImport::copiedBy(const QDomDocument &clipboard)
+{
+ return ElementAutoNumSchemeCommand::copiedSchemes(clipboard.documentElement());
+}
+
+/**
+ @brief PasteNumberingImport::push
+ Push the paste of @p content on the undo stack of @p diagram, after
+ asking whether to import the numberings of @p copied which the pasted
+ elements follow and the project does not have.
+ @param parent : parent of the question
+ @param copied : the numberings the copy carries
+*/
+void PasteNumberingImport::push(QWidget *parent,
+ Diagram *diagram,
+ const DiagramContent &content,
+ const QList &copied)
+{
+ QETProject *project = diagram->project();
+ auto *paste = new PasteDiagramCommand(diagram, content);
+
+ QList missing;
+ if (project && !copied.isEmpty()
+ && QSettings().value("diagramcommands/autonumber-pasted-elements", true).toBool()) {
+ missing = ElementAutoNumSchemeCommand::missingForPaste(project, copied, content.m_elements);
+ }
+ if (missing.isEmpty()) {
+ diagram->undoStack().push(paste);
+ return;
+ }
+
+ QStringList names;
+ for (const auto &scheme : std::as_const(missing)) {
+ names << QStringLiteral("« %1 »").arg(scheme.title);
+ }
+ const auto answer = QET::QetMessageBox::question(
+ parent,
+ tr("Numérotation absente de ce projet"),
+ tr("Les éléments collés suivent %n numérotation(s) qui n'existe(nt) pas dans ce projet : %1.\n\n"
+ "Les importer ? Les éléments recevront alors les prochains numéros, à partir de 1. "
+ "Sinon ils seront collés sans numérotation.", "", static_cast(missing.size()))
+ .arg(names.join(QStringLiteral(", "))),
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::Yes);
+ if (answer != QMessageBox::Yes) {
+ diagram->undoStack().push(paste);
+ return;
+ }
+
+ diagram->undoStack().beginMacro(paste->text());
+ for (const auto &scheme : std::as_const(missing))
+ {
+ //Under its own name, or one which is free here
+ QString title = scheme.title;
+ for (int n = 2 ; !project->elementAutoNumNameClash(title).isEmpty() ; ++n) {
+ title = QStringLiteral("%1 (%2)").arg(scheme.title).arg(n);
+ }
+ if (auto *cmd = ElementAutoNumSchemeCommand::create(
+ project, title,
+ ElementAutoNumSchemeCommand::resetForRenumber(scheme.context),
+ scheme.id, false)) {
+ diagram->undoStack().push(cmd);
+ }
+ }
+ diagram->undoStack().push(paste);
+ diagram->undoStack().endMacro();
+}
diff --git a/sources/autoNum/ui/pastenumberingimport.h b/sources/autoNum/ui/pastenumberingimport.h
new file mode 100644
index 000000000..0889ad813
--- /dev/null
+++ b/sources/autoNum/ui/pastenumberingimport.h
@@ -0,0 +1,57 @@
+/*
+ Copyright 2006-2026 The QElectroTech Team
+ This file is part of QElectroTech.
+
+ QElectroTech is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ QElectroTech is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with QElectroTech. If not, see .
+*/
+#ifndef PASTENUMBERINGIMPORT_H
+#define PASTENUMBERINGIMPORT_H
+
+#include "../elementautonumschemecommand.h"
+#include "../../diagramcontent.h"
+
+#include
+#include
+
+class Diagram;
+class QWidget;
+
+/**
+ @brief The PasteNumberingImport class
+ Pastes elements copied from another project, which may follow element
+ numberings this project does not have.
+
+ Such an element would be left with a formula which names no numbering
+ here, and with a label nothing explains. So the user is asked, once
+ for all the numberings missing, whether to import them. Imported, they
+ number the pasted elements from their first number (a number of the other
+ project means nothing here); not imported, the elements are pasted
+ without a numbering.
+
+ The import and the paste are one undo step.
+*/
+class PasteNumberingImport
+{
+ Q_DECLARE_TR_FUNCTIONS(PasteNumberingImport)
+
+ public:
+ static void push(QWidget *parent,
+ Diagram *diagram,
+ const DiagramContent &content,
+ const QList &copied);
+
+ static QList copiedBy(const QDomDocument &clipboard);
+};
+
+#endif // PASTENUMBERINGIMPORT_H
diff --git a/sources/autoNum/ui/renumberpreviewdialog.cpp b/sources/autoNum/ui/renumberpreviewdialog.cpp
new file mode 100644
index 000000000..9d7105960
--- /dev/null
+++ b/sources/autoNum/ui/renumberpreviewdialog.cpp
@@ -0,0 +1,187 @@
+/*
+ Copyright 2006-2026 The QElectroTech Team
+ This file is part of QElectroTech.
+
+ QElectroTech is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ QElectroTech is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with QElectroTech. If not, see .
+*/
+#include "renumberpreviewdialog.h"
+
+#include "../../diagram.h"
+#include "../../qetgraphicsitem/element.h"
+#include "../../qetinformation.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace {
+QString labelOf(const DiagramContext &info)
+{
+ return info.value(QETInformation::ELMT_LABEL).toString();
+}
+
+QString folioOf(const Element *element)
+{
+ return element && element->diagram()
+ ? QString::number(element->diagram()->folioIndex() + 1)
+ : QString();
+}
+} // namespace
+
+/**
+ @brief RenumberPreviewDialog::changedLabelCount
+ @return how many of @p changes give their element another label
+*/
+int RenumberPreviewDialog::changedLabelCount(
+ const QVector &changes)
+{
+ int count = 0;
+ for (const auto &change : changes) {
+ if (labelOf(change.old_infos) != labelOf(change.new_infos)) {
+ ++count;
+ }
+ }
+ return count;
+}
+
+/**
+ @brief RenumberPreviewDialog::ask
+ @param intro : text above the list
+ @param changes : what the operation would do to elements
+ @param left_alone : the elements it leaves as they are, each with the
+ reason shown in place of its new name
+ @param replace_text : when not empty, a third button of that text
+ which goes on and also replaces the elements left alone
+ @return what the user chose
+*/
+RenumberPreviewDialog::Answer RenumberPreviewDialog::ask(
+ QWidget *parent,
+ const QString &title,
+ const QString &intro,
+ const QVector &changes,
+ const QVector &left_alone,
+ const QString &replace_text)
+{
+ QDialog dialog(parent);
+ dialog.setWindowTitle(title);
+ dialog.resize(640, 460);
+ auto *layout = new QVBoxLayout(&dialog);
+
+ const int changed = changedLabelCount(changes);
+ QString text = intro;
+ if (changes.size() > changed) {
+ text += QLatin1Char('\n') + tr("%n élément(s) gardent le nom qu'ils ont.", "", changes.size() - changed);
+ }
+ if (!left_alone.isEmpty()) {
+ text += QLatin1Char('\n') + tr("%n élément(s) restent comme ils sont.", "", left_alone.size());
+ }
+ auto *intro_label = new QLabel(text, &dialog);
+ intro_label->setWordWrap(true);
+ layout->addWidget(intro_label);
+
+ auto *table = new QTableWidget(&dialog);
+ table->setColumnCount(4);
+ table->setHorizontalHeaderLabels({tr("Folio"), tr("Élément"), tr("Nom actuel"), tr("Nouveau nom")});
+ table->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ table->setSelectionBehavior(QAbstractItemView::SelectRows);
+ table->verticalHeader()->setVisible(false);
+ table->horizontalHeader()->setStretchLastSection(true);
+ table->setAlternatingRowColors(true);
+
+ const auto add_row = [table](const QStringList &cells, bool grey) {
+ const int row = table->rowCount();
+ table->insertRow(row);
+ for (int column = 0 ; column < cells.size() ; ++column) {
+ auto *item = new QTableWidgetItem(cells.at(column));
+ if (grey) {
+ QFont font = item->font();
+ font.setItalic(true);
+ item->setFont(font);
+ item->setForeground(QBrush(Qt::gray));
+ }
+ table->setItem(row, column, item);
+ }
+ };
+
+ for (const auto &change : changes) {
+ const QString before = labelOf(change.old_infos);
+ const QString after = labelOf(change.new_infos);
+ if (before != after) {
+ add_row({folioOf(change.element),
+ change.element ? change.element->name() : QString(),
+ before, after}, false);
+ }
+ }
+ for (const LeftAlone &left : left_alone) {
+ if (left.element) {
+ add_row({folioOf(left.element), left.element->name(),
+ labelOf(left.element->elementInformations()), left.note}, true);
+ }
+ }
+ table->resizeColumnsToContents();
+ layout->addWidget(table, 1);
+
+ //With no label to change, OK would do nothing: only Cancel (and, where
+ //there is one, the button which replaces what is left alone) is offered
+ const bool something_to_do = !changes.isEmpty();
+ auto *buttons = new QDialogButtonBox(
+ something_to_do ? (QDialogButtonBox::Ok | QDialogButtonBox::Cancel)
+ : QDialogButtonBox::Cancel, &dialog);
+ Answer answer = Answer::Cancel;
+ QPushButton *replace = nullptr;
+ if (!replace_text.isEmpty()) {
+ replace = buttons->addButton(replace_text, QDialogButtonBox::ActionRole);
+ }
+ QObject::connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
+ QObject::connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
+ if (replace) {
+ QObject::connect(replace, &QPushButton::clicked, &dialog, [&]() {
+ answer = Answer::GoAndReplace;
+ dialog.accept();
+ });
+ }
+ layout->addWidget(buttons);
+
+ if (dialog.exec() == QDialog::Accepted && answer != Answer::GoAndReplace) {
+ answer = Answer::Go;
+ }
+ return answer;
+}
+
+/**
+ @brief RenumberPreviewDialog::confirm
+ @param intro : text above the list
+ @param changes : what the operation would do to elements
+ @param frozen : the elements it leaves as they are, a frozen label
+ @return true if the user agrees to go on
+*/
+bool RenumberPreviewDialog::confirm(
+ QWidget *parent,
+ const QString &title,
+ const QString &intro,
+ const QVector &changes,
+ const QVector &frozen)
+{
+ QVector left;
+ for (Element *el : frozen) {
+ left.append({el, tr("(figé)")});
+ }
+ return ask(parent, title, intro, changes, left) == Answer::Go;
+}
diff --git a/sources/autoNum/ui/renumberpreviewdialog.h b/sources/autoNum/ui/renumberpreviewdialog.h
new file mode 100644
index 000000000..5107d605b
--- /dev/null
+++ b/sources/autoNum/ui/renumberpreviewdialog.h
@@ -0,0 +1,68 @@
+/*
+ Copyright 2006-2026 The QElectroTech Team
+ This file is part of QElectroTech.
+
+ QElectroTech is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ QElectroTech is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with QElectroTech. If not, see .
+*/
+#ifndef RENUMBERPREVIEWDIALOG_H
+#define RENUMBERPREVIEWDIALOG_H
+
+#include "../renumberelementscommand.h"
+
+#include
+#include
+
+class Element;
+class QWidget;
+
+/**
+ @brief The RenumberPreviewDialog class
+ Shows what a numbering operation is about to do to the labels of
+ elements -- each changed element with its label now and the one it
+ would get -- and the elements with a frozen label it leaves as they are,
+ and asks to go on or not. Nothing is changed by the dialog itself.
+*/
+class RenumberPreviewDialog
+{
+ Q_DECLARE_TR_FUNCTIONS(RenumberPreviewDialog)
+
+ public:
+ /// An element an operation leaves as it is, and why
+ struct LeftAlone
+ {
+ Element *element = nullptr;
+ QString note;
+ };
+ enum class Answer { Cancel, Go, GoAndReplace };
+
+ static Answer ask(
+ QWidget *parent,
+ const QString &title,
+ const QString &intro,
+ const QVector &changes,
+ const QVector &left_alone,
+ const QString &replace_text = QString());
+
+ static bool confirm(
+ QWidget *parent,
+ const QString &title,
+ const QString &intro,
+ const QVector &changes,
+ const QVector &frozen);
+
+ static int changedLabelCount(
+ const QVector &changes);
+};
+
+#endif // RENUMBERPREVIEWDIALOG_H
diff --git a/sources/autoNum/ui/selectautonumw.cpp b/sources/autoNum/ui/selectautonumw.cpp
index 1d820fde0..9b0865cfd 100644
--- a/sources/autoNum/ui/selectautonumw.cpp
+++ b/sources/autoNum/ui/selectautonumw.cpp
@@ -18,13 +18,17 @@
#include "selectautonumw.h"
#include "../assignvariables.h"
-#include "../numerotationcontextcommands.h"
#include "formulaautonumberingw.h"
#include "numparteditorw.h"
#include "ui_formulaautonumberingw.h"
#include "ui_selectautonumw.h"
+#include "../../qeticons.h"
+
+#include
#include
+#include
+#include
/**
@brief SelectAutonumW::SelectAutonumW
@@ -39,6 +43,24 @@ SelectAutonumW::SelectAutonumW(int type, QWidget *parent) :
{
ui->setupUi(this);
ui->m_comboBox->lineEdit()->setClearButtonEnabled(true);
+
+ //The add button goes under the last part, at the left; each part
+ //has its own remove and move buttons at its right. The one remove
+ //button above the parts, which could only remove the last one, is
+ //not needed any more.
+ ui->remove_button->hide();
+ ui->horizontalLayout->removeWidget(ui->add_button);
+ m_add_row = new QWidget(this);
+ auto *add_layout = new QHBoxLayout(m_add_row);
+ add_layout->setContentsMargins(0, 0, 0, 0);
+ ui->add_button->setParent(m_add_row);
+ add_layout->addWidget(ui->add_button);
+ add_layout->addStretch();
+ ui->editor_layout->addWidget(m_add_row);
+ ui->buttonBox->button(QDialogButtonBox::Reset)->setText(tr("Annuler"));
+ ui->buttonBox->button(QDialogButtonBox::Reset)->setToolTip(
+ tr("Revenir à la définition enregistrée"));
+
if (m_edited_type == 0)
{
m_feaw = new FormulaAutonumberingW();
@@ -103,28 +125,125 @@ void SelectAutonumW::setContext(const NumerotationContext &context)
{
m_context = context;
- qDeleteAll(num_part_list_);
- num_part_list_.clear();
+ clearPartRows();
if (m_context.size() == 0) { //@context contain nothing, build a default numPartEditor
- on_add_button_clicked();
+ insertPartRow(new NumPartEditorW(m_edited_type, this));
}
else {
for (int i=0; i editor_layout -> addWidget(part);
+ insertPartRow(new NumPartEditorW(m_context, i, m_edited_type, this));
}
}
- num_part_list_.size() == 1 ?
- ui -> remove_button -> setDisabled(true):
- ui -> remove_button -> setEnabled (true);
-
+ updatePartButtons();
applyEnable(false);
}
+/**
+ @brief SelectAutonumW::insertPartRow
+ Show @p part as the last part of the definition, with the buttons which
+ remove it and move it up or down at its right.
+*/
+void SelectAutonumW::insertPartRow(NumPartEditorW *part)
+{
+ connect(part, &NumPartEditorW::changed, this, [this]() { applyEnable(); });
+
+ PartRow r;
+ r.part = part;
+ r.row = new QWidget(this);
+ auto *layout = new QHBoxLayout(r.row);
+ layout->setContentsMargins(0, 0, 0, 0);
+ layout->setSpacing(2);
+ part->setParent(r.row);
+ layout->addWidget(part, 1);
+
+ const auto make_button = [&](const QIcon &icon, const QString &tip) {
+ auto *button = new QToolButton(r.row);
+ button->setIcon(icon);
+ button->setToolTip(tip);
+ button->setAutoRaise(true);
+ layout->addWidget(button);
+ return button;
+ };
+ r.up = make_button(QET::Icons::GoUp, tr("Monter cette variable"));
+ r.down = make_button(QET::Icons::GoDown, tr("Descendre cette variable"));
+ r.remove = make_button(QET::Icons::EditDelete, tr("Supprimer cette variable"));
+ QWidget *row = r.row;
+ connect(r.up, &QToolButton::clicked, this, [this, row]() { movePartRow(row, -1); });
+ connect(r.down, &QToolButton::clicked, this, [this, row]() { movePartRow(row, +1); });
+ connect(r.remove, &QToolButton::clicked, this, [this, row]() { removePartRow(row); });
+
+ m_rows << r;
+ num_part_list_ << part;
+ //Before the add button, which stays under the last part
+ ui->editor_layout->insertWidget(ui->editor_layout->count() - 1, r.row);
+ updatePartButtons();
+}
+
+/// Take away every part of the definition shown
+void SelectAutonumW::clearPartRows()
+{
+ for (const PartRow &r : std::as_const(m_rows)) {
+ delete r.row; //with the part, which it holds
+ }
+ m_rows.clear();
+ num_part_list_.clear();
+}
+
+/**
+ @brief SelectAutonumW::updatePartButtons
+ The first part cannot move up, the last cannot move down, and the only
+ one cannot be removed: those buttons are greyed out.
+*/
+void SelectAutonumW::updatePartButtons()
+{
+ for (int i = 0 ; i < m_rows.size() ; ++i) {
+ m_rows.at(i).up->setEnabled(i > 0);
+ m_rows.at(i).down->setEnabled(i < m_rows.size() - 1);
+ m_rows.at(i).remove->setEnabled(m_rows.size() > 1);
+ }
+}
+
+/// Remove the part shown in @p row; the definition keeps at least one
+void SelectAutonumW::removePartRow(QWidget *row)
+{
+ if (m_rows.size() <= 1) {
+ return;
+ }
+ for (int i = 0 ; i < m_rows.size() ; ++i) {
+ if (m_rows.at(i).row == row) {
+ const PartRow r = m_rows.takeAt(i);
+ num_part_list_.removeAll(r.part);
+ delete r.row; //with the part, which it holds
+ break;
+ }
+ }
+ updatePartButtons();
+ applyEnable();
+}
+
+/// Move the part shown in @p row up (-1) or down (+1)
+void SelectAutonumW::movePartRow(QWidget *row, int step)
+{
+ for (int i = 0 ; i < m_rows.size() ; ++i) {
+ if (m_rows.at(i).row != row) {
+ continue;
+ }
+ const int to = i + step;
+ if (to < 0 || to >= m_rows.size()) {
+ return;
+ }
+ m_rows.swapItemsAt(i, to);
+ num_part_list_.swapItemsAt(i, to);
+ ui->editor_layout->removeWidget(row);
+ ui->editor_layout->insertWidget(1 + to, row); //after the header of the columns
+ break;
+ }
+ updatePartButtons();
+ applyEnable();
+}
+
/**
@brief SelectAutonumW::toNumContext
@return the content to num_part_list to NumerotationContext
@@ -143,12 +262,8 @@ NumerotationContext SelectAutonumW::toNumContext() const
*/
void SelectAutonumW::on_add_button_clicked()
{
- applyEnable(false);
- NumPartEditorW *part = new NumPartEditorW(m_edited_type, this);
- connect(part, &NumPartEditorW::changed, this, [this]() { applyEnable(); });
- num_part_list_ << part;
- ui -> editor_layout -> addWidget(part);
- ui -> remove_button -> setEnabled(true);
+ insertPartRow(new NumPartEditorW(m_edited_type, this));
+ applyEnable();
}
/**
@@ -157,16 +272,9 @@ void SelectAutonumW::on_add_button_clicked()
*/
void SelectAutonumW::on_remove_button_clicked()
{
- //remove if @num_part_list contains more than one item
- if (num_part_list_.size() > 1) {
- NumPartEditorW *part = num_part_list_.takeLast();
- // deliberately not disconnecting as not possible to resolve with lambda and will happen automatically when the object "part" is destroyed.
- delete part;
- if (num_part_list_.size() == 1) {
- ui -> remove_button -> setDisabled(true);
- }
+ if (!m_rows.isEmpty()) {
+ removePartRow(m_rows.last().row);
}
- applyEnable();
}
/**
@@ -292,6 +400,8 @@ void SelectAutonumW::on_buttonBox_clicked(QAbstractButton *button)
*/
void SelectAutonumW::applyEnable(bool b)
{
+ //Apply and Cancel both stand for a change: grey until there is one
+ ui->buttonBox->button(QDialogButtonBox::Reset)->setEnabled(b);
if (b){
bool valid= true;
foreach (NumPartEditorW *npe, num_part_list_)
@@ -331,28 +441,6 @@ void SelectAutonumW::contextToFormula()
}
}
-/**
- @brief SelectAutonumW::on_m_next_pb_clicked
- Increase NumerotationContext
-*/
-void SelectAutonumW::on_m_next_pb_clicked()
-{
- NumerotationContextCommands ncc (toNumContext());
- setContext(ncc.next());
- applyEnable(true);
-}
-
-/**
- @brief SelectAutonumW::on_m_previous_pb_clicked
- Decrease NumerotationContext
-*/
-void SelectAutonumW::on_m_previous_pb_clicked()
-{
- NumerotationContextCommands ncc (toNumContext());
- setContext(ncc.previous());
- applyEnable(true);
-}
-
void SelectAutonumW::on_m_comboBox_currentTextChanged(const QString &arg1)
{
Q_UNUSED(arg1);
@@ -363,3 +451,29 @@ void SelectAutonumW::on_m_remove_pb_clicked()
{
emit removeClicked();
}
+
+/**
+ @brief SelectAutonumW::setExplicitNaming
+ Make the list of numberings a plain choice: no typing a name into it
+ (an easy way to create a numbering by accident, or one with an empty
+ name), but a button to create a numbering and one to rename the shown
+ one, which emit newClicked() and renameClicked() for the owner to ask
+ for the name.
+*/
+void SelectAutonumW::setExplicitNaming()
+{
+ ui->m_comboBox->setEditable(false);
+ auto *row = ui->horizontalLayout_2;
+ const int index = row->indexOf(ui->m_remove_pb);
+
+ auto *new_pb = new QPushButton(QET::Icons::Add, QString(), this);
+ new_pb->setToolTip(tr("Nouvelle numérotation…"));
+ connect(new_pb, &QPushButton::clicked, this, &SelectAutonumW::newClicked);
+
+ auto *rename_pb = new QPushButton(QET::Icons::EditRename, QString(), this);
+ rename_pb->setToolTip(tr("Renommer la numérotation…"));
+ connect(rename_pb, &QPushButton::clicked, this, &SelectAutonumW::renameClicked);
+
+ row->insertWidget(index, rename_pb);
+ row->insertWidget(index, new_pb);
+}
diff --git a/sources/autoNum/ui/selectautonumw.h b/sources/autoNum/ui/selectautonumw.h
index 790f81c55..77055b764 100644
--- a/sources/autoNum/ui/selectautonumw.h
+++ b/sources/autoNum/ui/selectautonumw.h
@@ -27,6 +27,8 @@ class NumPartEditorW;
class QAbstractButton;
class FormulaAutonumberingW;
class QComboBox;
+class QToolButton;
+class QHBoxLayout;
namespace Ui {
class SelectAutonumW;
@@ -52,10 +54,14 @@ class SelectAutonumW : public QWidget
void contextToFormula ();
QString formula();
QComboBox *contextComboBox() const;
+ void setExplicitNaming();
signals:
void applyPressed();
void removeClicked();
+ /// Only with setExplicitNaming()
+ void newClicked();
+ void renameClicked();
//SLOT
private slots:
@@ -63,16 +69,30 @@ class SelectAutonumW : public QWidget
void on_remove_button_clicked();
void on_buttonBox_clicked(QAbstractButton *);
void applyEnable (bool = true);
- void on_m_next_pb_clicked();
- void on_m_previous_pb_clicked();
void on_m_comboBox_currentTextChanged(const QString &arg1);
//ATTRIBUTES
void on_m_remove_pb_clicked();
-
+
private:
+ /// A part of the definition, with the buttons at its right
+ struct PartRow
+ {
+ QWidget *row = nullptr;
+ NumPartEditorW *part = nullptr;
+ QToolButton *up = nullptr;
+ QToolButton *down = nullptr;
+ QToolButton *remove = nullptr;
+ };
+ void insertPartRow(NumPartEditorW *part);
+ void removePartRow(QWidget *row);
+ void movePartRow(QWidget *row, int step);
+ void updatePartButtons();
+ void clearPartRows();
Ui::SelectAutonumW *ui;
QList num_part_list_;
+ QList m_rows;
+ QWidget *m_add_row = nullptr; ///< holds the add button, under the last part
NumerotationContext m_context;
FormulaAutonumberingW *m_feaw;
FormulaAutonumberingW *m_fcaw;
diff --git a/sources/autoNum/ui/selectautonumw.ui b/sources/autoNum/ui/selectautonumw.ui
index 4c4626415..2c0ebd63b 100644
--- a/sources/autoNum/ui/selectautonumw.ui
+++ b/sources/autoNum/ui/selectautonumw.ui
@@ -158,32 +158,6 @@
- -
-
-
- Précédent
-
-
-
-
-
-
-
-
-
- -
-
-
- Suivant
-
-
-
-
-
-
-
-
-
-
@@ -227,6 +201,9 @@
-
+
+ Pour un numéro : le prochain numéro attribué. Pour un texte : le texte fixe.
+
Valeur
diff --git a/sources/bordertitleblock.h b/sources/bordertitleblock.h
index 4065bf488..6fc3ea737 100644
--- a/sources/bordertitleblock.h
+++ b/sources/bordertitleblock.h
@@ -133,6 +133,8 @@ class BorderTitleBlock : public QObject
return (additional_fields_); }
/// @return the value of the title block
QString autoPageNum() const { return(btb_auto_page_num_); }
+ /// The title of the folio numbering this folio follows, nothing else changes
+ void setAutoPageNum(const QString &title) { btb_auto_page_num_ = title; }
/// @return the value of the total number of folios
int folioTotal() const { return(folio_total_);}
diff --git a/sources/diagram.cpp b/sources/diagram.cpp
index f7c336ed6..f0268cb42 100644
--- a/sources/diagram.cpp
+++ b/sources/diagram.cpp
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see .
*/
#include "diagram.h"
+#include "autoNum/elementautonumschemecommand.h"
#include "ElementsCollection/elementcollectionhandler.h"
#include "TerminalStrip/GraphicsItem/terminalstripitem.h"
@@ -1290,6 +1291,13 @@ QDomDocument Diagram::toXml(bool whole_content, bool is_copy_command) {
std::stable_sort(list_conductors.begin(), list_conductors.end(),
[](Conductor *a, Conductor *b) { return conductorSortKey(a) < conductorSortKey(b); });
+ // A copy carries the numberings its elements follow: pasted into
+ // another project, which does not know them, it can offer to import
+ // them (see PasteNumberingImport)
+ if (is_copy_command) {
+ ElementAutoNumSchemeCommand::writeCopiedSchemes(document, dom_root, m_project, list_elements);
+ }
+
// correspondence table between the addresses of the terminals and their ids
// table de correspondance entre les adresses des bornes et leurs ids
QHash table_adr_id;
diff --git a/sources/diagramcommands.cpp b/sources/diagramcommands.cpp
index d13a75f4d..23d792df3 100644
--- a/sources/diagramcommands.cpp
+++ b/sources/diagramcommands.cpp
@@ -17,8 +17,10 @@
*/
#include "diagramcommands.h"
+#include "autoNum/elementautonumschemecommand.h"
#include "diagram.h"
#include "itemgroups.h"
+#include "qetproject.h"
#include "qetgraphicsitem/conductortextitem.h"
#include "qetgraphicsitem/diagramimageitem.h"
#include "qetgraphicsitem/dynamicelementtextitem.h"
@@ -29,6 +31,19 @@
#include "qetinformation.h"
#include "qgimanager.h"
+namespace {
+/// Is @p element among those the numberings in @p schemes are given to?
+bool pasted_schemes_has(const QMap> &schemes, const Element *element)
+{
+ for (auto it = schemes.constBegin() ; it != schemes.constEnd() ; ++it) {
+ if (it.value().contains(const_cast(element))) {
+ return true;
+ }
+ }
+ return false;
+}
+} // namespace
+
/**
@brief PasteDiagramCommand::PasteDiagramCommand
Constructor
@@ -64,6 +79,10 @@ void PasteDiagramCommand::undo()
{
diagram -> showMe();
+ //The numbering of the pasted elements, if any: gives the numbers
+ //back to the numberings they came from
+ QUndoCommand::undo();
+
foreach(QGraphicsItem *item, content.items(filter))
diagram->removeItem(item);
}
@@ -98,6 +117,18 @@ void PasteDiagramCommand::redo()
e->initLink(elmts_list);
}
+ //The numberings the pasted elements follow, known now: the label
+ //erasing below empties their formula. Pasted elements get the next
+ //number of their numbering, as placed ones do, instead of the
+ //label of the element they were copied from.
+ const bool autonumber = m_autonumber && settings.value(
+ "diagramcommands/autonumber-pasted-elements", true).toBool();
+ QETProject *project = diagram->project();
+ QMap> pasted_schemes;
+ if (project && autonumber) {
+ pasted_schemes = ElementAutoNumSchemeCommand::pastedSchemes(project, elmts_list);
+ }
+
//make new uuid for every pasted conductor, because old uuid are
//the uuid of the copied conductor
const QList all_pasted_conductors = content.conductors();
@@ -221,6 +252,44 @@ void PasteDiagramCommand::redo()
}
}
+ //What the pasted elements keep of their numbering names a
+ //numbering of this project, or none (a paste from another
+ //project, or from a file written before the ids)
+ if (project) {
+ ElementAutoNumSchemeCommand::linkPasted(project, elmts_list);
+ if (autonumber)
+ {
+ //A formula which names no numbering of this project is
+ //not kept: it would stand for a numbering which does
+ //not exist here, with a label which is not its result.
+ //The label is what the erase preference made of it.
+ for (Element *e : elmts_list)
+ {
+ const DiagramContext &info = e->elementInformations();
+ if (e->linkType() == Element::Slave || (e->linkType() & Element::AllReport)
+ || info.value(QETInformation::ELMT_FORMULA).toString().isEmpty()
+ || !project->elementAutoNumTitle(QUuid(info.value(QETInformation::ELMT_FORMULA_ID).toString())).isEmpty()
+ || pasted_schemes_has(pasted_schemes, e)) {
+ continue;
+ }
+ DiagramContext dc = info;
+ dc.addValue(QETInformation::ELMT_FORMULA, QString());
+ for (DynamicElementTextItem *deti : e->dynamicTextItems())
+ deti->m_block_alignment = true;
+ for (auto *group : e->textGroups())
+ group->blockAlignmentUpdate(true);
+ e->setElementInformations(dc);
+ for (DynamicElementTextItem *deti : e->dynamicTextItems())
+ deti->m_block_alignment = false;
+ for (auto *group : e->textGroups())
+ group->blockAlignmentUpdate(false);
+ }
+ }
+ if (!pasted_schemes.isEmpty()) {
+ ElementAutoNumSchemeCommand::numberPasted(project, pasted_schemes, this);
+ }
+ }
+
//Pasted groups become new groups: the members of one source
//group all get the same new uuid, never the source's, or the
//copy would join the original's group. After the elements got
@@ -250,6 +319,10 @@ void PasteDiagramCommand::redo()
}
}
+ //The numbering of the pasted elements, if any, on the first redo
+ //as well as on the next ones
+ QUndoCommand::redo();
+
const QList qgis_list = content.items();
for (QGraphicsItem *qgi : qgis_list)
qgi -> setSelected(true);
diff --git a/sources/diagramcommands.h b/sources/diagramcommands.h
index 070214b85..aeefea6c1 100644
--- a/sources/diagramcommands.h
+++ b/sources/diagramcommands.h
@@ -42,6 +42,12 @@ class PasteDiagramCommand : public QUndoCommand {
public:
void undo() override;
void redo() override;
+ /**
+ Whether pasted elements which follow an element numbering are
+ numbered by the paste (default true, if the preference allows it).
+ Off for a caller which numbers them itself.
+ */
+ void setAutoNumbering(bool enabled) {m_autonumber = enabled;}
// attributes
private:
@@ -53,6 +59,8 @@ class PasteDiagramCommand : public QUndoCommand {
int filter;
/// prevent the first call to redo()
bool first_redo;
+ /// number the pasted elements which follow an element numbering
+ bool m_autonumber = true;
};
/**
diff --git a/sources/diagramevent/diagrameventaddmacro.cpp b/sources/diagramevent/diagrameventaddmacro.cpp
index ecdf7a007..98fa8ae42 100644
--- a/sources/diagramevent/diagrameventaddmacro.cpp
+++ b/sources/diagramevent/diagrameventaddmacro.cpp
@@ -274,7 +274,10 @@ void DiagramEventAddMacro::addMacro(QPointF final_pos)
QSettings settings;
bool saved_erase = settings.value("diagramcommands/erase-label-on-copy", true).toBool();
settings.setValue("diagramcommands/erase-label-on-copy", false);
- m_diagram->undoStack().push(new PasteDiagramCommand(m_diagram, pasted_content));
+ //and does not number them either: the labels are the ones saved
+ auto *paste = new PasteDiagramCommand(m_diagram, pasted_content);
+ paste->setAutoNumbering(false);
+ m_diagram->undoStack().push(paste);
settings.setValue("diagramcommands/erase-label-on-copy", saved_erase);
}
}
diff --git a/sources/diagramevent/diagrameventaddpaste.cpp b/sources/diagramevent/diagrameventaddpaste.cpp
index e4b1b4ba5..6b1491e52 100644
--- a/sources/diagramevent/diagrameventaddpaste.cpp
+++ b/sources/diagramevent/diagrameventaddpaste.cpp
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see .
*/
#include "diagrameventaddpaste.h"
+#include "../autoNum/ui/pastenumberingimport.h"
#include "../diagram.h"
#include "../diagramcommands.h"
@@ -74,6 +75,7 @@
//Load items at their original XML coordinates.
m_diagram->fromXml(document_xml, QPointF(), false, &m_content);
+ m_copied_schemes = PasteNumberingImport::copiedBy(document_xml);
if (db) {
db->blockSignals(false);
@@ -385,7 +387,10 @@ void DiagramEventAddPaste::commit()
m_finished = true;
m_running = false;
- m_diagram->undoStack().push(new PasteDiagramCommand(m_diagram, m_content));
+ //Asks whether to import the numberings the copy brings, if the
+ //project has not got them
+ PasteNumberingImport::push(m_diagram->views().isEmpty() ? nullptr : m_diagram->views().first(),
+ m_diagram, m_content, m_copied_schemes);
emit finish();
}
diff --git a/sources/diagramevent/diagrameventaddpaste.h b/sources/diagramevent/diagrameventaddpaste.h
index fb56113cc..12aafdd26 100644
--- a/sources/diagramevent/diagrameventaddpaste.h
+++ b/sources/diagramevent/diagrameventaddpaste.h
@@ -20,6 +20,7 @@
#include "diagrameventinterface.h"
#include "../diagramcontent.h"
+#include "../autoNum/elementautonumschemecommand.h"
#include
#include
@@ -90,6 +91,8 @@ class DiagramEventAddPaste : public DiagramEventInterface
void removeItems();
DiagramContent m_content;
+ /// The numberings the pasted copy carries
+ QList m_copied_schemes;
///Each movable item's position relative to the group's top left,
///taken once so repeated moves cannot accumulate rounding drift.
QHash m_relative_pos;
diff --git a/sources/diagramview.cpp b/sources/diagramview.cpp
index f0cd7ca5f..826deac4e 100644
--- a/sources/diagramview.cpp
+++ b/sources/diagramview.cpp
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see .
*/
#include "diagramview.h"
+#include "autoNum/ui/pastenumberingimport.h"
#include "cellruler.h"
#include "lastusedstyle.h"
#include "utils/colordialogdoubleclick.h"
@@ -571,7 +572,10 @@ void DiagramView::paste(const QPointF &pos, QClipboard::Mode clipboard_mode) {
if (content_pasted.count())
{
m_diagram -> clearSelection();
- m_diagram -> undoStack().push(new PasteDiagramCommand(m_diagram, content_pasted));
+ //Asks whether to import the numberings the copy brings, if the
+ //project has not got them
+ PasteNumberingImport::push(this, m_diagram, content_pasted,
+ PasteNumberingImport::copiedBy(document_xml));
adjustSceneRect();
}
}
diff --git a/sources/elementspanelwidget.cpp b/sources/elementspanelwidget.cpp
index 71a0b6b9e..a32099de6 100644
--- a/sources/elementspanelwidget.cpp
+++ b/sources/elementspanelwidget.cpp
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see .
*/
#include "elementspanelwidget.h"
+#include "autoNum/elementautonumschemecommand.h"
#include "diagram.h"
#include "qetgraphicsitem/conductor.h"
#include "editor/ui/qetelementeditor.h"
@@ -699,6 +700,14 @@ void ElementsPanelWidget::duplicateDiagram()
elmt->initLink(new_elements);
}
+ // The numberings the copies follow, known before the label
+ // erasing below empties their formula
+ const bool autonumber = QSettings().value(
+ "diagramcommands/autonumber-pasted-elements", true).toBool();
+ const QMap> copy_schemes = autonumber
+ ? ElementAutoNumSchemeCommand::pastedSchemes(project, new_elements)
+ : QMap>();
+
for (QGraphicsItem *item : new_diagram->items()) {
if (Element *elmt = dynamic_cast(item)) {
// The XML round-trip kept the source elements' uuids. Give the
@@ -835,6 +844,18 @@ void ElementsPanelWidget::duplicateDiagram()
}
}
+ // The copies follow the numberings of the project: they get the
+ // next numbers, as pasted elements do. This duplication is not
+ // undoable, so neither is the numbering: it is done at once.
+ ElementAutoNumSchemeCommand::linkPasted(project, new_elements);
+ if (!copy_schemes.isEmpty())
+ {
+ QUndoCommand numbering;
+ if (ElementAutoNumSchemeCommand::numberPasted(project, copy_schemes, &numbering)) {
+ numbering.redo();
+ }
+ }
+
// Groups too: a group of the copy is its own, so selecting it
// is the same on both folios but the database tells them apart.
QHash renewed_groups;
diff --git a/sources/print/projectprintwindow.cpp b/sources/print/projectprintwindow.cpp
index 3204a8527..2bd922c1c 100644
--- a/sources/print/projectprintwindow.cpp
+++ b/sources/print/projectprintwindow.cpp
@@ -427,7 +427,7 @@ void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter
lines << QETInformation::translatedInfoKey(key) + ": " + info.value(key).toString();
}
for (const QString &key : info.keys()) {
- if (key == "formula") continue;
+ if (key == "formula" || key == QETInformation::ELMT_FORMULA_ID) continue;
QString translated = QETInformation::translatedInfoKey(key);
if (lines.contains(translated + ": " + info.value(key).toString())) continue;
if (info.value(key).toString().isEmpty()) continue;
diff --git a/sources/qetgraphicsitem/element.cpp b/sources/qetgraphicsitem/element.cpp
index 77507f3da..b003b1046 100644
--- a/sources/qetgraphicsitem/element.cpp
+++ b/sources/qetgraphicsitem/element.cpp
@@ -20,6 +20,7 @@
#include "../qetproject.h"
#include "../PropertiesEditor/propertieseditordialog.h"
#include "../autoNum/assignvariables.h"
+#include "../autoNum/elementautonumschemecommand.h"
#include "../autoNum/numerotationcontextcommands.h"
#include "../diagram.h"
#include "../diagramcommands.h"
@@ -1495,8 +1496,41 @@ void Element::setGroupIndexForElement(Element *elmt, int index)
If new information is different of current infotmation emit elementInfoChange
@param dc
*/
+namespace {
+/**
+ An element follows a numbering scheme only through a formula: an
+ information context whose formula is emptied (paste with labels
+ erased, the formula deleted by hand...) no longer names one.
+*/
+void dropOrphanFormulaId(DiagramContext &dc)
+{
+ if (dc.contains(QETInformation::ELMT_FORMULA_ID)
+ && dc.value(QETInformation::ELMT_FORMULA).toString().isEmpty()) {
+ dc.remove(QETInformation::ELMT_FORMULA_ID);
+ }
+}
+} // namespace
+
+/**
+ @brief Element::setFormulaSchemeId
+ Make this element follow the element numbering scheme with uuid @p id,
+ or none if @p id is null, without touching anything else: no label
+ update, no signal, no undo. For QETProject when it ties freshly loaded
+ elements to their schemes.
+*/
+void Element::setFormulaSchemeId(const QUuid &id)
+{
+ if (id.isNull()) {
+ m_data.m_informations.remove(QETInformation::ELMT_FORMULA_ID);
+ } else {
+ m_data.m_informations.addValue(QETInformation::ELMT_FORMULA_ID,
+ id.toString(), false);
+ }
+}
+
void Element::setElementInformations(DiagramContext dc)
{
+ dropOrphanFormulaId(dc);
if (m_data.m_informations == dc) {
return;
}
@@ -1549,6 +1583,7 @@ ElementData Element::elementData() const
*/
void Element::setElementData(ElementData data)
{
+ dropOrphanFormulaId(data.m_informations);
auto old_info = m_data.m_informations;
auto old_plc = m_data.m_type == ElementData::Master && m_data.m_master_type == ElementData::PLC
? m_data.plcMasterData() : ElementData::PlcMasterData();
@@ -1776,15 +1811,48 @@ void Element::setUpFormula(bool code_letter, QUndoCommand *parent_undo)
QString element_currentAutoNum = diagram()
->project()
->elementCurrentAutoNum();
+ const QUuid scheme_id = diagram()->project()->elementAutoNumId(element_currentAutoNum);
+ setFormulaSchemeId(formula.isEmpty() ? QUuid() : scheme_id);
NumerotationContext nc = diagram()
->project()
->elementAutoNum(element_currentAutoNum);
- NumerotationContextCommands ncc (nc);
+
+ //The number given is the first whose label no other element of
+ //the project carries: a counter set back, or an element which
+ //was given a number by hand, must not make a second element
+ //with the same label. The counter then goes on from there.
+ NumerotationContext given = nc;
+ if (!formula.isEmpty())
+ {
+ const QSet held = ElementAutoNumSchemeCommand::labelsHeldBesides(
+ diagram()->project(), this);
+ for (int attempt = 0 ; !held.isEmpty() && attempt < 100000 ; ++attempt)
+ {
+ autonum::sequentialNumbers probe;
+ autonum::setSequential(formula, probe, given, diagram(), element_currentAutoNum);
+ const QString candidate = autonum::AssignVariables::formulaToLabel(
+ formula, probe, diagram(), this, nullptr);
+ if (candidate.isEmpty() || !held.contains(candidate)) {
+ break;
+ }
+ NumerotationContextCommands step (given);
+ const NumerotationContext advanced = step.next();
+ bool moved = advanced.size() != given.size();
+ for (int i = 0 ; !moved && i < given.size() ; ++i) {
+ moved = advanced.itemAt(i) != given.itemAt(i);
+ }
+ if (!moved) {
+ break; //a numbering which cannot go on: nothing to skip to
+ }
+ given = advanced;
+ }
+ }
+ NumerotationContextCommands ncc (given);
m_autoNum_seq.clear();
autonum::setSequential(formula,
m_autoNum_seq,
- nc,
+ given,
diagram(),
element_currentAutoNum);
diff --git a/sources/qetgraphicsitem/element.h b/sources/qetgraphicsitem/element.h
index 33d8b752e..f4c82853f 100644
--- a/sources/qetgraphicsitem/element.h
+++ b/sources/qetgraphicsitem/element.h
@@ -150,6 +150,7 @@ class Element : public QetGraphicsItem
autonum::sequentialNumbers& rSequenceStruct()
{return m_autoNum_seq;}
void setUpFormula(bool code_letter = true, QUndoCommand *parent_undo = nullptr);
+ void setFormulaSchemeId(const QUuid &id);
void setPrefix(QString);
QString getPrefix() const;
void freezeLabel(bool freeze);
diff --git a/sources/qetinformation.h b/sources/qetinformation.h
index ba110b0d1..b0ba1c0a4 100644
--- a/sources/qetinformation.h
+++ b/sources/qetinformation.h
@@ -40,6 +40,13 @@ namespace QETInformation
/** Default information related to element **/
static QString ELMT_LABEL = "label";
static QString ELMT_FORMULA = "formula";
+ /**
+ Uuid of the project's element numbering scheme the element's
+ formula follows. Internal: never shown, edited, exported or
+ printed as an information of its own; it only exists alongside a
+ non-empty ELMT_FORMULA (see Element::setElementInformations()).
+ */
+ static QString ELMT_FORMULA_ID = "formula_id";
static QString ELMT_COMMENT = "comment";
static QString ELMT_FUNCTION = "function";
static QString ELMT_DESCRIPTION = "description";
diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp
index e8ce5998d..cc1817ce0 100644
--- a/sources/qetproject.cpp
+++ b/sources/qetproject.cpp
@@ -23,6 +23,7 @@
#include "autoNum/numerotationcontext.h"
#include "autoNum/numerotationcontextcommands.h"
#include "autoNum/renumberelementscommand.h"
+#include "autoNum/elementautonumschemecommand.h"
#include "diagram.h"
#include "qetgraphicsitem/element.h"
#include "qetapp.h"
@@ -35,6 +36,7 @@
#include "ui/importelementdialog.h"
#include "TerminalStrip/terminalstrip.h"
#include "qetxml.h"
+#include "qetinformation.h"
#include "qetversion.h"
#include "undocommand/adddiagramcommand.h"
@@ -43,27 +45,6 @@
#include
#include
-namespace {
-
-/**
- * @brief Reset numeric fields of a NumerotationContext so renumbering starts at 1.
- * Keeps non-numeric parts (string/idfolio/folio/plant/locmach/elementline/elementcolumn/elementprefix) unchanged.
- */
-NumerotationContext resetContextForRenumber(const NumerotationContext &tmpl)
-{
- NumerotationContext out = tmpl;
- for (int i = 0; i < out.size(); ++i) {
- const QStringList parts = out.itemAt(i);
- if (parts.isEmpty()) continue;
- const QString type = parts.at(0);
- if (out.keyIsNumber(type)) {
- out.replaceValue(i, QStringLiteral("1"));
- }
- }
- return out;
-}
-
-} // namespace
#include
#include
#include
@@ -884,9 +865,8 @@ void QETProject::setCurrrentElementAutonum(QString autoNum) {
@brief QETProject::renumberElementsBySchemeTitle
Renumber existing elements by element autonumbering scheme title.
- Elements do not store the scheme title; they store a "formula" (elementInformations["formula"]).
- This method matches elements to schemes by comparing the stored formula with the formula derived
- from each scheme's NumerotationContext.
+ Elements follow a scheme by its uuid (elementInformations["formula_id"]),
+ see elementsUsingElementAutoNum().
If scheme_title is empty, all schemes are renumbered. Otherwise only that scheme is renumbered.
The operation is undoable.
@@ -894,93 +874,14 @@ void QETProject::setCurrrentElementAutonum(QString autoNum) {
void QETProject::renumberElementsBySchemeTitle(const QString &scheme_title)
{
if (!m_undo_stack) return;
- if (isReadOnly()) return;
- // Build map: scheme title -> canonical formula
- QHash scheme_formula;
- for (const QString &k : m_element_autonum.keys()) {
- if (!scheme_title.isEmpty() && k != scheme_title) continue;
- scheme_formula.insert(k, autonum::numerotationContextToFormula(m_element_autonum.value(k)));
+ auto *cmd = ElementAutoNumSchemeCommand::renumber(
+ this, scheme_title, nullptr,
+ scheme_title.isEmpty() ? tr("Renumber elements")
+ : tr("Renumber elements (%1)").arg(scheme_title));
+ if (cmd) {
+ m_undo_stack->push(cmd);
}
- if (scheme_formula.isEmpty()) return;
-
- // Collect elements per scheme by formula match
- QHash> by_key;
- for (Diagram *d : diagrams()) {
- if (!d) continue;
- const auto items = d->items();
- for (QGraphicsItem *it : items) {
- auto *el = qgraphicsitem_cast(it);
- if (!el) continue;
- if (el->linkType() == Element::Slave || (el->linkType() & Element::AllReport))
- continue;
-
- const QString el_formula = el->elementInformations().value(QStringLiteral("formula")).toString();
- if (el_formula.isEmpty()) continue;
-
- QString matched_key;
- for (auto itf = scheme_formula.constBegin(); itf != scheme_formula.constEnd(); ++itf) {
- if (itf.value() == el_formula) { matched_key = itf.key(); break; }
- }
- if (matched_key.isEmpty()) continue;
- by_key[matched_key].append(el);
- }
- }
- if (by_key.isEmpty()) return;
-
- QVector changes;
- QHash old_ctx;
- QHash new_ctx;
-
- for (auto it = by_key.constBegin(); it != by_key.constEnd(); ++it) {
- old_ctx.insert(it.key(), m_element_autonum.value(it.key()));
- }
-
- for (auto it = by_key.begin(); it != by_key.end(); ++it) {
- const QString key = it.key();
- auto &elements = it.value();
- std::sort(elements.begin(), elements.end(), [](Element *a, Element *b){ return comparPos(a, b); });
-
- NumerotationContext base_tmpl = m_element_autonum.value(key);
- NumerotationContext nc = resetContextForRenumber(base_tmpl);
- NumerotationContextCommands ncc(nc);
-
- for (Element *el : elements) {
- RenumberElementsCommand::ElementChange ch;
- ch.element = el;
- ch.old_infos = el->elementInformations();
- ch.old_seq = el->sequenceStruct();
- ch.old_frozen = el->isFreezeLabel();
- ch.new_frozen = ch.old_frozen; // preserve frozen state
-
- const QString formula = ch.old_infos.value(QStringLiteral("formula")).toString();
- autonum::sequentialNumbers new_seq;
- new_seq.clear();
- autonum::setSequential(formula, new_seq, nc, el->diagram(), key);
-
- DiagramContext new_infos = ch.old_infos;
- new_infos.addValue(QStringLiteral("label"), autonum::AssignVariables::formulaToLabel(formula, new_seq, el->diagram(), el, nullptr));
- ch.new_infos = new_infos;
- ch.new_seq = new_seq;
- changes.append(ch);
-
- // advance
- nc = ncc.next();
- ncc = NumerotationContextCommands(nc);
- }
-
- new_ctx.insert(key, nc);
- }
-
- if (changes.isEmpty()) return;
-
- auto *cmd = new RenumberElementsCommand(
- this,
- changes,
- old_ctx,
- new_ctx,
- scheme_title.isEmpty() ? tr("Renumber elements") : tr("Renumber elements (%1)").arg(scheme_title));
- m_undo_stack->push(cmd);
}
/**
@@ -1043,11 +944,232 @@ void QETProject::addConductorAutoNum(const QString& key, const NumerotationConte
*/
void QETProject::addElementAutoNum(const QString& key, const NumerotationContext& context)
{
+ addElementAutoNum(key, context, QUuid());
+}
+
+/**
+ @brief QETProject::addElementAutoNum
+ Add or replace the element numbering scheme @p key.
+ A scheme that already exists keeps its uuid unless @p id is given;
+ a new one takes @p id, or a new uuid when @p id is null or already
+ used by another scheme.
+ @param key : title of the scheme
+ @param context : its numerotation context
+ @param id : its uuid, null to keep or create one
+*/
+void QETProject::addElementAutoNum(const QString &key,
+ const NumerotationContext &context,
+ const QUuid &id)
+{
+ QUuid scheme_id = id;
+ if (!scheme_id.isNull()) {
+ const QString owner = elementAutoNumTitle(scheme_id);
+ if (!owner.isEmpty() && owner != key) {
+ scheme_id = QUuid();
+ }
+ }
+ if (scheme_id.isNull()) {
+ scheme_id = m_element_autonum_id.value(key);
+ }
+ if (scheme_id.isNull()) {
+ scheme_id = QUuid::createUuid();
+ }
+ m_element_autonum_id.insert(key, scheme_id);
m_element_autonum.insert(key, context);
emit elementAutoNumAdded(key);
emit autoNumContextUpdated();
}
+/**
+ @brief QETProject::elementAutoNumId
+ @return the uuid of the element numbering scheme @p title, null if
+ there is no such scheme
+*/
+QUuid QETProject::elementAutoNumId(const QString &title) const
+{
+ return m_element_autonum_id.value(title);
+}
+
+/**
+ @brief QETProject::elementAutoNumTitle
+ @return the title of the element numbering scheme with uuid @p id,
+ empty if there is none
+*/
+QString QETProject::elementAutoNumTitle(const QUuid &id) const
+{
+ if (id.isNull()) {
+ return QString();
+ }
+ for (auto it = m_element_autonum_id.constBegin();
+ it != m_element_autonum_id.constEnd(); ++it) {
+ if (it.value() == id) {
+ return it.key();
+ }
+ }
+ return QString();
+}
+
+/**
+ @brief QETProject::renameElementAutoNum
+ Give the element numbering scheme @p old_title the title @p new_title.
+ It keeps its uuid, so the elements following it are not touched;
+ the folios' per-scheme maxima of folio sequential numbers and the
+ project's current scheme follow the new title.
+ Not undoable by itself: see ElementAutoNumSchemeCommand.
+ @return false if there is no scheme @p old_title or a scheme
+ @p new_title already exists
+*/
+bool QETProject::renameElementAutoNum(const QString &old_title, const QString &new_title)
+{
+ if (old_title == new_title) {
+ return m_element_autonum.contains(old_title);
+ }
+ if (!m_element_autonum.contains(old_title)
+ || m_element_autonum.contains(new_title)
+ || new_title.isEmpty()) {
+ return false;
+ }
+
+ m_element_autonum.insert(new_title, m_element_autonum.take(old_title));
+ m_element_autonum_id.insert(new_title, m_element_autonum_id.take(old_title));
+ if (m_current_element_autonum == old_title) {
+ m_current_element_autonum = new_title;
+ }
+
+ for (Diagram *d : std::as_const(m_diagrams_list)) {
+ if (!d) continue;
+ for (auto *hash : {&d->m_elmt_unitfolio_max,
+ &d->m_elmt_tenfolio_max,
+ &d->m_elmt_hundredfolio_max}) {
+ if (hash->contains(old_title)) {
+ hash->insert(new_title, hash->take(old_title));
+ }
+ }
+ }
+
+ emit elementAutoNumRemoved(old_title);
+ emit elementAutoNumAdded(new_title);
+ emit autoNumContextUpdated();
+ return true;
+}
+
+/**
+ @brief QETProject::normalizedAutoNumName
+ @return @p name as it is stored as the title of a numbering scheme
+*/
+QString QETProject::normalizedAutoNumName(const QString &name)
+{
+ return name.simplified();
+}
+
+/**
+ @brief QETProject::elementAutoNumNameClash
+ Two element numbering schemes may not have the same name, compared
+ without regard to case or surrounding white space, so that the name
+ alone identifies a scheme for the user and for scripts.
+ @param name : the name to check
+ @param ignored_title : a scheme not to compare with (the one being
+ renamed)
+ @return the title of the existing scheme @p name clashes with, empty
+ if none
+*/
+QString QETProject::elementAutoNumNameClash(const QString &name,
+ const QString &ignored_title) const
+{
+ const QString wanted = normalizedAutoNumName(name);
+ for (auto it = m_element_autonum.constBegin();
+ it != m_element_autonum.constEnd(); ++it) {
+ if (it.key() == ignored_title) {
+ continue;
+ }
+ if (QString::compare(normalizedAutoNumName(it.key()), wanted,
+ Qt::CaseInsensitive) == 0) {
+ return it.key();
+ }
+ }
+ return QString();
+}
+
+/**
+ @brief QETProject::elementsUsingElementAutoNum
+ @return the elements whose label follows the element numbering
+ scheme @p title, in no particular order
+*/
+QVector QETProject::elementsUsingElementAutoNum(const QString &title) const
+{
+ QVector list;
+ const QUuid id = elementAutoNumId(title);
+ if (id.isNull()) {
+ return list;
+ }
+ for (Diagram *d : m_diagrams_list) {
+ if (!d) continue;
+ const auto items = d->items();
+ for (QGraphicsItem *it : items) {
+ auto *el = qgraphicsitem_cast(it);
+ if (!el) continue;
+ const DiagramContext &info = el->elementInformations();
+ if (info.value(QETInformation::ELMT_FORMULA).toString().isEmpty()) {
+ continue;
+ }
+ if (QUuid(info.value(QETInformation::ELMT_FORMULA_ID).toString()) == id) {
+ list << el;
+ }
+ }
+ }
+ return list;
+}
+
+/**
+ @brief QETProject::linkElementsToElementAutoNums
+ Called once the diagrams of a file are loaded. Makes every element's
+ ELMT_FORMULA_ID name an element numbering scheme of this project:
+ - an id naming one of the schemes is kept;
+ - an id naming none (the scheme was renamed by a version of
+ QElectroTech that lost the ids, or the element was pasted from
+ another project) and, in a file written before the ids existed,
+ an element with no id at all, are linked to the one scheme whose
+ formula is the element's formula; with no such scheme, or more than
+ one, the element is left unlinked.
+ The label is not touched: nothing it is built from changes.
+*/
+void QETProject::linkElementsToElementAutoNums()
+{
+ QHash titles_by_formula;
+ for (auto it = m_element_autonum.constBegin();
+ it != m_element_autonum.constEnd(); ++it) {
+ titles_by_formula[autonum::numerotationContextToFormula(it.value())] << it.key();
+ }
+
+ for (Diagram *d : std::as_const(m_diagrams_list)) {
+ if (!d) continue;
+ const auto items = d->items();
+ for (QGraphicsItem *it : items) {
+ auto *el = qgraphicsitem_cast(it);
+ if (!el) continue;
+ const DiagramContext &info = el->elementInformations();
+ const QString formula = info.value(QETInformation::ELMT_FORMULA).toString();
+ const bool has_id = info.contains(QETInformation::ELMT_FORMULA_ID);
+ const QUuid id(info.value(QETInformation::ELMT_FORMULA_ID).toString());
+
+ if (formula.isEmpty()) {
+ if (has_id) el->setFormulaSchemeId(QUuid());
+ continue;
+ }
+ if (!elementAutoNumTitle(id).isEmpty()) {
+ continue;
+ }
+ if (!has_id && !m_legacy_element_autonums) {
+ continue; //A formula typed by hand
+ }
+ const QStringList matches = titles_by_formula.value(formula);
+ el->setFormulaSchemeId(matches.size() == 1
+ ? elementAutoNumId(matches.first())
+ : QUuid());
+ }
+ }
+}
+
/**
@brief QETProject::addFolioAutoNum
Add a new folio numerotation context. If key already exist,
@@ -1077,6 +1199,7 @@ void QETProject::removeConductorAutoNum(const QString& key) {
void QETProject::removeElementAutoNum(const QString& key)
{
m_element_autonum.remove(key);
+ m_element_autonum_id.remove(key);
emit elementAutoNumRemoved(key);
}
@@ -1934,6 +2057,9 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
readDiagramsXml(xml_project);
const qint64 diagrams_ms = phase_timer.restart();
+ //Tie the elements to the numbering schemes they follow
+ linkElementsToElementAutoNums();
+
//Load the terminal strip
readTerminalStripXml(xml_project);
const qint64 strips_ms = phase_timer.restart();
@@ -2206,7 +2332,22 @@ void QETProject::readDefaultPropertiesXml(QDomDocument &xml_project)
{
NumerotationContext nc;
nc.fromXml(elmt);
- m_element_autonum.insert(elmt.attribute(QStringLiteral("title")), nc);
+ const QString title = elmt.attribute(QStringLiteral("title"));
+ QUuid id(elmt.attribute(QStringLiteral("id")));
+ if (id.isNull() || !elementAutoNumTitle(id).isEmpty()) {
+ //Saved before schemes had an id: derive one, the
+ //same on every load of the file.
+ m_legacy_element_autonums = true;
+ id = derivedItemUuid(QStringLiteral("element_autonum"), title);
+ }
+ m_element_autonum.insert(title, nc);
+ m_element_autonum_id.insert(title, id);
+ }
+ //The id is authoritative, the title is kept for older versions
+ const QString current_title = elementAutoNumTitle(
+ QUuid(element_autonums.attribute(QStringLiteral("current_autonum_id"))));
+ if (!current_title.isEmpty()) {
+ m_current_element_autonum = current_title;
}
}
// Read guides from XML (if missing, e.g. in old projects, list stays empty)
@@ -2372,6 +2513,10 @@ void QETProject::writeDefaultPropertiesXml(QDomElement &xml_element)
//Export Element Autonums
QDomElement element_autonums = xml_document.createElement("element_autonums");
element_autonums.setAttribute("current_autonum", m_current_element_autonum);
+ if (!elementAutoNumId(m_current_element_autonum).isNull()) {
+ element_autonums.setAttribute("current_autonum_id",
+ elementAutoNumId(m_current_element_autonum).toString());
+ }
element_autonums.setAttribute("freeze_new_elements", m_freeze_new_elements ? "true" : "false");
QStringList element_autonum_keys = elementAutoNum().keys();
element_autonum_keys.sort();
@@ -2379,6 +2524,7 @@ void QETProject::writeDefaultPropertiesXml(QDomElement &xml_element)
QDomElement element_autonum = elementAutoNum(key).toXml(xml_document, "element_autonum");
if (key != "" && elementAutoNumFormula(key) != "") {
element_autonum.setAttribute("title", key);
+ element_autonum.setAttribute("id", elementAutoNumId(key).toString());
element_autonum.setAttribute("formula", elementAutoNumFormula(key));
element_autonums.appendChild(element_autonum);
}
diff --git a/sources/qetproject.h b/sources/qetproject.h
index 90fcbd0fb..220ae0e49 100644
--- a/sources/qetproject.h
+++ b/sources/qetproject.h
@@ -39,11 +39,14 @@
#include
#include
+#include
+#include
#include
#include
class Diagram;
+class Element;
class ElementsLocation;
class QETResult;
class TitleBlockTemplate;
@@ -179,12 +182,26 @@ class QETProject : public QObject
QString elementCurrentAutoNum() const;
void setCurrrentElementAutonum(QString autoNum);
+ //Identity of the element numbering schemes. The title is the
+ //name shown to the user and the lookup key of the API; the uuid
+ //is what an element's ELMT_FORMULA_ID refers to, so a scheme can
+ //be renamed or edited without its elements losing track of it.
+ void addElementAutoNum(const QString &key,
+ const NumerotationContext &context,
+ const QUuid &id);
+ QUuid elementAutoNumId(const QString &title) const;
+ QString elementAutoNumTitle(const QUuid &id) const;
+ bool renameElementAutoNum(const QString &old_title, const QString &new_title);
+ QString elementAutoNumNameClash(const QString &name,
+ const QString &ignored_title = QString()) const;
+ static QString normalizedAutoNumName(const QString &name);
+ QVector elementsUsingElementAutoNum(const QString &title) const;
+
/**
* @brief Renumber existing elements by element autonumbering scheme.
*
- * Elements do not store the scheme title but they store the corresponding formula.
- * This operation matches elements to schemes by comparing the stored formula
- * with the formula derived from the scheme's NumerotationContext.
+ * Elements follow a scheme by its uuid (QETInformation::ELMT_FORMULA_ID),
+ * see elementsUsingElementAutoNum().
*
* If @p scheme_title is empty, all schemes are renumbered.
* If @p scheme_title is non-empty, only that scheme is renumbered.
@@ -374,7 +391,13 @@ class QETProject : public QObject
QHash m_folio_autonum;
/// Element Auto Numbering
QHash m_element_autonum; //Title and NumContext hash
+ /// Title -> uuid of each element numbering scheme
+ QHash m_element_autonum_id;
QString m_current_element_autonum;
+ /// True when the loaded file had element numbering schemes
+ /// saved without an id (written before ids existed)
+ bool m_legacy_element_autonums = false;
+ void linkElementsToElementAutoNums();
bool m_auto_conductor = true;
WireHops::Mode m_wire_hops = WireHops::Mode::None;
WiringRules::Settings m_wiring_rules;
diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp
index a00f5d78c..0de2b7497 100644
--- a/sources/scripting/qetscriptapi.cpp
+++ b/sources/scripting/qetscriptapi.cpp
@@ -68,6 +68,8 @@
#include "../TerminalStrip/physicalterminal.h"
#include "../TerminalStrip/realterminal.h"
#include "../TerminalStrip/terminalstrip.h"
+#include "../autoNum/autonumschemecommand.h"
+#include "../autoNum/elementautonumschemecommand.h"
#include "../autoNum/assignvariables.h"
#include "../autoNum/numerotationcontext.h"
#include "../borderproperties.h"
@@ -609,6 +611,11 @@ bool QetScriptApi::setInfoKey(int folioIndex, const QString &elementUuid,
log(QStringLiteral("qet.%1: empty information key").arg(caller));
return false;
}
+ if (key == QETInformation::ELMT_FORMULA_ID) {
+ log(QStringLiteral("qet.%1: \"%2\" is internal, follow a numbering with numberElement()")
+ .arg(caller, key));
+ return false;
+ }
Element *element = findElement(folioIndex, elementUuid);
if (!element) return false;
@@ -616,6 +623,10 @@ bool QetScriptApi::setInfoKey(int folioIndex, const QString &elementUuid,
if (old_info.value(key).toString() == value) return true; // nothing to push
DiagramContext new_info = old_info;
new_info.addValue(key, value);
+ // A formula written by hand follows no numbering scheme
+ if (key == QETInformation::ELMT_FORMULA) {
+ new_info.remove(QETInformation::ELMT_FORMULA_ID);
+ }
auto *cmd = new ChangeElementInformationCommand(element, old_info, new_info);
m_project->undoStack()->push(cmd);
@@ -3110,9 +3121,108 @@ bool QetScriptApi::addAutoNum(const QString &kind, const QString &name, const QS
}
}
- if (kind == QLatin1String("conductor")) m_project->addConductorAutoNum(name, context);
- else if (kind == QLatin1String("element")) m_project->addElementAutoNum(name, context);
- else m_project->addFolioAutoNum(name, context);
+ if (kind == QLatin1String("element")) {
+ // Through the undo stack, and an existing scheme is edited
+ // rather than replaced: the elements following it follow
+ // the new definition (see ElementAutoNumSchemeCommand)
+ ElementAutoNumSchemeCommand *cmd = nullptr;
+ if (m_project->elementAutoNum().contains(name)) {
+ const int frozen = ElementAutoNumSchemeCommand::editBlockedBy(m_project, name, context).size();
+ if (frozen) {
+ log(QStringLiteral("qet.addAutoNum: %1 element(s) with a frozen label follow '%2', "
+ "its formula cannot change").arg(frozen).arg(name));
+ return false;
+ }
+ if (const auto conflict = ElementAutoNumSchemeCommand::counterConflict(m_project, name, context)) {
+ log(QStringLiteral("qet.addAutoNum: the counter %1 of '%2' is at or below %3 number(s) in use "
+ "(up to %4); new elements will skip them")
+ .arg(conflict->counter).arg(name).arg(conflict->count).arg(conflict->highest));
+ }
+ cmd = ElementAutoNumSchemeCommand::edit(m_project, name, name, context, false);
+ } else {
+ const QString problem = ElementAutoNumSchemeCommand::nameProblem(m_project, name);
+ if (!problem.isEmpty()) {
+ log(QStringLiteral("qet.addAutoNum: %1").arg(problem));
+ return false;
+ }
+ cmd = ElementAutoNumSchemeCommand::create(m_project, name, context, QUuid(), false);
+ }
+ if (cmd) m_project->undoStack()->push(cmd);
+ return true;
+ }
+ // Conductor and folio numberings: undoable too, an existing one is
+ // edited, and a new name is unique and not empty
+ const auto scheme_kind = kind == QLatin1String("conductor")
+ ? AutoNumSchemeCommand::Kind::Conductor
+ : AutoNumSchemeCommand::Kind::Folio;
+ AutoNumSchemeCommand *cmd = nullptr;
+ if (AutoNumSchemeCommand::contains(m_project, scheme_kind, name)) {
+ cmd = AutoNumSchemeCommand::edit(m_project, scheme_kind, name, name, context);
+ } else {
+ const QString problem = AutoNumSchemeCommand::nameProblem(m_project, scheme_kind, name);
+ if (!problem.isEmpty()) {
+ log(QStringLiteral("qet.addAutoNum: %1").arg(problem));
+ return false;
+ }
+ cmd = AutoNumSchemeCommand::create(m_project, scheme_kind, name, context);
+ }
+ if (cmd) m_project->undoStack()->push(cmd);
+ return true;
+}
+
+/**
+ @brief QetScriptApi::renameAutoNum
+ Rename a numbering context. What follows it follows it under its new
+ name: the elements for an element numbering, the folios for a conductor
+ or folio numbering. Undoable.
+*/
+bool QetScriptApi::renameAutoNum(const QString &kind, const QString &name, const QString &newName)
+{
+ if (!m_project) return false;
+ if (m_project->isReadOnly()) {
+ log(QStringLiteral("qet.renameAutoNum: project is read-only"));
+ return false;
+ }
+ if (kind != QLatin1String("element") && kind != QLatin1String("conductor")
+ && kind != QLatin1String("folio")) {
+ log(QStringLiteral("qet.renameAutoNum: unknown kind '%1'; expected conductor, element or folio").arg(kind));
+ return false;
+ }
+
+ if (kind != QLatin1String("element"))
+ {
+ // The folios which follow it follow it under its new name
+ const auto scheme_kind = kind == QLatin1String("conductor")
+ ? AutoNumSchemeCommand::Kind::Conductor
+ : AutoNumSchemeCommand::Kind::Folio;
+ if (!AutoNumSchemeCommand::contains(m_project, scheme_kind, name)) {
+ log(QStringLiteral("qet.renameAutoNum: no %1 auto-numbering named '%2'").arg(kind, name));
+ return false;
+ }
+ const QString problem = AutoNumSchemeCommand::nameProblem(m_project, scheme_kind, newName, name);
+ if (!problem.isEmpty()) {
+ log(QStringLiteral("qet.renameAutoNum: %1").arg(problem));
+ return false;
+ }
+ auto *cmd = AutoNumSchemeCommand::edit(
+ m_project, scheme_kind, name, newName,
+ AutoNumSchemeCommand::contextOf(m_project, scheme_kind, name));
+ if (cmd) m_project->undoStack()->push(cmd);
+ return true;
+ }
+
+ if (!m_project->elementAutoNum().contains(name)) {
+ log(QStringLiteral("qet.renameAutoNum: no element auto-numbering named '%1'").arg(name));
+ return false;
+ }
+ const QString problem = ElementAutoNumSchemeCommand::nameProblem(m_project, newName, name);
+ if (!problem.isEmpty()) {
+ log(QStringLiteral("qet.renameAutoNum: %1").arg(problem));
+ return false;
+ }
+ auto *cmd = ElementAutoNumSchemeCommand::edit(
+ m_project, name, newName, m_project->elementAutoNum(name), false);
+ if (cmd) m_project->undoStack()->push(cmd);
return true;
}
@@ -3129,9 +3239,30 @@ bool QetScriptApi::removeAutoNum(const QString &kind, const QString &name)
log(QStringLiteral("qet.removeAutoNum: no %1 auto-numbering named '%2'").arg(kind, name));
return false;
}
- if (kind == QLatin1String("conductor")) m_project->removeConductorAutoNum(name);
- else if (kind == QLatin1String("element")) m_project->removeElementAutoNum(name);
- else m_project->removeFolioAutoNum(name);
+ if (kind == QLatin1String("element")) {
+ const int used = m_project->elementsUsingElementAutoNum(name).size();
+ if (used) {
+ log(QStringLiteral("qet.removeAutoNum: %1 element(s) follow '%2', it cannot be removed")
+ .arg(used).arg(name));
+ return false;
+ }
+ if (auto *cmd = ElementAutoNumSchemeCommand::remove(m_project, name)) {
+ m_project->undoStack()->push(cmd);
+ }
+ return true;
+ }
+ const auto scheme_kind = kind == QLatin1String("conductor")
+ ? AutoNumSchemeCommand::Kind::Conductor
+ : AutoNumSchemeCommand::Kind::Folio;
+ const int used = AutoNumSchemeCommand::usersOf(m_project, scheme_kind, name).size();
+ if (used) {
+ log(QStringLiteral("qet.removeAutoNum: %1 folio(s) follow '%2', it cannot be removed")
+ .arg(used).arg(name));
+ return false;
+ }
+ if (auto *cmd = AutoNumSchemeCommand::remove(m_project, scheme_kind, name)) {
+ m_project->undoStack()->push(cmd);
+ }
return true;
}
@@ -3732,7 +3863,8 @@ bool QetScriptApi::numberElement(int folioIndex, const QString &elementUuid)
element->setUpFormula(true);
const DiagramContext new_info = element->elementInformations();
if (new_info.value(QETInformation::ELMT_LABEL) == old_info.value(QETInformation::ELMT_LABEL)
- && new_info.value(QStringLiteral("formula")) == old_info.value(QStringLiteral("formula"))) {
+ && new_info.value(QStringLiteral("formula")) == old_info.value(QStringLiteral("formula"))
+ && new_info.value(QETInformation::ELMT_FORMULA_ID) == old_info.value(QETInformation::ELMT_FORMULA_ID)) {
stack->endMacro();
return false;
}
@@ -3742,6 +3874,129 @@ bool QetScriptApi::numberElement(int folioIndex, const QString &elementUuid)
return true;
}
+/**
+ @brief QetScriptApi::renumberElementAutoNum
+ Number again, from the first number, the elements following the
+ element auto-numbering @p name, in folio and position order, as the
+ "Renumber" button of the project's numberings does. Undoable, one
+ step.
+
+ An element whose label is frozen is left as it is, and its label is
+ not given to another element.
+ @return how many elements were left as they are because their label
+ is frozen, -1 if there is no such numbering or the project is read-only
+*/
+int QetScriptApi::renumberElementAutoNum(const QString &name)
+{
+ if (!m_project) return -1;
+ const QString caller = QStringLiteral("renumberElementAutoNum");
+ if (m_project->isReadOnly()) {
+ log(QStringLiteral("qet.%1: project is read-only").arg(caller));
+ return -1;
+ }
+ if (!m_project->elementAutoNum().contains(name)) {
+ log(QStringLiteral("qet.%1: no element auto-numbering named '%2'").arg(caller, name));
+ return -1;
+ }
+ QVector frozen;
+ if (auto *cmd = ElementAutoNumSchemeCommand::renumber(
+ m_project, name, &frozen,
+ QObject::tr("Renuméroter les éléments (%1)").arg(name))) {
+ m_project->undoStack()->push(cmd);
+ }
+ return static_cast(frozen.size());
+}
+
+/**
+ @brief QetScriptApi::freeElementNumbers
+ The numbers an element which follows an element auto-numbering may be
+ given by hand with assignElementNumber(): from 1 to a few past the
+ highest in use, those no other element of the numbering carries and which
+ would not give it the label of another element. Empty if the element
+ follows no auto-numbering, or one whose numbers are not a single sequence
+ (a folio number, a cycle or letters in the definition).
+*/
+QVariantList QetScriptApi::freeElementNumbers(int folioIndex, const QString &elementUuid)
+{
+ QVariantList list;
+ if (!m_project) return list;
+ Element *element = findElement(folioIndex, elementUuid);
+ if (!element) return list;
+ const QString title = m_project->elementAutoNumTitle(
+ QUuid(element->elementInformations().value(QETInformation::ELMT_FORMULA_ID).toString()));
+ for (int n : ElementAutoNumSchemeCommand::freeNumbers(m_project, title, element)) {
+ list << n;
+ }
+ return list;
+}
+
+/**
+ @brief QetScriptApi::assignElementNumber
+ Give an element which follows an element auto-numbering the number
+ @p number of it, which must be free (see freeElementNumbers()): its label
+ is the formula worked out with that number and it keeps following the
+ numbering. If the counter of the numbering is at or below @p number it
+ moves past it. Undoable, one step.
+*/
+bool QetScriptApi::assignElementNumber(int folioIndex, const QString &elementUuid, int number)
+{
+ if (!m_project) return false;
+ const QString caller = QStringLiteral("assignElementNumber");
+ if (m_project->isReadOnly()) {
+ log(QStringLiteral("qet.%1: project is read-only").arg(caller));
+ return false;
+ }
+ Element *element = findElement(folioIndex, elementUuid);
+ if (!element) return false;
+
+ QString problem;
+ auto *cmd = ElementAutoNumSchemeCommand::assignNumber(m_project, element, number, &problem);
+ if (!cmd) {
+ log(QStringLiteral("qet.%1: %2").arg(caller, problem));
+ return false;
+ }
+ m_project->undoStack()->push(cmd);
+ return true;
+}
+
+/**
+ @brief QetScriptApi::assignElementAutoNum
+ Make an element follow the element auto-numbering @p name: it gets
+ the formula and the next number of that numbering, which moves on.
+ The same command as picking the numbering in the element's
+ information window. Undoable, one step.
+
+ The element is left alone, and false returned, when it follows that
+ numbering already, is a slave or a report, or when something would be
+ lost: a frozen label, or a formula it follows or holds already, unless
+ @p overwrite is true. A label typed by hand, with no formula, is
+ replaced.
+*/
+bool QetScriptApi::assignElementAutoNum(const QString &name, int folioIndex,
+ const QString &elementUuid, bool overwrite)
+{
+ if (!m_project) return false;
+ const QString caller = QStringLiteral("assignElementAutoNum");
+ if (m_project->isReadOnly()) {
+ log(QStringLiteral("qet.%1: project is read-only").arg(caller));
+ return false;
+ }
+ if (!m_project->elementAutoNum().contains(name)) {
+ log(QStringLiteral("qet.%1: no element auto-numbering named '%2'").arg(caller, name));
+ return false;
+ }
+ Element *element = findElement(folioIndex, elementUuid);
+ if (!element) return false;
+
+ auto *cmd = ElementAutoNumSchemeCommand::assign(m_project, name, {element}, overwrite);
+ if (!cmd) {
+ log(QStringLiteral("qet.%1: the element was left as it is").arg(caller));
+ return false;
+ }
+ m_project->undoStack()->push(cmd);
+ return true;
+}
+
/**
@brief QetScriptApi::duplicateElements
Copy elements, and the conductors between them, to another place.
@@ -3751,6 +4006,10 @@ bool QetScriptApi::numberElement(int folioIndex, const QString &elementUuid)
elements are selected for the moment and the previous selection restored
before returning; the paste is Diagram::fromXml() at the position,
followed by one PasteDiagramCommand so it is a single undo step.
+
+ As for a paste in the editor, the copies of elements which follow an
+ element numbering get the next numbers of it (the preference "number
+ pasted elements"), and the numbering moves on; undoing gives them back.
@return the uuids of the new elements, or an empty list on failure
*/
QStringList QetScriptApi::duplicateElements(int fromFolioIndex, const QStringList &elementUuids,
diff --git a/sources/scripting/qetscriptapi.h b/sources/scripting/qetscriptapi.h
index 3c4663a8d..572168a0f 100644
--- a/sources/scripting/qetscriptapi.h
+++ b/sources/scripting/qetscriptapi.h
@@ -523,9 +523,15 @@ class QetScriptApi : public QObject
Q_INVOKABLE QStringList autoNums(const QString &kind) const;
Q_INVOKABLE bool addAutoNum(const QString &kind, const QString &name, const QStringList &parts);
Q_INVOKABLE bool removeAutoNum(const QString &kind, const QString &name);
+ Q_INVOKABLE bool renameAutoNum(const QString &kind, const QString &name, const QString &newName);
Q_INVOKABLE bool useConductorAutoNum(int folioIndex, const QString &name);
Q_INVOKABLE bool useElementAutoNum(const QString &name);
Q_INVOKABLE bool numberElement(int folioIndex, const QString &elementUuid);
+ Q_INVOKABLE int renumberElementAutoNum(const QString &name);
+ Q_INVOKABLE QVariantList freeElementNumbers(int folioIndex, const QString &elementUuid);
+ Q_INVOKABLE bool assignElementNumber(int folioIndex, const QString &elementUuid, int number);
+ Q_INVOKABLE bool assignElementAutoNum(const QString &name, int folioIndex,
+ const QString &elementUuid, bool overwrite);
// -- images, embedded in the project --
Q_INVOKABLE QStringList images(int folioIndex) const;
diff --git a/sources/ui/configpage/generalconfigurationpage.cpp b/sources/ui/configpage/generalconfigurationpage.cpp
index 826679667..74e4a8b26 100644
--- a/sources/ui/configpage/generalconfigurationpage.cpp
+++ b/sources/ui/configpage/generalconfigurationpage.cpp
@@ -131,6 +131,7 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
}
ui->m_use_gesture_trackpad->setChecked(settings.value("diagramview/gestures", false).toBool());
ui->m_save_label_paste->setChecked(settings.value("diagramcommands/erase-label-on-copy", true).toBool());
+ ui->m_autonumber_pasted->setChecked(settings.value("diagramcommands/autonumber-pasted-elements", true).toBool());
ui->m_enable_scripting->setChecked(QetSettings::scriptingEnabled());
#ifdef QET_HAS_SCRIPTING
if (QetSettings::scriptingForcedByEnvironment()) {
@@ -322,6 +323,7 @@ void GeneralConfigurationPage::applyConf()
//DIAGRAM COMMAND
settings.setValue("diagramcommands/erase-label-on-copy", ui->m_save_label_paste->isChecked());
+ settings.setValue("diagramcommands/autonumber-pasted-elements", ui->m_autonumber_pasted->isChecked());
//SCRIPTING
//Left alone while the environment forces it on: the box is disabled
diff --git a/sources/ui/configpage/generalconfigurationpage.ui b/sources/ui/configpage/generalconfigurationpage.ui
index 427c9fc7b..b82d71b83 100644
--- a/sources/ui/configpage/generalconfigurationpage.ui
+++ b/sources/ui/configpage/generalconfigurationpage.ui
@@ -327,6 +327,16 @@
+ -
+
+
+ Numéroter automatiquement les éléments collés ou dupliqués qui suivent une numérotation
+
+
+ Un élément collé reçoit le numéro suivant de sa numérotation au lieu du nom de l'élément copié, et la numérotation avance.
+
+
+
-
@@ -354,7 +364,7 @@
- -
+
-
Qt::Vertical
@@ -1373,6 +1383,7 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
m_use_windows_mode_rb
m_use_tab_mode_rb
m_save_label_paste
+ m_autonumber_pasted
m_use_folio_label
m_border_0
m_autosave_sb
diff --git a/sources/ui/configpage/projectconfigpages.cpp b/sources/ui/configpage/projectconfigpages.cpp
index fdb9444fb..d2873cff8 100644
--- a/sources/ui/configpage/projectconfigpages.cpp
+++ b/sources/ui/configpage/projectconfigpages.cpp
@@ -17,6 +17,10 @@
*/
#include "projectconfigpages.h"
+#include "../autoNum/autonumschemecommand.h"
+#include "../autoNum/elementautonumschemecommand.h"
+#include "../autoNum/ui/counterwarning.h"
+#include "../autoNum/ui/renumberpreviewdialog.h"
#include "../autoNum/numerotationcontext.h"
#include "../autoNum/ui/autonumberingmanagementw.h"
#include "../autoNum/ui/folioautonumbering.h"
@@ -28,6 +32,8 @@
#include "../qetproject.h"
#include "../wiringrules.h"
#include "../wiringruleswarning.h"
+#include "../qetgraphicsitem/element.h"
+#include "../diagram.h"
#include "../borderpropertieswidget.h"
#include "../conductorpropertieswidget.h"
#include "../diagramcontextwidget.h"
@@ -38,6 +44,7 @@
//#include "ui_autonumberingmanagementw.h"
#include
+#include
/**
Constructor
@@ -450,6 +457,33 @@ QIcon ProjectAutoNumConfigPage::icon() const
void ProjectAutoNumConfigPage::applyProjectConf()
{}
+namespace {
+/**
+ A tab of a numbering list over a table of the folios which follow the
+ numbering shown
+*/
+QWidget *schemeTab(SelectAutonumW *saw, QGroupBox *&box, QTableWidget *&table)
+{
+ auto *tab = new QWidget();
+ auto *layout = new QVBoxLayout(tab);
+ layout->setContentsMargins(0, 0, 0, 0);
+ layout->addWidget(saw);
+
+ box = new QGroupBox(tab);
+ auto *box_layout = new QVBoxLayout(box);
+ table = new QTableWidget(0, 2, box);
+ table->setHorizontalHeaderLabels({QObject::tr("Folio"), QObject::tr("Titre")});
+ table->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ table->setSelectionBehavior(QAbstractItemView::SelectRows);
+ table->verticalHeader()->setVisible(false);
+ table->horizontalHeader()->setStretchLastSection(true);
+ table->setAlternatingRowColors(true);
+ box_layout->addWidget(table);
+ layout->addWidget(box, 1);
+ return tab;
+}
+} // namespace
+
/**
@brief ProjectAutoNumConfigPage::initWidgets
Init some widget of this page
@@ -457,6 +491,7 @@ void ProjectAutoNumConfigPage::applyProjectConf()
void ProjectAutoNumConfigPage::initWidgets()
{
QTabWidget *tab_widget = new QTabWidget(this);
+ m_tab_widget = tab_widget;
//Management tab
m_amw = new AutoNumberingManagementW(project());
@@ -464,15 +499,44 @@ void ProjectAutoNumConfigPage::initWidgets()
//Conductor tab
m_saw_conductor = new SelectAutonumW(1);
- tab_widget->addTab(m_saw_conductor, tr("Conducteurs"));
+ m_saw_conductor->setExplicitNaming();
+ tab_widget->addTab(schemeTab(m_saw_conductor, m_conductor_users_box, m_conductor_users), tr("Conducteurs"));
//Element tab
m_saw_element = new SelectAutonumW(0);
- tab_widget->addTab(m_saw_element, tr("Eléments"));
+ m_saw_element->setExplicitNaming();
+ auto *element_tab = new QWidget(this);
+ auto *element_layout = new QVBoxLayout(element_tab);
+ element_layout->setContentsMargins(0, 0, 0, 0);
+ element_layout->addWidget(m_saw_element);
+
+ //The elements which follow the numbering shown: what an edit or a
+ //renumbering would touch, and which of them stop it
+ m_element_users_box = new QGroupBox(element_tab);
+ auto *users_layout = new QVBoxLayout(m_element_users_box);
+ m_element_users = new QTableWidget(0, 5, m_element_users_box);
+ m_element_users->setHorizontalHeaderLabels({tr("N°"), tr("Nom"), tr("Folio"), tr("Élément"), tr("Figé")});
+ m_element_users->setEditTriggers(QAbstractItemView::NoEditTriggers);
+ m_element_users->setSelectionBehavior(QAbstractItemView::SelectRows);
+ m_element_users->verticalHeader()->setVisible(false);
+ m_element_users->horizontalHeader()->setStretchLastSection(true);
+ m_element_users->setAlternatingRowColors(true);
+ users_layout->addWidget(m_element_users);
+ m_assign_number_pb = new QPushButton(tr("Attribuer un numéro libre…"), m_element_users_box);
+ m_assign_number_pb->setToolTip(tr("Donner à l'élément sélectionné un numéro que personne n'a : "
+ "il garde sa numérotation, seul son numéro change."));
+ m_assign_number_pb->setEnabled(false);
+ users_layout->addWidget(m_assign_number_pb, 0, Qt::AlignLeft);
+ connect(m_assign_number_pb, &QPushButton::clicked, this, &ProjectAutoNumConfigPage::assignFreeNumber);
+ connect(m_element_users, &QTableWidget::itemSelectionChanged,
+ this, &ProjectAutoNumConfigPage::updateAssignNumberButton);
+ element_layout->addWidget(m_element_users_box, 1);
+ tab_widget->addTab(element_tab, tr("Eléments"));
//Folio Tab
m_saw_folio = new SelectAutonumW(2);
- tab_widget->addTab(m_saw_folio, tr("Folios"));
+ m_saw_folio->setExplicitNaming();
+ tab_widget->addTab(schemeTab(m_saw_folio, m_folio_users_box, m_folio_users), tr("Folios"));
//AutoNumbering Tab
m_faw = new FolioAutonumberingW(project());
@@ -509,16 +573,13 @@ void ProjectAutoNumConfigPage::readValuesFromProject()
m_saw_folio->contextComboBox()->clear();
//Conductor Tab
- const QStringList strlc(m_project->conductorAutoNum().keys());
- m_saw_conductor->contextComboBox()->addItems(strlc);
+ refreshSchemes(SchemeKind::Conductor, m_project->conductorCurrentAutoNum());
//Element Tab
- const QStringList strle(m_project->elementAutoNum().keys());
- m_saw_element->contextComboBox()->addItems(strle);
+ refreshElementSchemes(m_project->elementCurrentAutoNum());
//Folio Tab
- const QStringList strlf(m_project->folioAutoNum().keys());
- m_saw_folio->contextComboBox()->addItems(strlf);
+ refreshSchemes(SchemeKind::Folio, QString());
//Folio AutoNumbering Tab
m_faw->setContext(m_project->folioAutoNum().keys());
@@ -547,16 +608,22 @@ void ProjectAutoNumConfigPage::buildConnections()
//Conductor Tab
connect(m_saw_conductor, &SelectAutonumW::applyPressed, this, &ProjectAutoNumConfigPage::saveContextConductor);
connect(m_saw_conductor, &SelectAutonumW::removeClicked, this, &ProjectAutoNumConfigPage::removeContextConductor);
+ connect(m_saw_conductor, &SelectAutonumW::newClicked, this, &ProjectAutoNumConfigPage::newContextConductor);
+ connect(m_saw_conductor, &SelectAutonumW::renameClicked, this, &ProjectAutoNumConfigPage::renameContextConductor);
connect(m_saw_conductor->contextComboBox(), &QComboBox::textActivated, this, &ProjectAutoNumConfigPage::updateContextConductor);
//Element Tab
connect(m_saw_element, &SelectAutonumW::applyPressed, this, &ProjectAutoNumConfigPage::saveContextElement);
connect(m_saw_element, &SelectAutonumW::removeClicked, this, &ProjectAutoNumConfigPage::removeContextElement);
+ connect(m_saw_element, &SelectAutonumW::newClicked, this, &ProjectAutoNumConfigPage::newContextElement);
+ connect(m_saw_element, &SelectAutonumW::renameClicked, this, &ProjectAutoNumConfigPage::renameContextElement);
connect(m_saw_element->contextComboBox(), &QComboBox::textActivated, this, &ProjectAutoNumConfigPage::updateContextElement);
//Folio Tab
connect(m_saw_folio, &SelectAutonumW::applyPressed, this, &ProjectAutoNumConfigPage::saveContextFolio);
connect(m_saw_folio, &SelectAutonumW::removeClicked, this, &ProjectAutoNumConfigPage::removeContextFolio);
+ connect(m_saw_folio, &SelectAutonumW::newClicked, this, &ProjectAutoNumConfigPage::newContextFolio);
+ connect(m_saw_folio, &SelectAutonumW::renameClicked, this, &ProjectAutoNumConfigPage::renameContextFolio);
connect(m_saw_folio->contextComboBox(), &QComboBox::textActivated, this, &ProjectAutoNumConfigPage::updateContextFolio);
// Auto Folio Numbering
@@ -572,18 +639,12 @@ void ProjectAutoNumConfigPage::buildConnections()
@param str : key of context stored in project
*/
void ProjectAutoNumConfigPage::updateContextConductor(const QString& str) {
- if (str == tr("Nom de la nouvelle numérotation")) m_saw_conductor -> setContext(NumerotationContext());
- else m_saw_conductor ->setContext(m_project->conductorAutoNum(str));
+ m_saw_conductor->setContext(AutoNumSchemeCommand::contextOf(m_project, SchemeKind::Conductor, str));
+ refreshSchemeUsers(SchemeKind::Conductor);
}
-
-/**
- @brief ProjectAutoNumConfigPage::updateContext_folio
- Display the current selected context for folio
- @param str : key of context stored in project
-*/
void ProjectAutoNumConfigPage::updateContextFolio(const QString& str) {
- if (str == tr("Nom de la nouvelle numérotation")) m_saw_folio -> setContext(NumerotationContext());
- else m_saw_folio ->setContext(m_project->folioAutoNum(str));
+ m_saw_folio->setContext(AutoNumSchemeCommand::contextOf(m_project, SchemeKind::Folio, str));
+ refreshSchemeUsers(SchemeKind::Folio);
}
/**
@@ -593,7 +654,7 @@ void ProjectAutoNumConfigPage::updateContextFolio(const QString& str) {
*/
void ProjectAutoNumConfigPage::updateContextElement(const QString& str)
{
- if (str == tr("Nom de la nouvelle numérotation"))
+ if (str.isEmpty() || !m_project->elementAutoNum().contains(str))
{
m_saw_element->setContext(NumerotationContext());
}
@@ -601,6 +662,306 @@ void ProjectAutoNumConfigPage::updateContextElement(const QString& str)
{
m_saw_element->setContext(m_project->elementAutoNum(str));
}
+ refreshElementUsers();
+}
+
+/**
+ @brief ProjectAutoNumConfigPage::refreshElementUsers
+ List, in folio and position order, the elements which follow the
+ element numbering shown, with their label and whether it is frozen.
+*/
+void ProjectAutoNumConfigPage::refreshElementUsers()
+{
+ if (!m_element_users || !m_element_users_box) {
+ return;
+ }
+ const QString title = m_saw_element->contextComboBox()->currentText();
+ QVector users = m_project->elementsUsingElementAutoNum(title);
+ std::sort(users.begin(), users.end(),
+ [](Element *a, Element *b) { return comparPos(a, b); });
+
+ //With a single sequence of numbers the elements are listed by number,
+ //and the numbers no element has between the lowest and the highest
+ //are shown too: where an element was deleted or given another number
+ using Support = ElementAutoNumSchemeCommand::NumberSupport;
+ const Support support = m_project->elementAutoNum().contains(title)
+ ? ElementAutoNumSchemeCommand::numberSupport(m_project->elementAutoNum().value(title))
+ : Support();
+ struct Entry {
+ int number = 0;
+ int to = 0; ///< the end of a range of free numbers
+ Element *element = nullptr;
+ };
+ QVector entries;
+ QVector unnumbered;
+ for (Element *el : std::as_const(users)) {
+ const auto number = support.supported ? ElementAutoNumSchemeCommand::numberOf(support, el)
+ : std::nullopt;
+ if (support.supported && !number) {
+ unnumbered << Entry{0, 0, el};
+ } else {
+ entries << Entry{number.value_or(0), 0, el};
+ }
+ }
+ if (support.supported) {
+ for (const auto &gap : ElementAutoNumSchemeCommand::gapRanges(m_project, title)) {
+ entries << Entry{gap.from, gap.to, nullptr};
+ }
+ std::stable_sort(entries.begin(), entries.end(),
+ [](const Entry &a, const Entry &b) { return a.number < b.number; });
+ entries += unnumbered;
+ }
+
+ int frozen = 0;
+ int gaps = 0;
+ m_element_users->setRowCount(0);
+ m_element_rows.clear();
+ for (const Entry &entry : std::as_const(entries))
+ {
+ const int row = m_element_users->rowCount();
+ m_element_users->insertRow(row);
+ m_element_rows << QPointer(entry.element);
+
+ QStringList cells;
+ if (entry.element)
+ {
+ const bool is_frozen = entry.element->isFreezeLabel();
+ if (is_frozen) ++frozen;
+ const auto number = support.supported ? ElementAutoNumSchemeCommand::numberOf(support, entry.element)
+ : std::nullopt;
+ cells = QStringList{
+ number ? QString::number(*number) : QString(),
+ entry.element->elementInformations().value(QStringLiteral("label")).toString(),
+ entry.element->diagram() ? QString::number(entry.element->diagram()->folioIndex() + 1) : QString(),
+ entry.element->name(),
+ is_frozen ? tr("figé") : QString()};
+ }
+ else
+ {
+ ++gaps;
+ cells = QStringList{
+ entry.to > entry.number ? QStringLiteral("%1 – %2").arg(entry.number).arg(entry.to)
+ : QString::number(entry.number),
+ tr("— libre —"), QString(),
+ tr("aucun élément n'a ce numéro (supprimé ou renuméroté)"), QString()};
+ }
+ for (int column = 0 ; column < cells.size() ; ++column) {
+ auto *item = new QTableWidgetItem(cells.at(column));
+ if (!entry.element) {
+ QFont font = item->font();
+ font.setItalic(true);
+ item->setFont(font);
+ item->setForeground(QBrush(Qt::gray));
+ }
+ m_element_users->setItem(row, column, item);
+ }
+ }
+ m_element_users->setColumnHidden(0, !support.supported);
+ m_element_users->resizeColumnsToContents();
+ m_element_users_box->setTitle(
+ users.isEmpty()
+ ? tr("Aucun élément ne suit cette numérotation")
+ : tr("%n élément(s) suivent cette numérotation, dont %1 au nom figé", "", users.size())
+ .arg(frozen)
+ + (gaps ? tr(" ; %n numéro(s) sans élément", "", gaps) : QString()));
+ updateAssignNumberButton();
+}
+
+/**
+ @brief ProjectAutoNumConfigPage::updateAssignNumberButton
+ The button which gives an element a free number needs an element row
+ selected, in a numbering whose numbers are one sequence.
+*/
+void ProjectAutoNumConfigPage::updateAssignNumberButton()
+{
+ if (!m_assign_number_pb) {
+ return;
+ }
+ const int row = m_element_users->currentRow();
+ const Element *element = row >= 0 ? m_element_rows.value(row).data() : nullptr;
+ const QString title = m_saw_element->contextComboBox()->currentText();
+ const bool supported = m_project->elementAutoNum().contains(title)
+ && ElementAutoNumSchemeCommand::numberSupport(m_project->elementAutoNum().value(title)).supported;
+ m_assign_number_pb->setEnabled(element && supported && !m_project->isReadOnly());
+ m_assign_number_pb->setToolTip(
+ !supported
+ ? tr("Cette numérotation a plusieurs numéros (ou un numéro par folio) : "
+ "on ne peut pas en choisir un à la main.")
+ : tr("Donner à l'élément sélectionné un numéro que personne n'a : "
+ "il garde sa numérotation, seul son numéro change."));
+}
+
+/**
+ @brief ProjectAutoNumConfigPage::assignFreeNumber
+ Give the element selected in the table a free number, chosen in the list
+ of those which are, each with the label it would give. The element keeps
+ following the numbering. One undo step.
+*/
+void ProjectAutoNumConfigPage::assignFreeNumber()
+{
+ const int row = m_element_users->currentRow();
+ Element *element = row >= 0 ? m_element_rows.value(row).data() : nullptr;
+ const QString title = m_saw_element->contextComboBox()->currentText();
+ const QString caption = tr("Attribuer un numéro libre");
+ if (!element || m_project->isReadOnly()) {
+ return;
+ }
+ if (element->isFreezeLabel()) {
+ QMessageBox::information(this, caption,
+ tr("Le nom de cet élément est figé : dégelez-le d'abord."));
+ return;
+ }
+ const QList numbers = ElementAutoNumSchemeCommand::freeNumbers(m_project, title, element);
+ if (numbers.isEmpty()) {
+ QMessageBox::information(this, caption, tr("Aucun numéro n'est libre."));
+ return;
+ }
+ QStringList items;
+ for (int n : numbers) {
+ items << tr("%1 → %2").arg(n).arg(ElementAutoNumSchemeCommand::labelForNumber(
+ m_project, title, element, n));
+ }
+ bool ok = false;
+ const QString chosen = QInputDialog::getItem(
+ this, caption,
+ tr("Numéro libre pour l'élément « %1 » (%2) :")
+ .arg(element->elementInformations().value(QStringLiteral("label")).toString(), element->name()),
+ items, 0, false, &ok);
+ if (!ok) {
+ return;
+ }
+ QString problem;
+ auto *cmd = ElementAutoNumSchemeCommand::assignNumber(
+ m_project, element, numbers.at(items.indexOf(chosen)), &problem);
+ if (!cmd) {
+ QMessageBox::warning(this, caption, problem);
+ return;
+ }
+ m_project->undoStack()->push(cmd);
+ updateContextElement(title); // the counter may have moved; lists the elements again
+}
+
+/**
+ @brief ProjectAutoNumConfigPage::refreshElementSchemes
+ Fill the list of element numberings from the project and show
+ @p selected, or the first one if there is no such numbering
+*/
+void ProjectAutoNumConfigPage::refreshElementSchemes(const QString &selected)
+{
+ QComboBox *cb = m_saw_element->contextComboBox();
+ const QSignalBlocker blocker(cb);
+ cb->clear();
+ QStringList titles(m_project->elementAutoNum().keys());
+ titles.sort(Qt::CaseInsensitive);
+ cb->addItems(titles);
+ const int index = cb->findText(selected);
+ cb->setCurrentIndex(index >= 0 ? index : 0);
+ updateContextElement(cb->currentText());
+}
+
+/**
+ @brief ProjectAutoNumConfigPage::askElementSchemeName
+ Ask the user for the name of an element numbering until it is an
+ acceptable one or the user gives up.
+ @param title : title of the dialog
+ @param name : name proposed first
+ @param ignored_title : the numbering being renamed, if any
+ @return the name, empty if the user cancelled
+*/
+QString ProjectAutoNumConfigPage::askElementSchemeName(const QString &title,
+ QString name,
+ const QString &ignored_title)
+{
+ for (;;)
+ {
+ bool ok = false;
+ name = QInputDialog::getText(this, title, tr("Nom de la numérotation :"),
+ QLineEdit::Normal, name, &ok);
+ if (!ok) {
+ return QString();
+ }
+ const QString problem = ElementAutoNumSchemeCommand::nameProblem(
+ m_project, name, ignored_title);
+ if (problem.isEmpty()) {
+ return QETProject::normalizedAutoNumName(name);
+ }
+ QMessageBox::warning(this, title, problem);
+ }
+}
+
+/**
+ @brief ProjectAutoNumConfigPage::pushElementSchemeCommand
+ Push @p cmd on the project's undo stack, after the user agreed when it
+ changes the label of elements.
+ @return true if pushed; @p cmd is deleted otherwise
+*/
+bool ProjectAutoNumConfigPage::pushElementSchemeCommand(ElementAutoNumSchemeCommand *cmd)
+{
+ if (!cmd) {
+ return false;
+ }
+ if (const int changed = cmd->changedElementCount())
+ {
+ if (!RenumberPreviewDialog::confirm(
+ this,
+ tr("Modifier la numérotation"),
+ tr("%n élément(s) suivent cette numérotation et vont "
+ "changer de formule et de nom.", "", changed),
+ cmd->changes(), {})) {
+ delete cmd;
+ return false;
+ }
+ }
+ m_project->undoStack()->push(cmd);
+ return true;
+}
+
+/**
+ @brief ProjectAutoNumConfigPage::newContextElement
+ Create an element numbering under a name asked to the user, with a
+ default definition, and show it to be defined.
+*/
+void ProjectAutoNumConfigPage::newContextElement()
+{
+ if (m_project->isReadOnly()) {
+ return;
+ }
+ const QString name = askElementSchemeName(tr("Nouvelle numérotation"),
+ QString(), QString());
+ if (name.isEmpty()) {
+ return;
+ }
+ m_saw_element->setContext(NumerotationContext());
+ if (pushElementSchemeCommand(ElementAutoNumSchemeCommand::create(
+ m_project, name,
+ m_saw_element->toNumContext(),
+ QUuid(), false))) {
+ refreshElementSchemes(name);
+ }
+}
+
+/**
+ @brief ProjectAutoNumConfigPage::renameContextElement
+ Rename the shown element numbering; the elements following it keep
+ following it.
+*/
+void ProjectAutoNumConfigPage::renameContextElement()
+{
+ const QString old_title = m_saw_element->contextComboBox()->currentText();
+ if (m_project->isReadOnly() || !m_project->elementAutoNum().contains(old_title)) {
+ return;
+ }
+ const QString name = askElementSchemeName(tr("Renommer la numérotation"),
+ old_title, old_title);
+ if (name.isEmpty() || name == old_title) {
+ return;
+ }
+ if (pushElementSchemeCommand(ElementAutoNumSchemeCommand::edit(
+ m_project, old_title, name,
+ m_project->elementAutoNum(old_title),
+ false))) {
+ refreshElementSchemes(name);
+ }
}
/**
@@ -609,28 +970,49 @@ void ProjectAutoNumConfigPage::updateContextElement(const QString& str)
*/
void ProjectAutoNumConfigPage::saveContextElement()
{
- // If the text is the default text "Name of new numerotation" save the edited context
- // With the the name "No name"
- if (m_saw_element->contextComboBox()->currentText() == tr("Nom de la nouvelle numérotation"))
- {
- QString title(tr("Sans nom"));
-
- m_project->addElementAutoNum (title, m_saw_element -> toNumContext());
- m_project->setCurrrentElementAutonum(title);
- m_saw_element->contextComboBox()->addItem(tr("Sans nom"));
+ if (m_project->isReadOnly()) {
+ return;
}
- // If the text isn't yet to the autonum of the project, add this new item to the combo box.
- else if ( !m_project -> elementAutoNum().contains( m_saw_element->contextComboBox()->currentText()))
+ const QString title = m_saw_element->contextComboBox()->currentText();
+ ElementAutoNumSchemeCommand *cmd = nullptr;
+ QString shown = title;
+ if (!m_project->elementAutoNum().contains(title))
{
- m_project->addElementAutoNum(m_saw_element->contextComboBox()->currentText(), m_saw_element->toNumContext());
- m_project->setCurrrentElementAutonum(m_saw_element->contextComboBox()->currentText());
- m_saw_element->contextComboBox()->addItem(m_saw_element->contextComboBox()->currentText());
+ //No numbering yet: the definition needs a name
+ shown = askElementSchemeName(tr("Nouvelle numérotation"),
+ QString(), QString());
+ if (shown.isEmpty()) {
+ return;
+ }
+ cmd = ElementAutoNumSchemeCommand::create(
+ m_project, shown, m_saw_element->toNumContext());
}
- // Else, the text already exist in the autonum of the project, just update the context
else
{
- m_project->addElementAutoNum (m_saw_element->contextComboBox() -> currentText(), m_saw_element -> toNumContext());
- m_project->setCurrrentElementAutonum(m_saw_element->contextComboBox()->currentText());
+ const NumerotationContext wanted = m_saw_element->toNumContext();
+ if (const int frozen = ElementAutoNumSchemeCommand::editBlockedBy(
+ m_project, title, wanted).size())
+ {
+ QMessageBox::warning(
+ this, tr("Modifier la numérotation"),
+ tr("La numérotation « %1 » ne peut pas être modifiée : "
+ "%n élément(s) au nom figé la suivent.\n"
+ "Dégelez-les d'abord (voir la liste), ou renommez seulement la "
+ "numérotation.", "", frozen).arg(title));
+ refreshElementSchemes(title); //Back to the definition the project has
+ return;
+ }
+ //A counter put back on numbers in use: the next elements skip them
+ if (!CounterWarning::confirm(this, m_project, title, wanted))
+ {
+ refreshElementSchemes(title); //Back to the definition the project has
+ return;
+ }
+ cmd = ElementAutoNumSchemeCommand::edit(
+ m_project, title, title, wanted);
+ }
+ if (pushElementSchemeCommand(cmd)) {
+ refreshElementSchemes(shown);
}
}
@@ -736,7 +1118,7 @@ void ProjectAutoNumConfigPage::importFromProject()
bool exists = false;
switch (i) {
case 0: exists = m_project->conductorAutoNum().contains(title); break;
- case 1: exists = m_project->elementAutoNum().contains(title); break;
+ case 1: exists = !m_project->elementAutoNumNameClash(title).isEmpty(); break;
default: exists = m_project->folioAutoNum().contains(title); break;
}
@@ -750,6 +1132,7 @@ void ProjectAutoNumConfigPage::importFromProject()
item->setCheckState(exists ? Qt::Unchecked : Qt::Checked);
item->setData(Qt::UserRole, i);
item->setData(Qt::UserRole + 1, title);
+ item->setData(Qt::UserRole + 3, entry.attribute(QStringLiteral("id")));
NumerotationContext nc;
nc.fromXml(entry);
@@ -778,7 +1161,8 @@ void ProjectAutoNumConfigPage::importFromProject()
return;
}
- int imported = 0, skipped = 0, conductors = 0;
+ int imported = 0, skipped = 0, conductors = 0, relabelled = 0, blocked = 0;
+ m_project->undoStack()->beginMacro(tr("Importer des numérotations"));
for (int row = 0 ; row < list->count() ; ++row)
{
QListWidgetItem *item = list->item(row);
@@ -791,9 +1175,9 @@ void ProjectAutoNumConfigPage::importFromProject()
bool exists = false;
switch (category) {
- case 0: exists = m_project->conductorAutoNum().contains(title); break;
- case 1: exists = m_project->elementAutoNum().contains(title); break;
- default: exists = m_project->folioAutoNum().contains(title); break;
+ case 0: exists = !AutoNumSchemeCommand::nameClash(m_project, SchemeKind::Conductor, title).isEmpty(); break;
+ case 1: exists = !m_project->elementAutoNumNameClash(title).isEmpty(); break;
+ default: exists = !AutoNumSchemeCommand::nameClash(m_project, SchemeKind::Folio, title).isEmpty(); break;
}
if (exists && !overwrite_cb->isChecked()) {
++skipped;
@@ -804,14 +1188,54 @@ void ProjectAutoNumConfigPage::importFromProject()
contexts.at(item->data(Qt::UserRole + 2).toInt());
switch (category) {
case 0:
- m_project->addConductorAutoNum(title, nc);
+ {
+ const QString clash = AutoNumSchemeCommand::nameClash(m_project, SchemeKind::Conductor, title);
+ auto *cmd = clash.isEmpty()
+ ? AutoNumSchemeCommand::create(m_project, SchemeKind::Conductor, title, nc)
+ : AutoNumSchemeCommand::edit(m_project, SchemeKind::Conductor, clash, clash, nc);
+ if (cmd) m_project->undoStack()->push(cmd);
++conductors;
break;
- case 1: m_project->addElementAutoNum(title, nc); break;
- default: m_project->addFolioAutoNum(title, nc); break;
+ }
+ case 1:
+ {
+ //Undoable, and an existing numbering is edited, not
+ //replaced: the elements following it follow the
+ //imported definition. A new one keeps the id it has
+ //in the other project when free here, so elements
+ //pasted from there follow it too.
+ const QString clash = m_project->elementAutoNumNameClash(title);
+ if (!clash.isEmpty()
+ && !ElementAutoNumSchemeCommand::editBlockedBy(m_project, clash, nc).isEmpty()) {
+ //Elements with a frozen label follow it: kept as it is
+ ++blocked;
+ continue;
+ }
+ ElementAutoNumSchemeCommand *cmd = clash.isEmpty()
+ ? ElementAutoNumSchemeCommand::create(
+ m_project, title, nc,
+ QUuid(item->data(Qt::UserRole + 3).toString()), false)
+ : ElementAutoNumSchemeCommand::edit(
+ m_project, clash, clash, nc, false);
+ if (cmd) {
+ relabelled += cmd->changedElementCount();
+ m_project->undoStack()->push(cmd);
+ }
+ break;
+ }
+ default:
+ {
+ const QString clash = AutoNumSchemeCommand::nameClash(m_project, SchemeKind::Folio, title);
+ auto *cmd = clash.isEmpty()
+ ? AutoNumSchemeCommand::create(m_project, SchemeKind::Folio, title, nc)
+ : AutoNumSchemeCommand::edit(m_project, SchemeKind::Folio, clash, clash, nc);
+ if (cmd) m_project->undoStack()->push(cmd);
+ break;
+ }
}
++imported;
}
+ m_project->undoStack()->endMacro();
readValuesFromProject();
if (conductors) {
@@ -820,10 +1244,17 @@ void ProjectAutoNumConfigPage::importFromProject()
QMessageBox::information(
this, tr("Import terminé"),
- skipped ? tr("%1 numérotation(s) importée(s), "
- "%2 conservée(s) telles quelles.")
- .arg(imported).arg(skipped)
- : tr("%1 numérotation(s) importée(s).").arg(imported));
+ (skipped ? tr("%1 numérotation(s) importée(s), "
+ "%2 conservée(s) telles quelles.")
+ .arg(imported).arg(skipped)
+ : tr("%1 numérotation(s) importée(s).").arg(imported))
+ + (blocked ? QStringLiteral("\n")
+ + tr("%n numérotation(s) suivie(s) par des éléments au nom figé "
+ "n'ont pas été remplacées.", "", blocked)
+ : QString())
+ + (relabelled ? QStringLiteral("\n")
+ + tr("%n élément(s) ont changé de formule.", "", relabelled)
+ : QString()));
}
/**
@@ -832,15 +1263,23 @@ void ProjectAutoNumConfigPage::importFromProject()
*/
void ProjectAutoNumConfigPage::removeContextElement()
{
- //if default text, return
- if (m_saw_element->contextComboBox()->currentText() == tr("Nom de la nouvelle numérotation"))
+ const QString title = m_saw_element->contextComboBox()->currentText();
+ if (m_project->isReadOnly() || !m_project->elementAutoNum().contains(title)) {
return;
- m_project->removeElementAutoNum (m_saw_element->contextComboBox()->currentText());
- m_saw_element->contextComboBox()->removeItem (m_saw_element->contextComboBox()->currentIndex());
- // removeItem() removes the current selection programmatically but
- // textActivated() does not react to (by design, see buildConnections()).
- // Refresh the displayed pattern explicitly so it matches the new selection.
- updateContextElement(m_saw_element->contextComboBox()->currentText());
+ }
+ if (const int used = m_project->elementsUsingElementAutoNum(title).size())
+ {
+ QMessageBox::information(
+ this, tr("Supprimer la numérotation"),
+ tr("%n élément(s) suivent la numérotation « %1 », elle ne "
+ "peut pas être supprimée.\nDonnez-leur une autre "
+ "numérotation ou un nom fixe d'abord.", "", used)
+ .arg(title));
+ return;
+ }
+ if (pushElementSchemeCommand(ElementAutoNumSchemeCommand::remove(m_project, title))) {
+ refreshElementSchemes(m_project->elementCurrentAutoNum());
+ }
}
/**
@@ -849,52 +1288,16 @@ void ProjectAutoNumConfigPage::removeContextElement()
*/
void ProjectAutoNumConfigPage::saveContextConductor()
{
- // If the text is the default text "Name of new numerotation" save the edited context
- // With the the name "No name"
- if (m_saw_conductor->contextComboBox()-> currentText() == tr("Nom de la nouvelle numérotation"))
- {
- m_project->addConductorAutoNum (tr("Sans nom"), m_saw_conductor -> toNumContext());
- project()->setCurrentConductorAutoNum(tr("Sans nom"));
- m_saw_conductor->contextComboBox()-> addItem(tr("Sans nom"));
- }
- // If the text isn't yet to the autonum of the project, add this new item to the combo box.
- else if ( !m_project -> conductorAutoNum().contains( m_saw_conductor->contextComboBox()->currentText()))
- {
- project()->addConductorAutoNum(m_saw_conductor->contextComboBox()->currentText(), m_saw_conductor->toNumContext());
- project()->setCurrentConductorAutoNum(m_saw_conductor->contextComboBox()->currentText());
- m_saw_conductor->contextComboBox()-> addItem(m_saw_conductor->contextComboBox()->currentText());
- }
- // Else, the text already exist in the autonum of the project, just update the context
- else
- {
- project()->setCurrentConductorAutoNum(m_saw_conductor->contextComboBox()->currentText());
- m_project->addConductorAutoNum (m_saw_conductor->contextComboBox()-> currentText(), m_saw_conductor -> toNumContext());
- }
- project()->conductorAutoNumAdded();
+ saveScheme(SchemeKind::Conductor);
}
/**
- @brief ProjectAutoNumConfigPage::saveContext_folio
- Save the current displayed folio context in project
+ @brief ProjectAutoNumConfigPage::saveContextFolio
+ Apply the definition shown to the folio numbering shown
*/
void ProjectAutoNumConfigPage::saveContextFolio()
{
- // If the text is the default text "Name of new numerotation" save the edited context
- // With the the name "No name"
- if (m_saw_folio->contextComboBox() -> currentText() == tr("Nom de la nouvelle numérotation")) {
- m_project->addFolioAutoNum (tr("Sans nom"), m_saw_folio -> toNumContext());
- m_saw_folio->contextComboBox() -> addItem(tr("Sans nom"));
- }
- // If the text isn't yet to the autonum of the project, add this new item to the combo box.
- else if ( !m_project -> folioAutoNum().contains( m_saw_folio->contextComboBox()->currentText())) {
- project()->addFolioAutoNum(m_saw_folio->contextComboBox()->currentText(), m_saw_folio->toNumContext());
- m_saw_folio->contextComboBox() -> addItem(m_saw_folio->contextComboBox()->currentText());
- }
- // Else, the text already exist in the autonum of the project, just update the context
- else {
- m_project->addFolioAutoNum (m_saw_folio->contextComboBox() -> currentText(), m_saw_folio -> toNumContext());
- }
- project()->folioAutoNumAdded();
+ saveScheme(SchemeKind::Folio);
}
/**
@@ -1024,32 +1427,201 @@ void ProjectAutoNumConfigPage::applyManagement()
*/
void ProjectAutoNumConfigPage::removeContextConductor()
{
- //if default text, return
- if ( m_saw_conductor->contextComboBox()-> currentText() == tr("Nom de la nouvelle numérotation") ) return;
- m_project -> removeConductorAutoNum (m_saw_conductor->contextComboBox()-> currentText() );
- m_saw_conductor->contextComboBox()-> removeItem (m_saw_conductor->contextComboBox()-> currentIndex() );
- // removeItem() removes the current selection programmatically but
- // textActivated() does not react to (by design, see buildConnections()).
- // Refresh the displayed pattern explicitly so it matches the new selection.
- updateContextConductor(m_saw_conductor->contextComboBox()->currentText());
- project()->conductorAutoNumRemoved();
+ removeScheme(SchemeKind::Conductor);
}
/**
- @brief ProjectAutoNumConfigPage::removeContext_folio
- Remove from project the current folio numerotation context
+ @brief ProjectAutoNumConfigPage::removeContextFolio
+ Remove from project the folio numerotation context shown
*/
void ProjectAutoNumConfigPage::removeContextFolio()
{
- //if default text, return
- if ( m_saw_folio->contextComboBox() -> currentText() == tr("Nom de la nouvelle numérotation") ) return;
- m_project -> removeFolioAutoNum (m_saw_folio->contextComboBox() -> currentText() );
- m_saw_folio->contextComboBox() -> removeItem (m_saw_folio->contextComboBox() -> currentIndex() );
- // removeItem() removes the current selection programmatically but
- // textActivated() does not react to (by design, see buildConnections()).
- // Refresh the displayed pattern explicitly so it matches the new selection.
- updateContextFolio(m_saw_folio->contextComboBox()->currentText());
- project()->folioAutoNumRemoved();
+ removeScheme(SchemeKind::Folio);
+}
+
+void ProjectAutoNumConfigPage::newContextConductor() {newScheme(SchemeKind::Conductor);}
+void ProjectAutoNumConfigPage::renameContextConductor() {renameScheme(SchemeKind::Conductor);}
+void ProjectAutoNumConfigPage::newContextFolio() {newScheme(SchemeKind::Folio);}
+void ProjectAutoNumConfigPage::renameContextFolio() {renameScheme(SchemeKind::Folio);}
+
+SelectAutonumW *ProjectAutoNumConfigPage::sawFor(SchemeKind kind) const
+{
+ return kind == SchemeKind::Conductor ? m_saw_conductor : m_saw_folio;
+}
+
+/**
+ @brief ProjectAutoNumConfigPage::refreshSchemes
+ Fill the list of numberings of @p kind from the project and show
+ @p selected, or the first one if there is no such numbering
+*/
+void ProjectAutoNumConfigPage::refreshSchemes(SchemeKind kind, const QString &selected)
+{
+ QComboBox *cb = sawFor(kind)->contextComboBox();
+ {
+ const QSignalBlocker blocker(cb);
+ cb->clear();
+ cb->addItems(AutoNumSchemeCommand::titles(m_project, kind));
+ const int index = cb->findText(selected);
+ cb->setCurrentIndex(index >= 0 ? index : 0);
+ }
+ sawFor(kind)->setContext(AutoNumSchemeCommand::contextOf(m_project, kind, cb->currentText()));
+ refreshSchemeUsers(kind);
+}
+
+/**
+ @brief ProjectAutoNumConfigPage::refreshSchemeUsers
+ List the folios which follow the numbering of @p kind shown
+*/
+void ProjectAutoNumConfigPage::refreshSchemeUsers(SchemeKind kind)
+{
+ QTableWidget *table = kind == SchemeKind::Conductor ? m_conductor_users : m_folio_users;
+ QGroupBox *box = kind == SchemeKind::Conductor ? m_conductor_users_box : m_folio_users_box;
+ if (!table || !box) {
+ return;
+ }
+ const QString title = sawFor(kind)->contextComboBox()->currentText();
+ //A folio numbering is named by the title blocks it numbered, whose
+ //folio field holds the number since: they are what is listed
+ const QList users = AutoNumSchemeCommand::usersOf(
+ m_project, kind, title, kind == SchemeKind::Folio);
+
+ table->setRowCount(0);
+ for (Diagram *d : users) {
+ const int row = table->rowCount();
+ table->insertRow(row);
+ table->setItem(row, 0, new QTableWidgetItem(QString::number(d->folioIndex() + 1)));
+ table->setItem(row, 1, new QTableWidgetItem(d->border_and_titleblock.title()));
+ }
+ table->resizeColumnsToContents();
+ if (kind == SchemeKind::Folio) {
+ box->setTitle(users.isEmpty()
+ ? tr("Aucun folio ne nomme cette numérotation")
+ : tr("%n folio(s) nomment cette numérotation", "", users.size()));
+ } else {
+ box->setTitle(users.isEmpty()
+ ? tr("Aucun folio ne suit cette numérotation")
+ : tr("%n folio(s) suivent cette numérotation", "", users.size()));
+ }
+}
+
+QString ProjectAutoNumConfigPage::askSchemeName(SchemeKind kind, const QString &title,
+ QString name, const QString &ignored_title)
+{
+ for (;;)
+ {
+ bool ok = false;
+ name = QInputDialog::getText(this, title, tr("Nom de la numérotation :"),
+ QLineEdit::Normal, name, &ok);
+ if (!ok) {
+ return QString();
+ }
+ const QString problem = AutoNumSchemeCommand::nameProblem(m_project, kind, name, ignored_title);
+ if (problem.isEmpty()) {
+ return QETProject::normalizedAutoNumName(name);
+ }
+ QMessageBox::warning(this, title, problem);
+ }
+}
+
+/**
+ @brief ProjectAutoNumConfigPage::newScheme
+ Create a numbering under a name asked to the user, and show it to be defined.
+*/
+void ProjectAutoNumConfigPage::newScheme(SchemeKind kind)
+{
+ if (m_project->isReadOnly()) {
+ return;
+ }
+ const QString name = askSchemeName(kind, tr("Nouvelle numérotation"), QString(), QString());
+ if (name.isEmpty()) {
+ return;
+ }
+ sawFor(kind)->setContext(NumerotationContext());
+ if (auto *cmd = AutoNumSchemeCommand::create(m_project, kind, name,
+ sawFor(kind)->toNumContext(), true)) {
+ m_project->undoStack()->push(cmd);
+ refreshSchemes(kind, name);
+ }
+}
+
+/**
+ @brief ProjectAutoNumConfigPage::renameScheme
+ Rename the numbering shown; the folios which follow it follow it under its new name.
+*/
+void ProjectAutoNumConfigPage::renameScheme(SchemeKind kind)
+{
+ const QString old_title = sawFor(kind)->contextComboBox()->currentText();
+ if (m_project->isReadOnly() || !AutoNumSchemeCommand::contains(m_project, kind, old_title)) {
+ return;
+ }
+ const QString name = askSchemeName(kind, tr("Renommer la numérotation"), old_title, old_title);
+ if (name.isEmpty() || name == old_title) {
+ return;
+ }
+ if (auto *cmd = AutoNumSchemeCommand::edit(
+ m_project, kind, old_title, name,
+ AutoNumSchemeCommand::contextOf(m_project, kind, old_title))) {
+ m_project->undoStack()->push(cmd);
+ refreshSchemes(kind, name);
+ }
+}
+
+/**
+ @brief ProjectAutoNumConfigPage::saveScheme
+ Give the numbering shown the definition shown; with no numbering yet,
+ create one under a name asked to the user. A conductor numbering applied
+ becomes the one new conductors take.
+*/
+void ProjectAutoNumConfigPage::saveScheme(SchemeKind kind)
+{
+ if (m_project->isReadOnly()) {
+ return;
+ }
+ const QString title = sawFor(kind)->contextComboBox()->currentText();
+ const NumerotationContext wanted = sawFor(kind)->toNumContext();
+ QString shown = title;
+ AutoNumSchemeCommand *cmd = nullptr;
+ if (!AutoNumSchemeCommand::contains(m_project, kind, title))
+ {
+ shown = askSchemeName(kind, tr("Nouvelle numérotation"), QString(), QString());
+ if (shown.isEmpty()) {
+ return;
+ }
+ cmd = AutoNumSchemeCommand::create(m_project, kind, shown, wanted, true);
+ }
+ else
+ {
+ cmd = AutoNumSchemeCommand::edit(m_project, kind, title, title, wanted, true);
+ }
+ if (cmd) {
+ m_project->undoStack()->push(cmd);
+ }
+ refreshSchemes(kind, shown);
+}
+
+/**
+ @brief ProjectAutoNumConfigPage::removeScheme
+ Remove the numbering shown, unless a folio still follows it
+*/
+void ProjectAutoNumConfigPage::removeScheme(SchemeKind kind)
+{
+ const QString title = sawFor(kind)->contextComboBox()->currentText();
+ if (m_project->isReadOnly() || !AutoNumSchemeCommand::contains(m_project, kind, title)) {
+ return;
+ }
+ if (const int used = AutoNumSchemeCommand::usersOf(m_project, kind, title).size())
+ {
+ QMessageBox::information(
+ this, tr("Supprimer la numérotation"),
+ tr("%n folio(s) suivent la numérotation « %1 », elle ne peut pas être "
+ "supprimée.\nDonnez-leur une autre numérotation d'abord.", "", used)
+ .arg(title));
+ return;
+ }
+ if (auto *cmd = AutoNumSchemeCommand::remove(m_project, kind, title)) {
+ m_project->undoStack()->push(cmd);
+ refreshSchemes(kind, kind == SchemeKind::Conductor ? m_project->conductorCurrentAutoNum() : QString());
+ }
}
/**
@@ -1059,5 +1631,7 @@ void ProjectAutoNumConfigPage::removeContextFolio()
*/
void ProjectAutoNumConfigPage::changeToTab(int i)
{
- qDebug()<<"Q_UNUSED"<= 0 && i < m_tab_widget->count()) {
+ m_tab_widget->setCurrentIndex(i);
+ }
}
diff --git a/sources/ui/configpage/projectconfigpages.h b/sources/ui/configpage/projectconfigpages.h
index e911b5a43..98b040d9e 100644
--- a/sources/ui/configpage/projectconfigpages.h
+++ b/sources/ui/configpage/projectconfigpages.h
@@ -18,6 +18,9 @@
#ifndef PROJECTCONFIGPAGES_H
#define PROJECTCONFIGPAGES_H
#include "configpage.h"
+#include
+#include
+#include "../../autoNum/autonumschemecommand.h"
class QLabel;
class QComboBox;
@@ -33,6 +36,11 @@ class DiagramContextWidget;
class ReportPropertieWidget;
class XRefPropertiesWidget;
class SelectAutonumW;
+class QTabWidget;
+class QGroupBox;
+class Element;
+class QTableWidget;
+class ElementAutoNumSchemeCommand;
class FolioAutonumberingW;
class FormulaAutonumberingW;
class AutoNumberingManagementW;
@@ -168,11 +176,37 @@ class ProjectAutoNumConfigPage : public ProjectConfigPage {
void updateContextElement(const QString&);//element
void saveContextElement();
void removeContextElement();
+ void newContextElement();
+ void renameContextElement();
+ void refreshElementSchemes(const QString &selected);
+ void refreshElementUsers();
+ void assignFreeNumber();
+ void updateAssignNumberButton();
+ void newContextConductor();
+ void renameContextConductor();
+ void newContextFolio();
+ void renameContextFolio();
+ QString askElementSchemeName(const QString &title, QString name,
+ const QString &ignored_title);
+ bool pushElementSchemeCommand(ElementAutoNumSchemeCommand *cmd);
void importFromProject();
void applyAutoNum();
void applyManagement();
+ private:
+ //Conductor and folio numberings
+ using SchemeKind = AutoNumSchemeCommand::Kind;
+ SelectAutonumW *sawFor(SchemeKind kind) const;
+ void refreshSchemes(SchemeKind kind, const QString &selected);
+ void refreshSchemeUsers(SchemeKind kind);
+ QString askSchemeName(SchemeKind kind, const QString &title, QString name,
+ const QString &ignored_title);
+ void newScheme(SchemeKind kind);
+ void renameScheme(SchemeKind kind);
+ void saveScheme(SchemeKind kind);
+ void removeScheme(SchemeKind kind);
+
signals:
void setAutoNum(QString);
void setAutoNum(int,int);
@@ -184,6 +218,15 @@ class ProjectAutoNumConfigPage : public ProjectConfigPage {
SelectAutonumW *m_saw_conductor;
SelectAutonumW *m_saw_folio;
SelectAutonumW *m_saw_element;
+ QTabWidget *m_tab_widget = nullptr;
+ QGroupBox *m_conductor_users_box = nullptr;
+ QTableWidget *m_conductor_users = nullptr;
+ QGroupBox *m_folio_users_box = nullptr;
+ QTableWidget *m_folio_users = nullptr;
+ QGroupBox *m_element_users_box = nullptr;
+ QTableWidget *m_element_users = nullptr;
+ QPushButton *m_assign_number_pb = nullptr;
+ QVector> m_element_rows; ///< the element of each row of the table, none for a gap
FolioAutonumberingW *m_faw;
AutoNumberingManagementW *m_amw;
QPushButton *m_import_pb = nullptr;
diff --git a/sources/ui/elementinfowidget.cpp b/sources/ui/elementinfowidget.cpp
index 78aab5395..9cbab800e 100644
--- a/sources/ui/elementinfowidget.cpp
+++ b/sources/ui/elementinfowidget.cpp
@@ -18,7 +18,13 @@
#include "elementinfowidget.h"
#include "../qet.h"
#include
+#include
+#include
+#include
+#include
#include
+#include "../autoNum/elementautonumschemecommand.h"
+#include "../undocommand/freezeelementlabelcommand.h"
#include "../diagram.h"
#include "../materiallist/materiallist.h"
#include "../materiallist/materialselectiondialog.h"
@@ -28,6 +34,7 @@
#include "../dataBase/projectdatabase.h"
#include "../qetinformation.h"
#include "../qetproject.h"
+#include "../ui/projectpropertiesdialog.h"
#include "../ui_elementinfowidget.h"
#include "../undocommand/changeelementinformationcommand.h"
#include "customelementinfopartwidget.h"
@@ -113,13 +120,76 @@ void ElementInfoWidget::apply()
*/
QUndoCommand* ElementInfoWidget::associatedUndo() const
{
- const auto new_info = currentInfo();
+ auto new_info = currentInfo();
const auto old_info = m_element -> elementInformations();
- if (old_info != new_info)
- return (new ChangeElementInformationCommand(m_element, old_info, new_info));
+ const bool was_frozen = m_element->isFreezeLabel();
+ const bool want_frozen = m_freeze_cb ? m_freeze_cb->isChecked() : was_frozen;
- return nullptr;
+ //A numbering chosen in the list is given to the element the way
+ //a placed one gets it: its formula, and the next number of the
+ //numbering, which moves on. That is a command of its own, first,
+ //so the other edited fields are changed on top of its result.
+ const QString chosen = chosenScheme();
+ const bool assign_wanted = !chosen.isEmpty()
+ && chosen != followedScheme()
+ && m_element->diagram();
+ //A number picked among the free ones, for an element which keeps its numbering
+ const bool number_wanted = !assign_wanted && m_number_cb && m_number_row
+ && !m_number_row->isHidden() && m_number_cb->isEnabled()
+ && m_number_cb->currentData().toInt() > 0
+ && m_number_cb->currentData().toInt() != m_number_current
+ && m_element->diagram();
+
+ if (!assign_wanted && !number_wanted && was_frozen == want_frozen)
+ {
+ if (old_info != new_info)
+ return (new ChangeElementInformationCommand(m_element, old_info, new_info));
+ return nullptr;
+ }
+
+ auto *macro = new QUndoCommand(
+ QObject::tr("Modifier les informations de l'élément : %1")
+ .arg(m_element->name()));
+ DiagramContext base_info = old_info;
+ bool state_frozen = was_frozen;
+
+ if (assign_wanted || number_wanted)
+ {
+ RenumberElementsCommand *assign = assign_wanted
+ ? ElementAutoNumSchemeCommand::assign(
+ m_element->diagram()->project(), chosen,
+ {m_element.data()}, true, nullptr, macro)
+ : ElementAutoNumSchemeCommand::assignNumber(
+ m_element->diagram()->project(), m_element.data(),
+ m_number_cb->currentData().toInt(), nullptr, macro);
+ if (assign && !assign->changes().isEmpty())
+ {
+ const auto &change = assign->changes().first();
+ base_info = change.new_infos;
+ state_frozen = change.new_frozen;
+ for (const QString &key : {QETInformation::ELMT_FORMULA,
+ QETInformation::ELMT_FORMULA_ID,
+ QETInformation::ELMT_LABEL}) {
+ if (base_info.contains(key)) {
+ new_info.addValue(key, base_info.value(key), key != QETInformation::ELMT_FORMULA_ID);
+ }
+ }
+ }
+ }
+
+ if (base_info != new_info) {
+ new ChangeElementInformationCommand(m_element, base_info, new_info, macro);
+ }
+ if (state_frozen != want_frozen) {
+ new FreezeElementLabelCommand(m_element, state_frozen, want_frozen, macro);
+ }
+
+ if (macro->childCount() == 0) {
+ delete macro;
+ return nullptr;
+ }
+ return macro;
}
/**
@@ -177,6 +247,12 @@ void ElementInfoWidget::enableLiveEdit()
if (m_exclude_from_bom_cb) {
connect(m_exclude_from_bom_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply);
}
+ if (m_freeze_cb) {
+ connect(m_freeze_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply);
+ }
+ if (m_number_cb) {
+ connect(m_number_cb, QOverload::of(&QComboBox::activated), this, &ElementInfoWidget::apply);
+ }
}
/**
@@ -192,6 +268,12 @@ void ElementInfoWidget::disableLiveEdit()
if (m_potential_isolating_cb) {
disconnect(m_potential_isolating_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply);
}
+ if (m_freeze_cb) {
+ disconnect(m_freeze_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply);
+ }
+ if (m_number_cb) {
+ disconnect(m_number_cb, QOverload::of(&QComboBox::activated), this, &ElementInfoWidget::apply);
+ }
if (m_exclude_from_bom_cb) {
disconnect(m_exclude_from_bom_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply);
}
@@ -226,6 +308,9 @@ void ElementInfoWidget::buildInterface()
}
setupMaterialButtons();
+ setupSchemeRow();
+ setupNumberRow();
+ setupFreezeRow();
m_add_custom_property_btn = new QPushButton(tr("Ajouter une propriété personnalisée"), this);
connect(m_add_custom_property_btn, &QPushButton::clicked, this, [this]() { addCustomProperty(); });
@@ -273,7 +358,8 @@ QStringList ElementInfoWidget::predefinedKeys() const
? QETInformation::terminalElementInfoKeys()
: QETInformation::elementInfoKeys();
- keys << QStringLiteral("auto_num_locked")
+ keys << QETInformation::ELMT_FORMULA_ID
+ << QStringLiteral("auto_num_locked")
<< QStringLiteral("potential_isolating")
<< QStringLiteral("exclude_from_bom");
@@ -573,6 +659,12 @@ void ElementInfoWidget::updateUi()
for (ElementInfoPartWidget *eipw : m_eipw_list) {
eipw -> setText (element_info[eipw->key()].toString());
}
+ refreshSchemeRow();
+ if (m_freeze_cb) {
+ m_freeze_cb->setChecked(m_element->isFreezeLabel());
+ updateFreezeRow();
+ }
+ refreshNumberRow();
updateSuggestions();
// Rebuild the custom-property rows to match whatever
@@ -658,8 +750,362 @@ DiagramContext ElementInfoWidget::currentInfo() const
if (m_exclude_from_bom_cb) {
info_.addValue(QStringLiteral("exclude_from_bom"), m_exclude_from_bom_cb->isChecked() ? QStringLiteral("true") : QStringLiteral("false"));
}
+
+ //The element keeps following its numbering scheme as long as its
+ //formula is left as it is; a formula edited by hand is its own.
+ const DiagramContext &elmt_info = m_element->elementInformations();
+ if (elmt_info.contains(QETInformation::ELMT_FORMULA_ID)
+ && !info_.value(QETInformation::ELMT_FORMULA).toString().isEmpty()
+ && info_.value(QETInformation::ELMT_FORMULA) == elmt_info.value(QETInformation::ELMT_FORMULA)) {
+ info_.addValue(QETInformation::ELMT_FORMULA_ID,
+ elmt_info.value(QETInformation::ELMT_FORMULA_ID), false);
+ }
return info_;
}
+/**
+ @brief ElementInfoWidget::setupSchemeRow
+ Add above the formula the list of the numberings of the project, the
+ formula of the element being the one of the numbering picked there:
+ no formula to type, a numbering to choose, and a button to open the
+ numberings without leaving this window.
+*/
+void ElementInfoWidget::setupSchemeRow()
+{
+ ElementInfoPartWidget *formula = infoPartWidgetForKey(QETInformation::ELMT_FORMULA);
+ if (!formula || !m_element || !m_element->diagram()) {
+ return;
+ }
+
+ m_scheme_row = new QWidget(this);
+ auto *layout = new QHBoxLayout(m_scheme_row);
+ layout->setContentsMargins(0, 0, 0, 0);
+ layout->addWidget(new QLabel(tr("Numérotation automatique"), m_scheme_row));
+
+ m_scheme_cb = new QComboBox(m_scheme_row);
+ m_scheme_cb->setObjectName(QStringLiteral("m_scheme_cb"));
+ m_scheme_cb->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
+ layout->addWidget(m_scheme_cb, 1);
+
+ auto *open = new QPushButton(tr("…"), m_scheme_row);
+ open->setToolTip(tr("Ouvrir les numérotations d'éléments du projet"));
+ open->setMaximumWidth(32);
+ layout->addWidget(open);
+
+ ui->scroll_vlayout->insertWidget(ui->scroll_vlayout->indexOf(formula), m_scheme_row);
+
+ //The formula is shown, not typed: it comes from the numbering
+ formula->setDisabled(true);
+
+ connect(m_scheme_cb, QOverload::of(&QComboBox::activated),
+ this, &ElementInfoWidget::schemeChosen);
+ connect(m_scheme_cb, QOverload::of(&QComboBox::activated),
+ this, &ElementInfoWidget::updateNumberRow);
+ connect(open, &QPushButton::clicked, this, &ElementInfoWidget::openSchemePage);
+}
+
+/**
+ @brief ElementInfoWidget::setupFreezeRow
+ Add below the label a check box which freezes it: a frozen label is
+ not touched by the numbering of elements, and its number is not given
+ to another element.
+*/
+void ElementInfoWidget::setupFreezeRow()
+{
+ ElementInfoPartWidget *label = infoPartWidgetForKey(QETInformation::ELMT_LABEL);
+ if (!label || !m_element) {
+ return;
+ }
+ m_freeze_cb = new QCheckBox(tr("Figer le nom"), this);
+ m_freeze_cb->setToolTip(tr("Un nom figé n'est pas changé par la numérotation automatique, "
+ "et son numéro n'est pas donné à un autre élément."));
+ ui->scroll_vlayout->insertWidget(ui->scroll_vlayout->indexOf(label) + 1, m_freeze_cb);
+ connect(m_freeze_cb, &QCheckBox::toggled, this, &ElementInfoWidget::updateNumberRow);
+}
+
+/**
+ @brief ElementInfoWidget::updateFreezeRow
+ Freezing means something for a label a formula gives: the box can be
+ ticked when there is one, and always unticked, so that a frozen
+ element can be given back.
+*/
+void ElementInfoWidget::updateFreezeRow()
+{
+ ElementInfoPartWidget *formula = infoPartWidgetForKey(QETInformation::ELMT_FORMULA);
+ if (!m_freeze_cb || !formula) {
+ return;
+ }
+ m_freeze_cb->setEnabled(!formula->text().isEmpty() || m_freeze_cb->isChecked());
+ updateNumberRow();
+}
+
+/**
+ @brief ElementInfoWidget::setupNumberRow
+ Add under the numbering a list of the numbers the element may be given
+ by hand: its own, then the free ones, each with the label it would give.
+ For an element which follows a numbering whose numbers are one sequence.
+*/
+void ElementInfoWidget::setupNumberRow()
+{
+ if (!m_scheme_row || !m_element || !m_element->diagram()) {
+ return;
+ }
+ m_number_row = new QWidget(this);
+ auto *layout = new QHBoxLayout(m_number_row);
+ layout->setContentsMargins(0, 0, 0, 0);
+ layout->addWidget(new QLabel(tr("Numéro"), m_number_row));
+ m_number_cb = new QComboBox(m_number_row);
+ m_number_cb->setObjectName(QStringLiteral("m_number_cb"));
+ m_number_cb->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
+ m_number_cb->setToolTip(tr("Seuls les numéros libres sont proposés : un élément qui doit garder son "
+ "numéro réel peut le retrouver si personne ne l'a."));
+ layout->addWidget(m_number_cb, 1);
+ ui->scroll_vlayout->insertWidget(ui->scroll_vlayout->indexOf(m_scheme_row) + 1, m_number_row);
+ m_number_row->hide();
+ connect(m_number_cb, QOverload::of(&QComboBox::activated), this, &ElementInfoWidget::numberChosen);
+}
+
+/**
+ @brief ElementInfoWidget::refreshNumberRow
+ Fill the list of numbers: the element's own, then the free ones. Hidden
+ when the element follows no numbering, or one whose numbers are not one
+ sequence.
+*/
+void ElementInfoWidget::refreshNumberRow()
+{
+ if (!m_number_cb || !m_element || !m_element->diagram()) {
+ return;
+ }
+ QETProject *project = m_element->diagram()->project();
+ const QString title = followedScheme();
+ const auto support = title.isEmpty()
+ ? ElementAutoNumSchemeCommand::NumberSupport()
+ : ElementAutoNumSchemeCommand::numberSupport(project->elementAutoNum().value(title));
+
+ const QSignalBlocker blocker(m_number_cb);
+ m_number_cb->clear();
+ m_number_current = -1;
+ if (!support.supported) {
+ m_number_row->hide();
+ return;
+ }
+ if (const auto own = ElementAutoNumSchemeCommand::numberOf(support, m_element)) {
+ m_number_current = *own;
+ m_number_cb->addItem(tr("%1 (actuel)").arg(*own), *own);
+ } else {
+ m_number_cb->addItem(tr("— (numéro inconnu)"), 0);
+ }
+ for (int n : ElementAutoNumSchemeCommand::freeNumbers(project, title, m_element)) {
+ if (n == m_number_current) continue;
+ m_number_cb->addItem(tr("%1 → %2").arg(n).arg(
+ ElementAutoNumSchemeCommand::labelForNumber(project, title, m_element, n)), n);
+ }
+ m_number_cb->setCurrentIndex(0);
+ m_number_row->show();
+ updateNumberRow();
+}
+
+/**
+ @brief ElementInfoWidget::updateNumberRow
+ A number is chosen only for an element which keeps its numbering and
+ whose name is not frozen.
+*/
+void ElementInfoWidget::updateNumberRow()
+{
+ if (!m_number_cb || !m_number_row || m_number_row->isHidden()) {
+ return;
+ }
+ const bool keeps = chosenScheme() == followedScheme() && !chosenScheme().isEmpty();
+ const bool frozen = m_freeze_cb ? m_freeze_cb->isChecked() : m_element->isFreezeLabel();
+ m_number_cb->setEnabled(keeps && !frozen);
+ m_number_cb->setToolTip(
+ frozen ? tr("Le nom est figé : dégelez-le pour changer son numéro.")
+ : !keeps ? tr("Le numéro se choisit quand l'élément garde sa numérotation.")
+ : tr("Seuls les numéros libres sont proposés : un élément qui doit garder son "
+ "numéro réel peut le retrouver si personne ne l'a."));
+}
+
+/**
+ @brief ElementInfoWidget::numberChosen
+ Show the label the number picked gives, in the field of the label.
+*/
+void ElementInfoWidget::numberChosen()
+{
+ ElementInfoPartWidget *label = infoPartWidgetForKey(QETInformation::ELMT_LABEL);
+ if (!label || !m_element || !m_element->diagram()) {
+ return;
+ }
+ const int number = m_number_cb->currentData().toInt();
+ if (number <= 0 || number == m_number_current) {
+ label->setText(m_element->elementInformations().value(QETInformation::ELMT_LABEL).toString());
+ return;
+ }
+ label->setText(ElementAutoNumSchemeCommand::labelForNumber(
+ m_element->diagram()->project(), followedScheme(), m_element, number));
+}
+
+/**
+ @brief ElementInfoWidget::followedScheme
+ @return the title of the numbering the element follows, empty if none
+*/
+QString ElementInfoWidget::followedScheme() const
+{
+ if (!m_element || !m_element->diagram()) {
+ return QString();
+ }
+ return m_element->diagram()->project()->elementAutoNumTitle(
+ QUuid(m_element->elementInformations()
+ .value(QETInformation::ELMT_FORMULA_ID).toString()));
+}
+
+/**
+ @brief ElementInfoWidget::chosenScheme
+ @return the title of the numbering picked in the list, empty if the
+ element is to follow none
+*/
+QString ElementInfoWidget::chosenScheme() const
+{
+ const QString data = m_scheme_cb ? m_scheme_cb->currentData().toString() : QString();
+ return data == QLatin1String("\x01") ? QString() : data;
+}
+
+/**
+ @brief ElementInfoWidget::refreshSchemeRow
+ Fill the list with the numberings of the project and show the one the
+ element follows. An element with a formula of its own, followed from
+ no numbering, gets an entry for it, so that it is neither hidden nor
+ lost by looking at it.
+*/
+void ElementInfoWidget::refreshSchemeRow()
+{
+ if (!m_scheme_cb || !m_element || !m_element->diagram()) {
+ return;
+ }
+ QETProject *project = m_element->diagram()->project();
+
+ const QSignalBlocker blocker(m_scheme_cb);
+ m_scheme_cb->clear();
+ m_scheme_cb->addItem(tr("Aucune (nom saisi à la main)"), QString());
+
+ QStringList titles(project->elementAutoNum().keys());
+ titles.sort(Qt::CaseInsensitive);
+ for (const QString &title : titles) {
+ m_scheme_cb->addItem(title, title);
+ }
+
+ const QString followed = followedScheme();
+ if (!followed.isEmpty()) {
+ m_scheme_cb->setCurrentIndex(m_scheme_cb->findData(followed));
+ m_scheme_index = m_scheme_cb->currentIndex();
+ return;
+ }
+
+ const QString formula = m_element->elementInformations()
+ .value(QETInformation::ELMT_FORMULA).toString();
+ if (!formula.isEmpty()) {
+ //Same data as "none" would clash: the entry is for display only
+ m_scheme_cb->insertItem(1, tr("Formule propre : %1").arg(formula), QStringLiteral("\x01"));
+ m_scheme_cb->setCurrentIndex(1);
+ }
+ m_scheme_index = m_scheme_cb->currentIndex();
+}
+
+/**
+ @brief ElementInfoWidget::schemeChosen
+ The user picked an entry of the list.
+
+ The formula shown is the one of the numbering picked; the label is
+ typed only when there is no formula. Leaving a numbering, or the
+ element's own formula, for "none" also empties the label: what stays
+ would pass for a number the numbering gave, and could be given again
+ to another element. Going back to what the element has at present
+ gives its label back.
+
+ An element whose label is frozen is changed only if the user agrees.
+*/
+void ElementInfoWidget::schemeChosen()
+{
+ ElementInfoPartWidget *formula = infoPartWidgetForKey(QETInformation::ELMT_FORMULA);
+ ElementInfoPartWidget *label = infoPartWidgetForKey(QETInformation::ELMT_LABEL);
+ if (!formula || !label || !m_element || !m_element->diagram()) {
+ return;
+ }
+
+ const DiagramContext info = m_element->elementInformations();
+ const QString own_formula = info.value(QETInformation::ELMT_FORMULA).toString();
+ const bool own_entry = m_scheme_cb->currentData().toString() == QLatin1String("\x01");
+ const QString chosen = chosenScheme();
+ const bool unchanged = own_entry
+ || (!chosen.isEmpty() && chosen == followedScheme())
+ || (chosen.isEmpty() && own_formula.isEmpty());
+
+ const bool frozen_now = m_freeze_cb ? m_freeze_cb->isChecked() : m_element->isFreezeLabel();
+ if (!unchanged && frozen_now)
+ {
+ const auto answer = QET::QetMessageBox::question(
+ this,
+ tr("Nom figé"),
+ tr("Le nom de cet élément est figé.\n"
+ "Changer sa numérotation le remplacera ou l'effacera.\n\nContinuer ?"),
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::No);
+ if (answer != QMessageBox::Yes) {
+ m_scheme_cb->setCurrentIndex(m_scheme_index);
+ return;
+ }
+ }
+ m_scheme_index = m_scheme_cb->currentIndex();
+ //The user agreed: the element is no longer protected
+ if (!unchanged && m_freeze_cb) {
+ m_freeze_cb->setChecked(false);
+ }
+
+ //Setting the fields one by one must not apply each step
+ const bool live = m_live_edit;
+ if (live) disableLiveEdit();
+
+ if (unchanged) {
+ formula->setText(own_formula);
+ label->setText(info.value(QETInformation::ELMT_LABEL).toString());
+ } else if (chosen.isEmpty()) {
+ formula->setText(QString());
+ if (!own_formula.isEmpty()) {
+ label->setText(QString());
+ }
+ } else {
+ formula->setText(m_element->diagram()->project()->elementAutoNumFormula(chosen));
+ }
+ updateFreezeRow();
+
+ if (live) {
+ enableLiveEdit();
+ apply();
+ }
+}
+
+/**
+ @brief ElementInfoWidget::openSchemePage
+ Open the numberings of the project, on the one of elements, then show
+ again the list, which may have changed.
+*/
+void ElementInfoWidget::openSchemePage()
+{
+ if (!m_element || !m_element->diagram()) {
+ return;
+ }
+ const QString before = chosenScheme();
+
+ ProjectPropertiesDialog ppd(m_element->diagram()->project(), this);
+ ppd.setCurrentPage(ProjectPropertiesDialog::Autonum);
+ ppd.changeToElement();
+ ppd.exec();
+
+ refreshSchemeRow();
+ const int index = m_scheme_cb->findData(before);
+ if (index >= 0) {
+ m_scheme_cb->setCurrentIndex(index);
+ }
+}
+
/**
@brief ElementInfoWidget::firstActivated
Slot activated when this widget is show.
diff --git a/sources/ui/elementinfowidget.h b/sources/ui/elementinfowidget.h
index 9eaeefdba..3b24789df 100644
--- a/sources/ui/elementinfowidget.h
+++ b/sources/ui/elementinfowidget.h
@@ -29,7 +29,9 @@ class ElementInfoPartWidget;
class CustomElementInfoPartWidget;
class ChangeElementInformationCommand;
class QCheckBox;
+class QComboBox;
class QPushButton;
+class QWidget;
struct MaterialRecord;
namespace Ui {
@@ -71,6 +73,18 @@ class ElementInfoWidget : public AbstractElementPropertiesEditorWidget
ElementInfoPartWidget *infoPartWidgetForKey(const QString &key) const;
QStringList predefinedKeys() const;
void updateSuggestions();
+ void setupSchemeRow();
+ void setupFreezeRow();
+ void setupNumberRow();
+ void refreshNumberRow();
+ void updateNumberRow();
+ void numberChosen();
+ void updateFreezeRow();
+ void refreshSchemeRow();
+ void schemeChosen();
+ void openSchemePage();
+ QString chosenScheme() const;
+ QString followedScheme() const;
private slots:
void firstActivated();
@@ -86,6 +100,13 @@ class ElementInfoWidget : public AbstractElementPropertiesEditorWidget
QPushButton *m_add_custom_property_btn = nullptr;
QCheckBox *m_potential_isolating_cb = nullptr;
QCheckBox *m_exclude_from_bom_cb = nullptr;
+ QComboBox *m_scheme_cb = nullptr;
+ QCheckBox *m_freeze_cb = nullptr;
+ QWidget *m_number_row = nullptr;
+ QComboBox *m_number_cb = nullptr;
+ int m_number_current = -1; ///< the number the element has, -1 if unknown
+ int m_scheme_index = 0;
+ QWidget *m_scheme_row = nullptr;
bool m_first_activation;
bool m_ui_builded = false;
};
diff --git a/sources/ui/multipastedialog.cpp b/sources/ui/multipastedialog.cpp
index c4c8dda4f..74108740f 100644
--- a/sources/ui/multipastedialog.cpp
+++ b/sources/ui/multipastedialog.cpp
@@ -17,6 +17,7 @@
*/
#include "multipastedialog.h"
+#include "../autoNum/elementautonumschemecommand.h"
#include "../qetproject.h"
#include "../conductorautonumerotation.h"
#include "../diagram.h"
@@ -105,22 +106,28 @@ void MultiPasteDialog::on_m_button_box_accepted()
{
m_diagram->undoStack().beginMacro(tr("Multi-collage"));
- QSettings settings;
- bool erase_label = settings.value("diagramcommands/erase-label-on-copy", true).toBool();
- //Ensure when 'auto_num' is checked, the settings 'save_label' is to true.
- //Because in the class PasteDiagramCommand, if the settings 'save_label' is to false,
- //the function redo of PasteDiagramCommand, clear the formula and the label of the pasted element
- //and so the auto_num below do nothing (there is not a formula to compare)
- if(ui->m_auto_num_cb->isChecked())
- settings.setValue("diagramcommands/erase-label-on-copy", false);
-
+ QETProject *project = m_diagram->project();
+ //The element numberings the copies follow, read now: the paste
+ //erases the formulas of its elements, unless the preference
+ //keeps them, and the numbering below is meant to work either way
+ QList>> copy_schemes;
+ for(const DiagramContent &dc : std::as_const(m_pasted_content_list))
+ {
+ copy_schemes << (ui->m_auto_num_cb->isChecked()
+ ? ElementAutoNumSchemeCommand::pastedSchemes(project, dc.m_elements)
+ : QMap>());
+ }
m_diagram->clearSelection();
- m_diagram->undoStack().push(new PasteDiagramCommand(m_diagram, m_pasted_content));
+ //The copies are numbered by this dialog, copy by copy, below
+ auto *paste = new PasteDiagramCommand(m_diagram, m_pasted_content);
+ paste->setAutoNumbering(false);
+ m_diagram->undoStack().push(paste);
- for(DiagramContent dc : m_pasted_content_list)
+ for(int copy = 0 ; copy < m_pasted_content_list.size() ; ++copy)
{
+ const DiagramContent &dc = m_pasted_content_list.at(copy);
QList pasted_elements = dc.m_elements;
//Sort the list element by there pos (top -> bottom)
std::sort(pasted_elements.begin(), pasted_elements.end(), [](Element *a, Element *b){return (a->pos().y() < b->pos().y());});
@@ -147,32 +154,21 @@ void MultiPasteDialog::on_m_button_box_accepted()
}
}
- //Set up the label of element
- //Instead of use the current autonum of project,
- //we try to fetch the same formula of the pasted element, in the several autonum of the project
- //for apply the good formula for each elements
- if(ui->m_auto_num_cb->isChecked())
+ //Number the elements of this copy with the numberings they
+ //follow: the next numbers, in the order of the Renumber
+ //button, the counters moving on, so that the next copy goes
+ //on from there. The project's current numbering is not
+ //changed, and labels which other elements keep are not given.
+ if(!copy_schemes.at(copy).isEmpty())
{
- for(Element *elmt : pasted_elements)
- {
- QString formula = elmt->elementInformations()["formula"].toString();
- if(!formula.isEmpty())
- {
- QHash autonums = m_diagram->project()->elementAutoNum();
- QHashIterator hash_iterator(autonums);
-
- while(hash_iterator.hasNext())
- {
- hash_iterator.next();
- if(autonum::numerotationContextToFormula(hash_iterator.value()) == formula)
- {
- m_diagram->project()->setCurrrentElementAutonum(hash_iterator.key());
- elmt->setUpFormula();
- }
- }
- }
- }
+ auto *numbering = new QUndoCommand(tr("Numéroter les éléments collés"));
+ ElementAutoNumSchemeCommand::numberPasted(project, copy_schemes.at(copy), numbering);
+ if(numbering->childCount())
+ m_diagram->undoStack().push(numbering);
+ else
+ delete numbering;
}
+
//Like elements, we compare formula of pasted conductor with the autonums available in the project.
if(ui->m_auto_num_cond_cb->isChecked())
{
@@ -216,7 +212,6 @@ void MultiPasteDialog::on_m_button_box_accepted()
m_diagram->adjustSceneRect();
m_accept = true;
- settings.setValue("diagramcommands/erase-label-on-copy", erase_label);
m_diagram->undoStack().endMacro();
}
}
diff --git a/sources/ui/projectpropertiesdialog.cpp b/sources/ui/projectpropertiesdialog.cpp
index f427d923e..286a66e02 100644
--- a/sources/ui/projectpropertiesdialog.cpp
+++ b/sources/ui/projectpropertiesdialog.cpp
@@ -93,3 +93,15 @@ void ProjectPropertiesDialog::changeToFolio()
m_properties_dialog->pages.at(2));
autoNumPage->changeToTab(3);
}
+
+/**
+ @brief ProjectPropertiesDialog::changeToElement
+ Change the current displayed tab to the element numberings tab.
+*/
+void ProjectPropertiesDialog::changeToElement()
+{
+ ProjectAutoNumConfigPage *autoNumPage =
+ static_cast (
+ m_properties_dialog->pages.at(2));
+ autoNumPage->changeToTab(2);
+}
diff --git a/sources/ui/projectpropertiesdialog.h b/sources/ui/projectpropertiesdialog.h
index 096656e33..075423907 100644
--- a/sources/ui/projectpropertiesdialog.h
+++ b/sources/ui/projectpropertiesdialog.h
@@ -41,6 +41,7 @@ class ProjectPropertiesDialog : public QObject {
void exec();
void setCurrentPage(ProjectPropertiesDialog::Page);
void changeToFolio();
+ void changeToElement();
private:
ConfigDialog *m_properties_dialog;
diff --git a/sources/undocommand/freezeelementlabelcommand.cpp b/sources/undocommand/freezeelementlabelcommand.cpp
new file mode 100644
index 000000000..87b82b01e
--- /dev/null
+++ b/sources/undocommand/freezeelementlabelcommand.cpp
@@ -0,0 +1,46 @@
+/*
+ Copyright 2006-2026 The QElectroTech Team
+ This file is part of QElectroTech.
+
+ QElectroTech is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ QElectroTech is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with QElectroTech. If not, see .
+*/
+#include "freezeelementlabelcommand.h"
+
+#include "../qetgraphicsitem/element.h"
+
+FreezeElementLabelCommand::FreezeElementLabelCommand(Element *element,
+ bool old_frozen,
+ bool new_frozen,
+ QUndoCommand *parent) :
+ QUndoCommand(new_frozen ? QObject::tr("Figer le nom de l'élément")
+ : QObject::tr("Dégeler le nom de l'élément"),
+ parent),
+ m_element(element),
+ m_old_frozen(old_frozen),
+ m_new_frozen(new_frozen)
+{}
+
+void FreezeElementLabelCommand::undo()
+{
+ if (m_element) {
+ m_element->freezeLabel(m_old_frozen);
+ }
+}
+
+void FreezeElementLabelCommand::redo()
+{
+ if (m_element) {
+ m_element->freezeLabel(m_new_frozen);
+ }
+}
diff --git a/sources/undocommand/freezeelementlabelcommand.h b/sources/undocommand/freezeelementlabelcommand.h
new file mode 100644
index 000000000..90f375f65
--- /dev/null
+++ b/sources/undocommand/freezeelementlabelcommand.h
@@ -0,0 +1,49 @@
+/*
+ Copyright 2006-2026 The QElectroTech Team
+ This file is part of QElectroTech.
+
+ QElectroTech is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ QElectroTech is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with QElectroTech. If not, see .
+*/
+#ifndef FREEZEELEMENTLABELCOMMAND_H
+#define FREEZEELEMENTLABELCOMMAND_H
+
+#include
+#include
+
+class Element;
+
+/**
+ @brief The FreezeElementLabelCommand class
+ Freezes or unfreezes the label of an element: a frozen label is not
+ touched by the numbering of elements, and its number is not given to
+ another element.
+*/
+class FreezeElementLabelCommand : public QUndoCommand
+{
+ public:
+ FreezeElementLabelCommand(Element *element,
+ bool old_frozen,
+ bool new_frozen,
+ QUndoCommand *parent = nullptr);
+
+ void undo() override;
+ void redo() override;
+
+ private:
+ QPointer m_element;
+ bool m_old_frozen;
+ bool m_new_frozen;
+};
+
+#endif // FREEZEELEMENTLABELCOMMAND_H
diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt
index 20b257ca1..ae8b70e77 100644
--- a/tests/qttest/CMakeLists.txt
+++ b/tests/qttest/CMakeLists.txt
@@ -633,6 +633,21 @@ target_compile_definitions(tst_derivedwireuuid PRIVATE
"QET_TEST_BINARY_PATH=\"$\""
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
+# Element numbering schemes carry an id and elements follow them by it:
+# a legacy file gets stable ids on --resave; through --run, rename, edit,
+# refused removal and undo/redo keep the elements following the scheme.
+if(QET_HAS_SCRIPTING)
+ add_executable(
+ tst_elementautonumids
+ tst_elementautonumids.cpp)
+ add_test(NAME tst_elementautonumids COMMAND tst_elementautonumids)
+ add_dependencies(tst_elementautonumids qelectrotech)
+ target_link_libraries(tst_elementautonumids PRIVATE Qt::Test Qt::Xml)
+ target_compile_definitions(tst_elementautonumids PRIVATE
+ "QET_TEST_BINARY_PATH=\"$\""
+ "QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
+endif()
+
# Saving a project that was just saved changes nothing: runs the real
# binary's --resave twice on every project in examples/, and on one whose
# title block holds a single-space value (#973).
@@ -714,3 +729,15 @@ add_executable(
add_test(NAME tst_xrefpos COMMAND tst_xrefpos)
target_include_directories(tst_xrefpos PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_xrefpos PRIVATE Qt::Test Qt::Widgets Qt::Xml pugixml::pugixml)
+
+# Qt 6.4.2 writes the attributes of an element in an order that changes from
+# one process to the next, so a project saved twice by two runs of the binary
+# differs in bytes only (seen on projects without any numbering, too). A fixed
+# hash seed makes the order stable; it changes nothing for a newer Qt.
+if(Qt6_VERSION VERSION_LESS 6.5.0)
+ foreach(qet_test tst_resaveunchanged tst_elementautonumids)
+ if(TEST ${qet_test})
+ set_tests_properties(${qet_test} PROPERTIES ENVIRONMENT "QT_HASH_SEED=0")
+ endif()
+ endforeach()
+endif()
diff --git a/tests/qttest/tst_elementautonumids.cpp b/tests/qttest/tst_elementautonumids.cpp
new file mode 100644
index 000000000..28ebf6253
--- /dev/null
+++ b/tests/qttest/tst_elementautonumids.cpp
@@ -0,0 +1,976 @@
+/*
+ Copyright 2006-2026 The QElectroTech Team
+ This file is part of QElectroTech.
+
+ QElectroTech is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ QElectroTech is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with QElectroTech. If not, see .
+*/
+// Element numbering schemes have an id, and elements follow a scheme by it
+// (elementInformation "formula_id"). Runs the real binary on
+// examples/industrial.qet, written before the ids existed: its schemes
+// "Equipment" and "XV" are followed by elements, and it also holds
+// elements whose formula no scheme defines any more (%prefixV1:%sequ_1...,
+// left behind by earlier edits of a scheme), which must stay unlinked.
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+class tst_elementautonumids : public QObject
+{
+ Q_OBJECT
+
+ QTemporaryDir m_dir;
+ int m_run = 0;
+
+ QString m_preferences; // contents of the settings file of the next run
+
+ QProcessEnvironment environment()
+ {
+ const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run++));
+ QDir().mkpath(home);
+ if (!m_preferences.isEmpty()) {
+ QDir().mkpath(home + QStringLiteral("/config/QElectroTech"));
+ QFile settings(home + QStringLiteral("/config/QElectroTech/QElectroTech.conf"));
+ if (settings.open(QIODevice::WriteOnly)) {
+ settings.write(m_preferences.toUtf8());
+ }
+ }
+ QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
+ env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
+ env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
+ env.insert(QStringLiteral("HOME"), home);
+ env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
+ env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
+ env.insert(QStringLiteral("TMPDIR"), m_dir.path());
+ return env;
+ }
+
+ QString resave(const QString &in)
+ {
+ const QString out = m_dir.filePath(QStringLiteral("out%1.qet").arg(m_run));
+ QProcess proc;
+ proc.setProcessEnvironment(environment());
+ proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--resave"), in, out});
+ if (!proc.waitForFinished(180000) || proc.exitCode() != 0) return {};
+ return out;
+ }
+
+ QJsonObject run(const QString &script, const QString &project)
+ {
+ const QString path = m_dir.filePath(QStringLiteral("probe%1.js").arg(m_run));
+ QFile f(path);
+ if (!f.open(QIODevice::WriteOnly)) return {};
+ f.write(script.toUtf8());
+ f.close();
+
+ QProcess proc;
+ proc.setProcessEnvironment(environment());
+ proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--run"), path, project});
+ if (!proc.waitForFinished(180000)) return {};
+ const QString out = QString::fromUtf8(proc.readAllStandardOutput()
+ + proc.readAllStandardError());
+ const QString mark = QStringLiteral("PROBE ");
+ for (const QString &line : out.split(QLatin1Char('\n'))) {
+ const int i = line.indexOf(mark);
+ if (i >= 0)
+ return QJsonDocument::fromJson(line.mid(i + mark.size()).toUtf8()).object();
+ }
+ qWarning().noquote() << out;
+ return {};
+ }
+
+ static QByteArray read(const QString &path)
+ {
+ QFile f(path);
+ return f.open(QIODevice::ReadOnly) ? f.readAll() : QByteArray();
+ }
+
+ struct Saved
+ {
+ QHash scheme_id; // title -> id
+ QHash scheme_formula; // title -> formula
+ QString current, current_id;
+ // per element: formula and formula_id
+ QList> elements;
+ };
+
+ static Saved parse(const QByteArray &xml)
+ {
+ Saved s;
+ QDomDocument doc;
+ if (!doc.setContent(xml)) return s;
+ const QDomNodeList groups = doc.elementsByTagName(QStringLiteral("element_autonums"));
+ if (!groups.isEmpty()) {
+ const QDomElement g = groups.at(0).toElement();
+ s.current = g.attribute(QStringLiteral("current_autonum"));
+ s.current_id = g.attribute(QStringLiteral("current_autonum_id"));
+ const QDomNodeList schemes = g.elementsByTagName(QStringLiteral("element_autonum"));
+ for (int i = 0 ; i < schemes.count() ; ++i) {
+ const QDomElement e = schemes.at(i).toElement();
+ s.scheme_id.insert(e.attribute(QStringLiteral("title")),
+ QUuid(e.attribute(QStringLiteral("id"))));
+ s.scheme_formula.insert(e.attribute(QStringLiteral("title")),
+ e.attribute(QStringLiteral("formula")));
+ }
+ }
+ const QDomNodeList elements = doc.elementsByTagName(QStringLiteral("element"));
+ for (int i = 0 ; i < elements.count() ; ++i) {
+ const QDomElement infos = elements.at(i).toElement()
+ .firstChildElement(QStringLiteral("elementInformations"));
+ if (infos.isNull()) continue;
+ QString formula, id;
+ for (QDomElement info = infos.firstChildElement(QStringLiteral("elementInformation"));
+ !info.isNull();
+ info = info.nextSiblingElement(QStringLiteral("elementInformation"))) {
+ if (info.attribute(QStringLiteral("name")) == QLatin1String("formula"))
+ formula = info.text();
+ else if (info.attribute(QStringLiteral("name")) == QLatin1String("formula_id"))
+ id = info.text();
+ }
+ s.elements << qMakePair(formula, id);
+ }
+ return s;
+ }
+
+ // industrial.qet with the element labelled @p label frozen
+ QString withFrozen(const QString &label)
+ {
+ QFile in(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
+ if (!in.open(QIODevice::ReadOnly)) return {};
+ QDomDocument doc;
+ if (!doc.setContent(&in)) return {};
+ int frozen = 0;
+ const QDomNodeList elements = doc.elementsByTagName(QStringLiteral("element"));
+ for (int i = 0 ; i < elements.count() ; ++i) {
+ QDomElement e = elements.at(i).toElement();
+ const QDomElement infos = e.firstChildElement(QStringLiteral("elementInformations"));
+ for (QDomElement info = infos.firstChildElement(QStringLiteral("elementInformation"));
+ !info.isNull();
+ info = info.nextSiblingElement(QStringLiteral("elementInformation"))) {
+ if (info.attribute(QStringLiteral("name")) == QLatin1String("label")
+ && info.text() == label) {
+ e.setAttribute(QStringLiteral("freezeLabel"), QStringLiteral("true"));
+ ++frozen;
+ }
+ }
+ }
+ if (frozen != 1) return {};
+ const QString out = m_dir.filePath(QStringLiteral("frozen%1.qet").arg(m_run));
+ QFile f(out);
+ if (!f.open(QIODevice::WriteOnly)) return {};
+ f.write(doc.toByteArray());
+ return out;
+ }
+
+ static QStringList diagramAttributes(const QByteArray &xml, const QString &attribute)
+ {
+ QStringList values;
+ QDomDocument doc;
+ if (!doc.setContent(xml)) return values;
+ const QDomNodeList diagrams = doc.elementsByTagName(QStringLiteral("diagram"));
+ for (int i = 0 ; i < diagrams.count() ; ++i)
+ values << diagrams.at(i).toElement().attribute(attribute);
+ return values;
+ }
+
+ // qet.autoNums() lists the numberings as "name: formula='...'"
+ static QStringList titlesOf(const QJsonValue &list)
+ {
+ QStringList titles;
+ for (const QJsonValue &entry : list.toArray()) {
+ const QString text = entry.toString();
+ const int cut = text.indexOf(QLatin1String(": formula="));
+ titles << (cut >= 0 ? text.left(cut) : text);
+ }
+ titles.sort();
+ return titles;
+ }
+
+ static int linkedTo(const Saved &s, const QString &title)
+ {
+ int n = 0;
+ for (const auto &e : s.elements)
+ if (QUuid(e.second) == s.scheme_id.value(title)) ++n;
+ return n;
+ }
+
+private slots:
+ void initTestCase()
+ {
+ QVERIFY(m_dir.isValid());
+ QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
+ }
+
+ // A file without ids: every scheme gets one, the same on every load;
+ // an element is linked to the one scheme with its formula, and only then.
+ void legacyFileGetsIds()
+ {
+ const QString project = QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet");
+ const QString first = resave(project);
+ QVERIFY2(!first.isEmpty(), "--resave failed");
+ const QString again = resave(project);
+ QVERIFY2(!again.isEmpty(), "second --resave of the original failed");
+ //Two separate loads write attributes in their own order, so
+ //compare what they saved, not the bytes
+ const Saved s = parse(read(first));
+ const Saved s_again = parse(read(again));
+ QVERIFY2(s.scheme_id == s_again.scheme_id,
+ "the ids derived for a legacy file differ between loads");
+ QVERIFY2(s.elements == s_again.elements,
+ "the elements were linked differently on another load");
+ QVERIFY(!s.scheme_id.isEmpty());
+ QSet ids;
+ for (auto it = s.scheme_id.constBegin() ; it != s.scheme_id.constEnd() ; ++it) {
+ QVERIFY2(!it.value().isNull(), qPrintable(it.key() + QStringLiteral(" has no id")));
+ ids << it.value();
+ }
+ QCOMPARE(ids.size(), s.scheme_id.size());
+
+ QMultiHash titles_by_formula;
+ for (auto it = s.scheme_formula.constBegin() ; it != s.scheme_formula.constEnd() ; ++it)
+ titles_by_formula.insert(it.value(), it.key());
+
+ for (const auto &e : s.elements) {
+ const QStringList titles = titles_by_formula.values(e.first);
+ if (!e.first.isEmpty() && titles.size() == 1) {
+ QCOMPARE(QUuid(e.second), s.scheme_id.value(titles.first()));
+ } else {
+ QVERIFY2(e.second.isEmpty(),
+ qPrintable(QStringLiteral("element with formula '%1' is linked")
+ .arg(e.first)));
+ }
+ }
+ QVERIFY(linkedTo(s, QStringLiteral("Equipment")) > 0);
+ QVERIFY(linkedTo(s, QStringLiteral("XV")) > 0);
+
+ const QString second = resave(first);
+ QVERIFY2(!second.isEmpty(), "--resave of the resaved file failed");
+ QVERIFY2(read(first) == read(second), "the second save changed the file");
+ }
+
+ // Rename, refused rename and removal, an edit that keeps the numbers,
+ // an edit that renumbers, and undo/redo: through the scripting API,
+ // which goes through the same ElementAutoNumSchemeCommand as the UI.
+ void renameEditRemoveUndo()
+ {
+ const QString saved = m_dir.filePath(QStringLiteral("scripted.qet"));
+ const QString script = QStringLiteral(R"JS(
+function linked(formula) {
+ var r = [];
+ for (var f = 0; f < qet.folioCount(); ++f) {
+ var u = qet.elementUuids(f);
+ for (var i = 0; i < u.length; ++i)
+ if (qet.elementInfo(f, u[i], 'formula') === formula) r.push([f, u[i]]);
+ }
+ return r;
+}
+function label(e) { return qet.elementInfo(e[0], e[1], 'label'); }
+function fid(e) { return qet.elementInfo(e[0], e[1], 'formula_id'); }
+var xv = linked('%prefix2:%sequ_1');
+var eq = linked('%id%prefix%sequ_1');
+var r = {xv: xv.length, eq: eq.length};
+var e0 = xv[0];
+r.id0 = fid(e0);
+r.label0 = label(e0);
+r.renamed = qet.renameAutoNum('element', 'XV', 'Terminals XV');
+r.idAfterRename = fid(e0);
+r.clash = qet.renameAutoNum('element', 'Terminals XV', ' equipment ');
+r.empty = qet.renameAutoNum('element', 'Terminals XV', ' ');
+r.removeUsed = qet.removeAutoNum('element', 'Terminals XV');
+r.removeUnused = qet.removeAutoNum('element', 'TB "XPE"');
+r.edited = qet.addAutoNum('element', 'Terminals XV', ['elementprefix', 'string:X', 'unit:1']);
+r.formulaAfterEdit = qet.elementInfo(e0[0], e0[1], 'formula');
+r.labelAfterEdit = label(e0);
+r.idAfterEdit = fid(e0);
+qet.undo();
+r.formulaAfterUndo = qet.elementInfo(e0[0], e0[1], 'formula');
+r.labelAfterUndo = label(e0);
+qet.redo();
+r.labelAfterRedo = label(e0);
+r.renumbered = qet.addAutoNum('element', 'Equipment', ['idfolio', 'elementprefix', 'string:-', 'ten:1']);
+r.eqStillLinked = linked('%id%prefix-%seqt_1').length;
+r.typed = qet.setElementInfo(eq[0][0], eq[0][1], 'formula', '%id%prefix%sequ_1');
+r.typedId = fid(eq[0]);
+r.internal = qet.setElementInfo(eq[1][0], eq[1][1], 'formula_id', '{00000000-0000-0000-0000-000000000001}');
+r.names = qet.autoNums('element');
+r.save = qet.save(%1);
+qet.log('PROBE ' + JSON.stringify(r));
+)JS").arg(QStringLiteral("'") + saved + QStringLiteral("'"));
+
+ const QJsonObject r = run(script, QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
+ QVERIFY2(!r.isEmpty(), "the script logged nothing");
+ QVERIFY(r.value(QStringLiteral("xv")).toInt() > 0);
+ QVERIFY(r.value(QStringLiteral("eq")).toInt() > 1);
+
+ const QString id0 = r.value(QStringLiteral("id0")).toString();
+ QVERIFY(!QUuid(id0).isNull());
+
+ // a rename touches no element
+ QVERIFY(r.value(QStringLiteral("renamed")).toBool());
+ QCOMPARE(r.value(QStringLiteral("idAfterRename")).toString(), id0);
+ // names are unique ignoring case and surrounding spaces, never empty
+ QVERIFY(!r.value(QStringLiteral("clash")).toBool());
+ QVERIFY(!r.value(QStringLiteral("empty")).toBool());
+ // a scheme elements follow cannot be removed, an unused one can
+ QVERIFY(!r.value(QStringLiteral("removeUsed")).toBool());
+ QVERIFY(r.value(QStringLiteral("removeUnused")).toBool());
+
+ // same sequential parts: new formula, same number, still linked
+ QVERIFY(r.value(QStringLiteral("edited")).toBool());
+ QCOMPARE(r.value(QStringLiteral("formulaAfterEdit")).toString(),
+ QStringLiteral("%prefixX%sequ_1"));
+ QCOMPARE(r.value(QStringLiteral("idAfterEdit")).toString(), id0);
+ QString expected = r.value(QStringLiteral("label0")).toString();
+ const int colon = expected.lastIndexOf(QStringLiteral("2:"));
+ QVERIFY(colon >= 0);
+ expected.replace(colon, 2, QStringLiteral("X"));
+ QCOMPARE(r.value(QStringLiteral("labelAfterEdit")).toString(), expected);
+
+ // one undo step, both ways
+ QCOMPARE(r.value(QStringLiteral("formulaAfterUndo")).toString(),
+ QStringLiteral("%prefix2:%sequ_1"));
+ QCOMPARE(r.value(QStringLiteral("labelAfterUndo")).toString(),
+ r.value(QStringLiteral("label0")).toString());
+ QCOMPARE(r.value(QStringLiteral("labelAfterRedo")).toString(), expected);
+
+ // other sequential parts: renumbered, every element still linked
+ QVERIFY(r.value(QStringLiteral("renumbered")).toBool());
+ QCOMPARE(r.value(QStringLiteral("eqStillLinked")).toInt(),
+ r.value(QStringLiteral("eq")).toInt());
+
+ // a formula written by hand follows no scheme, formula_id is internal
+ QVERIFY(r.value(QStringLiteral("typed")).toBool());
+ QVERIFY(r.value(QStringLiteral("typedId")).toString().isEmpty());
+ QVERIFY(!r.value(QStringLiteral("internal")).toBool());
+
+ QVERIFY(r.value(QStringLiteral("save")).toBool());
+ const Saved s = parse(read(saved));
+ QVERIFY(s.scheme_id.contains(QStringLiteral("Terminals XV")));
+ QVERIFY(!s.scheme_id.contains(QStringLiteral("XV")));
+ QVERIFY(!s.scheme_id.contains(QStringLiteral("TB \"XPE\"")));
+ QCOMPARE(s.scheme_id.value(QStringLiteral("Terminals XV")), QUuid(id0));
+ QCOMPARE(linkedTo(s, QStringLiteral("Terminals XV")), r.value(QStringLiteral("xv")).toInt());
+ QCOMPARE(linkedTo(s, QStringLiteral("Equipment")), r.value(QStringLiteral("eq")).toInt() - 1);
+ }
+
+ // Giving a numbering to elements that have none (what picking it in the
+ // element's information window does): formula, id and the next number,
+ // the counter moving on, refused where something would be lost, and one
+ // undo step that gives the number back.
+ void assignScheme()
+ {
+ const QString script = QStringLiteral(R"JS(
+function find(pred, max) {
+ var r = [];
+ for (var f = 0; f < qet.folioCount() && r.length < max; ++f) {
+ var u = qet.elementUuids(f);
+ for (var i = 0; i < u.length && r.length < max; ++i)
+ if (pred(f, u[i])) r.push([f, u[i]]);
+ }
+ return r;
+}
+function info(e, k) { return qet.elementInfo(e[0], e[1], k); }
+var r = {};
+var candidates = find(function (f, u) { return qet.elementInfo(f, u, 'formula') === ''; }, 40);
+var done = [];
+for (var i = 0; i < candidates.length && done.length < 3; ++i)
+ if (qet.assignElementAutoNum('Equipment', candidates[i][0], candidates[i][1], false)) done.push(candidates[i]);
+r.assigned = done.length;
+var a = done[0], b = done[1], c = done[2];
+r.formulaA = info(a, 'formula');
+r.idA = info(a, 'formula_id');
+r.labelA = info(a, 'label');
+r.labelB = info(b, 'label');
+r.labelC = info(c, 'label');
+r.again = qet.assignElementAutoNum('Equipment', a[0], a[1], false);
+r.other = qet.assignElementAutoNum('XV', a[0], a[1], false);
+r.otherOverwrite = qet.assignElementAutoNum('XV', a[0], a[1], true);
+r.formulaAfterOverwrite = info(a, 'formula');
+r.idAfterOverwrite = info(a, 'formula_id');
+r.unknown = qet.assignElementAutoNum('Nope', a[0], a[1], true);
+qet.undo();
+r.formulaAfterUndo = info(a, 'formula');
+r.idAfterUndo = info(a, 'formula_id');
+r.save = qet.save(%1);
+qet.log('PROBE ' + JSON.stringify(r));
+)JS").arg(QStringLiteral("'") + m_dir.filePath(QStringLiteral("assigned.qet")) + QStringLiteral("'"));
+
+ const QJsonObject r = run(script, QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
+ QVERIFY2(!r.isEmpty(), "the script logged nothing");
+ QCOMPARE(r.value(QStringLiteral("assigned")).toInt(), 3);
+
+ QCOMPARE(r.value(QStringLiteral("formulaA")).toString(), QStringLiteral("%id%prefix%sequ_1"));
+ QVERIFY(!QUuid(r.value(QStringLiteral("idA")).toString()).isNull());
+ // the saved counter of "Equipment" is 9: the next numbers, in order
+ // (the label is the folio id, the prefix, then the number)
+ QVERIFY2(r.value(QStringLiteral("labelA")).toString().endsWith(QLatin1Char('9')),
+ qPrintable(r.value(QStringLiteral("labelA")).toString()));
+ QVERIFY2(r.value(QStringLiteral("labelB")).toString().endsWith(QStringLiteral("10")),
+ qPrintable(r.value(QStringLiteral("labelB")).toString()));
+ QVERIFY2(r.value(QStringLiteral("labelC")).toString().endsWith(QStringLiteral("11")),
+ qPrintable(r.value(QStringLiteral("labelC")).toString()));
+
+ QVERIFY(!r.value(QStringLiteral("again")).toBool());
+ QVERIFY(!r.value(QStringLiteral("other")).toBool());
+ QVERIFY(r.value(QStringLiteral("otherOverwrite")).toBool());
+ QCOMPARE(r.value(QStringLiteral("formulaAfterOverwrite")).toString(),
+ QStringLiteral("%prefix2:%sequ_1"));
+ QVERIFY(r.value(QStringLiteral("idAfterOverwrite")).toString()
+ != r.value(QStringLiteral("idA")).toString());
+ QVERIFY(!r.value(QStringLiteral("unknown")).toBool());
+
+ // one undo gives the element its previous numbering back
+ QCOMPARE(r.value(QStringLiteral("formulaAfterUndo")).toString(),
+ QStringLiteral("%id%prefix%sequ_1"));
+ QCOMPARE(r.value(QStringLiteral("idAfterUndo")).toString(),
+ r.value(QStringLiteral("idA")).toString());
+
+ QVERIFY(r.value(QStringLiteral("save")).toBool());
+ const Saved saved = parse(read(m_dir.filePath(QStringLiteral("assigned.qet"))));
+ // three elements gained the numbering, whose counter moved past them
+ QVERIFY(linkedTo(saved, QStringLiteral("Equipment")) >= 3);
+ }
+
+ // An element whose label is frozen is left as it is by a renumbering, and
+ // the numbering goes past its number instead of giving it to another
+ // element. On industrial.qet the element labelled 5F3 is frozen: without
+ // that, the numbering would give 5F3 to another element of folio 5.
+ void frozenElementsAreSkipped()
+ {
+ const QString body = QStringLiteral(R"JS(
+function labels() {
+ var r = {};
+ for (var f = 0; f < qet.folioCount(); ++f) {
+ var u = qet.elementUuids(f);
+ for (var i = 0; i < u.length; ++i)
+ if (qet.elementInfo(f, u[i], 'formula') === '%id%prefix%sequ_1')
+ r[f + '/' + u[i]] = qet.elementInfo(f, u[i], 'label');
+ }
+ return r;
+}
+var before = labels();
+var r = {before: before};
+r.frozen = qet.renumberElementAutoNum('Equipment');
+r.after = labels();
+r.missing = qet.renumberElementAutoNum('Nope');
+qet.undo();
+r.undone = labels();
+qet.log('PROBE ' + JSON.stringify(r));
+)JS");
+
+ // control: nothing frozen, 5F3 goes to another element
+ const QJsonObject c = run(body, QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
+ QVERIFY2(!c.isEmpty(), "the control script logged nothing");
+ QCOMPARE(c.value(QStringLiteral("frozen")).toInt(), 0);
+ const QJsonObject cb = c.value(QStringLiteral("before")).toObject();
+ const QJsonObject ca = c.value(QStringLiteral("after")).toObject();
+ QString holder;
+ int given = 0;
+ for (auto it = cb.constBegin() ; it != cb.constEnd() ; ++it) {
+ if (it.value().toString() == QLatin1String("5F3")) holder = it.key();
+ }
+ QVERIFY2(!holder.isEmpty(), "no element is labelled 5F3 in industrial.qet");
+ for (auto it = ca.constBegin() ; it != ca.constEnd() ; ++it) {
+ if (it.value().toString() == QLatin1String("5F3") && it.key() != holder) ++given;
+ }
+ QVERIFY2(given == 1, "the control renumbering does not give 5F3 to another element: "
+ "this test would prove nothing");
+
+ // with the element frozen
+ const QString project = withFrozen(QStringLiteral("5F3"));
+ QVERIFY2(!project.isEmpty(), "cannot freeze the element");
+ const QJsonObject r = run(body, project);
+ QVERIFY2(!r.isEmpty(), "the script logged nothing");
+ QCOMPARE(r.value(QStringLiteral("frozen")).toInt(), 1);
+ QCOMPARE(r.value(QStringLiteral("missing")).toInt(), -1);
+
+ const QJsonObject before = r.value(QStringLiteral("before")).toObject();
+ const QJsonObject after = r.value(QStringLiteral("after")).toObject();
+ QCOMPARE(after.size(), before.size());
+ QCOMPARE(after.value(holder).toString(), QStringLiteral("5F3"));
+
+ QSet seen;
+ int changed = 0;
+ for (auto it = after.constBegin() ; it != after.constEnd() ; ++it) {
+ const QString label = it.value().toString();
+ QVERIFY2(!seen.contains(label), qPrintable(QStringLiteral("two elements are labelled %1").arg(label)));
+ seen << label;
+ if (before.value(it.key()).toString() != label) ++changed;
+ }
+ QVERIFY(changed > 0); // the others were numbered again
+
+ // one undo gives every label back
+ QCOMPARE(r.value(QStringLiteral("undone")).toObject(), before);
+ }
+
+ // A scheme whose formula would change cannot be edited while an element
+ // with a frozen label follows it. A rename, and a change of the counter
+ // (same formula), still can: they touch no label.
+ void frozenElementsBlockAnEdit()
+ {
+ const QString project = withFrozen(QStringLiteral("5F3"));
+ QVERIFY2(!project.isEmpty(), "cannot freeze the element");
+ const QString script = QStringLiteral(R"JS(
+function find(label) {
+ for (var f = 0; f < qet.folioCount(); ++f) {
+ var u = qet.elementUuids(f);
+ for (var i = 0; i < u.length; ++i)
+ if (qet.elementInfo(f, u[i], 'label') === label) return [f, u[i]];
+ }
+ return null;
+}
+var frozen = find('5F3'), other = find('5F4');
+var r = {oldFormula: qet.elementInfo(frozen[0], frozen[1], 'formula')};
+r.refused = qet.addAutoNum('element', 'Equipment', ['idfolio', 'elementprefix', 'unit:1', 'string:x']);
+r.frozenFormula = qet.elementInfo(frozen[0], frozen[1], 'formula');
+r.frozenLabel = qet.elementInfo(frozen[0], frozen[1], 'label');
+r.otherFormula = qet.elementInfo(other[0], other[1], 'formula');
+r.otherLabel = qet.elementInfo(other[0], other[1], 'label');
+r.counter = qet.addAutoNum('element', 'Equipment', ['idfolio', 'elementprefix', 'unit:40']);
+r.otherAfterCounter = qet.elementInfo(other[0], other[1], 'label');
+r.renamed = qet.renameAutoNum('element', 'Equipment', 'Equipment 2');
+r.frozenId = qet.elementInfo(frozen[0], frozen[1], 'formula_id');
+r.names = qet.autoNums('element');
+qet.log('PROBE ' + JSON.stringify(r));
+)JS");
+ const QJsonObject r = run(script, project);
+ QVERIFY2(!r.isEmpty(), "the script logged nothing");
+ QVERIFY(!r.value(QStringLiteral("refused")).toBool());
+ QCOMPARE(r.value(QStringLiteral("frozenFormula")).toString(),
+ r.value(QStringLiteral("oldFormula")).toString());
+ QCOMPARE(r.value(QStringLiteral("frozenLabel")).toString(), QStringLiteral("5F3"));
+ QCOMPARE(r.value(QStringLiteral("otherFormula")).toString(), r.value(QStringLiteral("oldFormula")).toString());
+ QCOMPARE(r.value(QStringLiteral("otherLabel")).toString(), QStringLiteral("5F4"));
+
+ // same formula: allowed, and no label moves
+ QVERIFY(r.value(QStringLiteral("counter")).toBool());
+ QCOMPARE(r.value(QStringLiteral("otherAfterCounter")).toString(), QStringLiteral("5F4"));
+ // a rename is allowed, the frozen element still follows the numbering
+ QVERIFY(r.value(QStringLiteral("renamed")).toBool());
+ QVERIFY(!QUuid(r.value(QStringLiteral("frozenId")).toString()).isNull());
+ QVERIFY(QJsonDocument(r.value(QStringLiteral("names")).toArray()).toJson().contains("Equipment 2"));
+ }
+
+ // Duplicating elements which follow a numbering gives the copies the next
+ // numbers of it instead of the labels of the originals, and the numbering
+ // moves on; one undo takes the copies away and gives the numbers back, one
+ // redo brings the same numbers again.
+ void duplicatesAreNumbered()
+ {
+ const QString script = QStringLiteral(R"JS(
+function labelsOf(f) {
+ var r = [], u = qet.elementUuids(f);
+ for (var i = 0; i < u.length; ++i)
+ if (qet.elementInfo(f, u[i], 'formula') === '%id%prefix%sequ_1') r.push(qet.elementInfo(f, u[i], 'label'));
+ return r;
+}
+function find(label) {
+ for (var f = 0; f < qet.folioCount(); ++f) {
+ var u = qet.elementUuids(f);
+ for (var i = 0; i < u.length; ++i)
+ if (qet.elementInfo(f, u[i], 'label') === label) return [f, u[i]];
+ }
+ return null;
+}
+var a = find('8F1'), b = find('8F2');
+var r = {before: labelsOf(a[0])};
+var created = qet.duplicateElements(a[0], [a[1], b[1]], a[0], 0, 60);
+r.count = created.length;
+r.labels = created.map(function (u) { return qet.elementInfo(a[0], u, 'label'); });
+r.formulas = created.map(function (u) { return qet.elementInfo(a[0], u, 'formula'); });
+r.ids = created.map(function (u) { return qet.elementInfo(a[0], u, 'formula_id'); });
+r.originalId = qet.elementInfo(a[0], a[1], 'formula_id');
+r.afterAll = labelsOf(a[0]);
+qet.undo();
+r.afterUndo = labelsOf(a[0]);
+qet.redo();
+r.afterRedo = labelsOf(a[0]);
+qet.undo();
+// the counter went back: copying again gives the same numbers
+var again = qet.duplicateElements(a[0], [a[1], b[1]], a[0], 0, 60);
+r.again = again.map(function (u) { return qet.elementInfo(a[0], u, 'label'); });
+qet.log('PROBE ' + JSON.stringify(r));
+)JS");
+ m_preferences.clear();
+ const QJsonObject r = run(script, QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
+ QVERIFY2(!r.isEmpty(), "the script logged nothing");
+ QCOMPARE(r.value(QStringLiteral("count")).toInt(), 2);
+
+ const QJsonArray labels = r.value(QStringLiteral("labels")).toArray();
+ const QJsonArray before = r.value(QStringLiteral("before")).toArray();
+ QCOMPARE(labels.size(), 2);
+ for (const QJsonValue &label : labels) {
+ QVERIFY2(!label.toString().isEmpty(), "a copy has no label");
+ QVERIFY2(!before.contains(label), qPrintable(QStringLiteral("a copy has the label %1 of another element").arg(label.toString())));
+ }
+ QVERIFY(labels.at(0) != labels.at(1));
+ QVERIFY(labels.at(0).toString().startsWith(QLatin1String("8F")));
+
+ const QJsonArray formulas = r.value(QStringLiteral("formulas")).toArray();
+ const QJsonArray ids = r.value(QStringLiteral("ids")).toArray();
+ for (int i = 0 ; i < 2 ; ++i) {
+ QCOMPARE(formulas.at(i).toString(), QStringLiteral("%id%prefix%sequ_1"));
+ QCOMPARE(ids.at(i).toString(), r.value(QStringLiteral("originalId")).toString());
+ }
+
+ // the originals are untouched, and every label is unique
+ const QJsonArray all = r.value(QStringLiteral("afterAll")).toArray();
+ QCOMPARE(all.size(), before.size() + 2);
+ QSet unique;
+ for (const QJsonValue &label : all) unique << label.toString();
+ QCOMPARE(unique.size(), all.size());
+ for (const QJsonValue &label : before) QVERIFY(all.contains(label));
+
+ // undo, redo, undo and copy again: the same numbers
+ QCOMPARE(r.value(QStringLiteral("afterUndo")).toArray(), before);
+ QCOMPARE(r.value(QStringLiteral("afterRedo")).toArray().size(), all.size());
+ QCOMPARE(r.value(QStringLiteral("again")).toArray(), labels);
+ }
+
+ // The preference "number pasted elements" is off: a copy is what a copy
+ // was, whatever the label preference says. Erasing (the default) leaves
+ // it with no label and no numbering; keeping leaves it with the label of
+ // the original, still tied to the same numbering.
+ void duplicatesAreNotNumberedWhenSwitchedOff()
+ {
+ const QString script = QStringLiteral(R"JS(
+function find(label) {
+ for (var f = 0; f < qet.folioCount(); ++f) {
+ var u = qet.elementUuids(f);
+ for (var i = 0; i < u.length; ++i)
+ if (qet.elementInfo(f, u[i], 'label') === label) return [f, u[i]];
+ }
+ return null;
+}
+var a = find('8F1');
+var created = qet.duplicateElements(a[0], [a[1]], a[0], 0, 60);
+var r = {label: qet.elementInfo(a[0], created[0], 'label'),
+ formula: qet.elementInfo(a[0], created[0], 'formula'),
+ id: qet.elementInfo(a[0], created[0], 'formula_id')};
+qet.log('PROBE ' + JSON.stringify(r));
+)JS");
+ m_preferences = QStringLiteral("[diagramcommands]\nautonumber-pasted-elements=false\n");
+ const QJsonObject erased = run(script, QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
+ QVERIFY2(!erased.isEmpty(), "the script logged nothing");
+ QVERIFY(erased.value(QStringLiteral("label")).toString().isEmpty());
+ QVERIFY(erased.value(QStringLiteral("formula")).toString().isEmpty());
+ QVERIFY(erased.value(QStringLiteral("id")).toString().isEmpty());
+
+ m_preferences = QStringLiteral("[diagramcommands]\nautonumber-pasted-elements=false\nerase-label-on-copy=false\n");
+ const QJsonObject kept = run(script, QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
+ QVERIFY2(!kept.isEmpty(), "the script logged nothing");
+ QCOMPARE(kept.value(QStringLiteral("label")).toString(), QStringLiteral("8F1"));
+ QCOMPARE(kept.value(QStringLiteral("formula")).toString(), QStringLiteral("%id%prefix%sequ_1"));
+ QVERIFY(!QUuid(kept.value(QStringLiteral("id")).toString()).isNull());
+ m_preferences.clear();
+ }
+
+ // Conductor numberings: names are unique and never empty, a rename moves
+ // the folios which read the numbering with it (26 folios of
+ // industrial.qet read "Wire"), a numbering which folios read cannot be
+ // removed, an unused one can, and every step is one undo.
+ void conductorNumberings()
+ {
+ const QString saved = m_dir.filePath(QStringLiteral("conductors.qet"));
+ const QString script = QStringLiteral(R"JS(
+var r = {};
+r.names = qet.autoNums('conductor');
+r.clash = qet.addAutoNum('conductor', ' wire ', ['unit:1']);
+r.empty = qet.addAutoNum('conductor', ' ', ['unit:1']);
+r.added = qet.addAutoNum('conductor', 'Spare', ['string:S', 'unit:1']);
+r.renameClash = qet.renameAutoNum('conductor', 'Spare', 'WIRE');
+r.removeUsed = qet.removeAutoNum('conductor', 'Wire');
+r.renamed = qet.renameAutoNum('conductor', 'Wire', 'Cable');
+r.afterRename = qet.autoNums('conductor');
+r.missing = qet.renameAutoNum('conductor', 'Nope', 'X');
+r.removeSpare = qet.removeAutoNum('conductor', 'Spare');
+r.afterRemove = qet.autoNums('conductor');
+qet.undo(); // the removal
+r.afterUndoRemove = qet.autoNums('conductor');
+qet.undo(); qet.undo(); // the rename, then the creation
+qet.redo(); qet.redo(); // the creation, then the rename
+r.afterRedo = qet.autoNums('conductor');
+r.save = qet.save(%1);
+qet.log('PROBE ' + JSON.stringify(r));
+)JS").arg(QStringLiteral("'") + saved + QStringLiteral("'"));
+
+ const QJsonObject r = run(script, QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
+ QVERIFY2(!r.isEmpty(), "the script logged nothing");
+ const auto names = [&](const char *key) {
+ return titlesOf(r.value(QLatin1String(key)));
+ };
+ QCOMPARE(names("names"), QStringList{QStringLiteral("Wire")});
+ QVERIFY(!r.value(QStringLiteral("clash")).toBool()); // " wire " is "Wire"
+ QVERIFY(!r.value(QStringLiteral("empty")).toBool());
+ QVERIFY(r.value(QStringLiteral("added")).toBool());
+ QVERIFY(!r.value(QStringLiteral("renameClash")).toBool()); // "WIRE" is "Wire"
+ QVERIFY(!r.value(QStringLiteral("removeUsed")).toBool()); // 26 folios read it
+ QVERIFY(r.value(QStringLiteral("renamed")).toBool());
+ QCOMPARE(names("afterRename"), (QStringList{QStringLiteral("Cable"), QStringLiteral("Spare")}));
+ QVERIFY(!r.value(QStringLiteral("missing")).toBool());
+ QVERIFY(r.value(QStringLiteral("removeSpare")).toBool()); // no folio reads it
+ QCOMPARE(names("afterRemove"), QStringList{QStringLiteral("Cable")});
+ QCOMPARE(names("afterUndoRemove"), (QStringList{QStringLiteral("Cable"), QStringLiteral("Spare")}));
+ QCOMPARE(names("afterRedo"), (QStringList{QStringLiteral("Cable"), QStringLiteral("Spare")}));
+
+ // the folios read the numbering under its new name
+ QVERIFY(r.value(QStringLiteral("save")).toBool());
+ const QByteArray xml = read(saved);
+ const QStringList followed = diagramAttributes(xml, QStringLiteral("conductorAutonum"));
+ QCOMPARE(followed.count(QStringLiteral("Cable")), 26);
+ QCOMPARE(followed.count(QStringLiteral("Wire")), 0);
+ }
+
+ // Folio numberings. A folio numbering is applied once: the number is
+ // written into the folio field and the title block keeps the name (49 of
+ // the 50 folios of industrial.qet name "Name of the new numbering", none
+ // has a folio field which still holds %autonum). A rename moves every name,
+ // removing a numbering which folios only name is allowed, and undo and
+ // redo give back the same names.
+ void folioNumberings()
+ {
+ const QString renamed_file = m_dir.filePath(QStringLiteral("folio-renamed.qet"));
+ const QString undone_file = m_dir.filePath(QStringLiteral("folio-undone.qet"));
+ const QString script = QStringLiteral(R"JS(
+var r = {};
+r.names = qet.autoNums('folio');
+r.clash = qet.addAutoNum('folio', 'name of the new numbering', ['unit:1']);
+r.empty = qet.addAutoNum('folio', ' ', ['unit:1']);
+r.renamed = qet.renameAutoNum('folio', 'Name of the new numbering', 'Folios');
+r.afterRename = qet.autoNums('folio');
+r.saveRenamed = qet.save(%1);
+r.removed = qet.removeAutoNum('folio', 'Folios');
+r.afterRemove = qet.autoNums('folio');
+qet.undo(); // the removal
+r.afterUndoRemove = qet.autoNums('folio');
+qet.undo(); // the rename
+r.afterUndoRename = qet.autoNums('folio');
+r.saveUndone = qet.save(%2);
+qet.redo();
+r.afterRedo = qet.autoNums('folio');
+qet.log('PROBE ' + JSON.stringify(r));
+)JS").arg(QStringLiteral("'") + renamed_file + QStringLiteral("'"),
+ QStringLiteral("'") + undone_file + QStringLiteral("'"));
+
+ const QJsonObject r = run(script, QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
+ QVERIFY2(!r.isEmpty(), "the script logged nothing");
+ const auto names = [&](const char *key) {
+ return titlesOf(r.value(QLatin1String(key)));
+ };
+ const QString old_name = QStringLiteral("Name of the new numbering");
+ QCOMPARE(names("names"), QStringList{old_name});
+ QVERIFY(!r.value(QStringLiteral("clash")).toBool());
+ QVERIFY(!r.value(QStringLiteral("empty")).toBool());
+ QVERIFY(r.value(QStringLiteral("renamed")).toBool());
+ QCOMPARE(names("afterRename"), QStringList{QStringLiteral("Folios")});
+ QVERIFY(r.value(QStringLiteral("removed")).toBool());
+ QCOMPARE(names("afterRemove"), QStringList());
+ QCOMPARE(names("afterUndoRemove"), QStringList{QStringLiteral("Folios")});
+ QCOMPARE(names("afterUndoRename"), QStringList{old_name});
+ QCOMPARE(names("afterRedo"), QStringList{QStringLiteral("Folios")});
+
+ QVERIFY(r.value(QStringLiteral("saveRenamed")).toBool());
+ QStringList named = diagramAttributes(read(renamed_file), QStringLiteral("auto_page_num"));
+ QCOMPARE(named.size(), 50);
+ QCOMPARE(named.count(QStringLiteral("Folios")), 49);
+ QCOMPARE(named.count(old_name), 0);
+
+ QVERIFY(r.value(QStringLiteral("saveUndone")).toBool());
+ named = diagramAttributes(read(undone_file), QStringLiteral("auto_page_num"));
+ QCOMPARE(named.count(old_name), 49);
+ QCOMPARE(named.count(QStringLiteral("Folios")), 0);
+ }
+
+ // Free numbers. An element which follows a numbering of one sequence may be
+ // given a number nobody has, and keeps following the numbering: the case of
+ // an element which, after a reshuffle, must have its real-life number back.
+ // Only free numbers are offered and accepted; one undo gives the number back;
+ // a number past the counter moves the counter, so that the next element is
+ // not given the same label.
+ void freeNumbers()
+ {
+ const QString script = QStringLiteral(R"JS(
+function find(label) {
+ for (var f = 0; f < qet.folioCount(); ++f) {
+ var u = qet.elementUuids(f);
+ for (var i = 0; i < u.length; ++i)
+ if (qet.elementInfo(f, u[i], 'label') === label) return [f, u[i]];
+ }
+ return null;
+}
+function number(label) { var m = /(\d+)$/.exec(label); return m ? parseInt(m[1]) : -1; }
+function followers() {
+ var r = [];
+ for (var f = 0; f < qet.folioCount(); ++f) {
+ var u = qet.elementUuids(f);
+ for (var i = 0; i < u.length; ++i)
+ if (qet.elementInfo(f, u[i], 'formula') === '%id%prefix%sequ_1') r.push(number(qet.elementInfo(f, u[i], 'label')));
+ }
+ return r.sort(function (a, b) { return a - b; });
+}
+var e = find('8F2'), other = find('8F1');
+var r = {used: followers()};
+r.before = qet.elementInfo(e[0], e[1], 'label');
+r.own = number(r.before);
+r.free = qet.freeElementNumbers(e[0], e[1]);
+r.numberOfOther = number(qet.elementInfo(other[0], other[1], 'label'));
+r.idBefore = qet.elementInfo(e[0], e[1], 'formula_id');
+
+r.assignUsed = qet.assignElementNumber(e[0], e[1], r.numberOfOther); // taken: refused
+r.assignZero = qet.assignElementNumber(e[0], e[1], 0);
+r.assignFree = qet.assignElementNumber(e[0], e[1], r.free[0]);
+r.after = qet.elementInfo(e[0], e[1], 'label');
+r.formulaAfter = qet.elementInfo(e[0], e[1], 'formula');
+r.idAfter = qet.elementInfo(e[0], e[1], 'formula_id');
+r.freeAfter = qet.freeElementNumbers(e[0], e[1]); // its old number is free now
+qet.undo();
+r.undone = qet.elementInfo(e[0], e[1], 'label');
+
+// a number past the counter: the counter moves past it
+var high = r.free[r.free.length - 1];
+r.high = high;
+r.assignHigh = qet.assignElementNumber(e[0], e[1], high);
+r.highLabel = qet.elementInfo(e[0], e[1], 'label');
+var plain = null;
+for (var f = 0; f < qet.folioCount() && !plain; ++f) {
+ var u = qet.elementUuids(f);
+ for (var i = 0; i < u.length; ++i)
+ if (qet.elementInfo(f, u[i], 'formula') === '') { plain = [f, u[i]]; break; }
+}
+// an element which follows nothing has no numbers to choose from, nor can it be given one
+r.noFree = qet.freeElementNumbers(plain[0], plain[1]).length;
+r.assignNothing = qet.assignElementNumber(plain[0], plain[1], 5);
+// the next element of the numbering is past the number given by hand
+r.assignNext = qet.assignElementAutoNum('Equipment', plain[0], plain[1], false);
+r.nextNumber = number(qet.elementInfo(plain[0], plain[1], 'label'));
+qet.log('PROBE ' + JSON.stringify(r));
+)JS");
+ const QJsonObject r = run(script, QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
+ QVERIFY2(!r.isEmpty(), "the script logged nothing");
+
+ const QJsonArray used = r.value(QStringLiteral("used")).toArray();
+ const QJsonArray free = r.value(QStringLiteral("free")).toArray();
+ QVERIFY(!free.isEmpty());
+ const int own = r.value(QStringLiteral("own")).toInt();
+ for (const QJsonValue &n : free)
+ QVERIFY2(!used.contains(n) || n.toInt() == own, "a number in use is offered");
+ QVERIFY(!free.contains(r.value(QStringLiteral("numberOfOther"))));
+
+ QVERIFY(!r.value(QStringLiteral("assignUsed")).toBool());
+ QVERIFY(!r.value(QStringLiteral("assignZero")).toBool());
+ QVERIFY(r.value(QStringLiteral("assignFree")).toBool());
+ const QString after = r.value(QStringLiteral("after")).toString();
+ QVERIFY(after != r.value(QStringLiteral("before")).toString());
+ QVERIFY2(after.endsWith(QString::number(free.at(0).toInt())), qPrintable(after));
+ QVERIFY(after.startsWith(QLatin1String("8F")));
+ QCOMPARE(r.value(QStringLiteral("formulaAfter")).toString(), QStringLiteral("%id%prefix%sequ_1"));
+ QCOMPARE(r.value(QStringLiteral("idAfter")).toString(), r.value(QStringLiteral("idBefore")).toString());
+ // the number it had is free for another element now
+ QVERIFY(r.value(QStringLiteral("freeAfter")).toArray().size() >= free.size());
+ QCOMPARE(r.value(QStringLiteral("undone")).toString(), r.value(QStringLiteral("before")).toString());
+
+ // a number past the counter moves the counter on
+ QVERIFY(r.value(QStringLiteral("assignHigh")).toBool());
+ QVERIFY(r.value(QStringLiteral("assignNext")).toBool());
+ QVERIFY2(r.value(QStringLiteral("nextNumber")).toInt() > r.value(QStringLiteral("high")).toInt(),
+ "the next element was given a number which is not past the one given by hand");
+ QCOMPARE(r.value(QStringLiteral("noFree")).toInt(), 0);
+ QVERIFY(!r.value(QStringLiteral("assignNothing")).toBool());
+ }
+
+ // A numbering whose counter is set back over numbers in use does not hand
+ // them out again: placing an element takes the first number whose label
+ // nobody carries, and the counter goes on from there; one undo gives the
+ // counter back. A label held by an element which does not follow the
+ // numbering counts too.
+ void placementSkipsLabelsInUse()
+ {
+ const QString script = QStringLiteral(R"JS(
+function labels() {
+ var r = [];
+ for (var f = 0; f < qet.folioCount(); ++f) {
+ var u = qet.elementUuids(f);
+ for (var i = 0; i < u.length; ++i) {
+ var l = qet.elementInfo(f, u[i], 'label');
+ if (l !== '') r.push(l);
+ }
+ }
+ return r;
+}
+// number the first element without a formula which accepts one (a slave or a report does not)
+function numberOne(skip) {
+ for (var f = 0; f < qet.folioCount(); ++f) {
+ var u = qet.elementUuids(f);
+ for (var i = 0; i < u.length; ++i) {
+ if (qet.elementInfo(f, u[i], 'formula') !== '' || skip.indexOf(u[i]) >= 0) continue;
+ if (qet.numberElement(f, u[i])) return [f, u[i]];
+ }
+ }
+ return null;
+}
+var r = {};
+// the counter of "Equipment" back at 1, over the 29 elements which have numbers: allowed
+r.back = qet.addAutoNum('element', 'Equipment', ['idfolio', 'elementprefix', 'unit:1']);
+r.use = qet.useElementAutoNum('Equipment');
+var held = labels();
+var a = numberOne([]);
+r.placedA = a !== null;
+r.labelA = qet.elementInfo(a[0], a[1], 'label');
+r.heldA = held.indexOf(r.labelA) >= 0;
+var b = numberOne([a[1]]);
+r.placedB = b !== null;
+r.labelB = qet.elementInfo(b[0], b[1], 'label');
+r.heldB = held.indexOf(r.labelB) >= 0 || r.labelB === r.labelA;
+r.formulaA = qet.elementInfo(a[0], a[1], 'formula');
+// one undo takes B's number back, the next undo A's
+qet.undo();
+r.afterUndoB = qet.elementInfo(b[0], b[1], 'label');
+qet.undo();
+r.afterUndoA = qet.elementInfo(a[0], a[1], 'label');
+r.again = qet.numberElement(a[0], a[1]);
+r.labelAgain = qet.elementInfo(a[0], a[1], 'label');
+qet.log('PROBE ' + JSON.stringify(r));
+)JS");
+ const QJsonObject r = run(script, QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet"));
+ QVERIFY2(!r.isEmpty(), "the script logged nothing");
+ QVERIFY(r.value(QStringLiteral("back")).toBool());
+ QVERIFY(r.value(QStringLiteral("use")).toBool());
+ QVERIFY(r.value(QStringLiteral("placedA")).toBool());
+ QVERIFY(r.value(QStringLiteral("placedB")).toBool());
+ const QString a = r.value(QStringLiteral("labelA")).toString();
+ const QString b = r.value(QStringLiteral("labelB")).toString();
+ QVERIFY2(!a.isEmpty() && !b.isEmpty(), "an element was not given a label");
+ QVERIFY2(!r.value(QStringLiteral("heldA")).toBool(), qPrintable(QStringLiteral("%1 is held by another element").arg(a)));
+ QVERIFY2(!r.value(QStringLiteral("heldB")).toBool(), qPrintable(QStringLiteral("%1 is held by another element").arg(b)));
+ QVERIFY(a != b);
+ QCOMPARE(r.value(QStringLiteral("formulaA")).toString(), QStringLiteral("%id%prefix%sequ_1"));
+ // undoing gives the numbers back, and numbering again gives the same label
+ QVERIFY(r.value(QStringLiteral("afterUndoB")).toString() != b);
+ QVERIFY(r.value(QStringLiteral("afterUndoA")).toString() != a);
+ QVERIFY(r.value(QStringLiteral("again")).toBool());
+ QCOMPARE(r.value(QStringLiteral("labelAgain")).toString(), a);
+ }
+};
+
+QTEST_APPLESS_MAIN(tst_elementautonumids)
+
+#include "tst_elementautonumids.moc"
diff --git a/tests/qttest/tst_resaveunchanged.cpp b/tests/qttest/tst_resaveunchanged.cpp
index 042a934d6..bc333ea44 100644
--- a/tests/qttest/tst_resaveunchanged.cpp
+++ b/tests/qttest/tst_resaveunchanged.cpp
@@ -122,6 +122,9 @@ private slots:
// (#973), and a value with accents comes back as it went in.
void singleSpaceValueKept()
{
+#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
+ QSKIP("QDomDocument::PreserveSpacingOnlyNodes needs Qt 6.5 (see QETProject::openFile)");
+#endif
QByteArray xml = read(QStringLiteral(QET_EXAMPLES_DIR "/Projet_vierge.qet"));
QVERIFY(xml.contains(""));
xml.replace("",