From a8f883601d1fd781b7b20e2d4e77a95abf81de52 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Tue, 22 Sep 2026 11:10:19 +1200 Subject: [PATCH] Let a script list, embed and apply a folio's title block template titleBlockTemplates() embedded + common/company/custom, by name embedTitleBlockTemplate(name) copy one into the project's own collection setFolioProperty(f,"template",name) embed-if-needed, then apply folioProperty(f,"template") Not the trivial addition to the existing title-block-field list it looked like at first. Diagram::setTitleBlockTemplate() resolves a name only against QETProject::embeddedTitleBlockTemplatesCollection() -- the exact same copy-into-the-project step addElement() already goes through for elements, and for the same reason: a project opened on another machine must not depend on files only this one has. embedTitleBlockTemplate() does that copy through get/setTemplateXmlDescription(), the same round trip the template editor itself uses to save one -- not scripting-specific code, and unlike defining an auto-numbering context, not undoable, for the same reason that isn't: the application does both through direct collection/project calls with no undo command of their own. Two things found only by testing, not by reading: - "default" is a real template name in the common collection, and setting a folio's template to it is legitimate -- but BorderTitleBlock::titleBlockTemplateName() normalises a template literally named "default" back to "", indistinguishable from no override, since that is genuinely what "no override" renders with. The first version compared the raw name and reported success as failure; fixed by comparing against that same normalised form, which folioProperty() now also documents. - QElectroTech resolves the common template collection from a compiled-in path (here, an absolute /usr/share/qelectrotech/titleblocks, not relative to the binary), and --common-tbt-dir, the CLI override, is read by QETApp::parseArguments() -- which the --run headless path never reaches, confirmed by the CLI itself swallowing the flag as a stray positional argument. There is no QSettings fallback the way commonElementsDir() has. So testing this at all needed the path to genuinely exist; no environment trick from inside the process reaches it. Verified: 10 common templates listed; DIN_A4 embedded and applied, folioProperty reading it back; re-applying the same name a no-op success; an unknown name refused; "default" applied and correctly read back as "" per the note above; both folios exported to PNG and visually compared -- plain default rendering vs. DIN_A4's logo, revision table and field layout, genuinely different, not just an API call returning true. The choice survives a save and reload. Qt 6.10.2, ctest 12/12, coherence gate clean. Co-Authored-By: Claude Sonnet 5 --- sources/scripting/qetscriptapi.cpp | 102 ++++++++++++++++++++++++++++- sources/scripting/qetscriptapi.h | 25 +++++++ 2 files changed, 125 insertions(+), 2 deletions(-) diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index b44b1c210..8e692c8e2 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -28,6 +28,7 @@ #include "../qetgraphicsitem/element.h" #include "../qetmessagebox.h" #include "../dataBase/projectdatabase.h" +#include "../qetapp.h" #include "../qetproject.h" #include "../qetresult.h" #include "../qetgraphicsitem/conductor.h" @@ -42,6 +43,7 @@ #include "../autoNum/assignvariables.h" #include "../autoNum/numerotationcontext.h" #include "../borderproperties.h" +#include "../titleblock/templatescollection.h" #include "../diagramcommands.h" #include "../qetgraphicsitem/terminal.h" #include "../qetgraphicsitem/terminalelement.h" @@ -1504,13 +1506,17 @@ QString *titleBlockField(TitleBlockProperties &p, const QString &name) // QElectroTech writes on every save, so a value set here reports success // and is overwritten -- measured: set "V9-USER", read back "0.200.1-dev". if (name == QLatin1String("folio")) return &p.folio; + // Not "template" either: template_name resolves against the project's + // embedded collection, not free text, so it goes through + // setFolioProperty()'s own branch (embedTitleBlockTemplate() first) + // rather than this direct field lookup. return nullptr; } const QStringList &titleBlockFieldNames() { static const QStringList n{QStringLiteral("title"), QStringLiteral("author"), QStringLiteral("filename"), QStringLiteral("plant"), QStringLiteral("locmach"), - QStringLiteral("indexrev"), QStringLiteral("folio")}; + QStringLiteral("indexrev"), QStringLiteral("folio"), QStringLiteral("template")}; return n; } } // namespace @@ -1520,7 +1526,10 @@ QString QetScriptApi::folioProperty(int folioIndex, const QString &property) con if (!m_project) return QString(); const QList diagrams = m_project->diagrams(); if (folioIndex < 0 || folioIndex >= diagrams.count()) return QString(); - TitleBlockProperties p = diagrams.at(folioIndex)->border_and_titleblock.exportTitleBlock(); + Diagram *diagram = diagrams.at(folioIndex); + if (property == QLatin1String("template")) + return diagram->border_and_titleblock.titleBlockTemplateName(); + TitleBlockProperties p = diagram->border_and_titleblock.exportTitleBlock(); QString *field = titleBlockField(p, property); return field ? *field : QString(); } @@ -1544,6 +1553,27 @@ bool QetScriptApi::setFolioProperty(int folioIndex, const QString &property, con if (folioIndex < 0 || folioIndex >= diagrams.count()) return false; Diagram *diagram = diagrams.at(folioIndex); + // Not a TitleBlockProperties field like the others below: the template + // is named by whatever the project's embedded collection calls it, not + // by a value stored on this folio's own properties, so it has to be + // embedded (or already present) before Diagram::setTitleBlockTemplate() + // -- the same public slot BorderTitleBlock's own needTitleBlockTemplate + // signal calls -- can find it. + if (property == QLatin1String("template")) { + // BorderTitleBlock::titleBlockTemplateName() normalises a template + // literally named "default" back to "" -- indistinguishable, once + // set, from no override at all (a template named "default" ships + // in the common collection and is genuinely what "no override" + // renders with). Compare against that same normalised form, or a + // script setting "default" would see this report failure although + // the application applied it correctly -- measured: it did. + const QString normalised = (value == QLatin1String("default")) ? QString() : value; + if (diagram->border_and_titleblock.titleBlockTemplateName() == normalised) return true; + if (!embedTitleBlockTemplate(value)) return false; + diagram->setTitleBlockTemplate(value); + return diagram->border_and_titleblock.titleBlockTemplateName() == normalised; + } + TitleBlockProperties old_p = diagram->border_and_titleblock.exportTitleBlock(); TitleBlockProperties new_p = old_p; QString *field = titleBlockField(new_p, property); @@ -2427,6 +2457,74 @@ int QetScriptApi::insertFolio(int position) return m_project->diagrams().indexOf(diagram); } +/** + @brief QetScriptApi::titleBlockTemplates + Every title block template this project can use right now (embedded) + or could embed and then use (common, company, custom), each name + suffixed with which. A name can appear more than once, under different + sources -- embedding does not remove it from where it came from, and a + project can have its own embedded copy of a name the common collection + also has, which then shadows it (Diagram::setTitleBlockTemplate() only + ever looks in the embedded one). +*/ +QStringList QetScriptApi::titleBlockTemplates() const +{ + QStringList list; + if (!m_project) return list; + auto describe = [&list](TitleBlockTemplatesCollection *c, const QString &source) { + if (!c) return; + const QStringList names = c->templates(); + for (const QString &n : names) { + list << QStringLiteral("%1 (%2)").arg(n, source); + } + }; + describe(m_project->embeddedTitleBlockTemplatesCollection(), QStringLiteral("embedded")); + describe(QETApp::commonTitleBlockTemplatesCollection(), QStringLiteral("common")); + describe(QETApp::companyTitleBlockTemplatesCollection(), QStringLiteral("company")); + describe(QETApp::customTitleBlockTemplatesCollection(), QStringLiteral("custom")); + return list; +} + +/** + @brief QetScriptApi::embedTitleBlockTemplate + Copy a template's XML into the project's own embedded collection, from + the first of common/company/custom that has it -- the same + get/setTemplateXmlDescription() round trip the template editor itself + uses to save one, not scripting-specific code. A no-op, reporting + success, if the project already has an embedded copy of that name: the + embedded one is what Diagram::setTitleBlockTemplate() will use either + way, so re-embedding would only discard a project-specific edit to it + for no reason. +*/ +bool QetScriptApi::embedTitleBlockTemplate(const QString &name) +{ + if (!m_project) return false; + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.embedTitleBlockTemplate: project is read-only")); + return false; + } + if (name.isEmpty()) { + log(QStringLiteral("qet.embedTitleBlockTemplate: empty name")); + return false; + } + auto *embedded = m_project->embeddedTitleBlockTemplatesCollection(); + if (embedded->templates().contains(name)) return true; + + const QList sources{ + QETApp::commonTitleBlockTemplatesCollection(), + QETApp::companyTitleBlockTemplatesCollection(), + QETApp::customTitleBlockTemplatesCollection()}; + for (TitleBlockTemplatesCollection *source : sources) + { + if (!source || !source->templates().contains(name)) continue; + const QDomElement xml = source->getTemplateXmlDescription(name); + if (xml.isNull()) continue; + return embedded->setTemplateXmlDescription(name, xml); + } + log(QStringLiteral("qet.embedTitleBlockTemplate: no collection has a template named '%1'").arg(name)); + return false; +} + int QetScriptApi::addFolio() { if (!m_project) return -1; diff --git a/sources/scripting/qetscriptapi.h b/sources/scripting/qetscriptapi.h index 20d9f007d..695bbde35 100644 --- a/sources/scripting/qetscriptapi.h +++ b/sources/scripting/qetscriptapi.h @@ -211,6 +211,27 @@ class DynamicElementTextItem; the folio properties panel offers; the title block's header sizes, which it does not, are left alone. Changing the project title is not undoable: the application sets it directly too. + + A folio's title block @b template is a seventh, separate case: + Diagram::setTitleBlockTemplate() resolves a name only against + QETProject::embeddedTitleBlockTemplatesCollection() -- the same + copy-into-the-project step addElement() already does for elements, + and for the same reason (a project opened on another machine must not + depend on files only this one has). titleBlockTemplates() lists what + is embedded and what is available to embed from the common/company + /custom collections, each name suffixed with its source; + embedTitleBlockTemplate() does the copy (QDomElement in, unmodified, + via *TemplatesCollection::get/setTemplateXmlDescription() -- neither + side is scripting-specific code, both already exist for the template + editor to call). setFolioProperty(folio, "template", name) then + embeds it first if it is not already, refusing only if no collection + has that name at all. Embedding is not undoable, the same as defining + an auto-numbering context is not: the application does both through + direct collection/project calls with no undo command of their own. + A template literally named "default" reads back as folioProperty() + "" afterwards, not "default": BorderTitleBlock::titleBlockTemplateName() + treats the two as the same thing, since "no override" already renders + with the template named "default". - @b Geometry and folio order: elementGeometry() reads where an element is -- x, y (its origin), rotation, and the box it occupies on the folio (left, top, right, bottom) -- so a script can lay one thing out relative @@ -418,6 +439,10 @@ class QetScriptApi : public QObject Q_INVOKABLE QString folioBorder(int folioIndex, const QString &property) const; Q_INVOKABLE bool setFolioBorder(int folioIndex, const QString &property, const QString &value); + // -- title block templates: which exist, embedding one into the project -- + Q_INVOKABLE QStringList titleBlockTemplates() const; + Q_INVOKABLE bool embedTitleBlockTemplate(const QString &name); + // -- read an element's geometry -- Q_INVOKABLE QVariantMap elementGeometry(int folioIndex, const QString &elementUuid) const;