mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-06 11:14:14 +02:00
Live mode: print, save/close, changes, snapshot; house-style layout rules; saved symbol index
QElectroTech (LiveServer): - print: folios to the default (or a named) printer with no print dialog, always after asking the user (paper cannot be undone); or to a new PDF file. Rendered as the PDF export draws a folio (no grid, no terminals). - save_project (own file, or save as a new file, never over one) and close_project (only without unsaved changes: never discards work). - changes: the undo history, each step marked assistant or user, with "since" to ask what changed after a point. - snapshot: a copy of the open project as on screen, for a check to read, without saving it or changing its file. - qet.elementTextGeometry(): where a symbol's text is drawn on the folio. qet MCP server: - qet_live_print, qet_live_save_project, qet_live_close_project, qet_live_changes, qet_live_layout_check (snapshot + qet_layout_check, no "run this script?" window); write paths never overwrite. - Layout rules from the house style, for file and live checks alike: label_on_wire, four_way_junction, misaligned_branch. - The element index is saved under ~/.cache/qet-mcp: a new server process loads 8839 symbols in 0.4 s instead of parsing them for 6.9 s; the change check is a third faster. - qet_about lists the installation's standard symbols (standard-symbols.json in QElectroTech's data folder) when there are any. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
#include "scriptlibrary.h"
|
||||
#include "../diagram.h"
|
||||
#include "../diagramview.h"
|
||||
#include "../qet.h"
|
||||
#include "../qetapp.h"
|
||||
#include "../qetdiagrameditor.h"
|
||||
#include "../qetproject.h"
|
||||
@@ -51,6 +52,10 @@
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QMdiSubWindow>
|
||||
#include <QPageLayout>
|
||||
#include <QPainter>
|
||||
#include <QPrinterInfo>
|
||||
#include <QPrinter>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include "../projectview.h"
|
||||
@@ -260,6 +265,16 @@ void LiveServer::handle(const QJsonObject &request)
|
||||
answer = openProject(request.value(QStringLiteral("path")).toString());
|
||||
} else if (cmd == QLatin1String("switch_project")) {
|
||||
answer = switchProject(request);
|
||||
} else if (cmd == QLatin1String("save_project")) {
|
||||
answer = saveProject(request);
|
||||
} else if (cmd == QLatin1String("close_project")) {
|
||||
answer = closeProject(request);
|
||||
} else if (cmd == QLatin1String("print")) {
|
||||
answer = print(request);
|
||||
} else if (cmd == QLatin1String("snapshot")) {
|
||||
answer = snapshot(request.value(QStringLiteral("path")).toString());
|
||||
} else if (cmd == QLatin1String("changes")) {
|
||||
answer = changes(request);
|
||||
} else if (cmd == QLatin1String("show_folio")) {
|
||||
answer = showFolio(request.value(QStringLiteral("folio")).toInt(-1));
|
||||
} else if (cmd == QLatin1String("undo_last")) {
|
||||
@@ -596,6 +611,228 @@ QJsonObject LiveServer::switchProject(const QJsonObject &request)
|
||||
return status();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief LiveServer::saveProject
|
||||
Save the current project to its own file, or, with "path", to a new
|
||||
file that becomes its file (Save As) -- never over an existing one.
|
||||
*/
|
||||
QJsonObject LiveServer::saveProject(const QJsonObject &request)
|
||||
{
|
||||
QETDiagramEditor *e = editor();
|
||||
QETProject *project = e ? e->currentProject() : nullptr;
|
||||
if (!project) return failure(QStringLiteral("no project is open in QElectroTech"));
|
||||
if (project->isReadOnly()) return failure(QStringLiteral("the project is read-only"));
|
||||
|
||||
const QString path = request.value(QStringLiteral("path")).toString().trimmed();
|
||||
if (path.isEmpty()) {
|
||||
if (project->filePath().isEmpty())
|
||||
return failure(QStringLiteral("the project has no file yet: give \"path\" to save it as one"));
|
||||
} else {
|
||||
const QFileInfo info(path);
|
||||
if (info.isRelative()) return failure(QStringLiteral("path must be absolute: %1").arg(path));
|
||||
if (info.exists())
|
||||
return failure(QStringLiteral("%1 already exists; save as never replaces a file").arg(path));
|
||||
if (!info.dir().exists())
|
||||
return failure(QStringLiteral("the folder %1 does not exist").arg(info.absolutePath()));
|
||||
project->setFilePath(info.absoluteFilePath());
|
||||
}
|
||||
const QETResult result = project->write();
|
||||
if (!result.isOk()) return failure(QStringLiteral("not saved: %1").arg(result.errorMessage()));
|
||||
QJsonObject answer = status();
|
||||
answer.insert(QStringLiteral("saved"), project->filePath());
|
||||
return answer;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief LiveServer::closeProject
|
||||
Close an open project (the current one, or "index"), only when it has
|
||||
no unsaved changes: closing never discards the user's work, and never
|
||||
asks them a question on the assistant's behalf.
|
||||
*/
|
||||
QJsonObject LiveServer::closeProject(const QJsonObject &request)
|
||||
{
|
||||
QETDiagramEditor *e = editor();
|
||||
if (!e) return failure(QStringLiteral("no QElectroTech editor window is open"));
|
||||
const QList<ProjectView *> views = e->openedProjects();
|
||||
ProjectView *pv = nullptr;
|
||||
if (request.contains(QStringLiteral("index"))) {
|
||||
const int index = request.value(QStringLiteral("index")).toInt(-1);
|
||||
if (index < 0 || index >= views.count())
|
||||
return failure(QStringLiteral("no open project %1: %2 are open, counted from 0")
|
||||
.arg(index).arg(views.count()));
|
||||
pv = views.at(index);
|
||||
} else {
|
||||
pv = e->currentProjectView();
|
||||
if (!pv) return failure(QStringLiteral("no project is open in QElectroTech"));
|
||||
}
|
||||
QETProject *project = pv->project();
|
||||
if (project->projectWasModified())
|
||||
return failure(QStringLiteral("\"%1\" has unsaved changes; save it first "
|
||||
"(save_project) -- closing never discards them")
|
||||
.arg(project->title()));
|
||||
const QString title = project->title();
|
||||
if (!e->closeProject(pv)) return failure(QStringLiteral("QElectroTech did not close \"%1\"").arg(title));
|
||||
QJsonObject answer = status();
|
||||
answer.insert(QStringLiteral("closed"), title);
|
||||
return answer;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief LiveServer::print
|
||||
Print folios of the current project with no print dialog: "folios" is
|
||||
"all" (default), "current" or a list of indexes; "printer" names one,
|
||||
else the system's default printer. Paper cannot be taken back, so the
|
||||
user is always asked first, whatever "always" says for scripts.
|
||||
"output_file" prints to a new PDF file instead (no paper, so no
|
||||
question; never over an existing file).
|
||||
*/
|
||||
QJsonObject LiveServer::print(const QJsonObject &request)
|
||||
{
|
||||
QETDiagramEditor *e = editor();
|
||||
ProjectView *pv = e ? e->currentProjectView() : nullptr;
|
||||
if (!pv) return failure(QStringLiteral("no project is open in QElectroTech"));
|
||||
QETProject *project = pv->project();
|
||||
const QList<Diagram *> all = project->diagrams();
|
||||
|
||||
QList<Diagram *> folios;
|
||||
const QJsonValue which = request.value(QStringLiteral("folios"));
|
||||
if (which.isUndefined() || which.toString() == QLatin1String("all")) {
|
||||
folios = all;
|
||||
} else if (which.toString() == QLatin1String("current")) {
|
||||
if (DiagramView *dv = e->currentDiagramView()) folios << dv->diagram();
|
||||
} else if (which.isArray()) {
|
||||
for (const QJsonValue &v : which.toArray()) {
|
||||
const int i = v.toInt(-1);
|
||||
if (i < 0 || i >= all.count())
|
||||
return failure(QStringLiteral("no folio %1: the project has %2, counted from 0")
|
||||
.arg(v.toVariant().toString()).arg(all.count()));
|
||||
folios << all.at(i);
|
||||
}
|
||||
} else {
|
||||
return failure(QStringLiteral("folios must be \"all\", \"current\" or a list of indexes"));
|
||||
}
|
||||
if (folios.isEmpty()) return failure(QStringLiteral("no folio to print"));
|
||||
|
||||
const QString file = request.value(QStringLiteral("output_file")).toString().trimmed();
|
||||
QPrinterInfo info;
|
||||
if (file.isEmpty()) {
|
||||
const QString name = request.value(QStringLiteral("printer")).toString().trimmed();
|
||||
info = name.isEmpty() ? QPrinterInfo::defaultPrinter() : QPrinterInfo::printerInfo(name);
|
||||
if (info.isNull())
|
||||
return failure(name.isEmpty()
|
||||
? QStringLiteral("this computer has no default printer")
|
||||
: QStringLiteral("no printer called \"%1\" (printers: %2)")
|
||||
.arg(name, QPrinterInfo::availablePrinterNames().join(QStringLiteral(", "))));
|
||||
QMessageBox box(QMessageBox::Question, tr("Impression"),
|
||||
tr("L'assistant veut imprimer %n folio(s) de « %1 » sur « %2 ».", "", int(folios.count()))
|
||||
.arg(project->title(), info.printerName()),
|
||||
QMessageBox::NoButton, e);
|
||||
QPushButton *go = box.addButton(tr("&Imprimer"), QMessageBox::AcceptRole);
|
||||
box.addButton(tr("&Annuler"), QMessageBox::RejectRole);
|
||||
box.setDefaultButton(go);
|
||||
box.exec();
|
||||
if (box.clickedButton() != go) return failure(QStringLiteral("refused by the user"));
|
||||
} else {
|
||||
const QFileInfo fi(file);
|
||||
if (fi.isRelative()) return failure(QStringLiteral("output_file must be absolute: %1").arg(file));
|
||||
if (fi.exists()) return failure(QStringLiteral("%1 already exists; printing never replaces a file").arg(file));
|
||||
}
|
||||
|
||||
QPrinter printer = file.isEmpty() ? QPrinter(info, QPrinter::HighResolution)
|
||||
: QPrinter(QPrinter::HighResolution);
|
||||
if (!file.isEmpty()) {
|
||||
printer.setOutputFormat(QPrinter::PdfFormat);
|
||||
printer.setOutputFileName(file);
|
||||
}
|
||||
printer.setDocName(project->title().isEmpty() ? QStringLiteral("QElectroTech") : project->title());
|
||||
const auto folioRect = [](Diagram *d) {
|
||||
QRectF r = d->border_and_titleblock.borderAndTitleBlockRect();
|
||||
r.adjust(0, 0, 1, 1);
|
||||
return r.toAlignedRect();
|
||||
};
|
||||
//One orientation for the job: some printer drivers ignore a change
|
||||
//between pages. The first folio decides.
|
||||
const QRect first = folioRect(folios.first());
|
||||
printer.setPageOrientation(first.width() > first.height() ? QPageLayout::Landscape
|
||||
: QPageLayout::Portrait);
|
||||
QPainter painter;
|
||||
if (!painter.begin(&printer)) return failure(QStringLiteral("the printer could not be opened"));
|
||||
for (int i = 0; i < folios.count(); ++i) {
|
||||
Diagram *d = folios.at(i);
|
||||
if (i) printer.newPage();
|
||||
//As the PDF export draws a folio: no grid, guides or terminals
|
||||
const bool grid = d->displayGrid(), guides = d->displayGuides();
|
||||
const bool terms = d->drawTerminals(), names = d->drawTerminalNames();
|
||||
d->setDisplayGrid(false); d->setDisplayGuides(false);
|
||||
d->setDrawTerminals(false); d->setDrawTerminalNames(false);
|
||||
d->render(&painter, printer.pageLayout().paintRectPixels(printer.resolution()),
|
||||
folioRect(d), Qt::KeepAspectRatio);
|
||||
d->setDisplayGrid(grid); d->setDisplayGuides(guides);
|
||||
d->setDrawTerminals(terms); d->setDrawTerminalNames(names);
|
||||
}
|
||||
painter.end();
|
||||
return {{QStringLiteral("ok"), true}, {QStringLiteral("printed"), int(folios.count())},
|
||||
{QStringLiteral("to"), file.isEmpty() ? info.printerName() : file}};
|
||||
}
|
||||
|
||||
/**
|
||||
@brief LiveServer::snapshot
|
||||
Write a copy of the current project, as it is on screen (unsaved
|
||||
changes included), to a new file -- for a check to read -- without
|
||||
saving the project or changing its file. Never over an existing file.
|
||||
*/
|
||||
QJsonObject LiveServer::snapshot(const QString &raw_path)
|
||||
{
|
||||
QETDiagramEditor *e = editor();
|
||||
QETProject *project = e ? e->currentProject() : nullptr;
|
||||
if (!project) return failure(QStringLiteral("no project is open in QElectroTech"));
|
||||
const QFileInfo info(raw_path.trimmed());
|
||||
if (raw_path.trimmed().isEmpty() || info.isRelative())
|
||||
return failure(QStringLiteral("path must be absolute"));
|
||||
if (info.exists()) return failure(QStringLiteral("%1 already exists").arg(raw_path));
|
||||
QDomDocument xml(project->toXml());
|
||||
QString error;
|
||||
if (!QET::writeXmlFile(xml, info.absoluteFilePath(), &error))
|
||||
return failure(QStringLiteral("snapshot not written: %1").arg(error));
|
||||
return {{QStringLiteral("ok"), true}, {QStringLiteral("path"), info.absoluteFilePath()},
|
||||
{QStringLiteral("folio"), e->currentDiagramView()
|
||||
? int(project->diagrams().indexOf(e->currentDiagramView()->diagram())) : -1}};
|
||||
}
|
||||
|
||||
/**
|
||||
@brief LiveServer::changes
|
||||
The current project's undo history: each step's name, whether the
|
||||
assistant made it, and whether it is undone. "since" (an index from an
|
||||
earlier answer) keeps only the steps after it, so an assistant can say
|
||||
what it changed since a point. Changes nothing.
|
||||
*/
|
||||
QJsonObject LiveServer::changes(const QJsonObject &request)
|
||||
{
|
||||
QETDiagramEditor *e = editor();
|
||||
QETProject *project = e ? e->currentProject() : nullptr;
|
||||
if (!project) return failure(QStringLiteral("no project is open in QElectroTech"));
|
||||
QUndoStack *stack = project->undoStack();
|
||||
const QString prefix = tr("Assistant : %1").arg(QString());
|
||||
const int since = request.value(QStringLiteral("since")).toInt(-1);
|
||||
QJsonArray steps;
|
||||
int assistant = 0, user = 0;
|
||||
for (int i = qMax(0, since + 1); i < stack->count(); ++i) {
|
||||
const QString text = stack->text(i);
|
||||
const bool mine = text.startsWith(prefix);
|
||||
const bool undone = i >= stack->index();
|
||||
if (!undone) (mine ? assistant : user)++;
|
||||
steps.append(QJsonObject{{QStringLiteral("index"), i}, {QStringLiteral("step"), text},
|
||||
{QStringLiteral("by"), mine ? QStringLiteral("assistant")
|
||||
: QStringLiteral("user")},
|
||||
{QStringLiteral("undone"), undone}});
|
||||
}
|
||||
return {{QStringLiteral("ok"), true}, {QStringLiteral("project"), project->title()},
|
||||
{QStringLiteral("steps"), steps},
|
||||
{QStringLiteral("now"), stack->index() - 1},
|
||||
{QStringLiteral("saved_at"), stack->cleanIndex() - 1},
|
||||
{QStringLiteral("done_by_assistant"), assistant}, {QStringLiteral("done_by_user"), user}};
|
||||
}
|
||||
|
||||
/**
|
||||
@brief LiveServer::undoLast
|
||||
Undo the newest step, only if the assistant made it: what the user did
|
||||
|
||||
Reference in New Issue
Block a user