mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-06-12 06:43:14 +02:00
Compare commits
33 Commits
be21604ad0
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e7787daa2c | |||
| c7fd3416f6 | |||
| b782a1612a | |||
| 2fdbc3c243 | |||
| a219c3f587 | |||
| 6b3b55b0e1 | |||
| 36d0121038 | |||
| 8235ecdbc9 | |||
| b6e4cd4786 | |||
| fb35027624 | |||
| 08a441d1f6 | |||
| e9840728b4 | |||
| ffbcd12d9b | |||
| 0a124f6695 | |||
| 3f6f99b50f | |||
| 42b64a7f0a | |||
| 361719ca74 | |||
| 181e2b555d | |||
| 420512595d | |||
| 9b4bed361d | |||
| 339bc8700b | |||
| 87d5ae5580 | |||
| 1070179617 | |||
| 6c4711a8d0 | |||
| 8a8a338a2e | |||
| 407cc7a4c2 | |||
| 5cb8930732 | |||
| a24acfac24 | |||
| 8b0b1d10d4 | |||
| 57dfa28674 | |||
| 3848c7821a | |||
| 1572c23d51 | |||
| e234f063f8 |
+2
-1
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
include(cmake/hoto_update_cmake_message.cmake)
|
include(cmake/hoto_update_cmake_message.cmake)
|
||||||
|
|
||||||
cmake_minimum_required(VERSION 3.14...3.19 FATAL_ERROR)
|
cmake_minimum_required(VERSION 3.5...4.2)
|
||||||
|
|
||||||
project(qelectrotech
|
project(qelectrotech
|
||||||
VERSION 0.100.1
|
VERSION 0.100.1
|
||||||
@@ -145,6 +145,7 @@ target_include_directories(
|
|||||||
${QET_DIR}/sources/dataBase/ui
|
${QET_DIR}/sources/dataBase/ui
|
||||||
${QET_DIR}/sources/factory/ui
|
${QET_DIR}/sources/factory/ui
|
||||||
${QET_DIR}/sources/print
|
${QET_DIR}/sources/print
|
||||||
|
${QET_DIR}/sources/svg
|
||||||
)
|
)
|
||||||
|
|
||||||
install(TARGETS ${PROJECT_NAME})
|
install(TARGETS ${PROJECT_NAME})
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ if(BUILD_PUGIXML)
|
|||||||
FetchContent_Declare(
|
FetchContent_Declare(
|
||||||
pugixml
|
pugixml
|
||||||
GIT_REPOSITORY https://github.com/zeux/pugixml.git
|
GIT_REPOSITORY https://github.com/zeux/pugixml.git
|
||||||
GIT_TAG v1.11.4)
|
GIT_TAG v1.15)
|
||||||
|
|
||||||
FetchContent_MakeAvailable(pugixml)
|
FetchContent_MakeAvailable(pugixml)
|
||||||
else()
|
else()
|
||||||
|
|||||||
@@ -107,6 +107,10 @@ set(QET_RES_FILES
|
|||||||
${QET_DIR}/sources/ui/configpage/generalconfigurationpage.ui
|
${QET_DIR}/sources/ui/configpage/generalconfigurationpage.ui
|
||||||
)
|
)
|
||||||
set(QET_SRC_FILES
|
set(QET_SRC_FILES
|
||||||
|
${QET_DIR}/sources/cli_export.cpp
|
||||||
|
${QET_DIR}/sources/cli_export.h
|
||||||
|
${QET_DIR}/sources/pdf_links.cpp
|
||||||
|
${QET_DIR}/sources/pdf_links.h
|
||||||
${QET_DIR}/sources/borderproperties.cpp
|
${QET_DIR}/sources/borderproperties.cpp
|
||||||
${QET_DIR}/sources/borderproperties.h
|
${QET_DIR}/sources/borderproperties.h
|
||||||
${QET_DIR}/sources/bordertitleblock.cpp
|
${QET_DIR}/sources/bordertitleblock.cpp
|
||||||
@@ -500,6 +504,8 @@ set(QET_SRC_FILES
|
|||||||
${QET_DIR}/sources/SearchAndReplace/ui/replacefoliowidget.h
|
${QET_DIR}/sources/SearchAndReplace/ui/replacefoliowidget.h
|
||||||
${QET_DIR}/sources/SearchAndReplace/ui/searchandreplacewidget.cpp
|
${QET_DIR}/sources/SearchAndReplace/ui/searchandreplacewidget.cpp
|
||||||
${QET_DIR}/sources/SearchAndReplace/ui/searchandreplacewidget.h
|
${QET_DIR}/sources/SearchAndReplace/ui/searchandreplacewidget.h
|
||||||
|
${QET_DIR}/sources/svg/qetsvg.cpp
|
||||||
|
${QET_DIR}/sources/svg/qetsvg.h
|
||||||
|
|
||||||
${QET_DIR}/sources/titleblock/dimension.cpp
|
${QET_DIR}/sources/titleblock/dimension.cpp
|
||||||
${QET_DIR}/sources/titleblock/dimension.h
|
${QET_DIR}/sources/titleblock/dimension.h
|
||||||
@@ -714,6 +720,8 @@ set(QET_SRC_FILES
|
|||||||
|
|
||||||
${QET_DIR}/sources/xml/terminalstripitemxml.cpp
|
${QET_DIR}/sources/xml/terminalstripitemxml.cpp
|
||||||
${QET_DIR}/sources/xml/terminalstripitemxml.h
|
${QET_DIR}/sources/xml/terminalstripitemxml.h
|
||||||
|
${QET_DIR}/sources/xml/terminalstriplayoutpatternxml.cpp
|
||||||
|
${QET_DIR}/sources/xml/terminalstriplayoutpatternxml.h
|
||||||
)
|
)
|
||||||
|
|
||||||
set(TS_FILES
|
set(TS_FILES
|
||||||
|
|||||||
+1
-1
Submodule elements updated: f6a422ab00...3aab395fc4
Binary file not shown.
+1266
-1237
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,733 @@
|
|||||||
|
/*
|
||||||
|
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 "wiringlistexport.h"
|
||||||
|
|
||||||
|
// Private Qt PDF engine for drawHyperlink() — see pdf_links / projectprintwindow.
|
||||||
|
#include <private/qpdf_p.h>
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QDirIterator>
|
||||||
|
#include <QDomDocument>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QMap>
|
||||||
|
#include <QPageLayout>
|
||||||
|
#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"},
|
||||||
|
};
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // 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);
|
||||||
|
return exportImages(project, format, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace CLIExport
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
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>
|
||||||
|
|
||||||
|
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).
|
||||||
|
*/
|
||||||
|
int run(const QStringList &args);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // CLI_EXPORT_H
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
#include "titleblock/templatedeleter.h"
|
#include "titleblock/templatedeleter.h"
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include "element.h"
|
#include "qetgraphicsitem/element.h"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
When the ENABLE_PANEL_WIDGET_DND_CHECKS flag is set, the panel
|
When the ENABLE_PANEL_WIDGET_DND_CHECKS flag is set, the panel
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
#include "cli_export.h"
|
||||||
#include "machine_info.h"
|
#include "machine_info.h"
|
||||||
#include "qet.h"
|
#include "qet.h"
|
||||||
#include "qetapp.h"
|
#include "qetapp.h"
|
||||||
@@ -22,6 +23,8 @@
|
|||||||
#include "utils/macosxopenevent.h"
|
#include "utils/macosxopenevent.h"
|
||||||
#include "utils/qetsettings.h"
|
#include "utils/qetsettings.h"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
|
||||||
#include <QStyleFactory>
|
#include <QStyleFactory>
|
||||||
#include <QtConcurrentRun>
|
#include <QtConcurrentRun>
|
||||||
|
|
||||||
@@ -194,6 +197,19 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto
|
|||||||
#endif
|
#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);
|
||||||
|
return CLIExport::run(export_app.arguments());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SingleApplication app(argc, argv, true);
|
SingleApplication app(argc, argv, true);
|
||||||
#ifdef Q_OS_MACOS
|
#ifdef Q_OS_MACOS
|
||||||
//Handle the opening of QET when user double click on a .qet .elmt .tbt file
|
//Handle the opening of QET when user double click on a .qet .elmt .tbt file
|
||||||
|
|||||||
@@ -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 "projectprintwindow.h"
|
||||||
|
|
||||||
#include "../diagram.h"
|
#include "../diagram.h"
|
||||||
|
#include "../pdf_links.h"
|
||||||
#include "../qeticons.h"
|
#include "../qeticons.h"
|
||||||
#include "../qetproject.h"
|
#include "../qetproject.h"
|
||||||
#include "../qetversion.h"
|
#include "../qetversion.h"
|
||||||
@@ -200,241 +201,6 @@ ProjectPrintWindow::~ProjectPrintWindow()
|
|||||||
* @brief ProjectPrintWindow::requestPaint
|
* @brief ProjectPrintWindow::requestPaint
|
||||||
* @param slot called when m_preview emit paintRequested
|
* @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()
|
void ProjectPrintWindow::requestPaint()
|
||||||
{
|
{
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
|
||||||
@@ -642,123 +408,17 @@ void ProjectPrintWindow::printDiagram(Diagram *diagram, bool fit_page, QPainter
|
|||||||
return QPointF(pt_scale * dx, fullH_pt - pt_scale * dy);
|
return QPointF(pt_scale * dx, fullH_pt - pt_scale * dy);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Compute, in PDF points on its OWN page, the rectangle to frame for a
|
PdfLinks::PageGeometry geom;
|
||||||
// target element (used as a /FitR destination so the link zooms onto it).
|
geom.sceneToDevice = fit;
|
||||||
auto destRectPdf = [&](Element *tgt) -> QRectF {
|
geom.target = target;
|
||||||
Diagram *dg = tgt ? tgt->diagram() : nullptr;
|
geom.pageBounds = pageBounds;
|
||||||
if (!dg) return QRectF();
|
geom.devToPdf = devToPdf;
|
||||||
const QRectF srcT = QRectF(diagramRect(dg, exportProperties()));
|
geom.sourceRectOf = [this](Diagram *dg) {
|
||||||
if (srcT.width() <= 0.0 || srcT.height() <= 0.0) return QRectF();
|
return QRectF(diagramRect(dg, exportProperties()));
|
||||||
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::injectCrossRefLinks(
|
||||||
auto injectLink = [&](const QRectF &sceneRect, Element *targetElmt) {
|
pdfEngine, diagram, geom, diagramPageMap,
|
||||||
if (!targetElmt || !targetElmt->diagram()) return;
|
printer->outputFileName());
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
////PDF links end////
|
////PDF links end////
|
||||||
@@ -1235,7 +895,7 @@ void ProjectPrintWindow::print()
|
|||||||
QTimer::singleShot(0, this, [this, pdfFile]() {
|
QTimer::singleShot(0, this, [this, pdfFile]() {
|
||||||
// Convert URI link annotations into native internal GoTo/FitR
|
// Convert URI link annotations into native internal GoTo/FitR
|
||||||
// actions so cross-references jump inside the document.
|
// actions so cross-references jump inside the document.
|
||||||
pdfConvertUriToGoTo(pdfFile);
|
PdfLinks::convertUriToGoTo(pdfFile);
|
||||||
this->close();
|
this->close();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -961,22 +961,21 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in
|
|||||||
painter.drawPolyline(p2, 3);
|
painter.drawPolyline(p2, 3);
|
||||||
|
|
||||||
// Draw terminal names for switch contact (3 terminals)
|
// Draw terminal names for switch contact (3 terminals)
|
||||||
// terminal_names[0] = NO side (top left)
|
// terminal_names[0] = NC (bottom-left)
|
||||||
// terminal_names[1] = NC side (bottom left)
|
// terminal_names[1] = NO (top-left)
|
||||||
// terminal_names[2] = common side (right)
|
// terminal_names[2] = Common (right)
|
||||||
if (!terminal_names.isEmpty() && m_properties.showTerminalName()) {
|
if (!terminal_names.isEmpty() && m_properties.showTerminalName()) {
|
||||||
painter.setFont(QETApp::diagramTextsFont(4));
|
painter.setFont(QETApp::diagramTextsFont(4));
|
||||||
// Sort order from parseTerminal (top->bottom, left->right):
|
// Storage order set above: [0]=NC, [1]=NO, [2]=Common
|
||||||
// [0]=12 (NO, top-left), [1]=14 (common, top-center), [2]=13 (NC, bottom-center)
|
|
||||||
if (terminal_names.size() >= 1)
|
|
||||||
painter.drawText(QRectF(0, offset, 8, 8),
|
|
||||||
Qt::AlignLeft|Qt::AlignTop, terminal_names[1]); // 12 NO left
|
|
||||||
if (terminal_names.size() >= 2)
|
if (terminal_names.size() >= 2)
|
||||||
painter.drawText(QRectF(16, offset+4, 8, 6),
|
painter.drawText(QRectF(0, offset, 8, 8),
|
||||||
Qt::AlignRight|Qt::AlignTop, terminal_names[2]); // 14 common right
|
Qt::AlignLeft|Qt::AlignTop, terminal_names[1]); // NO top-left
|
||||||
if (terminal_names.size() >= 3)
|
if (terminal_names.size() >= 3)
|
||||||
|
painter.drawText(QRectF(16, offset+4, 8, 6),
|
||||||
|
Qt::AlignRight|Qt::AlignTop, terminal_names[2]); // Common right
|
||||||
|
if (terminal_names.size() >= 1)
|
||||||
painter.drawText(QRectF(0, offset+9, 8, 6),
|
painter.drawText(QRectF(0, offset+9, 8, 6),
|
||||||
Qt::AlignLeft|Qt::AlignTop, terminal_names[0]); // 13 NC left-bottom
|
Qt::AlignLeft|Qt::AlignTop, terminal_names[0]); // NC bottom-left
|
||||||
painter.setFont(QETApp::diagramTextsFont(5));
|
painter.setFont(QETApp::diagramTextsFont(5));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -273,9 +273,15 @@ void ElementInfoWidget::updateUi()
|
|||||||
}
|
}
|
||||||
// Load the lock status for auto numbering
|
// Load the lock status for auto numbering
|
||||||
if (m_element->elementData().m_type == ElementData::Terminal) {
|
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
|
// English: Load the BOM exclusion status from the element information mapping
|
||||||
if (m_exclude_from_bom_cb) {
|
if (m_exclude_from_bom_cb) {
|
||||||
QString exclude_bom_value = element_info.value(QStringLiteral("exclude_from_bom")).toString();
|
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))
|
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())
|
if (!eipw->text().isEmpty())
|
||||||
{
|
{
|
||||||
QString txt{eipw->text()};
|
QString txt{eipw->text()};
|
||||||
//remove line feed and carriage return
|
//remove line feed and carriage return
|
||||||
txt.remove(QStringLiteral("\r"));
|
txt.remove(QStringLiteral("\r"));
|
||||||
txt.remove(QStringLiteral("\n"));
|
txt.remove(QStringLiteral("\n"));
|
||||||
info_.addValue(eipw->key(), txt);
|
info_.addValue(eipw->key(), txt);
|
||||||
@@ -311,12 +316,16 @@ DiagramContext ElementInfoWidget::currentInfo() const
|
|||||||
|
|
||||||
// Save the auto numbering lock status
|
// Save the auto numbering lock status
|
||||||
if (m_element->elementData().m_type == ElementData::Terminal) {
|
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) {
|
if (m_exclude_from_bom_cb) {
|
||||||
info_.addValue(QStringLiteral("exclude_from_bom"), m_exclude_from_bom_cb->isChecked() ? QStringLiteral("true") : QStringLiteral("false"));
|
info_.addValue(QStringLiteral("exclude_from_bom"), m_exclude_from_bom_cb->isChecked() ? QStringLiteral("true") : QStringLiteral("false"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return info_;
|
return info_;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -151,13 +151,39 @@ void WiringListExport::toCsv()
|
|||||||
{
|
{
|
||||||
if (!m_project) return;
|
if (!m_project) return;
|
||||||
|
|
||||||
QDomDocument doc = m_project->toXml();
|
const QString csv = toCsvString();
|
||||||
|
if (csv.isEmpty()) {
|
||||||
if (doc.isNull()) {
|
|
||||||
QMessageBox::warning(m_parent, tr("Erreur"), tr("Impossible de lire la structure en mémoire du projet."));
|
QMessageBox::warning(m_parent, tr("Erreur"), tr("Impossible de lire la structure en mémoire du projet."));
|
||||||
return;
|
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;
|
QSet<QString> conductorDefinitionTypes;
|
||||||
QDomElement rootElem = doc.documentElement();
|
QDomElement rootElem = doc.documentElement();
|
||||||
QDomElement collection = rootElem.firstChildElement("collection");
|
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());
|
QMap<QString, ElementInfo> elementsInfo = collectElementsInfo(doc.documentElement());
|
||||||
QList<ConductorData> conductors = collectConductors(doc.documentElement());
|
QList<ConductorData> conductors = collectConductors(doc.documentElement());
|
||||||
|
|
||||||
@@ -353,7 +364,8 @@ void WiringListExport::toCsv()
|
|||||||
return a.terminalname2 < b.terminalname2;
|
return a.terminalname2 < b.terminalname2;
|
||||||
});
|
});
|
||||||
|
|
||||||
QTextStream out(&file);
|
QString csv;
|
||||||
|
QTextStream out(&csv);
|
||||||
out << tr("Page", "Wiring list CSV header") << ";"
|
out << tr("Page", "Wiring list CSV header") << ";"
|
||||||
<< tr("Composant 1", "Wiring list CSV header") << ";"
|
<< tr("Composant 1", "Wiring list CSV header") << ";"
|
||||||
<< tr("Borne 1", "Wiring list CSV header") << ";"
|
<< tr("Borne 1", "Wiring list CSV header") << ";"
|
||||||
@@ -376,6 +388,5 @@ void WiringListExport::toCsv()
|
|||||||
<< c.function << "\n";
|
<< c.function << "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
file.close();
|
return csv;
|
||||||
QMessageBox::information(m_parent, tr("Export réussi"), tr("Le plan de câblage a été exporté avec succès !"));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ class WiringListExport : public QObject
|
|||||||
public:
|
public:
|
||||||
explicit WiringListExport(QETProject *project, QWidget *parent = nullptr);
|
explicit WiringListExport(QETProject *project, QWidget *parent = nullptr);
|
||||||
void toCsv();
|
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:
|
private:
|
||||||
QETProject *m_project;
|
QETProject *m_project;
|
||||||
|
|||||||
Reference in New Issue
Block a user