mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-07-30 07:44:13 +02:00
Merge master: add cli_export, pdf_links; EDZ: 10-slot grid, group headers, ToS notice
- Resolve cmake/qet_compilation_vars.cmake conflict: keep both upstream's cli_export.cpp/h and pdf_links.cpp/h and the EDZ source additions. - 10-position grid alignment: pin_y values are multiples of 10 so terminals snap cleanly to QET's default grid. group_gap raised to 10 (one full slot). - Named connector groups get a header label (group name) placed in the gap above the first pin, so the electrician sees block names (XDI, XPOW, …) without reading individual terminal designations. - Device-tag dynamic_text now uses 9pt LABEL_FONT and y = min_y - 9 so it clears the element body and is legible at normal zoom. - Add EPLAN Data Portal Terms of Use disclaimer to sources/import/edz/README.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -469,7 +469,7 @@ bool ElementCollectionHandler::setNames(ElementsLocation &location,
|
||||
root.appendChild(name_list.toXml(document));
|
||||
|
||||
QString filepath = location.fileSystemPath()
|
||||
+ "/qet_directory";
|
||||
% "/qet_directory";
|
||||
if (!QET::writeXmlFile(document, filepath)) {
|
||||
qDebug() << "ElementCollectionHandler::setNames : write qet-directory file failed";
|
||||
return false;
|
||||
|
||||
@@ -538,6 +538,16 @@ QList<QETProject *> ElementsCollectionModel::project() const
|
||||
*/
|
||||
void ElementsCollectionModel::highlightUnusedElement()
|
||||
{
|
||||
//Reset only the items currently highlighted in red, so elements that
|
||||
//are no longer unused lose the highlight. Scoping to the red
|
||||
//Dense4Pattern avoids touching other backgrounds (e.g. the amber
|
||||
//"show this dir" highlight) and avoids needless updates on big
|
||||
//collections (issue #159).
|
||||
for (ElementCollectionItem *eci : items())
|
||||
if (eci->background().style() == Qt::Dense4Pattern &&
|
||||
eci->background().color() == Qt::red)
|
||||
eci->setBackground(QBrush());
|
||||
|
||||
QList <ElementsLocation> unused;
|
||||
|
||||
foreach (QETProject *project, m_project_list)
|
||||
|
||||
@@ -523,7 +523,9 @@ bool ElementsLocation::isCompanyCollection() const
|
||||
*/
|
||||
bool ElementsLocation::isCustomCollection() const
|
||||
{
|
||||
return fileSystemPath().startsWith(QETApp::customElementsDirN());
|
||||
const QString dir = QETApp::customElementsDirN();
|
||||
const QString path = fileSystemPath();
|
||||
return path == dir || path.startsWith(dir + QLatin1Char('/'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -274,7 +274,9 @@ bool FileElementCollectionItem::isCompanyCollection() const
|
||||
*/
|
||||
bool FileElementCollectionItem::isCustomCollection() const
|
||||
{
|
||||
return fileSystemPath().startsWith(QETApp::customElementsDirN());
|
||||
const QString dir = QETApp::customElementsDirN();
|
||||
const QString path = fileSystemPath();
|
||||
return path == dir || path.startsWith(dir + QLatin1Char('/'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,825 @@
|
||||
/*
|
||||
Copyright 2006-2025 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "cli_export.h"
|
||||
|
||||
#include "bordertitleblock.h"
|
||||
#include "conductornumexport.h"
|
||||
#include "conductorproperties.h"
|
||||
#include "dataBase/projectdatabase.h"
|
||||
#include "diagram.h"
|
||||
#include "diagramcontext.h"
|
||||
#include "pdf_links.h"
|
||||
#include "qetgraphicsitem/conductor.h"
|
||||
#include "qetgraphicsitem/element.h"
|
||||
#include "qetgraphicsitem/terminal.h"
|
||||
#include "qetproject.h"
|
||||
#include "titleblockproperties.h"
|
||||
#include "wiringlistexport.h"
|
||||
|
||||
// Private Qt PDF engine for drawHyperlink() — see pdf_links / projectprintwindow.
|
||||
#include <private/qpdf_p.h>
|
||||
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QDomDocument>
|
||||
#include <QDate>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QMap>
|
||||
#include <QPageLayout>
|
||||
#include <QPair>
|
||||
#include <QPainter>
|
||||
#include <QPdfWriter>
|
||||
#include <QSet>
|
||||
#include <QSqlError>
|
||||
#include <QSqlQuery>
|
||||
#include <QSvgGenerator>
|
||||
#include <QTextStream>
|
||||
#include <QTransform>
|
||||
|
||||
namespace {
|
||||
|
||||
QTextStream out(stdout);
|
||||
QTextStream err(stderr);
|
||||
|
||||
/// All CLI option flags, mapped to a short format name.
|
||||
const QHash<QString, QString> &exportFlags()
|
||||
{
|
||||
static const QHash<QString, QString> flags {
|
||||
{"--export-pdf", "pdf"},
|
||||
{"--export-png", "png"},
|
||||
{"--export-svg", "svg"},
|
||||
{"--export-cables", "cables"},
|
||||
{"--export-wires", "wires"},
|
||||
{"--export-bom", "bom"},
|
||||
{"--export-nets", "nets"},
|
||||
{"--export-links", "links"},
|
||||
{"--info", "info"},
|
||||
{"--check-elements", "check"},
|
||||
{"--resave", "resave"},
|
||||
{"--set-titleblock", "settb"},
|
||||
};
|
||||
return flags;
|
||||
}
|
||||
|
||||
/// Device tag of an element ("K1", "Q55"), falling back to its name.
|
||||
QString elementLabel(Element *element)
|
||||
{
|
||||
const QString label = element->elementInformations()["label"].toString();
|
||||
return label.isEmpty() ? element->name() : label;
|
||||
}
|
||||
|
||||
/// Pixel rect of a diagram's border + title block (the printable page area).
|
||||
QRect diagramRect(Diagram *diagram)
|
||||
{
|
||||
QRectF r = diagram->border_and_titleblock.borderAndTitleBlockRect();
|
||||
r.adjust(0, 0, 1, 1); // include the 1px border line
|
||||
return r.toAlignedRect();
|
||||
}
|
||||
|
||||
/// A filesystem-safe per-diagram file stem: "01_Title".
|
||||
QString diagramStem(Diagram *diagram, int index)
|
||||
{
|
||||
QString title = diagram->title();
|
||||
title.replace(QRegularExpression("[^\\w \\-]"), "_");
|
||||
title = title.simplified();
|
||||
if (title.isEmpty())
|
||||
title = "diagram";
|
||||
return QStringLiteral("%1_%2")
|
||||
.arg(index, 2, 10, QChar('0'))
|
||||
.arg(title);
|
||||
}
|
||||
|
||||
/// Render @p diagram into @p painter, fitting @p target to the page rect.
|
||||
void renderDiagram(Diagram *diagram, QPainter &painter, const QRectF &target)
|
||||
{
|
||||
const QRect source = diagramRect(diagram);
|
||||
// Export without the editor grid: drawBackground() only paints it when
|
||||
// draw_grid_ is set (default true), so toggle it off around the render
|
||||
// and restore it afterwards.
|
||||
const bool was_drawing_grid = diagram->displayGrid();
|
||||
diagram->setDisplayGrid(false);
|
||||
diagram->render(&painter, target, source, Qt::KeepAspectRatio);
|
||||
diagram->setDisplayGrid(was_drawing_grid);
|
||||
}
|
||||
|
||||
int exportPdf(QETProject &project, const QString &output)
|
||||
{
|
||||
const QList<Diagram *> diagrams = project.diagrams();
|
||||
if (diagrams.isEmpty()) {
|
||||
err << "No diagrams to export.\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Page numbers (1-based) for cross-reference hyperlink targets: each
|
||||
// diagram is exactly one page in the CLI export (no tiling).
|
||||
QMap<Diagram *, int> pageMap;
|
||||
for (int i = 0; i < diagrams.size(); ++i)
|
||||
pageMap.insert(diagrams.at(i), i + 1);
|
||||
|
||||
QPdfWriter writer(output);
|
||||
writer.setCreator("QElectroTech");
|
||||
writer.setResolution(96);
|
||||
|
||||
QPainter painter;
|
||||
bool first = true;
|
||||
for (Diagram *diagram : diagrams) {
|
||||
const QRect r = diagramRect(diagram);
|
||||
// Match the page to the diagram (in points: 1px @ 96dpi = 0.75pt).
|
||||
const QPageSize page(QSizeF(r.width() * 72.0 / 96.0,
|
||||
r.height() * 72.0 / 96.0),
|
||||
QPageSize::Point);
|
||||
writer.setPageSize(page);
|
||||
writer.setPageMargins(QMarginsF(0, 0, 0, 0));
|
||||
|
||||
if (first) {
|
||||
if (!painter.begin(&writer)) {
|
||||
err << "Cannot open '" << output << "' for writing.\n";
|
||||
return 1;
|
||||
}
|
||||
first = false;
|
||||
} else {
|
||||
writer.newPage();
|
||||
}
|
||||
const QRectF target(0, 0,
|
||||
writer.width(), writer.height());
|
||||
renderDiagram(diagram, painter, target);
|
||||
|
||||
// Inject clickable cross-reference / folio-report hyperlinks for this
|
||||
// page. The geometry is rebuilt from the QPdfWriter (not a QPrinter):
|
||||
// render() anchors the diagram top-left with KeepAspectRatio, and the
|
||||
// page is sized to the diagram so the scale is ~1.
|
||||
if (auto *engine = dynamic_cast<QPdfEngine *>(painter.paintEngine())) {
|
||||
const QRectF source(r);
|
||||
const qreal s = qMin(target.width() / source.width(),
|
||||
target.height() / source.height());
|
||||
QTransform fit;
|
||||
fit.translate(target.x(), target.y());
|
||||
fit.scale(s, s);
|
||||
fit.translate(-source.x(), -source.y());
|
||||
|
||||
// Device pixels -> PDF points, replicating the engine's page matrix
|
||||
// (72/resolution scale + Y flip; zero margins -> no paint offset).
|
||||
const qreal pt_scale = 72.0 / writer.resolution();
|
||||
const qreal fullH_pt = writer.pageLayout().fullRectPoints().height();
|
||||
const bool fullPageMode =
|
||||
(writer.pageLayout().mode() == QPageLayout::FullPageMode);
|
||||
const QRect paintPx =
|
||||
writer.pageLayout().paintRectPixels(writer.resolution());
|
||||
|
||||
PdfLinks::PageGeometry geom;
|
||||
geom.sceneToDevice = fit;
|
||||
geom.target = target;
|
||||
geom.pageBounds = QRectF(0, 0, target.width(), target.height());
|
||||
geom.devToPdf = [=](const QPointF &d) -> QPointF {
|
||||
qreal dx = d.x(), dy = d.y();
|
||||
if (!fullPageMode) { dx += paintPx.left(); dy += paintPx.top(); }
|
||||
return QPointF(pt_scale * dx, fullH_pt - pt_scale * dy);
|
||||
};
|
||||
geom.sourceRectOf = [](Diagram *dg) {
|
||||
return QRectF(diagramRect(dg));
|
||||
};
|
||||
PdfLinks::injectCrossRefLinks(engine, diagram, geom, pageMap, output);
|
||||
}
|
||||
}
|
||||
painter.end();
|
||||
|
||||
// Rewrite the URI link annotations into native internal GoTo actions, so
|
||||
// the cross-references jump inside the document in any PDF viewer.
|
||||
PdfLinks::convertUriToGoTo(output);
|
||||
|
||||
out << "Exported " << diagrams.size() << " page(s) -> " << output << "\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
int exportImages(QETProject &project, const QString &format,
|
||||
const QString &out_dir)
|
||||
{
|
||||
const QList<Diagram *> diagrams = project.diagrams();
|
||||
if (diagrams.isEmpty()) {
|
||||
err << "No diagrams to export.\n";
|
||||
return 1;
|
||||
}
|
||||
QDir().mkpath(out_dir);
|
||||
|
||||
int index = 0;
|
||||
for (Diagram *diagram : diagrams) {
|
||||
++index;
|
||||
const QRect r = diagramRect(diagram);
|
||||
const QString path = QDir(out_dir).filePath(
|
||||
diagramStem(diagram, index) + "." + format);
|
||||
|
||||
if (format == "svg") {
|
||||
QSvgGenerator gen;
|
||||
gen.setFileName(path);
|
||||
gen.setSize(r.size());
|
||||
gen.setViewBox(QRect(0, 0, r.width(), r.height()));
|
||||
gen.setTitle(diagram->title());
|
||||
QPainter painter(&gen);
|
||||
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()));
|
||||
painter.end();
|
||||
} else { // png
|
||||
QImage image(r.size(), QImage::Format_ARGB32);
|
||||
image.fill(Qt::white);
|
||||
QPainter painter(&image);
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
renderDiagram(diagram, painter, QRectF(QPointF(0, 0), r.size()));
|
||||
painter.end();
|
||||
if (!image.save(path)) {
|
||||
err << "Failed to write '" << path << "'.\n";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
out << " " << path << "\n";
|
||||
}
|
||||
out << "Exported " << diagrams.size() << " diagram(s) -> " << out_dir << "\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
int exportCsv(QETProject &project, const QString &format, const QString &output)
|
||||
{
|
||||
QString csv;
|
||||
if (format == "cables") {
|
||||
WiringListExport wle(&project, nullptr);
|
||||
csv = wle.toCsvString();
|
||||
} else { // wires
|
||||
ConductorNumExport cne(&project, nullptr);
|
||||
csv = cne.wiresNum();
|
||||
}
|
||||
if (csv.isEmpty()) {
|
||||
err << "Nothing to export (empty list).\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
QFile file(output);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
err << "Cannot open '" << output << "' for writing.\n";
|
||||
return 1;
|
||||
}
|
||||
QTextStream fout(&file);
|
||||
fout << csv;
|
||||
file.close();
|
||||
out << "Exported " << format << " list -> " << output << "\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Quote a field for CSV output (RFC-4180 style, ';' delimiter).
|
||||
QString csvField(const QString &value)
|
||||
{
|
||||
if (value.contains(';') || value.contains('"')
|
||||
|| value.contains('\n') || value.contains('\r')) {
|
||||
QString v = value;
|
||||
v.replace('"', "\"\"");
|
||||
return '"' % v % '"';
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/// Bill of materials: one row per element, key component-data fields.
|
||||
/// Pulls from QET's own project database (the same source as the GUI BOM
|
||||
/// export), so the output matches what the editor produces.
|
||||
int exportBom(QETProject &project, const QString &output)
|
||||
{
|
||||
// The project database is built lazily; force a (re)build before querying.
|
||||
project.dataBase()->updateDB();
|
||||
|
||||
static const QStringList columns {
|
||||
"label", "designation", "manufacturer", "manufacturer_reference",
|
||||
"quantity", "location", "function", "title", "folio"
|
||||
};
|
||||
|
||||
QSqlQuery query = project.dataBase()->newQuery(
|
||||
"SELECT " % columns.join(", ") %
|
||||
" FROM element_nomenclature_view ORDER BY label");
|
||||
if (!query.exec()) {
|
||||
err << "BOM query failed: " << query.lastError().text() << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
QString csv = columns.join(";") % "\n";
|
||||
int rows = 0;
|
||||
while (query.next()) {
|
||||
QStringList values;
|
||||
for (int i = 0; i < columns.size(); ++i)
|
||||
values << csvField(query.value(i).toString());
|
||||
csv += values.join(";") % "\n";
|
||||
++rows;
|
||||
}
|
||||
|
||||
QFile file(output);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
err << "Cannot open '" << output << "' for writing.\n";
|
||||
return 1;
|
||||
}
|
||||
QTextStream fout(&file);
|
||||
fout << csv;
|
||||
file.close();
|
||||
out << "Exported " << rows << " component(s) -> " << output << "\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Count terminals on @p element that no conductor connects to.
|
||||
int freeTerminals(Element *element)
|
||||
{
|
||||
int free = 0;
|
||||
const QList<Terminal *> terminals = element->terminals();
|
||||
for (Terminal *t : terminals)
|
||||
if (t->conductorsCount() == 0)
|
||||
++free;
|
||||
return free;
|
||||
}
|
||||
|
||||
/// Structural ground-truth dump of a project, as JSON, to stdout (or a file).
|
||||
/// Uses QET's own loaded model, so it reports what the editor actually sees:
|
||||
/// per-page element / conductor counts and unconnected terminals.
|
||||
int exportInfo(QETProject &project, const QString &output)
|
||||
{
|
||||
const QList<Diagram *> diagrams = project.diagrams();
|
||||
|
||||
int total_elements = 0, total_conductors = 0, total_free = 0;
|
||||
QJsonArray pages;
|
||||
int index = 0;
|
||||
for (Diagram *diagram : diagrams) {
|
||||
++index;
|
||||
const QList<Element *> elements = diagram->elements();
|
||||
const int conductors = diagram->conductors().size();
|
||||
int page_free = 0;
|
||||
for (Element *e : elements)
|
||||
page_free += freeTerminals(e);
|
||||
|
||||
const QRect r = diagramRect(diagram);
|
||||
QJsonObject page;
|
||||
page["index"] = index;
|
||||
page["title"] = diagram->title();
|
||||
page["folio"] = QStringLiteral("%1 of %2")
|
||||
.arg(index).arg(diagrams.size());
|
||||
page["width_px"] = r.width();
|
||||
page["height_px"] = r.height();
|
||||
page["elements"] = elements.size();
|
||||
page["conductors"] = conductors;
|
||||
page["free_terminals"] = page_free;
|
||||
pages.append(page);
|
||||
|
||||
total_elements += elements.size();
|
||||
total_conductors += conductors;
|
||||
total_free += page_free;
|
||||
}
|
||||
|
||||
QJsonObject root;
|
||||
root["project"] = project.title();
|
||||
root["diagrams"] = diagrams.size();
|
||||
root["elements"] = total_elements;
|
||||
root["conductors"] = total_conductors;
|
||||
root["free_terminals"] = total_free;
|
||||
root["pages"] = pages;
|
||||
|
||||
const QByteArray json =
|
||||
QJsonDocument(root).toJson(QJsonDocument::Indented);
|
||||
|
||||
if (output.isEmpty()) {
|
||||
out << QString::fromUtf8(json);
|
||||
} else {
|
||||
QFile file(output);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
err << "Cannot open '" << output << "' for writing.\n";
|
||||
return 1;
|
||||
}
|
||||
file.write(json);
|
||||
file.close();
|
||||
out << "Wrote project info -> " << output << "\n";
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Validate one .elmt file against QET's element schema.
|
||||
/// @return 0 = OK, 1 = warning (loads but suspicious), 2 = failure.
|
||||
int checkOneElement(const QString &path)
|
||||
{
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
out << "FAIL " << path << " (cannot open)\n";
|
||||
return 2;
|
||||
}
|
||||
QDomDocument doc;
|
||||
QString error;
|
||||
int line = 0;
|
||||
if (!doc.setContent(&file, &error, &line)) {
|
||||
file.close();
|
||||
out << "FAIL " << path << " (XML error line "
|
||||
<< line << ": " << error << ")\n";
|
||||
return 2;
|
||||
}
|
||||
file.close();
|
||||
|
||||
const QDomElement root = doc.documentElement();
|
||||
if (root.tagName() != "definition" || root.attribute("type") != "element") {
|
||||
out << "FAIL " << path << " (root is not <definition type=\"element\">)\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
bool w_ok = false, h_ok = false;
|
||||
const double w = root.attribute("width").toDouble(&w_ok);
|
||||
const double h = root.attribute("height").toDouble(&h_ok);
|
||||
if (!w_ok || !h_ok || w == 0 || h == 0) {
|
||||
out << "FAIL " << path << " (missing/zero bounding box "
|
||||
<< root.attribute("width") << "x"
|
||||
<< root.attribute("height") << ")\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
const int terminals = root.elementsByTagName("terminal").count();
|
||||
|
||||
// Negative dimensions are malformed but QET still loads them; surface as a
|
||||
// warning rather than a failure so this agrees with QET's own loader.
|
||||
if (w < 0 || h < 0) {
|
||||
out << "WARN " << path << " (negative bounding box "
|
||||
<< w << "x" << h << ", " << terminals << " terminals)\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (terminals == 0) {
|
||||
out << "WARN " << path << " (loads, but 0 terminals)\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
out << "OK " << path << " (" << terminals << " terminals)\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Validate a single .elmt file or every .elmt under a directory.
|
||||
int checkElements(const QString &path)
|
||||
{
|
||||
QStringList files;
|
||||
const QFileInfo info(path);
|
||||
if (info.isDir()) {
|
||||
QDirIterator it(path, {"*.elmt"}, QDir::Files,
|
||||
QDirIterator::Subdirectories);
|
||||
while (it.hasNext())
|
||||
files << it.next();
|
||||
files.sort();
|
||||
} else if (info.isFile()) {
|
||||
files << path;
|
||||
} else {
|
||||
err << "Not found: " << path << "\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (files.isEmpty()) {
|
||||
err << "No .elmt files found under: " << path << "\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
int warnings = 0, failures = 0;
|
||||
for (const QString &f : files) {
|
||||
const int r = checkOneElement(f);
|
||||
if (r == 1) ++warnings;
|
||||
else if (r == 2) ++failures;
|
||||
}
|
||||
out << files.size() << " file(s), " << warnings
|
||||
<< " warning(s), " << failures << " failure(s)\n";
|
||||
return failures > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
/// Map every element in the project to its 1-based folio (page) position.
|
||||
QHash<Element *, int> folioIndex(QETProject &project)
|
||||
{
|
||||
QHash<Element *, int> folio;
|
||||
int index = 0;
|
||||
const QList<Diagram *> diagrams = project.diagrams();
|
||||
for (Diagram *diagram : diagrams) {
|
||||
++index;
|
||||
const QList<Element *> elements = diagram->elements();
|
||||
for (Element *e : elements)
|
||||
folio.insert(e, index);
|
||||
}
|
||||
return folio;
|
||||
}
|
||||
|
||||
/// Electrical nets: groups of terminals joined into one potential.
|
||||
/// Walks QET's own potential graph, so each net is a connected component
|
||||
/// of terminals across all folios. The ground truth for connectivity.
|
||||
int exportNets(QETProject &project, const QString &output)
|
||||
{
|
||||
const QHash<Element *, int> folio = folioIndex(project);
|
||||
|
||||
QList<Conductor *> all_conductors;
|
||||
const QList<Diagram *> diagrams = project.diagrams();
|
||||
for (Diagram *diagram : diagrams)
|
||||
all_conductors << diagram->conductors();
|
||||
|
||||
QSet<Conductor *> visited;
|
||||
QJsonArray nets;
|
||||
int net_no = 0;
|
||||
for (Conductor *c : all_conductors) {
|
||||
if (visited.contains(c))
|
||||
continue;
|
||||
|
||||
// The whole potential this conductor belongs to. relatedPotential-
|
||||
// Conductors() also fills t_list with every terminal in the net
|
||||
// (following folio reports and terminal blocks too).
|
||||
QList<Terminal *> t_list;
|
||||
QSet<Conductor *> group = c->relatedPotentialConductors(true, &t_list);
|
||||
group.insert(c);
|
||||
for (Conductor *g : group)
|
||||
visited.insert(g);
|
||||
if (c->terminal1) t_list << c->terminal1;
|
||||
if (c->terminal2) t_list << c->terminal2;
|
||||
|
||||
// Wire number: smallest non-empty conductor text (deterministic).
|
||||
QStringList wire_nos;
|
||||
for (Conductor *g : group)
|
||||
if (!g->properties().text.isEmpty())
|
||||
wire_nos << g->properties().text;
|
||||
wire_nos.sort();
|
||||
|
||||
++net_no;
|
||||
QJsonArray terminals;
|
||||
QSet<Terminal *> seen;
|
||||
for (Terminal *t : t_list) {
|
||||
if (!t || seen.contains(t))
|
||||
continue;
|
||||
seen.insert(t);
|
||||
Element *pe = t->parentElement();
|
||||
QJsonObject to;
|
||||
to["element"] = pe ? elementLabel(pe) : QString();
|
||||
to["terminal"] = t->name();
|
||||
to["folio"] = pe ? folio.value(pe, 0) : 0;
|
||||
terminals.append(to);
|
||||
}
|
||||
QJsonObject net;
|
||||
net["net"] = net_no;
|
||||
net["wire_no"] = wire_nos.value(0);
|
||||
net["terminals"] = terminals;
|
||||
nets.append(net);
|
||||
}
|
||||
|
||||
QJsonObject root;
|
||||
root["project"] = project.title();
|
||||
root["nets"] = nets.size();
|
||||
root["list"] = nets;
|
||||
|
||||
QFile file(output);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
err << "Cannot open '" << output << "' for writing.\n";
|
||||
return 1;
|
||||
}
|
||||
file.write(QJsonDocument(root).toJson(QJsonDocument::Indented));
|
||||
file.close();
|
||||
out << "Exported " << nets.size() << " net(s) -> " << output << "\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Cross-references: each linkable element (coil / contact / report) and the
|
||||
/// elements it links to, flagging masters/slaves with no link as unresolved.
|
||||
int exportLinks(QETProject &project, const QString &output)
|
||||
{
|
||||
const QHash<Element *, int> folio = folioIndex(project);
|
||||
|
||||
QString csv("element;link_type;linked_to;folio;status\n");
|
||||
int linkable = 0, unresolved = 0;
|
||||
|
||||
const QList<Diagram *> diagrams = project.diagrams();
|
||||
for (Diagram *diagram : diagrams) {
|
||||
const QList<Element *> elements = diagram->elements();
|
||||
for (Element *e : elements) {
|
||||
if (e->linkType() == Element::Simple)
|
||||
continue;
|
||||
++linkable;
|
||||
|
||||
const QList<Element *> linked = e->linkedElements();
|
||||
QStringList names;
|
||||
for (Element *le : linked)
|
||||
names << elementLabel(le) % "(f"
|
||||
% QString::number(folio.value(le, 0)) % ")";
|
||||
|
||||
QString status = "linked";
|
||||
if ((e->linkType() == Element::Master
|
||||
|| e->linkType() == Element::Slave)
|
||||
&& linked.isEmpty()) {
|
||||
status = "UNRESOLVED";
|
||||
++unresolved;
|
||||
}
|
||||
|
||||
csv += csvField(elementLabel(e)) % ";"
|
||||
% e->linkTypeToString() % ";"
|
||||
% csvField(names.join(", ")) % ";"
|
||||
% QString::number(folio.value(e, 0)) % ";"
|
||||
% status % "\n";
|
||||
}
|
||||
}
|
||||
|
||||
QFile file(output);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
err << "Cannot open '" << output << "' for writing.\n";
|
||||
return 1;
|
||||
}
|
||||
QTextStream fout(&file);
|
||||
fout << csv;
|
||||
file.close();
|
||||
out << "Exported " << linkable << " linkable element(s), "
|
||||
<< unresolved << " unresolved -> " << output << "\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Round-trip: load the project and write its XML back out, so an external
|
||||
/// diff can reveal markup QET silently normalises (tolerated-but-invalid XML).
|
||||
int resaveProject(QETProject &project, const QString &output)
|
||||
{
|
||||
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 << "Re-saved project -> " << output << "\n";
|
||||
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
|
||||
|
||||
namespace CLIExport {
|
||||
|
||||
bool isExportRequest(const QStringList &args)
|
||||
{
|
||||
for (const QString &a : args)
|
||||
if (exportFlags().contains(a))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
int run(const QStringList &args)
|
||||
{
|
||||
QString flag;
|
||||
QStringList rest;
|
||||
for (int i = 0; i < args.size(); ++i) {
|
||||
if (exportFlags().contains(args.at(i))) {
|
||||
flag = args.at(i);
|
||||
for (int j = i + 1; j < args.size(); ++j)
|
||||
rest << args.at(j);
|
||||
break;
|
||||
}
|
||||
}
|
||||
const QString format = exportFlags().value(flag);
|
||||
|
||||
// --check-elements operates on an element file/directory, not a project.
|
||||
if (format == "check") {
|
||||
if (rest.isEmpty()) {
|
||||
err << "Usage: qelectrotech --check-elements "
|
||||
"<element.elmt | directory>\n";
|
||||
return 2;
|
||||
}
|
||||
return checkElements(rest.at(0));
|
||||
}
|
||||
|
||||
const QString input = rest.value(0);
|
||||
if (input.isEmpty()) {
|
||||
err << "Usage: qelectrotech " << flag << " <project.qet> <output>\n";
|
||||
return 2;
|
||||
}
|
||||
if (!QFileInfo::exists(input)) {
|
||||
err << "Project not found: " << input << "\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
QETProject project(input);
|
||||
if (project.state() != QETProject::Ok) {
|
||||
err << "Failed to open project: " << input
|
||||
<< " (state " << project.state() << ")\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// --info writes JSON to stdout, or to an optional output file.
|
||||
if (format == "info")
|
||||
return exportInfo(project, rest.value(1));
|
||||
|
||||
const QString output = rest.value(1);
|
||||
if (output.isEmpty()) {
|
||||
err << "Usage: qelectrotech " << flag
|
||||
<< " <project.qet> <output>\n";
|
||||
return 2;
|
||||
}
|
||||
if (format == "pdf")
|
||||
return exportPdf(project, output);
|
||||
if (format == "cables" || format == "wires")
|
||||
return exportCsv(project, format, output);
|
||||
if (format == "bom")
|
||||
return exportBom(project, output);
|
||||
if (format == "nets")
|
||||
return exportNets(project, output);
|
||||
if (format == "links")
|
||||
return exportLinks(project, output);
|
||||
if (format == "resave")
|
||||
return resaveProject(project, output);
|
||||
if (format == "settb")
|
||||
return setTitleBlock(project, output, rest.mid(2));
|
||||
return exportImages(project, format, output);
|
||||
}
|
||||
|
||||
} // namespace CLIExport
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
Copyright 2006-2025 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef CLI_EXPORT_H
|
||||
#define CLI_EXPORT_H
|
||||
|
||||
#include <QStringList>
|
||||
|
||||
/**
|
||||
@brief Headless command-line export.
|
||||
|
||||
Implements the long-requested batch/headless export
|
||||
(qelectrotech.org bugtracker #171, GitHub #309): render a project's
|
||||
diagrams to files without opening the GUI.
|
||||
|
||||
Detected and handled in main() before the GUI is created.
|
||||
*/
|
||||
namespace CLIExport {
|
||||
|
||||
/**
|
||||
@brief True if @p args request a CLI export
|
||||
(i.e. contain one of the export options).
|
||||
*/
|
||||
bool isExportRequest(const QStringList &args);
|
||||
|
||||
/**
|
||||
@brief Run the CLI export described by @p args.
|
||||
@return process exit code (0 on success).
|
||||
|
||||
Usage:
|
||||
qelectrotech --export-pdf <project.qet> <output.pdf>
|
||||
qelectrotech --export-png <project.qet> <output_dir>
|
||||
qelectrotech --export-svg <project.qet> <output_dir>
|
||||
qelectrotech --export-cables <project.qet> <output.csv>
|
||||
qelectrotech --export-wires <project.qet> <output.csv>
|
||||
qelectrotech --export-bom <project.qet> <output.csv>
|
||||
qelectrotech --export-nets <project.qet> <output.json>
|
||||
qelectrotech --export-links <project.qet> <output.csv>
|
||||
qelectrotech --info <project.qet> [output.json]
|
||||
qelectrotech --check-elements <element.elmt | directory>
|
||||
qelectrotech --resave <project.qet> <output.qet>
|
||||
qelectrotech --set-titleblock <project.qet> <output.qet> key=value...
|
||||
|
||||
PDF: one multi-page document (one diagram per page).
|
||||
PNG/SVG: one file per diagram, named <output_dir>/<NN>_<title>.<ext>.
|
||||
cables: wiring list (one row per conductor) as CSV.
|
||||
wires: list of distinct wire numbers as CSV.
|
||||
bom: bill of materials (one row per element) as CSV.
|
||||
nets: electrical nets (connected-terminal groups) as JSON.
|
||||
links: element cross-references (coil/contact) as CSV, with
|
||||
unresolved links flagged.
|
||||
info: structural project summary as JSON (stdout, or a file) —
|
||||
per-page element / conductor counts and unconnected terminals.
|
||||
check-elements: validate .elmt file(s) against the element schema.
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
#endif // CLI_EXPORT_H
|
||||
@@ -313,6 +313,12 @@ void PartText::setPlainText(const QString &text) {
|
||||
void PartText::setFont(const QFont &font) {
|
||||
if (font != this -> font()) {
|
||||
QGraphicsTextItem::setFont(font);
|
||||
// Re-anchor: the item's position transform is -margin(), and margin()
|
||||
// depends on the font ascent. Without re-running this on a font change,
|
||||
// the transform keeps the previous font's ascent — so the text renders
|
||||
// at a different spot after save/reopen (the position recomputes from
|
||||
// the saved font on load). See #158.
|
||||
adjustItemPosition();
|
||||
emit fontChanged(font);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -736,11 +736,13 @@ bool QETElementEditor::checkElement()
|
||||
QList<QETWarning> warnings;
|
||||
QList<QETWarning> errors;
|
||||
|
||||
// Warning #1: Element haven't got terminal
|
||||
// Warning #1: Element does not have (enough) terminals
|
||||
// (except for report and conductor definition, because they must have one terminal and this checking is done below)
|
||||
// (another exception: "thumbnails" aka "front-views" may/should not have terminals)
|
||||
if (!m_elmt_scene -> containsTerminals() &&
|
||||
!(m_elmt_scene->elementData().m_type & ElementData::AllReport) &&
|
||||
m_elmt_scene->elementData().m_type != ElementData::ConductorDefinition) {
|
||||
m_elmt_scene->elementData().m_type != ElementData::ConductorDefinition &&
|
||||
m_elmt_scene->elementData().m_type != ElementData::Thumbnail) {
|
||||
warnings << qMakePair(
|
||||
tr("Absence de borne", "warning title"),
|
||||
tr(
|
||||
@@ -749,50 +751,50 @@ bool QETElementEditor::checkElement()
|
||||
"warning description"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check folio report element
|
||||
if (m_elmt_scene->elementData().m_type & ElementData::AllReport)
|
||||
{
|
||||
int terminal =0;
|
||||
// Check folio report element
|
||||
if (m_elmt_scene->elementData().m_type & ElementData::AllReport)
|
||||
{
|
||||
int terminal =0;
|
||||
|
||||
for(auto qgi : m_elmt_scene -> items()) {
|
||||
if (qgraphicsitem_cast<PartTerminal *>(qgi)) {
|
||||
terminal ++;
|
||||
}
|
||||
}
|
||||
|
||||
//Error folio report must have only one terminal
|
||||
if (terminal != 1) {
|
||||
errors << qMakePair (tr("Absence de borne"),
|
||||
tr("<br><b>Erreur</b> :"
|
||||
"<br>Les reports de folio doivent posséder une seul borne."
|
||||
"<br><b>Solution</b> :"
|
||||
"<br>Verifier que l'élément ne possède qu'une seul borne"));
|
||||
for(auto qgi : m_elmt_scene -> items()) {
|
||||
if (qgraphicsitem_cast<PartTerminal *>(qgi)) {
|
||||
terminal ++;
|
||||
}
|
||||
}
|
||||
|
||||
// Check conductor definition element
|
||||
if (m_elmt_scene->elementData().m_type == ElementData::ConductorDefinition)
|
||||
{
|
||||
int terminal =0;
|
||||
//Error folio report must have only one terminal
|
||||
if (terminal != 1) {
|
||||
errors << qMakePair (tr("Absence de borne"),
|
||||
tr("<br><b>Erreur</b> :"
|
||||
"<br>Les reports de folio doivent posséder une seul borne."
|
||||
"<br><b>Solution</b> :"
|
||||
"<br>Verifier que l'élément ne possède qu'une seul borne"));
|
||||
}
|
||||
}
|
||||
|
||||
for(auto qgi : m_elmt_scene -> items()) {
|
||||
if (qgraphicsitem_cast<PartTerminal *>(qgi)) {
|
||||
terminal ++;
|
||||
}
|
||||
}
|
||||
// Check conductor definition element
|
||||
if (m_elmt_scene->elementData().m_type == ElementData::ConductorDefinition)
|
||||
{
|
||||
int terminal =0;
|
||||
|
||||
// Error: Conductor definition must have exactly one terminal
|
||||
if (terminal != 1) {
|
||||
errors << qMakePair (tr("Nombre de bornes incorrect"),
|
||||
tr("<br><b>Erreur</b> :"
|
||||
"<br>Les définitions de conducteur ne peuvent posséder qu'une seule borne."
|
||||
"<br><b>Solution</b> :"
|
||||
"<br>Vérifier que l'élément ne possède qu'une seule borne"));
|
||||
for(auto qgi : m_elmt_scene -> items()) {
|
||||
if (qgraphicsitem_cast<PartTerminal *>(qgi)) {
|
||||
terminal ++;
|
||||
}
|
||||
}
|
||||
|
||||
// Error: Conductor definition must have exactly one terminal
|
||||
if (terminal != 1) {
|
||||
errors << qMakePair (tr("Nombre de bornes incorrect"),
|
||||
tr("<br><b>Erreur</b> :"
|
||||
"<br>Les définitions de conducteur ne peuvent posséder qu'une seule borne."
|
||||
"<br><b>Solution</b> :"
|
||||
"<br>Vérifier que l'élément ne possède qu'une seule borne"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!errors.count() && !warnings.count()) {
|
||||
return(true);
|
||||
}
|
||||
|
||||
@@ -124,7 +124,17 @@ void ElementDialog::setUpWidget()
|
||||
} else if (m_mode == SaveTemplate) {
|
||||
m_text_field->setPlaceholderText(tr("Nom du nouveau template"));
|
||||
} else {
|
||||
m_text_field->setPlaceholderText(tr("Nom du nouvel élément"));
|
||||
// This is the element's file name, not its display name: the field
|
||||
// only accepts file-name characters (QFileNameEdit). The visible
|
||||
// element name is edited separately in the element properties.
|
||||
m_text_field->setPlaceholderText(
|
||||
tr("Nom de fichier de l'élément",
|
||||
"placeholder: the element's file name, not its display name"));
|
||||
m_text_field->setToolTip(
|
||||
tr("Nom de fichier de l'élément : chiffres, minuscules, « - », "
|
||||
"« _ » et « . » uniquement.\nLe nom affiché de l'élément se "
|
||||
"modifie séparément dans les propriétés de l'élément.",
|
||||
"tooltip for the element file-name field"));
|
||||
}
|
||||
|
||||
layout->addWidget(m_text_field);
|
||||
|
||||
@@ -44,6 +44,24 @@ used:
|
||||
The non-UI classes are deliberately decoupled from the widget so they can be
|
||||
tested headless.
|
||||
|
||||
## Terms of Use / Legal notice
|
||||
|
||||
The `.edz` files themselves are downloaded from the **EPLAN Data Portal**
|
||||
(data.eplan.com), which is operated by EPLAN Software & Service GmbH & Co. KG.
|
||||
The Data Portal Terms of Use (§ 5.4 at time of writing) restrict use of
|
||||
downloaded data to EPLAN products.
|
||||
|
||||
**QElectroTech does not endorse or encourage violation of those terms.**
|
||||
Whether use of an `.edz` outside EPLAN products is permitted under your
|
||||
specific licence depends on the agreement you have with EPLAN and the
|
||||
individual manufacturer. You are solely responsible for ensuring that your use
|
||||
of `.edz` data complies with the applicable Terms of Use and any applicable
|
||||
law. If in doubt, contact EPLAN or the part manufacturer directly.
|
||||
|
||||
QET's `.edz` reader parses only the portable, factual part data
|
||||
(pin designations, order numbers, manufacturer names). It does not reproduce
|
||||
any EPLAN-proprietary geometry or symbol data.
|
||||
|
||||
## Bundled LZMA SDK
|
||||
|
||||
`lzma/` contains the decode-only subset of Igor Pavlov's **LZMA SDK** (public
|
||||
|
||||
@@ -34,6 +34,8 @@ const QString FONT =
|
||||
QStringLiteral("Liberation Sans,5,-1,5,50,0,0,0,0,0,Regular");
|
||||
const QString TITLE_FONT =
|
||||
QStringLiteral("Liberation Sans,5,-1,5,75,0,0,0,0,0,Bold");
|
||||
const QString LABEL_FONT =
|
||||
QStringLiteral("Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular");
|
||||
|
||||
QString uuidStr()
|
||||
{
|
||||
@@ -71,19 +73,25 @@ QDomDocument EdzElementBuilder::build(const EdzPart &part)
|
||||
}
|
||||
const int n = pins.size();
|
||||
const int pitch = 10;
|
||||
const int group_gap = 5; // extra px inserted between designation groups
|
||||
const int group_gap = 10; // one full slot between named connector groups
|
||||
|
||||
// Pins arrive sorted by functional group then by designation within each
|
||||
// group (see EdzPart::parse). A group break occurs when the
|
||||
// functiondefinition block changes; fall back to designation comparison for
|
||||
// parts that carry no functiondefinition information.
|
||||
// No gap is inserted for power/busbar pins that carry no group label —
|
||||
// they flow consecutively so a 3-phase AC source stays connected.
|
||||
auto groupKey = [](const EdzPin &p) {
|
||||
return p.group.isEmpty() ? p.designation : p.group;
|
||||
};
|
||||
QVector<bool> is_group_break(n, false);
|
||||
for (int i = 1; i < n; ++i)
|
||||
is_group_break[i] = (groupKey(pins.at(i)) != groupKey(pins.at(i - 1)));
|
||||
is_group_break[i] = (groupKey(pins.at(i)) != groupKey(pins.at(i - 1)))
|
||||
&& (!pins.at(i).group.isEmpty());
|
||||
|
||||
// Place every terminal on a multiple-of-10 y coordinate so they align
|
||||
// cleanly with QET's default grid. The gap slot is also 10 px, giving
|
||||
// one empty grid row between connector groups.
|
||||
QVector<int> pin_y(n);
|
||||
int cur_y = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
@@ -218,10 +226,23 @@ QDomDocument EdzElementBuilder::build(const EdzPart &part)
|
||||
desc.appendChild(makeText(doc, 6, pin_y[i] + 2, label, FONT));
|
||||
}
|
||||
|
||||
// Connector group header labels — one per named group, in the gap above
|
||||
// the group's first pin so the electrician can see the terminal block name
|
||||
// (e.g. "XDI", "XPOW") without having to read individual terminal names.
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (pins.at(i).group.isEmpty()) continue;
|
||||
if (i > 0 && groupKey(pins.at(i)) == groupKey(pins.at(i - 1))) continue;
|
||||
// Place the label centred in the gap slot above the group's first pin.
|
||||
const int label_y = (i == 0) ? (pin_y[0] - group_gap / 2)
|
||||
: (pin_y[i] - group_gap / 2);
|
||||
desc.appendChild(makeText(doc, body_left + 2, label_y,
|
||||
pins.at(i).group, FONT));
|
||||
}
|
||||
|
||||
// Device-tag label (per-instance, above the body).
|
||||
QDomElement dyn = doc.createElement(QStringLiteral("dynamic_text"));
|
||||
dyn.setAttribute(QStringLiteral("x"), min_x + pad);
|
||||
dyn.setAttribute(QStringLiteral("y"), min_y - 2);
|
||||
dyn.setAttribute(QStringLiteral("y"), min_y - 9);
|
||||
dyn.setAttribute(QStringLiteral("z"), QStringLiteral("5"));
|
||||
dyn.setAttribute(QStringLiteral("text_width"), QStringLiteral("-1"));
|
||||
dyn.setAttribute(QStringLiteral("Halignment"), QStringLiteral("AlignLeft"));
|
||||
@@ -231,7 +252,7 @@ QDomDocument EdzElementBuilder::build(const EdzPart &part)
|
||||
dyn.setAttribute(QStringLiteral("keep_visual_rotation"), QStringLiteral("false"));
|
||||
dyn.setAttribute(QStringLiteral("text_from"), QStringLiteral("ElementInfo"));
|
||||
dyn.setAttribute(QStringLiteral("uuid"), uuidStr());
|
||||
dyn.setAttribute(QStringLiteral("font"), FONT);
|
||||
dyn.setAttribute(QStringLiteral("font"), LABEL_FONT);
|
||||
dyn.appendChild(doc.createElement(QStringLiteral("text")));
|
||||
QDomElement info_name = doc.createElement(QStringLiteral("info_name"));
|
||||
info_name.appendChild(doc.createTextNode(QStringLiteral("label")));
|
||||
|
||||
@@ -75,11 +75,11 @@ class MachineInfo
|
||||
{
|
||||
struct Screen
|
||||
{
|
||||
int32_t count;
|
||||
int32_t width[10];
|
||||
int32_t height[10];
|
||||
int32_t Max_width;
|
||||
int32_t Max_height;
|
||||
int32_t count = 0;
|
||||
int32_t width[10] = {};
|
||||
int32_t height[10]= {};
|
||||
int32_t Max_width = 0;
|
||||
int32_t Max_height= 0;
|
||||
}screen;
|
||||
struct Built
|
||||
{
|
||||
|
||||
@@ -15,13 +15,17 @@
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "cli_export.h"
|
||||
#include "machine_info.h"
|
||||
#include "qet.h"
|
||||
#include "qetapp.h"
|
||||
#include "qetproject.h"
|
||||
#include "singleapplication.h"
|
||||
#include "utils/macosxopenevent.h"
|
||||
#include "utils/qetsettings.h"
|
||||
|
||||
#include <QApplication>
|
||||
|
||||
#include <QStyleFactory>
|
||||
#include <QtConcurrentRun>
|
||||
|
||||
@@ -194,6 +198,23 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
|
||||
#endif
|
||||
|
||||
|
||||
// Headless command-line export: render a project to PDF/PNG/SVG without
|
||||
// opening the GUI, then exit. Must be handled before SingleApplication
|
||||
// (which would forward the args to an already-running instance).
|
||||
{
|
||||
QStringList raw_args;
|
||||
for (int i = 0; i < argc; ++i)
|
||||
raw_args << QString::fromLocal8Bit(argv[i]);
|
||||
if (CLIExport::isExportRequest(raw_args)) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
SingleApplication app(argc, argv, true);
|
||||
#ifdef Q_OS_MACOS
|
||||
//Handle the opening of QET when user double click on a .qet .elmt .tbt file
|
||||
@@ -220,6 +241,11 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
|
||||
QObject::connect(&app, &SingleApplication::receivedMessage,
|
||||
&qetapp, &QETApp::receiveMessage);
|
||||
|
||||
// Pre-initialise on the main (GUI) thread: the constructor calls
|
||||
// qApp->screens() which is not thread-safe in Qt5 — calling instance()
|
||||
// here guarantees the singleton is fully built before the worker runs.
|
||||
MachineInfo::instance();
|
||||
|
||||
QtConcurrent::run([=]()
|
||||
{
|
||||
// for debugging
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
/*
|
||||
Copyright 2006-2025 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "pdf_links.h"
|
||||
|
||||
#include "diagram.h"
|
||||
#include "qetgraphicsitem/crossrefitem.h"
|
||||
#include "qetgraphicsitem/dynamicelementtextitem.h"
|
||||
#include "qetgraphicsitem/element.h"
|
||||
#include "qetgraphicsitem/elementtextitemgroup.h"
|
||||
|
||||
// Private Qt PDF engine for drawHyperlink() — not public API, stable since Qt4.
|
||||
// Requires QT += gui-private in qelectrotech.pro / gui-private in CMake.
|
||||
#include <private/qpdf_p.h>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QFile>
|
||||
#include <QGraphicsTextItem>
|
||||
#include <QList>
|
||||
#include <QRegularExpression>
|
||||
#include <QUrl>
|
||||
#include <QVector>
|
||||
|
||||
namespace PdfLinks {
|
||||
|
||||
void injectCrossRefLinks(QPdfEngine *engine, Diagram *diagram,
|
||||
const PageGeometry &geom,
|
||||
const QMap<Diagram *, int> &pageMap,
|
||||
const QString &outputFileName)
|
||||
{
|
||||
if (!engine || !diagram)
|
||||
return;
|
||||
|
||||
const QTransform &fit = geom.sceneToDevice;
|
||||
const QRectF &target = geom.target;
|
||||
const QRectF &pageBounds = geom.pageBounds;
|
||||
|
||||
// Compute, in PDF points on its OWN page, the rectangle to frame for a
|
||||
// target element (used as a /FitR destination so the link zooms onto it).
|
||||
auto destRectPdf = [&](Element *tgt) -> QRectF {
|
||||
Diagram *dg = tgt ? tgt->diagram() : nullptr;
|
||||
if (!dg) return QRectF();
|
||||
const QRectF srcT = geom.sourceRectOf(dg);
|
||||
if (srcT.width() <= 0.0 || srcT.height() <= 0.0) return QRectF();
|
||||
const qreal sT = qMin(target.width() / srcT.width(),
|
||||
target.height() / srcT.height());
|
||||
QTransform fitT;
|
||||
fitT.translate(target.x(), target.y());
|
||||
fitT.scale(sT, sT);
|
||||
fitT.translate(-srcT.x(), -srcT.y());
|
||||
|
||||
QRectF elemScene = tgt->mapRectToScene(tgt->boundingRect());
|
||||
// Frame the element with a little context, and enforce a minimum
|
||||
// framed size so tiny contacts don't zoom in extremely.
|
||||
const qreal pad = 25.0;
|
||||
elemScene.adjust(-pad, -pad, pad, pad);
|
||||
const qreal minSide = 160.0;
|
||||
if (elemScene.width() < minSide)
|
||||
elemScene.adjust(-(minSide - elemScene.width()) / 2.0, 0,
|
||||
(minSide - elemScene.width()) / 2.0, 0);
|
||||
if (elemScene.height() < minSide)
|
||||
elemScene.adjust(0, -(minSide - elemScene.height()) / 2.0,
|
||||
0, (minSide - elemScene.height()) / 2.0);
|
||||
|
||||
const QRectF devT = fitT.mapRect(elemScene);
|
||||
const QPointF a = geom.devToPdf(devT.topLeft());
|
||||
const QPointF b = geom.devToPdf(devT.bottomRight());
|
||||
return QRectF(QPointF(qMin(a.x(), b.x()), qMin(a.y(), b.y())),
|
||||
QPointF(qMax(a.x(), b.x()), qMax(a.y(), b.y())));
|
||||
};
|
||||
|
||||
auto injectLink = [&](const QRectF &sceneRect, Element *targetElmt) {
|
||||
if (!targetElmt || !targetElmt->diagram()) return;
|
||||
const int targetPage = pageMap.value(targetElmt->diagram(), -1);
|
||||
if (targetPage < 1) return;
|
||||
const QRectF devRect = fit.mapRect(sceneRect);
|
||||
if (!devRect.isValid() || !pageBounds.intersects(devRect)) return;
|
||||
|
||||
QString frag = QString("page=%1").arg(targetPage);
|
||||
const QRectF d = destRectPdf(targetElmt); // /FitR L_B_R_T
|
||||
if (d.isValid())
|
||||
frag += QString("&fitr=%1_%2_%3_%4")
|
||||
.arg(qRound(d.left())).arg(qRound(d.top()))
|
||||
.arg(qRound(d.right())).arg(qRound(d.bottom()));
|
||||
|
||||
QUrl url = QUrl::fromLocalFile(outputFileName);
|
||||
url.setFragment(frag);
|
||||
engine->drawHyperlink(devRect, url);
|
||||
};
|
||||
|
||||
for (auto *item : diagram->items()) {
|
||||
|
||||
// --- CrossRefItem links ---
|
||||
if (auto *xref = dynamic_cast<CrossRefItem*>(item)) {
|
||||
for (auto it = xref->hoveredContactsMap().begin();
|
||||
it != xref->hoveredContactsMap().end(); ++it)
|
||||
{
|
||||
Element *targetElmt = it.key();
|
||||
if (!targetElmt || !targetElmt->diagram()) continue;
|
||||
// it.value() is in the CrossRefItem's LOCAL coords -> scene
|
||||
injectLink(xref->mapRectToScene(it.value()), targetElmt);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// --- Folio report links (DynamicElementTextItem) ---
|
||||
if (auto *deti = dynamic_cast<DynamicElementTextItem*>(item)) {
|
||||
Element *parent = deti->parentElement();
|
||||
if (!parent) continue;
|
||||
|
||||
// (a) Report element : label -> linked report on another folio
|
||||
if (parent->linkType() & Element::AllReport) {
|
||||
if (parent->linkedElements().isEmpty()) continue;
|
||||
|
||||
bool showsLabel =
|
||||
(deti->textFrom() == DynamicElementTextItem::ElementInfo
|
||||
&& deti->infoName() == QLatin1String("label")) ||
|
||||
(deti->textFrom() == DynamicElementTextItem::CompositeText
|
||||
&& deti->compositeText().contains(QStringLiteral("%{label}")));
|
||||
if (!showsLabel) continue;
|
||||
|
||||
Element *targetElmt = parent->linkedElements().first();
|
||||
if (!targetElmt || !targetElmt->diagram()) continue;
|
||||
|
||||
injectLink(deti->mapRectToScene(deti->boundingRect()), targetElmt);
|
||||
continue;
|
||||
}
|
||||
|
||||
// (b) Slave element : the "(folio-pos)" text -> master element
|
||||
if (parent->linkType() == Element::Slave) {
|
||||
QGraphicsTextItem *sx = deti->slaveXrefItem();
|
||||
Element *master = deti->masterElement();
|
||||
if (sx && master && master->diagram()) {
|
||||
injectLink(sx->mapRectToScene(sx->boundingRect()), master);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// --- Slave cross-reference carried by a grouped text ---
|
||||
if (auto *grp = dynamic_cast<ElementTextItemGroup*>(item)) {
|
||||
Element *parent = grp->parentElement();
|
||||
if (!parent || parent->linkType() != Element::Slave) continue;
|
||||
if (parent->linkedElements().isEmpty()) continue;
|
||||
QGraphicsTextItem *sx = grp->slaveXrefItem();
|
||||
if (!sx) continue;
|
||||
Element *master = parent->linkedElements().first();
|
||||
if (!master || !master->diagram()) continue;
|
||||
injectLink(sx->mapRectToScene(sx->boundingRect()), master);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void convertUriToGoTo(const QString &pdfPath)
|
||||
{
|
||||
// --- 1. Read raw bytes ---
|
||||
QFile f(pdfPath);
|
||||
if (!f.open(QIODevice::ReadOnly)) return;
|
||||
QByteArray data = f.readAll();
|
||||
f.close();
|
||||
|
||||
// --- 2. Collect page object numbers in document order ---
|
||||
// Read them from the page tree (/Type /Pages -> /Kids [ N 0 R ... ]).
|
||||
// This is reliable; scanning raw bytes for "/Type /Page" is NOT: that
|
||||
// marker also occurs inside content streams, and a forward lookahead
|
||||
// wrongly tags neighbouring objects (it found 280 "pages" for a 137-page
|
||||
// document). Qt writes a single, flat /Kids array listing every page.
|
||||
QVector<int> pageObjs;
|
||||
{
|
||||
int pagesPos = data.indexOf("/Type /Pages");
|
||||
int kidsPos = (pagesPos == -1) ? -1 : data.indexOf("/Kids", pagesPos);
|
||||
int lb = (kidsPos == -1) ? -1 : data.indexOf('[', kidsPos);
|
||||
int rb = (lb == -1) ? -1 : data.indexOf(']', lb);
|
||||
if (lb != -1 && rb != -1 && rb > lb) {
|
||||
const QString kids =
|
||||
QString::fromLatin1(data.mid(lb + 1, rb - lb - 1));
|
||||
QRegularExpression re(QStringLiteral("(\\d+)\\s+\\d+\\s+R"));
|
||||
auto it = re.globalMatch(kids);
|
||||
while (it.hasNext()) {
|
||||
int objNum = it.next().captured(1).toInt();
|
||||
if (objNum > 0) pageObjs.append(objNum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pageObjs.isEmpty()) return; // nothing to do
|
||||
|
||||
// --- 3. Replace URI annotations with GoTo ---
|
||||
// Pattern (Qt always writes exactly this):
|
||||
// /S /URI\n/URI (file:///...<anything>#page=N)\n
|
||||
// or (older patches without file://):
|
||||
// /S /URI\n/URI (page=N)\n
|
||||
bool changed = false;
|
||||
{
|
||||
// We do a manual scan to handle variable-length replacements.
|
||||
QByteArray out;
|
||||
out.reserve(data.size());
|
||||
|
||||
const QByteArray sUri = "/S /URI\n/URI (";
|
||||
const QByteArray sGoTo = "/S /GoTo\n/D [";
|
||||
int pos = 0;
|
||||
|
||||
while (pos < data.size()) {
|
||||
int found = data.indexOf(sUri, pos);
|
||||
if (found == -1) {
|
||||
out.append(data.mid(pos));
|
||||
break;
|
||||
}
|
||||
|
||||
// Copy everything up to the match
|
||||
out.append(data.mid(pos, found - pos));
|
||||
|
||||
// Find closing ')' of the URI value
|
||||
int uriStart = found + sUri.size();
|
||||
int closeParen = data.indexOf(')', uriStart);
|
||||
if (closeParen == -1) {
|
||||
// Malformed — copy rest verbatim
|
||||
out.append(data.mid(found));
|
||||
pos = data.size();
|
||||
break;
|
||||
}
|
||||
|
||||
QByteArray uriVal = data.mid(uriStart, closeParen - uriStart);
|
||||
|
||||
// Extract page number: look for #page=N or bare page=N
|
||||
int pageNum = -1;
|
||||
int hashPos = uriVal.lastIndexOf("#page=");
|
||||
int digitStart = -1;
|
||||
if (hashPos != -1) {
|
||||
digitStart = hashPos + 6;
|
||||
} else if (uriVal.startsWith("page=")) {
|
||||
digitStart = 5;
|
||||
}
|
||||
if (digitStart != -1) {
|
||||
// Take only the leading digits: the fragment may carry extra
|
||||
// parameters after the page number (e.g. "22&fitr=15_489_..."),
|
||||
// and QByteArray::toInt() would fail on the whole remainder.
|
||||
int e = digitStart;
|
||||
while (e < uriVal.size()
|
||||
&& uriVal[e] >= '0' && uriVal[e] <= '9')
|
||||
++e;
|
||||
if (e > digitStart)
|
||||
pageNum = uriVal.mid(digitStart, e - digitStart).toInt();
|
||||
}
|
||||
|
||||
if (pageNum >= 1 && pageNum <= pageObjs.size()) {
|
||||
// Valid page reference — emit GoTo action.
|
||||
int pageObjNum = pageObjs[pageNum - 1];
|
||||
|
||||
// Optional precise destination: &fitr=Left_Bottom_Right_Top
|
||||
// (integer PDF points). If present -> /FitR (frame the element);
|
||||
// otherwise -> /Fit (whole page, top).
|
||||
QByteArray dest = " /Fit]";
|
||||
int fr = uriVal.indexOf("fitr=");
|
||||
if (fr != -1) {
|
||||
QByteArray rest = uriVal.mid(fr + 5);
|
||||
// stop at first char that is not part of the number list
|
||||
int end = 0;
|
||||
while (end < rest.size()
|
||||
&& ((rest[end] >= '0' && rest[end] <= '9')
|
||||
|| rest[end] == '_' || rest[end] == '-'))
|
||||
++end;
|
||||
QList<QByteArray> parts = rest.left(end).split('_');
|
||||
if (parts.size() == 4) {
|
||||
dest = " /FitR " + parts[0] + " " + parts[1] + " "
|
||||
+ parts[2] + " " + parts[3] + "]";
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray goTo = sGoTo
|
||||
+ QByteArray::number(pageObjNum)
|
||||
+ " 0 R" + dest;
|
||||
out.append(goTo);
|
||||
changed = true;
|
||||
} else {
|
||||
// Unknown page — keep original URI
|
||||
out.append(sUri);
|
||||
out.append(uriVal);
|
||||
out.append(')');
|
||||
}
|
||||
|
||||
pos = closeParen + 1; // skip past ')'
|
||||
}
|
||||
|
||||
if (!changed) return; // nothing was replaced
|
||||
data = out;
|
||||
}
|
||||
|
||||
// --- 4. Rebuild xref table ---
|
||||
// Find start of existing xref (last occurrence)
|
||||
int xrefStart = data.lastIndexOf("\nxref\n");
|
||||
if (xrefStart == -1) xrefStart = data.lastIndexOf("\nxref ");
|
||||
if (xrefStart == -1) return; // malformed PDF
|
||||
++xrefStart; // skip the leading '\n'
|
||||
|
||||
QByteArray body = data.left(xrefStart);
|
||||
|
||||
// Collect all object offsets from the body
|
||||
QMap<int, int> offsets; // objNum -> byte offset
|
||||
{
|
||||
const QByteArray objMarker = " 0 obj";
|
||||
int pos = 0;
|
||||
while ((pos = body.indexOf(objMarker, pos)) != -1) {
|
||||
int numStart = pos - 1;
|
||||
while (numStart > 0 && body[numStart-1] != '\n' && body[numStart-1] != '\r')
|
||||
--numStart;
|
||||
QByteArray numStr = body.mid(numStart, pos - numStart).trimmed();
|
||||
bool ok = false;
|
||||
int objNum = numStr.toInt(&ok);
|
||||
if (ok && objNum > 0)
|
||||
offsets[objNum] = numStart;
|
||||
++pos;
|
||||
}
|
||||
}
|
||||
|
||||
if (offsets.isEmpty()) return;
|
||||
|
||||
int maxObj = offsets.lastKey();
|
||||
|
||||
// Build xref table
|
||||
QByteArray xref;
|
||||
xref += "xref\n";
|
||||
xref += "0 " + QByteArray::number(maxObj + 1) + "\n";
|
||||
xref += "0000000000 65535 f \n";
|
||||
for (int i = 1; i <= maxObj; ++i) {
|
||||
if (offsets.contains(i)) {
|
||||
xref += QByteArray::number(offsets[i]).rightJustified(10, '0')
|
||||
+ " 00000 n \n";
|
||||
} else {
|
||||
xref += "0000000000 65535 f \n";
|
||||
}
|
||||
}
|
||||
|
||||
// Find trailer dict from the original xref section
|
||||
int trailerPos = data.indexOf("trailer", xrefStart);
|
||||
int trailerEnd = -1;
|
||||
if (trailerPos != -1) {
|
||||
trailerEnd = data.indexOf("%%EOF", trailerPos);
|
||||
if (trailerEnd != -1) trailerEnd += 5;
|
||||
}
|
||||
|
||||
QByteArray trailer;
|
||||
if (trailerPos != -1 && trailerEnd != -1)
|
||||
trailer = data.mid(trailerPos, trailerEnd - trailerPos);
|
||||
else
|
||||
trailer = "trailer\n<<>>\n%%EOF";
|
||||
|
||||
int newXrefOffset = body.size();
|
||||
|
||||
QByteArray result;
|
||||
result.reserve(body.size() + xref.size() + trailer.size() + 30);
|
||||
result += body;
|
||||
result += xref;
|
||||
result += trailer;
|
||||
result += "\nstartxref\n";
|
||||
result += QByteArray::number(newXrefOffset);
|
||||
result += "\n%%EOF\n";
|
||||
|
||||
// --- 5. Write back ---
|
||||
QFile out(pdfPath);
|
||||
if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) return;
|
||||
out.write(result);
|
||||
out.close();
|
||||
}
|
||||
|
||||
} // namespace PdfLinks
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
Copyright 2006-2025 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef PDF_LINKS_H
|
||||
#define PDF_LINKS_H
|
||||
|
||||
#include <QMap>
|
||||
#include <QPointF>
|
||||
#include <QRectF>
|
||||
#include <QString>
|
||||
#include <QTransform>
|
||||
#include <functional>
|
||||
|
||||
class QPdfEngine;
|
||||
class Diagram;
|
||||
|
||||
/**
|
||||
Shared helper that turns a project's cross-references and folio reports
|
||||
into clickable internal hyperlinks in a Qt-generated PDF. Used by both the
|
||||
GUI print path (ProjectPrintWindow) and the headless CLI export, each of
|
||||
which builds its own page geometry and passes it in — this code never
|
||||
computes the scene-to-page mapping itself.
|
||||
*/
|
||||
namespace PdfLinks {
|
||||
|
||||
/**
|
||||
Geometry mapping for one rendered PDF page. Each caller builds this
|
||||
from its OWN page setup (printer page layout vs QPdfWriter), since the
|
||||
device-pixel and point conversions differ between them.
|
||||
*/
|
||||
struct PageGeometry {
|
||||
/// scene coordinates -> device pixels (the same "fit" render() applied)
|
||||
QTransform sceneToDevice;
|
||||
/// device paint rectangle, in pixels (the page area)
|
||||
QRectF target;
|
||||
/// links whose rectangle falls outside this are dropped
|
||||
QRectF pageBounds;
|
||||
/// device pixels -> PDF points (replicates the engine's page matrix)
|
||||
std::function<QPointF(const QPointF &)> devToPdf;
|
||||
/// a diagram -> its source rectangle in scene pixels (for /FitR framing)
|
||||
std::function<QRectF(Diagram *)> sourceRectOf;
|
||||
};
|
||||
|
||||
/**
|
||||
Inject clickable cross-reference / folio-report hyperlinks for @p diagram
|
||||
into the current page of @p engine. Each link is emitted as a URI
|
||||
annotation encoding the target page and a /FitR rectangle;
|
||||
convertUriToGoTo() then rewrites those into native internal GoTo actions.
|
||||
*/
|
||||
void injectCrossRefLinks(QPdfEngine *engine, Diagram *diagram,
|
||||
const PageGeometry &geom,
|
||||
const QMap<Diagram *, int> &pageMap,
|
||||
const QString &outputFileName);
|
||||
|
||||
/**
|
||||
Post-process a Qt-generated PDF file: rewrite every "/S /URI" link
|
||||
annotation into a native internal "/S /GoTo" action (page + /FitR or
|
||||
/Fit destination) and rebuild the xref table. No-op if the file has no
|
||||
such annotations.
|
||||
*/
|
||||
void convertUriToGoTo(const QString &pdfPath);
|
||||
|
||||
}
|
||||
|
||||
#endif // PDF_LINKS_H
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "projectprintwindow.h"
|
||||
|
||||
#include "../diagram.h"
|
||||
#include "../pdf_links.h"
|
||||
#include "../qeticons.h"
|
||||
#include "../qetproject.h"
|
||||
#include "../qetversion.h"
|
||||
@@ -200,241 +201,6 @@ ProjectPrintWindow::~ProjectPrintWindow()
|
||||
* @brief ProjectPrintWindow::requestPaint
|
||||
* @param slot called when m_preview emit paintRequested
|
||||
*/
|
||||
/**
|
||||
* @brief ProjectPrintWindow::pdfConvertUriToGoTo
|
||||
* Post-processes a Qt-generated PDF to replace URI link annotations
|
||||
* (file:///path/to/file.pdf#page=N) with native PDF GoTo actions
|
||||
* ([pageObj 0 R /Fit]). This makes cross-reference links work in all
|
||||
* PDF viewers regardless of where the file is stored.
|
||||
*
|
||||
* The function:
|
||||
* 1. Reads the PDF as raw bytes.
|
||||
* 2. Collects page object numbers in document order by scanning for
|
||||
* objects that contain "/Type /Page" (but not "/Type /Pages").
|
||||
* 3. Replaces every annotation action block
|
||||
* /S /URI\n/URI (file://...#page=N)
|
||||
* with
|
||||
* /S /GoTo\n/D [<pageObj> 0 R /Fit]
|
||||
* 4. Rebuilds the cross-reference table (offsets change because the
|
||||
* replacement strings have different lengths).
|
||||
* 5. Writes the result back to the same file.
|
||||
*
|
||||
* The function is intentionally conservative: if any step fails (file
|
||||
* not found, malformed PDF, no URI annotations) it returns silently
|
||||
* without corrupting the file.
|
||||
*/
|
||||
static void pdfConvertUriToGoTo(const QString &pdfPath)
|
||||
{
|
||||
// --- 1. Read raw bytes ---
|
||||
QFile f(pdfPath);
|
||||
if (!f.open(QIODevice::ReadOnly)) return;
|
||||
QByteArray data = f.readAll();
|
||||
f.close();
|
||||
|
||||
// --- 2. Collect page object numbers in document order ---
|
||||
// Read them from the page tree (/Type /Pages -> /Kids [ N 0 R ... ]).
|
||||
// This is reliable; scanning raw bytes for "/Type /Page" is NOT: that
|
||||
// marker also occurs inside content streams, and a forward lookahead
|
||||
// wrongly tags neighbouring objects (it found 280 "pages" for a 137-page
|
||||
// document). Qt writes a single, flat /Kids array listing every page.
|
||||
QVector<int> pageObjs;
|
||||
{
|
||||
int pagesPos = data.indexOf("/Type /Pages");
|
||||
int kidsPos = (pagesPos == -1) ? -1 : data.indexOf("/Kids", pagesPos);
|
||||
int lb = (kidsPos == -1) ? -1 : data.indexOf('[', kidsPos);
|
||||
int rb = (lb == -1) ? -1 : data.indexOf(']', lb);
|
||||
if (lb != -1 && rb != -1 && rb > lb) {
|
||||
const QString kids =
|
||||
QString::fromLatin1(data.mid(lb + 1, rb - lb - 1));
|
||||
QRegularExpression re(QStringLiteral("(\\d+)\\s+\\d+\\s+R"));
|
||||
auto it = re.globalMatch(kids);
|
||||
while (it.hasNext()) {
|
||||
int objNum = it.next().captured(1).toInt();
|
||||
if (objNum > 0) pageObjs.append(objNum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pageObjs.isEmpty()) return; // nothing to do
|
||||
|
||||
// --- 3. Replace URI annotations with GoTo ---
|
||||
// Pattern (Qt always writes exactly this):
|
||||
// /S /URI\n/URI (file:///...<anything>#page=N)\n
|
||||
// or (older patches without file://):
|
||||
// /S /URI\n/URI (page=N)\n
|
||||
bool changed = false;
|
||||
{
|
||||
// We do a manual scan to handle variable-length replacements.
|
||||
QByteArray out;
|
||||
out.reserve(data.size());
|
||||
|
||||
const QByteArray sUri = "/S /URI\n/URI (";
|
||||
const QByteArray sGoTo = "/S /GoTo\n/D [";
|
||||
int pos = 0;
|
||||
|
||||
while (pos < data.size()) {
|
||||
int found = data.indexOf(sUri, pos);
|
||||
if (found == -1) {
|
||||
out.append(data.mid(pos));
|
||||
break;
|
||||
}
|
||||
|
||||
// Copy everything up to the match
|
||||
out.append(data.mid(pos, found - pos));
|
||||
|
||||
// Find closing ')' of the URI value
|
||||
int uriStart = found + sUri.size();
|
||||
int closeParen = data.indexOf(')', uriStart);
|
||||
if (closeParen == -1) {
|
||||
// Malformed — copy rest verbatim
|
||||
out.append(data.mid(found));
|
||||
pos = data.size();
|
||||
break;
|
||||
}
|
||||
|
||||
QByteArray uriVal = data.mid(uriStart, closeParen - uriStart);
|
||||
|
||||
// Extract page number: look for #page=N or bare page=N
|
||||
int pageNum = -1;
|
||||
int hashPos = uriVal.lastIndexOf("#page=");
|
||||
int digitStart = -1;
|
||||
if (hashPos != -1) {
|
||||
digitStart = hashPos + 6;
|
||||
} else if (uriVal.startsWith("page=")) {
|
||||
digitStart = 5;
|
||||
}
|
||||
if (digitStart != -1) {
|
||||
// Take only the leading digits: the fragment may carry extra
|
||||
// parameters after the page number (e.g. "22&fitr=15_489_..."),
|
||||
// and QByteArray::toInt() would fail on the whole remainder.
|
||||
int e = digitStart;
|
||||
while (e < uriVal.size()
|
||||
&& uriVal[e] >= '0' && uriVal[e] <= '9')
|
||||
++e;
|
||||
if (e > digitStart)
|
||||
pageNum = uriVal.mid(digitStart, e - digitStart).toInt();
|
||||
}
|
||||
|
||||
if (pageNum >= 1 && pageNum <= pageObjs.size()) {
|
||||
// Valid page reference — emit GoTo action.
|
||||
int pageObjNum = pageObjs[pageNum - 1];
|
||||
|
||||
// Optional precise destination: &fitr=Left_Bottom_Right_Top
|
||||
// (integer PDF points). If present -> /FitR (frame the element);
|
||||
// otherwise -> /Fit (whole page, top).
|
||||
QByteArray dest = " /Fit]";
|
||||
int fr = uriVal.indexOf("fitr=");
|
||||
if (fr != -1) {
|
||||
QByteArray rest = uriVal.mid(fr + 5);
|
||||
// stop at first char that is not part of the number list
|
||||
int end = 0;
|
||||
while (end < rest.size()
|
||||
&& ((rest[end] >= '0' && rest[end] <= '9')
|
||||
|| rest[end] == '_' || rest[end] == '-'))
|
||||
++end;
|
||||
QList<QByteArray> parts = rest.left(end).split('_');
|
||||
if (parts.size() == 4) {
|
||||
dest = " /FitR " + parts[0] + " " + parts[1] + " "
|
||||
+ parts[2] + " " + parts[3] + "]";
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray goTo = sGoTo
|
||||
+ QByteArray::number(pageObjNum)
|
||||
+ " 0 R" + dest;
|
||||
out.append(goTo);
|
||||
changed = true;
|
||||
} else {
|
||||
// Unknown page — keep original URI
|
||||
out.append(sUri);
|
||||
out.append(uriVal);
|
||||
out.append(')');
|
||||
}
|
||||
|
||||
pos = closeParen + 1; // skip past ')'
|
||||
}
|
||||
|
||||
if (!changed) return; // nothing was replaced
|
||||
data = out;
|
||||
}
|
||||
|
||||
// --- 4. Rebuild xref table ---
|
||||
// Find start of existing xref (last occurrence)
|
||||
int xrefStart = data.lastIndexOf("\nxref\n");
|
||||
if (xrefStart == -1) xrefStart = data.lastIndexOf("\nxref ");
|
||||
if (xrefStart == -1) return; // malformed PDF
|
||||
++xrefStart; // skip the leading '\n'
|
||||
|
||||
QByteArray body = data.left(xrefStart);
|
||||
|
||||
// Collect all object offsets from the body
|
||||
QMap<int, int> offsets; // objNum -> byte offset
|
||||
{
|
||||
const QByteArray objMarker = " 0 obj";
|
||||
int pos = 0;
|
||||
while ((pos = body.indexOf(objMarker, pos)) != -1) {
|
||||
int numStart = pos - 1;
|
||||
while (numStart > 0 && body[numStart-1] != '\n' && body[numStart-1] != '\r')
|
||||
--numStart;
|
||||
QByteArray numStr = body.mid(numStart, pos - numStart).trimmed();
|
||||
bool ok = false;
|
||||
int objNum = numStr.toInt(&ok);
|
||||
if (ok && objNum > 0)
|
||||
offsets[objNum] = numStart;
|
||||
++pos;
|
||||
}
|
||||
}
|
||||
|
||||
if (offsets.isEmpty()) return;
|
||||
|
||||
int maxObj = offsets.lastKey();
|
||||
|
||||
// Build xref table
|
||||
QByteArray xref;
|
||||
xref += "xref\n";
|
||||
xref += "0 " + QByteArray::number(maxObj + 1) + "\n";
|
||||
xref += "0000000000 65535 f \n";
|
||||
for (int i = 1; i <= maxObj; ++i) {
|
||||
if (offsets.contains(i)) {
|
||||
xref += QByteArray::number(offsets[i]).rightJustified(10, '0')
|
||||
+ " 00000 n \n";
|
||||
} else {
|
||||
xref += "0000000000 65535 f \n";
|
||||
}
|
||||
}
|
||||
|
||||
// Find trailer dict from the original xref section
|
||||
int trailerPos = data.indexOf("trailer", xrefStart);
|
||||
int trailerEnd = -1;
|
||||
if (trailerPos != -1) {
|
||||
trailerEnd = data.indexOf("%%EOF", trailerPos);
|
||||
if (trailerEnd != -1) trailerEnd += 5;
|
||||
}
|
||||
|
||||
QByteArray trailer;
|
||||
if (trailerPos != -1 && trailerEnd != -1)
|
||||
trailer = data.mid(trailerPos, trailerEnd - trailerPos);
|
||||
else
|
||||
trailer = "trailer\n<<>>\n%%EOF";
|
||||
|
||||
int newXrefOffset = body.size();
|
||||
|
||||
QByteArray result;
|
||||
result.reserve(body.size() + xref.size() + trailer.size() + 30);
|
||||
result += body;
|
||||
result += xref;
|
||||
result += trailer;
|
||||
result += "\nstartxref\n";
|
||||
result += QByteArray::number(newXrefOffset);
|
||||
result += "\n%%EOF\n";
|
||||
|
||||
// --- 5. Write back ---
|
||||
QFile out(pdfPath);
|
||||
if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) return;
|
||||
out.write(result);
|
||||
out.close();
|
||||
}
|
||||
|
||||
void ProjectPrintWindow::requestPaint()
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
|
||||
@@ -489,6 +255,9 @@ void ProjectPrintWindow::requestPaint()
|
||||
const bool pdfExport =
|
||||
(m_printer->outputFormat() == QPrinter::PdfFormat)
|
||||
&& (dynamic_cast<QPdfEngine*>(painter.paintEngine()) != nullptr);
|
||||
// plc-user: added because of "Warning: unused variable 'pdfExport'":
|
||||
// should be fixed by original author by evaluating function-result!
|
||||
(void)pdfExport;
|
||||
|
||||
for (auto diagram : selectedDiagram())
|
||||
{
|
||||
@@ -642,123 +411,17 @@ void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter
|
||||
return QPointF(pt_scale * dx, fullH_pt - pt_scale * dy);
|
||||
};
|
||||
|
||||
// Compute, in PDF points on its OWN page, the rectangle to frame for a
|
||||
// target element (used as a /FitR destination so the link zooms onto it).
|
||||
auto destRectPdf = [&](Element *tgt) -> QRectF {
|
||||
Diagram *dg = tgt ? tgt->diagram() : nullptr;
|
||||
if (!dg) return QRectF();
|
||||
const QRectF srcT = QRectF(diagramRect(dg, exportProperties()));
|
||||
if (srcT.width() <= 0.0 || srcT.height() <= 0.0) return QRectF();
|
||||
const qreal sT = qMin(target.width() / srcT.width(),
|
||||
target.height() / srcT.height());
|
||||
QTransform fitT;
|
||||
fitT.translate(target.x(), target.y());
|
||||
fitT.scale(sT, sT);
|
||||
fitT.translate(-srcT.x(), -srcT.y());
|
||||
|
||||
QRectF elemScene = tgt->mapRectToScene(tgt->boundingRect());
|
||||
// Frame the element with a little context, and enforce a minimum
|
||||
// framed size so tiny contacts don't zoom in extremely.
|
||||
const qreal pad = 25.0;
|
||||
elemScene.adjust(-pad, -pad, pad, pad);
|
||||
const qreal minSide = 160.0;
|
||||
if (elemScene.width() < minSide)
|
||||
elemScene.adjust(-(minSide - elemScene.width()) / 2.0, 0,
|
||||
(minSide - elemScene.width()) / 2.0, 0);
|
||||
if (elemScene.height() < minSide)
|
||||
elemScene.adjust(0, -(minSide - elemScene.height()) / 2.0,
|
||||
0, (minSide - elemScene.height()) / 2.0);
|
||||
|
||||
const QRectF devT = fitT.mapRect(elemScene);
|
||||
const QPointF a = devToPdf(devT.topLeft());
|
||||
const QPointF b = devToPdf(devT.bottomRight());
|
||||
return QRectF(QPointF(qMin(a.x(), b.x()), qMin(a.y(), b.y())),
|
||||
QPointF(qMax(a.x(), b.x()), qMax(a.y(), b.y())));
|
||||
PdfLinks::PageGeometry geom;
|
||||
geom.sceneToDevice = fit;
|
||||
geom.target = target;
|
||||
geom.pageBounds = pageBounds;
|
||||
geom.devToPdf = devToPdf;
|
||||
geom.sourceRectOf = [this](Diagram *dg) {
|
||||
return QRectF(diagramRect(dg, exportProperties()));
|
||||
};
|
||||
|
||||
auto injectLink = [&](const QRectF &sceneRect, Element *targetElmt) {
|
||||
if (!targetElmt || !targetElmt->diagram()) return;
|
||||
const int targetPage =
|
||||
diagramPageMap.value(targetElmt->diagram(), -1);
|
||||
if (targetPage < 1) return;
|
||||
const QRectF devRect = fit.mapRect(sceneRect);
|
||||
if (!devRect.isValid() || !pageBounds.intersects(devRect)) return;
|
||||
|
||||
QString frag = QString("page=%1").arg(targetPage);
|
||||
const QRectF d = destRectPdf(targetElmt); // /FitR L_B_R_T
|
||||
if (d.isValid())
|
||||
frag += QString("&fitr=%1_%2_%3_%4")
|
||||
.arg(qRound(d.left())).arg(qRound(d.top()))
|
||||
.arg(qRound(d.right())).arg(qRound(d.bottom()));
|
||||
|
||||
QUrl url = QUrl::fromLocalFile(printer->outputFileName());
|
||||
url.setFragment(frag);
|
||||
pdfEngine->drawHyperlink(devRect, url);
|
||||
};
|
||||
|
||||
for (auto *item : diagram->items()) {
|
||||
|
||||
// --- CrossRefItem links ---
|
||||
if (auto *xref = dynamic_cast<CrossRefItem*>(item)) {
|
||||
for (auto it = xref->hoveredContactsMap().begin();
|
||||
it != xref->hoveredContactsMap().end(); ++it)
|
||||
{
|
||||
Element *targetElmt = it.key();
|
||||
if (!targetElmt || !targetElmt->diagram()) continue;
|
||||
// it.value() est en coords LOCALES du CrossRefItem -> scene
|
||||
injectLink(xref->mapRectToScene(it.value()), targetElmt);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// --- Folio report links (DynamicElementTextItem) ---
|
||||
if (auto *deti = dynamic_cast<DynamicElementTextItem*>(item)) {
|
||||
Element *parent = deti->parentElement();
|
||||
if (!parent) continue;
|
||||
|
||||
// (a) Report element : label -> linked report on another folio
|
||||
if (parent->linkType() & Element::AllReport) {
|
||||
if (parent->linkedElements().isEmpty()) continue;
|
||||
|
||||
bool showsLabel =
|
||||
(deti->textFrom() == DynamicElementTextItem::ElementInfo
|
||||
&& deti->infoName() == QLatin1String("label")) ||
|
||||
(deti->textFrom() == DynamicElementTextItem::CompositeText
|
||||
&& deti->compositeText().contains(QStringLiteral("%{label}")));
|
||||
if (!showsLabel) continue;
|
||||
|
||||
Element *targetElmt = parent->linkedElements().first();
|
||||
if (!targetElmt || !targetElmt->diagram()) continue;
|
||||
|
||||
injectLink(deti->mapRectToScene(deti->boundingRect()), targetElmt);
|
||||
continue;
|
||||
}
|
||||
|
||||
// (b) Slave element : the "(folio-pos)" text -> master element
|
||||
if (parent->linkType() == Element::Slave) {
|
||||
QGraphicsTextItem *sx = deti->slaveXrefItem();
|
||||
Element *master = deti->masterElement();
|
||||
if (sx && master && master->diagram()) {
|
||||
injectLink(sx->mapRectToScene(sx->boundingRect()), master);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// --- Slave cross-reference carried by a grouped text ---
|
||||
if (auto *grp = dynamic_cast<ElementTextItemGroup*>(item)) {
|
||||
Element *parent = grp->parentElement();
|
||||
if (!parent || parent->linkType() != Element::Slave) continue;
|
||||
if (parent->linkedElements().isEmpty()) continue;
|
||||
QGraphicsTextItem *sx = grp->slaveXrefItem();
|
||||
if (!sx) continue;
|
||||
Element *master = parent->linkedElements().first();
|
||||
if (!master || !master->diagram()) continue;
|
||||
injectLink(sx->mapRectToScene(sx->boundingRect()), master);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
PdfLinks::injectCrossRefLinks(
|
||||
pdfEngine, diagram, geom, diagramPageMap,
|
||||
printer->outputFileName());
|
||||
}
|
||||
}
|
||||
////PDF links end////
|
||||
@@ -1235,7 +898,7 @@ void ProjectPrintWindow::print()
|
||||
QTimer::singleShot(0, this, [this, pdfFile]() {
|
||||
// Convert URI link annotations into native internal GoTo/FitR
|
||||
// actions so cross-references jump inside the document.
|
||||
pdfConvertUriToGoTo(pdfFile);
|
||||
PdfLinks::convertUriToGoTo(pdfFile);
|
||||
this->close();
|
||||
});
|
||||
} else {
|
||||
|
||||
+48
-24
@@ -226,20 +226,20 @@ void QETApp::setLanguage(const QString &desired_language) {
|
||||
|
||||
// load translations for the QET application
|
||||
// charge les traductions pour l'application QET
|
||||
if (!qetTranslator.load("qet_" + desired_language, languages_path)) {
|
||||
/* in case of failure,
|
||||
* we fall back on the native channels for French
|
||||
* en cas d'echec,
|
||||
* on retombe sur les chaines natives pour le francais
|
||||
*/
|
||||
if (desired_language != "fr") {
|
||||
// use of the English version by default
|
||||
// utilisation de la version anglaise par defaut
|
||||
if(!qetTranslator.load("qet_en", languages_path))
|
||||
qWarning() << "failed to load"
|
||||
<< "qet_en" << languages_path << "(" << __FILE__
|
||||
<< __LINE__ << __FUNCTION__ << ")";
|
||||
}
|
||||
// desired_language may be a full locale such as "pt_BR": try that exact
|
||||
// translation, then the base language ("pt"), then fall back to English.
|
||||
// French is the application's source language and needs no translation.
|
||||
const QString base_language = desired_language.section('_', 0, 0);
|
||||
bool loaded = qetTranslator.load("qet_" + desired_language, languages_path);
|
||||
if (!loaded && base_language != desired_language)
|
||||
loaded = qetTranslator.load("qet_" + base_language, languages_path);
|
||||
if (!loaded && base_language != "fr") {
|
||||
// use of the English version by default
|
||||
// utilisation de la version anglaise par defaut
|
||||
if(!qetTranslator.load("qet_en", languages_path))
|
||||
qWarning() << "failed to load"
|
||||
<< "qet_en" << languages_path << "(" << __FILE__
|
||||
<< __LINE__ << __FUNCTION__ << ")";
|
||||
}
|
||||
qApp->installTranslator(&qetTranslator);
|
||||
|
||||
@@ -263,7 +263,11 @@ QString QETApp::langFromSetting()
|
||||
QSettings settings;
|
||||
system_language = settings.value("lang", "system").toString();
|
||||
if(system_language == "system") {
|
||||
system_language = QLocale::system().name().left(2);
|
||||
// Keep the full locale (e.g. "pt_BR"), not just the base language
|
||||
// ("pt"): QET ships regional translations (pt_BR, nl_BE, nl_NL) and
|
||||
// truncating here loaded the wrong one. setLanguage() falls back to
|
||||
// the base language when no regional translation exists.
|
||||
system_language = QLocale::system().name();
|
||||
}
|
||||
lang_is_set = true;
|
||||
}
|
||||
@@ -883,10 +887,13 @@ QString QETApp::configDir()
|
||||
#ifdef QET_ALLOW_OVERRIDE_CD_OPTION
|
||||
if (config_dir != QString()) return(config_dir);
|
||||
#endif
|
||||
QString configdir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
|
||||
while (configdir.endsWith('/')) {
|
||||
configdir.remove(configdir.length()-1, 1);
|
||||
}
|
||||
// C++11 static-local init runs exactly once across all threads — safe to
|
||||
// call from QtConcurrent background threads (QStandardPaths is not).
|
||||
static const QString configdir = []() {
|
||||
QString d = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
|
||||
while (d.endsWith('/')) d.chop(1);
|
||||
return d;
|
||||
}();
|
||||
return configdir;
|
||||
}
|
||||
|
||||
@@ -907,10 +914,13 @@ QString QETApp::dataDir()
|
||||
#ifdef QET_ALLOW_OVERRIDE_DD_OPTION
|
||||
if (data_dir != QString()) return(data_dir);
|
||||
#endif
|
||||
QString datadir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
||||
while (datadir.endsWith('/')) {
|
||||
datadir.remove(datadir.length()-1, 1);
|
||||
}
|
||||
// C++11 static-local init runs exactly once across all threads — safe to
|
||||
// call from QtConcurrent background threads (QStandardPaths is not).
|
||||
static const QString datadir = []() {
|
||||
QString d = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
||||
while (d.endsWith('/')) d.chop(1);
|
||||
return d;
|
||||
}();
|
||||
return datadir;
|
||||
}
|
||||
|
||||
@@ -1228,7 +1238,21 @@ QString QETApp::languagesPath()
|
||||
* en l'absence d'option de compilation, on utilise le dossier lang,
|
||||
* situe a cote du binaire executable
|
||||
*/
|
||||
return(QCoreApplication::applicationDirPath() + "/lang/");
|
||||
{
|
||||
const QString bin_dir = QCoreApplication::applicationDirPath();
|
||||
const QString next_to_bin = bin_dir + "/lang/";
|
||||
// Some packagings (notably the Windows installer) put the binary in a
|
||||
// "bin" subfolder while "lang" sits beside it (../lang). Fall back to
|
||||
// that layout when the folder next to the binary is absent, so the
|
||||
// translations are found without a --lang-dir argument. See issue #86.
|
||||
if (!QDir(next_to_bin).exists()) {
|
||||
const QString sibling_of_bin =
|
||||
QDir::cleanPath(bin_dir + "/../lang") + "/";
|
||||
if (QDir(sibling_of_bin).exists())
|
||||
return(sibling_of_bin);
|
||||
}
|
||||
return(next_to_bin);
|
||||
}
|
||||
#else
|
||||
#ifndef QET_LANG_PATH_RELATIVE_TO_BINARY_PATH
|
||||
/* the compilation option represents
|
||||
|
||||
@@ -2474,7 +2474,6 @@ void QETDiagramEditor::generateTerminalBlock()
|
||||
exeList << (QETApp::dataDir() + "/binary/qet_tb_generator.exe")
|
||||
<< (QDir::currentPath() + "/qet_tb_generator.exe")
|
||||
<< QStandardPaths::findExecutable("qet_tb_generator.exe")
|
||||
<< (QDir::homePath() + "/Application Data/qet/qet_tb_generator.exe")
|
||||
<< "qet_tb_generator.exe"
|
||||
<< "qet_tb_generator"; // from original code: missing ".exe" ???
|
||||
#elif defined(Q_OS_MACOS)
|
||||
|
||||
@@ -80,6 +80,11 @@ class QETDiagramEditor : public QETMainWindow
|
||||
protected:
|
||||
bool event(QEvent *) override;
|
||||
private:
|
||||
// Declared first so it is initialised before any member whose
|
||||
// constructor may dispatch a Qt event calling event() (e.g. the
|
||||
// QActionGroup members below trigger QObject::setParent events).
|
||||
bool m_first_show = true;
|
||||
|
||||
QETDiagramEditor(const QETDiagramEditor &);
|
||||
void setUpElementsPanel ();
|
||||
void setUpElementsCollectionWidget();
|
||||
@@ -253,7 +258,6 @@ class QETDiagramEditor : public QETMainWindow
|
||||
QUndoGroup undo_group;
|
||||
AutoNumberingDockWidget *m_autonumbering_dock;
|
||||
int activeSubWindowIndex;
|
||||
bool m_first_show = true;
|
||||
SearchAndReplaceWidget m_search_and_replace_widget;
|
||||
};
|
||||
#endif
|
||||
|
||||
+42
-8
@@ -43,6 +43,13 @@
|
||||
|
||||
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
|
||||
Create a empty project
|
||||
@@ -130,6 +137,11 @@ QETProject::QETProject(KAutoSaveFile *backup, QObject *parent) :
|
||||
*/
|
||||
QETProject::~QETProject()
|
||||
{
|
||||
//Wait for any in-flight async crash-recovery backup to finish: the worker
|
||||
//writes through &m_backup_file, a member that would otherwise be destroyed
|
||||
//under it (issue #492).
|
||||
m_backup_future.waitForFinished();
|
||||
|
||||
//We block database signal to avoid hundreds of unnecessary emitted signal
|
||||
//due to deletion (diagram, item, etc...) and as much update made in the not yet deleted things.
|
||||
m_data_base.blockSignals(true);
|
||||
@@ -332,6 +344,8 @@ void QETProject::setFilePath(const QString &filepath)
|
||||
}
|
||||
#ifdef BUILD_WITHOUT_KF5
|
||||
#else
|
||||
//Don't close/re-point the backup file while a backup is still writing it.
|
||||
m_backup_future.waitForFinished();
|
||||
if (m_backup_file.isOpen()) {
|
||||
m_backup_file.close();
|
||||
}
|
||||
@@ -1006,16 +1020,30 @@ QETResult QETProject::write()
|
||||
if (m_file_path.isEmpty())
|
||||
return(QString("unable to save project to file: no filepath was specified"));
|
||||
|
||||
// if the project was opened read-only
|
||||
// and the file is still non-writable, do not save the project
|
||||
if (isReadOnly() && !QFileInfo(m_file_path).isWritable())
|
||||
return(QString("the file %1 was opened read-only and thus will not be written").arg(m_file_path));
|
||||
// If the project was opened read-only, only refuse when the target
|
||||
// really can't be written: an existing file that is not writable, or a
|
||||
// new file (e.g. "Save As" to another location) whose directory is not
|
||||
// writable. A non-existent file reports isWritable() == false, so the
|
||||
// old check wrongly blocked saving a read-only project elsewhere.
|
||||
if (isReadOnly()) {
|
||||
const QFileInfo file_info(m_file_path);
|
||||
const bool can_write = file_info.exists()
|
||||
? file_info.isWritable()
|
||||
: QFileInfo(file_info.absolutePath()).isWritable();
|
||||
if (!can_write)
|
||||
return(QString("the file %1 was opened read-only and thus will not be written").arg(m_file_path));
|
||||
}
|
||||
|
||||
QDomDocument xml_project(toXml());
|
||||
QString error_message;
|
||||
if (!QET::writeXmlFile(xml_project, m_file_path, &error_message))
|
||||
return(error_message);
|
||||
|
||||
// The project has just been written to a writable file (e.g. saved to
|
||||
// a new location with "Save As"), so it is no longer read-only.
|
||||
if (isReadOnly())
|
||||
setReadOnly(false);
|
||||
|
||||
//title block variables should be updated after file save dialog is confirmed, before file is saved.
|
||||
m_project_properties.addValue("saveddate", QLocale::system().toString(QDate::currentDate(), QLocale::ShortFormat));
|
||||
m_project_properties.addValue("saveddate-us", QDate::currentDate().toString("yyyy-MM-dd"));
|
||||
@@ -1098,7 +1126,7 @@ ElementsLocation QETProject::importElement(ElementsLocation &location)
|
||||
//Get the path where the element must be imported
|
||||
QString import_path;
|
||||
if (location.isFileSystem()) {
|
||||
import_path = "import/" + location.collectionPath(false);
|
||||
import_path = "import/" % location.collectionPath(false);
|
||||
}
|
||||
else if (location.isProject()) {
|
||||
if (location.project() == this) {
|
||||
@@ -1363,7 +1391,7 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
|
||||
"\n qui est ultérieure à votre version !"
|
||||
" \n"
|
||||
"Vous utilisez actuellement QElectroTech en version %2")
|
||||
.arg(root_elmt.attribute(QStringLiteral("version")), QetVersion::currentVersion().toString() +
|
||||
.arg(root_elmt.attribute(QStringLiteral("version")), QetVersion::currentVersion().toString() %
|
||||
tr(".\n Il est alors possible que l'ouverture de tout ou partie de ce "
|
||||
"document échoue.\n"
|
||||
"Que désirez vous faire ?"),
|
||||
@@ -1387,7 +1415,7 @@ void QETProject::readProjectXml(QDomDocument &xml_project)
|
||||
tr("Avertissement ", "message box title"),
|
||||
tr("Le projet que vous tentez d'ouvrir est partiellement "
|
||||
"compatible avec votre version %1 de QElectroTech.\n")
|
||||
.arg(QetVersion::currentVersion().toString()) +
|
||||
.arg(QetVersion::currentVersion().toString()) %
|
||||
tr("Afin de le rendre totalement compatible veuillez ouvrir ce même projet "
|
||||
"avec la version 0.8, ou 0.80 de QElectroTech et sauvegarder le projet "
|
||||
"et l'ouvrir à nouveau avec cette version.\n"
|
||||
@@ -1783,11 +1811,17 @@ void QETProject::addDiagram(Diagram *diagram, int pos)
|
||||
*/
|
||||
void QETProject::writeBackup()
|
||||
{
|
||||
if (!m_backup_enabled)
|
||||
return;
|
||||
#ifdef BUILD_WITHOUT_KF5
|
||||
#else
|
||||
# if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) // ### Qt 6: remove
|
||||
//Don't launch a new backup while the previous one is still writing:
|
||||
//both would write through &m_backup_file on different threads.
|
||||
if (m_backup_future.isRunning())
|
||||
return;
|
||||
QDomDocument xml_project(toXml());
|
||||
QtConcurrent::run(
|
||||
m_backup_future = QtConcurrent::run(
|
||||
QET::writeToFile,xml_project,&m_backup_file,nullptr);
|
||||
# else
|
||||
# if TODO_LIST
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
#endif
|
||||
|
||||
#include <QHash>
|
||||
#include <QFuture>
|
||||
|
||||
class Diagram;
|
||||
class ElementsLocation;
|
||||
@@ -105,6 +106,12 @@ class QETProject : public QObject
|
||||
QVersionNumber declaredQElectroTechVersion();
|
||||
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
|
||||
BorderProperties defaultBorderProperties() const;
|
||||
void setDefaultBorderProperties(const BorderProperties &);
|
||||
@@ -241,6 +248,8 @@ class QETProject : public QObject
|
||||
|
||||
// attributes
|
||||
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
|
||||
QString m_file_path;
|
||||
/// Current state of the project
|
||||
@@ -287,6 +296,7 @@ class QETProject : public QObject
|
||||
bool m_freeze_new_conductors = false;
|
||||
QTimer m_save_backup_timer,
|
||||
m_autosave_timer;
|
||||
QFuture<bool> m_backup_future;
|
||||
#ifdef BUILD_WITHOUT_KF5
|
||||
#else
|
||||
KAutoSaveFile m_backup_file;
|
||||
|
||||
@@ -1756,6 +1756,10 @@ QString TitleBlockTemplate::interpreteVariables(
|
||||
QStringList TitleBlockTemplate::listOfVariables()
|
||||
{
|
||||
QStringList list;
|
||||
// Match every "%{name}" placeholder. The bare "%name" form can't be
|
||||
// extracted reliably without the variable list, and templates use the
|
||||
// braced form, so only that is collected here.
|
||||
static const QRegularExpression rx(QStringLiteral("%\\{([^}]+)\\}"));
|
||||
// run through each individual cell
|
||||
for (int j = 0 ; j < rows_heights_.count() ; ++ j) {
|
||||
for (int i = 0 ; i < columns_width_.count() ; ++ i) {
|
||||
@@ -1763,14 +1767,15 @@ QStringList TitleBlockTemplate::listOfVariables()
|
||||
|| cells_[i][j] -> cell_type
|
||||
== TitleBlockCell::EmptyCell)
|
||||
continue;
|
||||
#if TODO_LIST
|
||||
#pragma message("@TODO not works on all cases...")
|
||||
#endif
|
||||
// TODO: not works on all cases...
|
||||
list << cells_[i][j] -> value.name().replace("%","");
|
||||
const QString cell_value = cells_[i][j] -> value.name();
|
||||
auto it = rx.globalMatch(cell_value);
|
||||
while (it.hasNext()) {
|
||||
const QString name = it.next().captured(1);
|
||||
if (!name.isEmpty() && !list.contains(name))
|
||||
list << name;
|
||||
}
|
||||
}
|
||||
}
|
||||
qDebug() << list;
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
@@ -350,7 +350,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="label_11">
|
||||
<widget class="QLabel" name="label_23">
|
||||
<property name="text">
|
||||
<string>Répertoire des Macros utilisateur</string>
|
||||
</property>
|
||||
|
||||
@@ -273,9 +273,15 @@ void ElementInfoWidget::updateUi()
|
||||
}
|
||||
// Load the lock status for auto numbering
|
||||
if (m_element->elementData().m_type == ElementData::Terminal) {
|
||||
// ... (bestehender Terminal-Code für auto_num_locked und potential_isolating) ...
|
||||
}
|
||||
QString lock_value = element_info.value(QStringLiteral("auto_num_locked")).toString();
|
||||
ui->m_auto_num_locked_cb->setChecked(lock_value == QLatin1String("true"));
|
||||
|
||||
// English: Load the potential isolating status from the element information mapping
|
||||
if (m_potential_isolating_cb) {
|
||||
QString isolating_value = element_info.value(QStringLiteral("potential_isolating")).toString();
|
||||
m_potential_isolating_cb->setChecked(isolating_value == QLatin1String("true"));
|
||||
}
|
||||
}
|
||||
// English: Load the BOM exclusion status from the element information mapping
|
||||
if (m_exclude_from_bom_cb) {
|
||||
QString exclude_bom_value = element_info.value(QStringLiteral("exclude_from_bom")).toString();
|
||||
@@ -297,12 +303,11 @@ DiagramContext ElementInfoWidget::currentInfo() const
|
||||
|
||||
for (const auto &eipw : qAsConst(m_eipw_list))
|
||||
{
|
||||
|
||||
//add value only if they're something to store
|
||||
//add value only if they're something to store
|
||||
if (!eipw->text().isEmpty())
|
||||
{
|
||||
QString txt{eipw->text()};
|
||||
//remove line feed and carriage return
|
||||
//remove line feed and carriage return
|
||||
txt.remove(QStringLiteral("\r"));
|
||||
txt.remove(QStringLiteral("\n"));
|
||||
info_.addValue(eipw->key(), txt);
|
||||
@@ -311,12 +316,16 @@ DiagramContext ElementInfoWidget::currentInfo() const
|
||||
|
||||
// Save the auto numbering lock status
|
||||
if (m_element->elementData().m_type == ElementData::Terminal) {
|
||||
info_.addValue(QStringLiteral("auto_num_locked"), ui->m_auto_num_locked_cb->isChecked() ? QStringLiteral("true") : QStringLiteral("false"));
|
||||
|
||||
if (m_potential_isolating_cb) {
|
||||
info_.addValue(QStringLiteral("potential_isolating"), m_potential_isolating_cb->isChecked() ? QStringLiteral("true") : QStringLiteral("false"));
|
||||
}
|
||||
}
|
||||
|
||||
if (m_exclude_from_bom_cb) {
|
||||
info_.addValue(QStringLiteral("exclude_from_bom"), m_exclude_from_bom_cb->isChecked() ? QStringLiteral("true") : QStringLiteral("false"));
|
||||
}
|
||||
|
||||
return info_;
|
||||
}
|
||||
/**
|
||||
|
||||
@@ -143,7 +143,7 @@ void MasterPropertiesWidget::setElement(Element *element)
|
||||
disconnect(m_project, SIGNAL(diagramRemoved(QETProject*,Diagram*)),
|
||||
this, SLOT(diagramWasdeletedFromProject()));
|
||||
|
||||
if(Q_LIKELY(element->diagram() && element->diagram()->project()))
|
||||
if(Q_LIKELY(element->diagram() && element->diagram()->project()))
|
||||
{
|
||||
m_project = element->diagram()->project();
|
||||
connect(m_project, SIGNAL(diagramRemoved(QETProject*,Diagram*)),
|
||||
@@ -157,7 +157,7 @@ void MasterPropertiesWidget::setElement(Element *element)
|
||||
disconnect(m_element.data(), &Element::linkedElementChanged,
|
||||
this, &MasterPropertiesWidget::updateUi);
|
||||
|
||||
m_element = element;
|
||||
m_element = element;
|
||||
connect(m_element.data(), &Element::linkedElementChanged,
|
||||
this, &MasterPropertiesWidget::updateUi);
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "ui_titleblockpropertieswidget.h"
|
||||
|
||||
#include <QMenu>
|
||||
#include <QSet>
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
@@ -162,7 +163,11 @@ void TitleBlockPropertiesWidget::setProperties(
|
||||
}
|
||||
ui -> m_tbt_cb -> setCurrentIndex(index);
|
||||
|
||||
m_dcw -> setContext(properties.context);
|
||||
// Show the saved custom values, plus any of the template's custom variables
|
||||
// that aren't defined yet, so the user only fills in the missing ones (#271).
|
||||
DiagramContext context = properties.context;
|
||||
addTemplateVariables(context, index);
|
||||
m_dcw -> setContext(context);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -435,12 +440,15 @@ void TitleBlockPropertiesWidget::updateTemplateList()
|
||||
}
|
||||
|
||||
/**
|
||||
@brief TitleBlockPropertiesWidget::changeCurrentTitleBlockTemplate
|
||||
Load the additional field of title block "text"
|
||||
@brief TitleBlockPropertiesWidget::templateForIndex
|
||||
@param index : index in the collection-type map (= the template combo index)
|
||||
@return the TitleBlockTemplate currently selected for that collection, or
|
||||
nullptr.
|
||||
*/
|
||||
void TitleBlockPropertiesWidget::changeCurrentTitleBlockTemplate(int index)
|
||||
TitleBlockTemplate *TitleBlockPropertiesWidget::templateForIndex(int index) const
|
||||
{
|
||||
m_dcw -> clear();
|
||||
if (index < 0 || index >= m_map_index_to_collection_type.count())
|
||||
return nullptr;
|
||||
|
||||
QET::QetCollection qc = m_map_index_to_collection_type.at(index);
|
||||
TitleBlockTemplatesCollection *collection = nullptr;
|
||||
@@ -448,21 +456,55 @@ void TitleBlockPropertiesWidget::changeCurrentTitleBlockTemplate(int index)
|
||||
if (c -> collection() == qc)
|
||||
collection = c;
|
||||
|
||||
if (!collection) return;
|
||||
if (!collection) return nullptr;
|
||||
return collection -> getTemplate(ui -> m_tbt_cb -> currentText());
|
||||
}
|
||||
|
||||
// get template
|
||||
TitleBlockTemplate *tpl = collection -> getTemplate(ui -> m_tbt_cb -> currentText());
|
||||
if(tpl != nullptr) {
|
||||
// get all template fields
|
||||
QStringList fields = tpl -> listOfVariables();
|
||||
// set fields to additional_fields_ widget
|
||||
DiagramContext templateContext;
|
||||
for(int i =0; i<fields.count(); i++)
|
||||
templateContext.addValue(fields.at(i), "");
|
||||
m_dcw -> setContext(templateContext);
|
||||
/**
|
||||
@brief TitleBlockPropertiesWidget::addTemplateVariables
|
||||
Add to @p context every CUSTOM variable used by the currently selected
|
||||
template that is not already present, with an empty value — so the user
|
||||
only has to fill in the values instead of declaring the variables (#271).
|
||||
The standard fields (title, author, date, …) are handled by their own
|
||||
widgets and are skipped. Existing values in @p context are preserved.
|
||||
*/
|
||||
void TitleBlockPropertiesWidget::addTemplateVariables(
|
||||
DiagramContext &context, int index) const
|
||||
{
|
||||
TitleBlockTemplate *tpl = templateForIndex(index);
|
||||
if (!tpl) return;
|
||||
|
||||
// Variables rendered from the dedicated standard-field widgets; they must
|
||||
// not appear in the "Custom" tab.
|
||||
static const QSet<QString> reserved {
|
||||
QStringLiteral("author"), QStringLiteral("date"),
|
||||
QStringLiteral("title"), QStringLiteral("filename"),
|
||||
QStringLiteral("plant"), QStringLiteral("locmach"),
|
||||
QStringLiteral("indexrev"), QStringLiteral("version"),
|
||||
QStringLiteral("folio"), QStringLiteral("folio-id"),
|
||||
QStringLiteral("folio-total"), QStringLiteral("auto_page_num"),
|
||||
QStringLiteral("previous-folio-num"), QStringLiteral("next-folio-num")
|
||||
};
|
||||
|
||||
const QStringList variables = tpl -> listOfVariables();
|
||||
for (const QString &name : variables) {
|
||||
if (name.isEmpty() || reserved.contains(name)) continue;
|
||||
if (!context.contains(name)) context.addValue(name, "");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief TitleBlockPropertiesWidget::changeCurrentTitleBlockTemplate
|
||||
When the user picks a template, append its missing custom variables to the
|
||||
"Custom" tab while keeping the values already entered (#271).
|
||||
*/
|
||||
void TitleBlockPropertiesWidget::changeCurrentTitleBlockTemplate(int index)
|
||||
{
|
||||
DiagramContext context = m_dcw -> context();
|
||||
addTemplateVariables(context, index);
|
||||
m_dcw -> setContext(context);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief TitleBlockPropertiesWidget::on_m_date_now_pb_clicked
|
||||
Set the date to current date
|
||||
|
||||
@@ -30,6 +30,7 @@ class NumerotationContext;
|
||||
class QETProject;
|
||||
class QMenu;
|
||||
class TitleBlockTemplatesCollection;
|
||||
class TitleBlockTemplate;
|
||||
|
||||
namespace Ui {
|
||||
class TitleBlockPropertiesWidget;
|
||||
@@ -77,6 +78,8 @@ class TitleBlockPropertiesWidget : public QWidget
|
||||
void initDialog(const bool ¤t_date, QETProject *project);
|
||||
int getIndexFor (const QString &tbt_name,
|
||||
const QET::QetCollection collection) const;
|
||||
TitleBlockTemplate *templateForIndex (int index) const;
|
||||
void addTemplateVariables (DiagramContext &context, int index) const;
|
||||
|
||||
private slots:
|
||||
void editCurrentTitleBlockTemplate();
|
||||
|
||||
@@ -151,13 +151,39 @@ void WiringListExport::toCsv()
|
||||
{
|
||||
if (!m_project) return;
|
||||
|
||||
QDomDocument doc = m_project->toXml();
|
||||
|
||||
if (doc.isNull()) {
|
||||
const QString csv = toCsvString();
|
||||
if (csv.isEmpty()) {
|
||||
QMessageBox::warning(m_parent, tr("Erreur"), tr("Impossible de lire la structure en mémoire du projet."));
|
||||
return;
|
||||
}
|
||||
|
||||
QFileDialog dialog(m_parent);
|
||||
dialog.setAcceptMode(QFileDialog::AcceptSave);
|
||||
dialog.setWindowTitle(tr("Exporter le plan de câblage"));
|
||||
dialog.setDefaultSuffix("csv");
|
||||
dialog.setNameFilter(tr("Fichiers CSV (*.csv)"));
|
||||
|
||||
if (dialog.exec() != QDialog::Accepted) return;
|
||||
QString fileName = dialog.selectedFiles().first();
|
||||
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
QMessageBox::warning(m_parent, tr("Erreur"), tr("Impossible d'ouvrir le fichier pour l'écriture."));
|
||||
return;
|
||||
}
|
||||
QTextStream out(&file);
|
||||
out << csv;
|
||||
file.close();
|
||||
QMessageBox::information(m_parent, tr("Export réussi"), tr("Le plan de câblage a été exporté avec succès !"));
|
||||
}
|
||||
|
||||
QString WiringListExport::toCsvString() const
|
||||
{
|
||||
if (!m_project) return QString();
|
||||
|
||||
QDomDocument doc = m_project->toXml();
|
||||
if (doc.isNull()) return QString();
|
||||
|
||||
QSet<QString> conductorDefinitionTypes;
|
||||
QDomElement rootElem = doc.documentElement();
|
||||
QDomElement collection = rootElem.firstChildElement("collection");
|
||||
@@ -197,21 +223,6 @@ void WiringListExport::toCsv()
|
||||
}
|
||||
}
|
||||
|
||||
QFileDialog dialog(m_parent);
|
||||
dialog.setAcceptMode(QFileDialog::AcceptSave);
|
||||
dialog.setWindowTitle(tr("Exporter le plan de câblage"));
|
||||
dialog.setDefaultSuffix("csv");
|
||||
dialog.setNameFilter(tr("Fichiers CSV (*.csv)"));
|
||||
|
||||
if (dialog.exec() != QDialog::Accepted) return;
|
||||
QString fileName = dialog.selectedFiles().first();
|
||||
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
QMessageBox::warning(m_parent, tr("Erreur"), tr("Impossible d'ouvrir le fichier pour l'écriture."));
|
||||
return;
|
||||
}
|
||||
|
||||
QMap<QString, ElementInfo> elementsInfo = collectElementsInfo(doc.documentElement());
|
||||
QList<ConductorData> conductors = collectConductors(doc.documentElement());
|
||||
|
||||
@@ -353,7 +364,8 @@ void WiringListExport::toCsv()
|
||||
return a.terminalname2 < b.terminalname2;
|
||||
});
|
||||
|
||||
QTextStream out(&file);
|
||||
QString csv;
|
||||
QTextStream out(&csv);
|
||||
out << tr("Page", "Wiring list CSV header") << ";"
|
||||
<< tr("Composant 1", "Wiring list CSV header") << ";"
|
||||
<< tr("Borne 1", "Wiring list CSV header") << ";"
|
||||
@@ -376,6 +388,5 @@ void WiringListExport::toCsv()
|
||||
<< c.function << "\n";
|
||||
}
|
||||
|
||||
file.close();
|
||||
QMessageBox::information(m_parent, tr("Export réussi"), tr("Le plan de câblage a été exporté avec succès !"));
|
||||
return csv;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,11 @@ class WiringListExport : public QObject
|
||||
public:
|
||||
explicit WiringListExport(QETProject *project, QWidget *parent = nullptr);
|
||||
void toCsv();
|
||||
/**
|
||||
Build the wiring-list CSV and return it as a string (no GUI).
|
||||
Used by toCsv() and by the headless command-line export.
|
||||
*/
|
||||
QString toCsvString() const;
|
||||
|
||||
private:
|
||||
QETProject *m_project;
|
||||
|
||||
Reference in New Issue
Block a user