/* 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::followedScheme @return the title of the scheme of @p project which an element with the information @p info follows, empty if none. The id names it, but only counts when the scheme has the formula the element has: an id which comes from another project (a copy of the project edited since, two projects which both have a numbering of the same name...) may name a scheme here which is not the same numbering. */ QString ElementAutoNumSchemeCommand::followedScheme(const QETProject *project, const DiagramContext &info) { if (!project) { return QString(); } const QString title = project->elementAutoNumTitle( QUuid(info.value(QETInformation::ELMT_FORMULA_ID).toString())); if (title.isEmpty() || project->elementAutoNumFormula(title) != info.value(QETInformation::ELMT_FORMULA).toString()) { return QString(); } return title; } /** @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) { const QString formula = autonum::numerotationContextToFormula(scheme.context); const QString by_id = project->elementAutoNumTitle(scheme.id); //Present: under its id with the same formula, or under another id //(another name, other project) with the same formula const bool present = (!by_id.isEmpty() && project->elementAutoNumFormula(by_id) == formula) || !schemeForFormula(project, formula).isEmpty(); if (!followed.contains(scheme.id) || present) { 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 = followedScheme(project, info); 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 with the element's formula is kept (see followedScheme()); 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; } if (!followedScheme(project, info).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); }