Merge remote-tracking branch 'upstream/master' into perf/live-timing

# Conflicts:
#	sources/scripting/liveserver.cpp
This commit is contained in:
ispyisail
2026-10-05 21:36:17 +13:00
15 changed files with 1385 additions and 159 deletions
+6
View File
@@ -61,6 +61,7 @@ XRefProperties::XRefProperties()
m_show_power_ctc = true;
m_show_terminal_name = true;
m_show_all_configured_slaves = false;
m_stack_overlapping = true;
m_display = Cross;
m_snap_to = Bottom;
m_prefix_keys << "power" << "delay" << "switch";
@@ -83,6 +84,7 @@ void XRefProperties::toSettings(QSettings &settings,
settings.setValue(prefix % "showpowerctc", m_show_power_ctc);
settings.setValue(prefix % "showterminalname", m_show_terminal_name);
settings.setValue(prefix % "showallconfiguredslaves", m_show_all_configured_slaves);
settings.setValue(prefix % "stackoverlapping", m_stack_overlapping);
QString display = m_display == Cross? "cross" : "contacts";
settings.setValue(prefix % "displayhas", display);
QString snap = m_snap_to == Bottom? "bottom" : "label";
@@ -117,6 +119,7 @@ void XRefProperties::fromSettings(const QSettings &settings,
m_show_power_ctc = settings.value(prefix % "showpowerctc", true).toBool();
m_show_terminal_name = settings.value(prefix % "showterminalname", true).toBool();
m_show_all_configured_slaves = settings.value(prefix % "showallconfiguredslaves", false).toBool();
m_stack_overlapping = settings.value(prefix % "stackoverlapping", true).toBool();
QString display = settings.value(prefix % "displayhas", "cross").toString();
display == "cross"? m_display = Cross : m_display = Contacts;
QString snap = settings.value(prefix % "snapto", "label").toString();
@@ -148,6 +151,7 @@ QDomElement XRefProperties::toXml(QDomDocument &xml_document) const
xml_element.setAttribute("showpowerctc", m_show_power_ctc? "true" : "false");
xml_element.setAttribute("showterminalname", m_show_terminal_name? "true" : "false");
xml_element.setAttribute("showallconfiguredslaves", m_show_all_configured_slaves? "true" : "false");
xml_element.setAttribute("stackoverlapping", m_stack_overlapping? "true" : "false");
QString display = m_display == Cross? "cross" : "contacts";
xml_element.setAttribute("displayhas", display);
QString snap = m_snap_to == Bottom? "bottom" : "label";
@@ -181,6 +185,7 @@ bool XRefProperties::fromXml(const QDomElement &xml_element) {
m_show_power_ctc = xml_element.attribute("showpowerctc") == "true";
m_show_terminal_name = xml_element.attribute("showterminalname", "true") == "true";
m_show_all_configured_slaves = xml_element.attribute("showallconfiguredslaves", "false") == "true";
m_stack_overlapping = xml_element.attribute("stackoverlapping", "true") == "true";
QString display = xml_element.attribute("displayhas", "cross");
display == "cross"? m_display = Cross : m_display = Contacts;
QString snap = xml_element.attribute("snapto", "label");
@@ -228,6 +233,7 @@ bool XRefProperties::operator ==(const XRefProperties &xrp) const{
return (m_show_power_ctc == xrp.m_show_power_ctc
&& m_show_terminal_name == xrp.m_show_terminal_name
&& m_show_all_configured_slaves == xrp.m_show_all_configured_slaves
&& m_stack_overlapping == xrp.m_stack_overlapping
&& m_display == xrp.m_display
&& m_snap_to == xrp.m_snap_to
&& m_prefix == xrp.m_prefix
+4
View File
@@ -63,6 +63,9 @@ class XRefProperties : public PropertiesInterface
void setShowAllConfiguredSlaves (const bool a) {m_show_all_configured_slaves = a;}
bool showAllConfiguredSlaves () const {return m_show_all_configured_slaves;}
void setStackOverlapping (const bool a) {m_stack_overlapping = a;}
bool stackOverlapping () const {return m_stack_overlapping;}
void setDisplayHas (const DisplayHas dh) {m_display = dh;}
DisplayHas displayHas () const {return m_display;}
@@ -92,6 +95,7 @@ class XRefProperties : public PropertiesInterface
bool m_show_power_ctc;
bool m_show_terminal_name;
bool m_show_all_configured_slaves;
bool m_stack_overlapping;
DisplayHas m_display;
SnapTo m_snap_to;
Qt::AlignmentFlag m_xref_pos;
+3 -1
View File
@@ -1856,8 +1856,10 @@ bool QETDiagramEditor::openAndAddProject(
box.exec();
}
//Not when nobody is there to answer: an assistant opening a
//project in live mode would wait on a window it cannot see
BackupDialog backup_dialog(this);
if (backup_dialog.exec() == QDialog::Accepted)
if (interactive && backup_dialog.exec() == QDialog::Accepted)
{
QString backup_path = filepath_info.absolutePath() + QDir::separator() +
QDateTime::currentDateTime().toString("yyyy-MM-dd-hh-mm") + "_" +
+70 -36
View File
@@ -268,6 +268,7 @@ void CrossRefItem::updateLabel()
m_shape_path = QPainterPath();
prepareGeometryChange();
m_bounding_rect = QRectF();
m_text_rects.clear();
// Build geometry and m_hovered_contacts_map using a QImage-backed
// painter so font metrics match the screen painter in paint().
@@ -343,9 +344,12 @@ void CrossRefItem::autoPos()
@brief CrossRefItem::stackAtBottom
Places every cross reference of this folio that snaps to the bottom.
Each one is centred under its element at the bottom of the folio, and
when two would overlap, the one of the higher element goes above the
other: several coils in one column get their crosses stacked in the
same order as the coils, instead of all on the same spot.
when its texts would be drawn over the texts of another one, the one
of the higher element goes above the other: several coils in one
column get their crosses stacked in the same order as the coils,
instead of all on the same spot. Crosses that only touch, or overlap
without any text over text, stay where they are.
The types whose properties do not ask for stacking are only centred.
*/
void CrossRefItem::stackAtBottom()
{
@@ -374,41 +378,56 @@ void CrossRefItem::stackAtBottom()
return a->m_element->uuid() < b->m_element->uuid();
});
struct Placed { QRectF rect; QList<QRectF> texts; };
const qreal gap = 5;
QList<QRectF> placed;
QList<Placed> placed;
for (CrossRefItem *xref : std::as_const(xrefs)) {
const qreal offset = xref->m_properties.offset();
QGIUtility::centerToBottomDiagram(xref, xref->m_element,
offset <= 40 ? 5 : offset);
if (xref->boundingRect().isEmpty()) continue;
if (xref->boundingRect().isEmpty()
|| !xref->m_properties.stackOverlapping())
continue;
// Move up past every cross already placed that it would overlap.
// A list of contacts has an empty margin in its bounding rect:
// only its content is tested, so side by side lists do not
// count as overlapping.
const bool list = xref->m_properties.displayHas()
== XRefProperties::Contacts;
QRectF rect = xref->sceneBoundingRect();
if (list)
rect.adjust(list_margin_left, 0, -list_margin_right, 0);
const qreal bottom = rect.bottom();
Placed self;
self.rect = xref->sceneBoundingRect();
const QTransform to_scene = xref->sceneTransform();
for (const QRectF &text : std::as_const(xref->m_text_rects))
self.texts << to_scene.mapRect(text);
const qreal bottom = self.rect.bottom();
// Move up past every cross already placed whose texts it would
// draw over. Each move goes strictly up, so this ends.
for (bool moved = true; moved; ) {
moved = false;
for (const QRectF &other : std::as_const(placed))
if (rect.left() < other.right() && other.left() < rect.right()
&& rect.top() < other.bottom() + gap
&& other.top() - gap < rect.bottom()) {
rect.moveBottom(other.top() - gap);
moved = true;
}
for (const Placed &other : std::as_const(placed)) {
const qreal dy = other.rect.top() - gap - self.rect.bottom();
if (dy >= 0 || !textsOverlap(self.texts, other.texts))
continue;
self.rect.translate(0, dy);
for (QRectF &text : self.texts) text.translate(0, dy);
moved = true;
}
}
if (rect.bottom() != bottom)
if (self.rect.bottom() != bottom)
xref->setPos(xref->parentItem()->mapFromScene(
xref->scenePos() + QPointF(0, rect.bottom() - bottom)));
placed << rect;
xref->scenePos() + QPointF(0, self.rect.bottom() - bottom)));
placed << self;
}
}
/**
@brief CrossRefItem::textsOverlap
@return true if one of the rects of a intersects one of the rects of b
*/
bool CrossRefItem::textsOverlap(const QList<QRectF> &a, const QList<QRectF> &b)
{
for (const QRectF &ra : a)
for (const QRectF &rb : b)
if (ra.intersects(rb)) return true;
return false;
}
/**
@brief CrossRefItem::sceneEvent
@param event
@@ -1156,9 +1175,9 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in
painter.setFont(QETApp::diagramTextsFont(4));
QRectF bt(0, offset, 24, 10);
if (terminal_names.size() >= 1)
painter.drawText(bt, Qt::AlignLeft|Qt::AlignTop, terminal_names[0]);
drawText(painter, bt, Qt::AlignLeft|Qt::AlignTop, terminal_names[0]);
if (terminal_names.size() >= 2)
painter.drawText(bt, Qt::AlignRight|Qt::AlignTop, terminal_names[1]);
drawText(painter, bt, Qt::AlignRight|Qt::AlignTop, terminal_names[1]);
painter.setFont(QETApp::diagramTextsFont(5));
}
@@ -1241,7 +1260,7 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in
QRectF text_rect = painter.boundingRect(QRectF(30, offset, 5, 10), Qt::AlignLeft | Qt::AlignVCenter, str);
if (!str.isEmpty())
{
painter.drawText(text_rect, Qt::AlignLeft | Qt::AlignVCenter, str);
drawText(painter, text_rect, Qt::AlignLeft | Qt::AlignVCenter, str);
bounding_rect = bounding_rect.united(text_rect);
}
@@ -1286,13 +1305,13 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in
painter.setFont(QETApp::diagramTextsFont(4));
// Storage order set above: [0]=NC, [1]=NO, [2]=Common
if (terminal_names.size() >= 2)
painter.drawText(QRectF(0, offset, 8, 8),
drawText(painter, QRectF(0, offset, 8, 8),
Qt::AlignLeft|Qt::AlignTop, terminal_names[1]); // NO top-left
if (terminal_names.size() >= 3)
painter.drawText(QRectF(16, offset+4, 8, 6),
drawText(painter, 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),
drawText(painter, QRectF(0, offset+9, 8, 6),
Qt::AlignLeft|Qt::AlignTop, terminal_names[0]); // NC bottom-left
painter.setFont(QETApp::diagramTextsFont(5));
}
@@ -1325,7 +1344,7 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in
str);
if (!str.isEmpty())
{
painter.drawText(text_rect,
drawText(painter, text_rect,
Qt::AlignLeft | Qt::AlignVCenter,
str);
bounding_rect = bounding_rect.united(text_rect);
@@ -1361,7 +1380,7 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in
str);
if (!str.isEmpty())
{
painter.drawText(text_rect,
drawText(painter, text_rect,
Qt::AlignLeft | Qt::AlignVCenter,
str);
bounding_rect = bounding_rect.united(text_rect);
@@ -1454,7 +1473,7 @@ void CrossRefItem::fillCrossRef(QPainter &painter)
QSize(middle_cross, 1)),
Qt::AlignLeft,
str);
painter.drawText(bounding, Qt::AlignLeft, str);
drawText(painter, bounding, Qt::AlignLeft, str);
if (m_update_map) {
QString pos_str = elementPositionText(elmt, true);
@@ -1534,7 +1553,7 @@ void CrossRefItem::fillCrossRef(QPainter &painter)
QSize(middle_cross, 1)),
Qt::AlignRight,
str);
painter.drawText(bounding, Qt::AlignRight, str);
drawText(painter, bounding, Qt::AlignRight, str);
if (m_update_map) {
QString pos_str = elementPositionText(elmt, true);
@@ -1582,7 +1601,7 @@ void CrossRefItem::AddExtraInfo(QPainter &painter, const QString& type)
r,
Qt::TextWordWrap | Qt::AlignHCenter,
text);
painter.drawText(text_bounding,
drawText(painter, text_bounding,
Qt::TextWordWrap | Qt::AlignHCenter,
text);
@@ -1598,6 +1617,21 @@ void CrossRefItem::AddExtraInfo(QPainter &painter, const QString& type)
}
}
/**
@brief CrossRefItem::drawText
Draws text in rect with painter, like QPainter::drawText. While the
geometry is built (m_update_map), also stores the rect really covered
by the text, so stackAtBottom() can tell texts drawn over each other.
*/
void CrossRefItem::drawText(QPainter &painter, const QRectF &rect,
int flags, const QString &text)
{
QRectF drawn;
painter.drawText(rect, flags, text, &drawn);
if (m_update_map && !text.isEmpty())
m_text_rects << painter.transform().mapRect(drawn);
}
/**
@brief CrossRefItem::NOElements
@return The linked elements of m_element which are open or switch contact.
+5
View File
@@ -59,6 +59,8 @@ class CrossRefItem : public QGraphicsObject
void init();
void setUpConnection();
void stackAtBottom();
static bool textsOverlap(const QList<QRectF> &a,
const QList<QRectF> &b);
public:
enum { Type = UserType + 1009 };
@@ -138,6 +140,8 @@ class CrossRefItem : public QGraphicsObject
const QStringList &master_labels = QStringList());
void fillCrossRef(QPainter &painter);
void AddExtraInfo(QPainter &painter, const QString&);
void drawText(QPainter &painter, const QRectF &rect, int flags,
const QString &text);
QList<Element *> NOElements() const;
QList<Element *> NCElements() const;
@@ -150,6 +154,7 @@ class CrossRefItem : public QGraphicsObject
int m_drawed_contacts;
bool m_update_map = false;
QMultiMap <Element *, QRectF> m_hovered_contacts_map;
QList <QRectF> m_text_rects; //rects of the drawn texts (local coords)
Element *m_hovered_contact = nullptr;
DynamicElementTextItem *m_text = nullptr;
ElementTextItemGroup *m_group = nullptr;
+395
View File
@@ -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"
@@ -52,6 +53,13 @@
#include <QDateTime>
#include <functional>
#include <QJsonValue>
#include <QDir>
#include <QFileInfo>
#include <QMdiSubWindow>
#include <QPageLayout>
#include <QPainter>
#include <QPrinterInfo>
#include <QPrinter>
#include <QDialogButtonBox>
#include <QLabel>
#include "../projectview.h"
@@ -331,6 +339,22 @@ void LiveServer::handle(const QJsonObject &request, qint64 received_ns)
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")) {
@@ -420,6 +444,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()},
@@ -548,6 +573,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
+10
View File
@@ -18,6 +18,7 @@
#ifndef LIVESERVER_H
#define LIVESERVER_H
#include <QJsonArray>
#include <QJsonObject>
#include <QObject>
#include <QPointer>
@@ -84,6 +85,15 @@ class LiveServer : public QObject
QJsonObject runStored(const QString &id);
QJsonObject command(const QString &id);
QJsonObject showFolio(int folio);
QJsonObject newProject(const QJsonObject &request);
QJsonObject openProject(const QString &path);
QJsonObject switchProject(const QJsonObject &request);
QJsonArray openProjects() const;
QJsonObject saveProject(const QJsonObject &request);
QJsonObject closeProject(const QJsonObject &request);
QJsonObject print(const QJsonObject &request);
QJsonObject changes(const QJsonObject &request);
QJsonObject snapshot(const QString &path);
QJsonObject undoLast();
QJsonObject screenshot();
bool confirm(const QString &name, const QString &source);
+19
View File
@@ -3489,6 +3489,25 @@ bool QetScriptApi::deleteImage(int folioIndex, int imageIndex)
return true;
}
/**
@brief QetScriptApi::elementTextGeometry
Where one of a symbol's text fields is drawn on the folio, in folio
coordinates: left, top, right, bottom of the text as shown. A label
that sits on a wire is visible here, not from x/y alone (x/y are
relative to the symbol and say nothing about the text's size).
@return an empty map when the text does not exist
*/
QVariantMap QetScriptApi::elementTextGeometry(int folioIndex, const QString &elementUuid,
int textIndex) const
{
DynamicElementTextItem *t = findElementText(folioIndex, elementUuid, textIndex,
QStringLiteral("elementTextGeometry"));
if (!t) return {};
const QRectF r = t->sceneBoundingRect();
return {{QStringLiteral("left"), r.left()}, {QStringLiteral("top"), r.top()},
{QStringLiteral("right"), r.right()}, {QStringLiteral("bottom"), r.bottom()}};
}
/**
@brief QetScriptApi::addPdfPage
Render one page of a PDF file to an image and place it, as the "add
+2
View File
@@ -556,6 +556,8 @@ class QetScriptApi : public QObject
Q_INVOKABLE QString elementTextProperty(int folioIndex, const QString &elementUuid,
int textIndex, const QString &property) const;
Q_INVOKABLE bool deleteElementText(int folioIndex, const QString &elementUuid, int textIndex);
Q_INVOKABLE QVariantMap elementTextGeometry(int folioIndex, const QString &elementUuid,
int textIndex) const;
// -- copy elements (with the conductors between them) to a position --
Q_INVOKABLE QStringList duplicateElements(int fromFolioIndex, const QStringList &elementUuids,
+5
View File
@@ -146,6 +146,7 @@ void XRefPropertiesWidget::saveProperties(int index) {
xrp.setShowPowerContac(ui->m_show_power_cb->isChecked());
xrp.setShowTerminalName(ui->m_show_terminal_name_cb->isChecked());
xrp.setShowAllConfiguredSlaves(ui->m_show_all_slaves_cb->isChecked());
xrp.setStackOverlapping(ui->m_stack_overlapping_cb->isChecked());
xrp.setPrefix("power", ui->m_power_prefix_le->text());
xrp.setPrefix("delay", ui->m_delay_prefix_le->text());
xrp.setPrefix("switch", ui->m_switch_prefix_le->text());
@@ -201,6 +202,8 @@ void XRefPropertiesWidget::updateDisplay()
ui->m_snap_to_cb->setCurrentIndex(ui->m_snap_to_cb->findData("label"));
ui->m_offset_sb->setEnabled(false);
}
ui->m_stack_overlapping_cb->setChecked(xrp.stackOverlapping());
ui->m_stack_overlapping_cb->setEnabled(ui->m_offset_sb->isEnabled());
if(xrp.getXrefPos() == Qt::AlignTop) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("top"));
else if(xrp.getXrefPos() == Qt::AlignLeft) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("left"));
@@ -233,6 +236,7 @@ void XRefPropertiesWidget::updateDisplay()
ui->m_display_has_cross_rb->setVisible(!is_plc);
ui->m_show_terminal_name_cb->setVisible(!is_plc);
ui->m_show_all_slaves_cb->setVisible(!is_plc);
ui->m_stack_overlapping_cb->setVisible(!is_plc);
ui->m_cross_properties_gb->setVisible(!is_plc);
}
@@ -261,4 +265,5 @@ void XRefPropertiesWidget::enableOffsetSB(int i){
ui->m_offset_sb->setEnabled(false);
else
ui->m_offset_sb->setEnabled(true);
ui->m_stack_overlapping_cb->setEnabled(ui->m_offset_sb->isEnabled());
}
+11
View File
@@ -89,6 +89,16 @@
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="m_stack_overlapping_cb">
<property name="toolTip">
<string>Quand les textes de deux références croisées se chevauchent, celle de l'élément le plus haut est placée au-dessus de l'autre.</string>
</property>
<property name="text">
<string>Empiler les références dont les textes se chevauchent</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
@@ -330,6 +340,7 @@
<tabstop>m_type_cb</tabstop>
<tabstop>m_snap_to_cb</tabstop>
<tabstop>m_offset_sb</tabstop>
<tabstop>m_stack_overlapping_cb</tabstop>
<tabstop>m_xrefpos_cb</tabstop>
<tabstop>m_display_has_contacts_rb</tabstop>
<tabstop>m_display_has_cross_rb</tabstop>