mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-06 03:04:13 +02:00
Merge remote-tracking branch 'upstream/master' into perf/live-timing
# Conflicts: # sources/scripting/liveserver.cpp
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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") + "_" +
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user