Merge pull request #493 from ispyisail/cli-set-titleblock

CLI: add --set-titleblock, and fix headless backup crash
This commit is contained in:
Laurent Trinques
2026-06-12 11:42:04 +02:00
committed by GitHub
5 changed files with 119 additions and 0 deletions
+92
View File
@@ -28,6 +28,7 @@
#include "qetgraphicsitem/element.h" #include "qetgraphicsitem/element.h"
#include "qetgraphicsitem/terminal.h" #include "qetgraphicsitem/terminal.h"
#include "qetproject.h" #include "qetproject.h"
#include "titleblockproperties.h"
#include "wiringlistexport.h" #include "wiringlistexport.h"
// Private Qt PDF engine for drawHyperlink() — see pdf_links / projectprintwindow. // Private Qt PDF engine for drawHyperlink() — see pdf_links / projectprintwindow.
@@ -36,6 +37,7 @@
#include <QDir> #include <QDir>
#include <QDirIterator> #include <QDirIterator>
#include <QDomDocument> #include <QDomDocument>
#include <QDate>
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
#include <QJsonArray> #include <QJsonArray>
@@ -43,6 +45,7 @@
#include <QJsonObject> #include <QJsonObject>
#include <QMap> #include <QMap>
#include <QPageLayout> #include <QPageLayout>
#include <QPair>
#include <QPainter> #include <QPainter>
#include <QPdfWriter> #include <QPdfWriter>
#include <QSet> #include <QSet>
@@ -72,6 +75,7 @@ const QHash<QString, QString> &exportFlags()
{"--info", "info"}, {"--info", "info"},
{"--check-elements", "check"}, {"--check-elements", "check"},
{"--resave", "resave"}, {"--resave", "resave"},
{"--set-titleblock", "settb"},
}; };
return flags; return flags;
} }
@@ -652,6 +656,92 @@ int resaveProject(QETProject &project, const QString &output)
return 0; return 0;
} }
/// Stamp title-block fields onto every folio (and the project default), then
/// save. Each assignment is "key=value". Standard keys map to the documented
/// title-block fields; "date=today" uses the current date; any other key is
/// stored as a custom title-block field. Aimed at CI/revision workflows
/// (e.g. set revision + date before exporting a new revision).
int setTitleBlock(QETProject &project, const QString &output,
const QStringList &assignments)
{
if (assignments.isEmpty()) {
err << "No field assignments given (expected key=value).\n";
return 2;
}
// Parse "key=value" assignments up front so a bad one fails before writing.
QList<QPair<QString, QString>> fields;
for (const QString &a : assignments) {
const int eq = a.indexOf('=');
if (eq <= 0) {
err << "Bad assignment '" << a << "' (expected key=value).\n";
return 2;
}
const QString key = a.left(eq);
const QString val = a.mid(eq + 1);
if (key.compare("date", Qt::CaseInsensitive) == 0
&& val.compare("today", Qt::CaseInsensitive) != 0
&& !QDate::fromString(val, Qt::ISODate).isValid()) {
err << "Bad date '" << val << "' (expected YYYY-MM-DD or 'today').\n";
return 2;
}
fields << qMakePair(key, val);
}
auto apply = [&](TitleBlockProperties &p) {
for (const auto &f : fields) {
const QString k = f.first.toLower();
const QString &v = f.second;
if (k == "title") p.title = v;
else if (k == "author") p.author = v;
else if (k == "filename") p.filename = v;
else if (k == "plant") p.plant = v;
else if (k == "location") p.locmach = v;
else if (k == "revision") p.indexrev = v;
else if (k == "version") p.version = v;
else if (k == "date") {
p.date = (v.compare("today", Qt::CaseInsensitive) == 0)
? QDate::currentDate()
: QDate::fromString(v, Qt::ISODate);
// An explicit date is only honoured when the folio is in
// "use the date value" mode (not "now"/"null").
p.useDate = TitleBlockProperties::UseDateValue;
}
else // unknown key -> custom title-block field
p.context.addValue(f.first, v);
}
};
// Project default (the template applied to new folios).
TitleBlockProperties def = project.defaultTitleBlockProperties();
apply(def);
project.setDefaultTitleBlockProperties(def);
// Every existing folio's own title block.
int folios = 0;
const QList<Diagram *> diagrams = project.diagrams();
for (Diagram *diagram : diagrams) {
TitleBlockProperties p =
diagram->border_and_titleblock.exportTitleBlock();
apply(p);
diagram->border_and_titleblock.importTitleBlock(p);
++folios;
}
const QDomDocument doc = project.toXml();
QFile file(output);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
err << "Cannot open '" << output << "' for writing.\n";
return 1;
}
QTextStream fout(&file);
fout << doc.toString(4);
file.close();
out << "Stamped " << fields.size() << " field(s) on "
<< folios << " folio(s) -> " << output << "\n";
return 0;
}
} // anonymous namespace } // anonymous namespace
namespace CLIExport { namespace CLIExport {
@@ -727,6 +817,8 @@ int run(const QStringList &args)
return exportLinks(project, output); return exportLinks(project, output);
if (format == "resave") if (format == "resave")
return resaveProject(project, output); return resaveProject(project, output);
if (format == "settb")
return setTitleBlock(project, output, rest.mid(2));
return exportImages(project, format, output); return exportImages(project, format, output);
} }
+5
View File
@@ -53,6 +53,7 @@ namespace CLIExport {
qelectrotech --info <project.qet> [output.json] qelectrotech --info <project.qet> [output.json]
qelectrotech --check-elements <element.elmt | directory> qelectrotech --check-elements <element.elmt | directory>
qelectrotech --resave <project.qet> <output.qet> qelectrotech --resave <project.qet> <output.qet>
qelectrotech --set-titleblock <project.qet> <output.qet> key=value...
PDF: one multi-page document (one diagram per page). PDF: one multi-page document (one diagram per page).
PNG/SVG: one file per diagram, named <output_dir>/<NN>_<title>.<ext>. PNG/SVG: one file per diagram, named <output_dir>/<NN>_<title>.<ext>.
@@ -66,6 +67,10 @@ namespace CLIExport {
per-page element / conductor counts and unconnected terminals. per-page element / conductor counts and unconnected terminals.
check-elements: validate .elmt file(s) against the element schema. check-elements: validate .elmt file(s) against the element schema.
resave: load and rewrite the project XML (round-trip integrity). resave: load and rewrite the project XML (round-trip integrity).
set-titleblock: stamp title-block fields onto every folio, then save.
Keys: title, author, date (or date=today), plant, location,
revision, version, filename; any other key becomes a custom
field. E.g. --set-titleblock in.qet out.qet revision=B date=today
*/ */
int run(const QStringList &args); int run(const QStringList &args);
+5
View File
@@ -19,6 +19,7 @@
#include "machine_info.h" #include "machine_info.h"
#include "qet.h" #include "qet.h"
#include "qetapp.h" #include "qetapp.h"
#include "qetproject.h"
#include "singleapplication.h" #include "singleapplication.h"
#include "utils/macosxopenevent.h" #include "utils/macosxopenevent.h"
#include "utils/qetsettings.h" #include "utils/qetsettings.h"
@@ -206,6 +207,10 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
raw_args << QString::fromLocal8Bit(argv[i]); raw_args << QString::fromLocal8Bit(argv[i]);
if (CLIExport::isExportRequest(raw_args)) { if (CLIExport::isExportRequest(raw_args)) {
QApplication export_app(argc, argv); QApplication export_app(argc, argv);
// No crash-recovery backups in one-shot CLI mode: the backup write
// runs on a background thread referencing the project and races the
// process exit (intermittent segfault in QET::writeToFile).
QETProject::setBackupEnabled(false);
return CLIExport::run(export_app.arguments()); return CLIExport::run(export_app.arguments());
} }
} }
+9
View File
@@ -43,6 +43,13 @@
static int BACKUP_INTERVAL = 1200000; //interval in ms of backup = 20min static int BACKUP_INTERVAL = 1200000; //interval in ms of backup = 20min
bool QETProject::m_backup_enabled = true;
void QETProject::setBackupEnabled(bool enabled)
{
m_backup_enabled = enabled;
}
/** /**
@brief QETProject::QETProject @brief QETProject::QETProject
Create a empty project Create a empty project
@@ -1783,6 +1790,8 @@ void QETProject::addDiagram(Diagram *diagram, int pos)
*/ */
void QETProject::writeBackup() void QETProject::writeBackup()
{ {
if (!m_backup_enabled)
return;
#ifdef BUILD_WITHOUT_KF5 #ifdef BUILD_WITHOUT_KF5
#else #else
# if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove # if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
+8
View File
@@ -105,6 +105,12 @@ class QETProject : public QObject
QVersionNumber declaredQElectroTechVersion(); QVersionNumber declaredQElectroTechVersion();
void setTitle(const QString &); void setTitle(const QString &);
/// Enable/disable the asynchronous crash-recovery backup for all
/// projects. Disabled by the headless CLI: the backup write runs on a
/// background thread referencing the project, and a short-lived CLI
/// process can destroy the project before the write finishes (crash).
static void setBackupEnabled(bool enabled);
///DEFAULT PROPERTIES ///DEFAULT PROPERTIES
BorderProperties defaultBorderProperties() const; BorderProperties defaultBorderProperties() const;
void setDefaultBorderProperties(const BorderProperties &); void setDefaultBorderProperties(const BorderProperties &);
@@ -241,6 +247,8 @@ class QETProject : public QObject
// attributes // attributes
private: private:
/// When false, writeBackup() is a no-op (set by the headless CLI)
static bool m_backup_enabled;
/// File path this project is saved to /// File path this project is saved to
QString m_file_path; QString m_file_path;
/// Current state of the project /// Current state of the project