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("",