mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-06 11:14:14 +02:00
Merge remote-tracking branch 'upstream/master' into feature/live-remember
# Conflicts: # lang/qet_en.ts
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"
|
||||
@@ -49,6 +50,13 @@
|
||||
#include <QVBoxLayout>
|
||||
#include <QCheckBox>
|
||||
#include <QDialog>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QMdiSubWindow>
|
||||
#include <QPageLayout>
|
||||
#include <QPainter>
|
||||
#include <QPrinterInfo>
|
||||
#include <QPrinter>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include "../projectview.h"
|
||||
@@ -263,6 +271,22 @@ void LiveServer::handle(const QJsonObject &request)
|
||||
answer = runStored(request.value(QStringLiteral("script")).toString());
|
||||
} else if (cmd == QLatin1String("command")) {
|
||||
answer = command(request.value(QStringLiteral("action")).toString());
|
||||
} else if (cmd == QLatin1String("new_project")) {
|
||||
answer = newProject(request);
|
||||
} else if (cmd == QLatin1String("open_project")) {
|
||||
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")) {
|
||||
@@ -325,6 +349,7 @@ QJsonObject LiveServer::status()
|
||||
{QStringLiteral("name"), s.header.name}});
|
||||
answer.insert(QStringLiteral("stored_scripts"), scripts);
|
||||
//So an assistant can notice a recording the user just made
|
||||
answer.insert(QStringLiteral("projects"), openProjects());
|
||||
answer.insert(QStringLiteral("macro_recorder"), QJsonObject{
|
||||
{QStringLiteral("recording"), MacroRecorder::instance().isRecording()},
|
||||
{QStringLiteral("steps"), MacroRecorder::instance().stepCount()},
|
||||
@@ -454,6 +479,376 @@ QJsonObject LiveServer::showFolio(int folio)
|
||||
return {{QStringLiteral("ok"), true}, {QStringLiteral("folio"), folio}};
|
||||
}
|
||||
|
||||
/**
|
||||
@brief LiveServer::newProject
|
||||
A new project, as File > New makes it: the new-folio defaults of this
|
||||
QElectroTech, opened and made the current project, so the next script
|
||||
works on it. Optional: "title", "folios" (how many empty folios, 1 to
|
||||
100, default 1), and "path" to save it to at once -- never over an
|
||||
existing file, so an assistant cannot replace the user's work.
|
||||
*/
|
||||
QJsonObject LiveServer::newProject(const QJsonObject &request)
|
||||
{
|
||||
QETDiagramEditor *e = editor();
|
||||
if (!e) return failure(QStringLiteral("no QElectroTech editor window is open"));
|
||||
|
||||
const int folios = request.value(QStringLiteral("folios")).toInt(1);
|
||||
if (folios < 1 || folios > 100)
|
||||
return failure(QStringLiteral("folios must be between 1 and 100, not %1").arg(folios));
|
||||
const QString path = request.value(QStringLiteral("path")).toString().trimmed();
|
||||
if (!path.isEmpty()) {
|
||||
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; a new project is never "
|
||||
"saved over a file").arg(path));
|
||||
if (!info.dir().exists())
|
||||
return failure(QStringLiteral("the folder %1 does not exist").arg(info.absolutePath()));
|
||||
}
|
||||
|
||||
auto project = new QETProject(e);
|
||||
for (int i = 0; i < folios; ++i) project->addNewDiagram();
|
||||
const QString title = request.value(QStringLiteral("title")).toString().trimmed();
|
||||
if (!title.isEmpty()) project->setTitle(title);
|
||||
if (!e->addProject(project)) return failure(QStringLiteral("QElectroTech refused the new project"));
|
||||
|
||||
//Current at once, whether or not QElectroTech is the active
|
||||
//application: the next request runs on it (see editor())
|
||||
for (ProjectView *pv : e->openedProjects()) {
|
||||
if (pv->project() == project) {
|
||||
for (QMdiSubWindow *w : e->m_workspace.subWindowList())
|
||||
if (w->widget() == pv) e->m_workspace.setActiveSubWindow(w);
|
||||
}
|
||||
}
|
||||
|
||||
if (!path.isEmpty()) {
|
||||
project->setFilePath(path);
|
||||
const QETResult result = project->write();
|
||||
if (!result.isOk())
|
||||
return failure(QStringLiteral("the project was created but not saved to %1: %2")
|
||||
.arg(path, result.errorMessage()));
|
||||
}
|
||||
|
||||
QJsonObject answer = status();
|
||||
answer.insert(QStringLiteral("created"), true);
|
||||
return answer;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief LiveServer::openProjects
|
||||
The projects open in the editor window, in tab order: the index
|
||||
switch_project takes, title, file, folios, unsaved changes, and which
|
||||
one is current.
|
||||
*/
|
||||
QJsonArray LiveServer::openProjects() const
|
||||
{
|
||||
QJsonArray list;
|
||||
QETDiagramEditor *e = editor();
|
||||
if (!e) return list;
|
||||
const QETProject *current = e->currentProject();
|
||||
const QList<ProjectView *> views = e->openedProjects();
|
||||
for (int i = 0; i < views.count(); ++i) {
|
||||
QETProject *p = views.at(i)->project();
|
||||
list.append(QJsonObject{
|
||||
{QStringLiteral("index"), i},
|
||||
{QStringLiteral("title"), p->title()},
|
||||
{QStringLiteral("path"), p->filePath()},
|
||||
{QStringLiteral("folios"), int(p->diagrams().count())},
|
||||
{QStringLiteral("modified"), p->projectWasModified()},
|
||||
{QStringLiteral("read_only"), p->isReadOnly()},
|
||||
{QStringLiteral("current"), p == current}});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief LiveServer::openProject
|
||||
Open a saved project and make it current, as File > Open does but with
|
||||
no file dialog and no error windows: what is wrong comes back in the
|
||||
answer. A project already open is only made current.
|
||||
*/
|
||||
QJsonObject LiveServer::openProject(const QString &raw_path)
|
||||
{
|
||||
QETDiagramEditor *e = editor();
|
||||
if (!e) return failure(QStringLiteral("no QElectroTech editor window is open"));
|
||||
const QString path = raw_path.trimmed();
|
||||
if (path.isEmpty()) return failure(QStringLiteral("no path given"));
|
||||
const QFileInfo info(path);
|
||||
if (info.isRelative()) return failure(QStringLiteral("path must be absolute: %1").arg(path));
|
||||
if (!info.isFile()) return failure(QStringLiteral("%1 does not exist").arg(path));
|
||||
if (!info.isReadable()) return failure(QStringLiteral("%1 cannot be read (permissions)").arg(path));
|
||||
|
||||
QJsonObject extra{{QStringLiteral("opened"), true}};
|
||||
if (ProjectView *pv = e->viewForFile(info.absoluteFilePath())) {
|
||||
e->activateProject(pv);
|
||||
extra = {{QStringLiteral("opened"), false}, {QStringLiteral("already_open"), true}};
|
||||
} else if (QETApp::diagramEditorForFile(info.absoluteFilePath())) {
|
||||
return failure(QStringLiteral("%1 is open in another QElectroTech window; "
|
||||
"switch to it there").arg(path));
|
||||
} else {
|
||||
if (!e->openAndAddProject(info.absoluteFilePath(), false))
|
||||
return failure(QStringLiteral("QElectroTech could not open %1 (not a "
|
||||
"QElectroTech project, or a newer format)").arg(path));
|
||||
if (ProjectView *pv = e->viewForFile(info.absoluteFilePath())) e->activateProject(pv);
|
||||
}
|
||||
QJsonObject answer = status();
|
||||
for (auto it = extra.begin(); it != extra.end(); ++it) answer.insert(it.key(), it.value());
|
||||
return answer;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief LiveServer::switchProject
|
||||
Make another open project current, by its index in "projects" (status)
|
||||
or by its file path.
|
||||
*/
|
||||
QJsonObject LiveServer::switchProject(const QJsonObject &request)
|
||||
{
|
||||
QETDiagramEditor *e = editor();
|
||||
if (!e) return failure(QStringLiteral("no QElectroTech editor window is open"));
|
||||
const QList<ProjectView *> views = e->openedProjects();
|
||||
ProjectView *target = 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()));
|
||||
target = views.at(index);
|
||||
} else {
|
||||
const QString path = request.value(QStringLiteral("path")).toString().trimmed();
|
||||
if (path.isEmpty()) return failure(QStringLiteral("give \"index\" (from \"projects\") or \"path\""));
|
||||
target = e->viewForFile(QFileInfo(path).absoluteFilePath());
|
||||
if (!target) return failure(QStringLiteral("%1 is not open; open_project opens it").arg(path));
|
||||
}
|
||||
e->activateProject(target);
|
||||
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();
|
||||
//96 dpi, as the PDF export draws: symbols are replayed at the
|
||||
//device's resolution, so at a printer's 600-1200 dpi they came out
|
||||
//many times too big next to the wires (seen on a test print)
|
||||
printer.setResolution(96);
|
||||
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