From d76f3528ae93d70816d14f3cc31168272d09d159 Mon Sep 17 00:00:00 2001 From: Andre Rummler Date: Sat, 3 Oct 2026 17:53:27 +0200 Subject: [PATCH] AutoNum: a lot of changes Schemas now get saved with a UUID and do not have to be looked up according to the form. They can be renamed and it gets reflected in existing elements. The GUI for editing them is now much easier, e.g. forms can be reshuffled. A formula can be applied. Many options now also for conductors and sheets. --- cmake/qet_compilation_vars.cmake | 12 + misc/qet-mcp/qet_mcp.py | 22 +- misc/qet-mcp/test_qet_mcp.py | 7 + sources/autoNum/autonumschemecommand.cpp | 297 ++++ sources/autoNum/autonumschemecommand.h | 111 ++ .../autoNum/elementautonumschemecommand.cpp | 1263 +++++++++++++++++ sources/autoNum/elementautonumschemecommand.h | 243 ++++ sources/autoNum/renumberelementscommand.cpp | 36 +- sources/autoNum/renumberelementscommand.h | 8 +- .../autoNum/ui/autonumberingdockwidget.cpp | 192 +++ sources/autoNum/ui/autonumberingdockwidget.h | 7 + .../autoNum/ui/autonumberingmanagementw.cpp | 40 +- sources/autoNum/ui/counterwarning.cpp | 50 + sources/autoNum/ui/counterwarning.h | 46 + sources/autoNum/ui/pastenumberingimport.cpp | 94 ++ sources/autoNum/ui/pastenumberingimport.h | 57 + sources/autoNum/ui/renumberpreviewdialog.cpp | 187 +++ sources/autoNum/ui/renumberpreviewdialog.h | 68 + sources/autoNum/ui/selectautonumw.cpp | 212 ++- sources/autoNum/ui/selectautonumw.h | 26 +- sources/autoNum/ui/selectautonumw.ui | 29 +- sources/bordertitleblock.h | 2 + sources/diagram.cpp | 8 + sources/diagramcommands.cpp | 73 + sources/diagramcommands.h | 8 + sources/diagramevent/diagrameventaddmacro.cpp | 5 +- sources/diagramevent/diagrameventaddpaste.cpp | 7 +- sources/diagramevent/diagrameventaddpaste.h | 3 + sources/diagramview.cpp | 6 +- sources/elementspanelwidget.cpp | 21 + sources/print/projectprintwindow.cpp | 2 +- sources/qetgraphicsitem/element.cpp | 72 +- sources/qetgraphicsitem/element.h | 1 + sources/qetinformation.h | 7 + sources/qetproject.cpp | 366 +++-- sources/qetproject.h | 29 +- sources/scripting/qetscriptapi.cpp | 273 +++- sources/scripting/qetscriptapi.h | 6 + .../configpage/generalconfigurationpage.cpp | 2 + .../ui/configpage/generalconfigurationpage.ui | 13 +- sources/ui/configpage/projectconfigpages.cpp | 810 +++++++++-- sources/ui/configpage/projectconfigpages.h | 43 + sources/ui/elementinfowidget.cpp | 456 +++++- sources/ui/elementinfowidget.h | 21 + sources/ui/multipastedialog.cpp | 67 +- sources/ui/projectpropertiesdialog.cpp | 12 + sources/ui/projectpropertiesdialog.h | 1 + .../undocommand/freezeelementlabelcommand.cpp | 46 + .../undocommand/freezeelementlabelcommand.h | 49 + tests/qttest/CMakeLists.txt | 27 + tests/qttest/tst_elementautonumids.cpp | 976 +++++++++++++ tests/qttest/tst_resaveunchanged.cpp | 3 + 52 files changed, 6040 insertions(+), 382 deletions(-) create mode 100644 sources/autoNum/autonumschemecommand.cpp create mode 100644 sources/autoNum/autonumschemecommand.h create mode 100644 sources/autoNum/elementautonumschemecommand.cpp create mode 100644 sources/autoNum/elementautonumschemecommand.h create mode 100644 sources/autoNum/ui/counterwarning.cpp create mode 100644 sources/autoNum/ui/counterwarning.h create mode 100644 sources/autoNum/ui/pastenumberingimport.cpp create mode 100644 sources/autoNum/ui/pastenumberingimport.h create mode 100644 sources/autoNum/ui/renumberpreviewdialog.cpp create mode 100644 sources/autoNum/ui/renumberpreviewdialog.h create mode 100644 sources/undocommand/freezeelementlabelcommand.cpp create mode 100644 sources/undocommand/freezeelementlabelcommand.h create mode 100644 tests/qttest/tst_elementautonumids.cpp 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("",