diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index 0f0430e43..747176a59 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -30,6 +30,8 @@ #include "../qetproject.h" #include "../qetresult.h" #include "../qetgraphicsitem/conductor.h" +#include "../qetgraphicsitem/independenttextitem.h" +#include "../qetgraphicsitem/qetshapeitem.h" #include "../qetgraphicsitem/terminal.h" #include "../qetinformation.h" #include "../titleblockproperties.h" @@ -913,6 +915,255 @@ bool QetScriptApi::unlinkElement(int folioIndex, const QString &elementUuid) return element->linkedElements().isEmpty(); } +namespace { + +/** + Reading order for items that have no identity but their position: + top to bottom, then left to right. + + On sceneBoundingRect(), not pos(): a QetShapeItem keeps its geometry in + its line/rect/polygon, and its pos() stays at the origin, so three + shapes drawn in different places all sort as (0, 0) and the ordering + collapses -- which is exactly what the first version of this did, and + it made every shape index refer to whichever one the set happened to + yield first. The scene bounding rect reflects where the item actually + is for both kinds. +*/ +template +QList sortedByPosition(const QSet &items) +{ + QList list(items.cbegin(), items.cend()); + std::sort(list.begin(), list.end(), [](T *a, T *b) { + const QPointF pa = a->sceneBoundingRect().topLeft(); + const QPointF pb = b->sceneBoundingRect().topLeft(); + if (pa.y() != pb.y()) return pa.y() < pb.y(); + if (pa.x() != pb.x()) return pa.x() < pb.x(); + // Two items genuinely at the same point still need a total order, + // or std::sort's result depends on the set's iteration order. + return a < b; + }); + return list; +} + +} // namespace + +QList QetScriptApi::sortedTexts(int folioIndex) const +{ + if (!m_project) return {}; + const QList diagrams = m_project->diagrams(); + if (folioIndex < 0 || folioIndex >= diagrams.count()) return {}; + DiagramContent content(diagrams.at(folioIndex), false); + return sortedByPosition(content.m_text_fields); +} + +QList QetScriptApi::sortedShapes(int folioIndex) const +{ + if (!m_project) return {}; + const QList diagrams = m_project->diagrams(); + if (folioIndex < 0 || folioIndex >= diagrams.count()) return {}; + DiagramContent content(diagrams.at(folioIndex), false); + return sortedByPosition(content.m_shapes); +} + +IndependentTextItem *QetScriptApi::findText(int folioIndex, int textIndex, const QString &caller) +{ + const QList list = sortedTexts(folioIndex); + if (textIndex < 0 || textIndex >= list.count()) { + log(QStringLiteral("qet.%1: folio %2 has %3 independent text(s), no index %4") + .arg(caller).arg(folioIndex).arg(list.count()).arg(textIndex)); + return nullptr; + } + return list.at(textIndex); +} + +QStringList QetScriptApi::texts(int folioIndex) const +{ + QStringList out; + const QList list = sortedTexts(folioIndex); + for (int i = 0 ; i < list.count() ; ++i) + { + IndependentTextItem *t = list.at(i); + const QPointF at = t->sceneBoundingRect().topLeft(); + out << QStringLiteral("%1: '%2' at (%3, %4)") + .arg(i) + .arg(t->toPlainText()) + .arg(at.x()) + .arg(at.y()); + } + return out; +} + +/** + @brief QetScriptApi::addText + Place a free-standing text, as the "add text" tool does. + @return its index in texts(), or -1 +*/ +int QetScriptApi::addText(int folioIndex, const QString &text, double x, double y) +{ + if (!m_project) return -1; + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.addText: project is read-only")); + return -1; + } + const QList diagrams = m_project->diagrams(); + if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1; + Diagram *diagram = diagrams.at(folioIndex); + + auto *item = new IndependentTextItem(); + item->setPlainText(text); + diagram->undoStack().push(new AddGraphicsObjectCommand(item, diagram, QPointF(x, y))); + return sortedTexts(folioIndex).indexOf(item); +} + +bool QetScriptApi::setTextContent(int folioIndex, int textIndex, const QString &text) +{ + if (!m_project) return false; + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.setTextContent: project is read-only")); + return false; + } + IndependentTextItem *item = findText(folioIndex, textIndex, QStringLiteral("setTextContent")); + if (!item) return false; + if (item->toPlainText() == text) return true; + + auto *cmd = new QPropertyUndoCommand(item, "plainText", + QVariant(item->toPlainText()), QVariant(text)); + cmd->setText(QObject::tr("Modifier un texte")); + m_project->undoStack()->push(cmd); + return true; +} + +bool QetScriptApi::setTextColor(int folioIndex, int textIndex, const QString &color) +{ + if (!m_project) return false; + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.setTextColor: project is read-only")); + return false; + } + const QColor new_color(color); + if (!new_color.isValid()) { + log(QStringLiteral("qet.setTextColor: '%1' is not a valid colour").arg(color)); + return false; + } + IndependentTextItem *item = findText(folioIndex, textIndex, QStringLiteral("setTextColor")); + if (!item) return false; + if (item->color() == new_color) return true; + + auto *cmd = new QPropertyUndoCommand(item, "color", + QVariant(item->color()), QVariant(new_color)); + cmd->setText(QObject::tr("Modifier la couleur d'un texte")); + m_project->undoStack()->push(cmd); + return true; +} + +bool QetScriptApi::setTextRotation(int folioIndex, int textIndex, double angle) +{ + if (!m_project) return false; + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.setTextRotation: project is read-only")); + return false; + } + IndependentTextItem *item = findText(folioIndex, textIndex, QStringLiteral("setTextRotation")); + if (!item) return false; + + auto *cmd = new QPropertyUndoCommand(item, "rotation", + QVariant(item->rotation()), + QVariant(item->rotation() + angle)); + cmd->setText(QObject::tr("Pivoter un texte")); + m_project->undoStack()->push(cmd); + return true; +} + +bool QetScriptApi::deleteText(int folioIndex, int textIndex) +{ + if (!m_project) return false; + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.deleteText: project is read-only")); + return false; + } + IndependentTextItem *item = findText(folioIndex, textIndex, QStringLiteral("deleteText")); + if (!item) return false; + Diagram *diagram = m_project->diagrams().at(folioIndex); + + DiagramContent content; + content.m_text_fields << item; + diagram->undoStack().push(new DeleteQGraphicsItemCommand(diagram, content)); + return true; +} + +QStringList QetScriptApi::shapes(int folioIndex) const +{ + QStringList out; + const QList list = sortedShapes(folioIndex); + for (int i = 0 ; i < list.count() ; ++i) + { + QetShapeItem *shape = list.at(i); + const QRectF r = shape->sceneBoundingRect(); + out << QStringLiteral("%1: %2 (%3, %4) to (%5, %6)") + .arg(i) + .arg(shape->name()) + .arg(r.left()).arg(r.top()).arg(r.right()).arg(r.bottom()); + } + return out; +} + +/** + @brief QetScriptApi::addShape + Draw a line, rectangle, ellipse or polygon, as the shape tools do. + Path is deliberately absent: it is built by successive clicks and has + no two-point form to give here. + @return the shape's index in shapes(), or -1 +*/ +int QetScriptApi::addShape(int folioIndex, const QString &type, + double x1, double y1, double x2, double y2) +{ + if (!m_project) return -1; + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.addShape: project is read-only")); + return -1; + } + const QList diagrams = m_project->diagrams(); + if (folioIndex < 0 || folioIndex >= diagrams.count()) return -1; + + QetShapeItem::ShapeType shape_type; + const QString t = type.toLower(); + if (t == QLatin1String("line")) shape_type = QetShapeItem::Line; + else if (t == QLatin1String("rectangle")) shape_type = QetShapeItem::Rectangle; + else if (t == QLatin1String("ellipse")) shape_type = QetShapeItem::Ellipse; + else if (t == QLatin1String("polygon")) shape_type = QetShapeItem::Polygon; + else { + log(QStringLiteral("qet.addShape: unknown shape '%1'; expected line, " + "rectangle, ellipse or polygon").arg(type)); + return -1; + } + + Diagram *diagram = diagrams.at(folioIndex); + auto *shape = new QetShapeItem(QPointF(x1, y1), QPointF(x2, y2), shape_type); + diagram->undoStack().push(new AddGraphicsObjectCommand(shape, diagram, QPointF(0, 0))); + return sortedShapes(folioIndex).indexOf(shape); +} + +bool QetScriptApi::deleteShape(int folioIndex, int shapeIndex) +{ + if (!m_project) return false; + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.deleteShape: project is read-only")); + return false; + } + const QList list = sortedShapes(folioIndex); + if (shapeIndex < 0 || shapeIndex >= list.count()) { + log(QStringLiteral("qet.deleteShape: folio %1 has %2 shape(s), no index %3") + .arg(folioIndex).arg(list.count()).arg(shapeIndex)); + return false; + } + Diagram *diagram = m_project->diagrams().at(folioIndex); + + DiagramContent content; + content.m_shapes << list.at(shapeIndex); + diagram->undoStack().push(new DeleteQGraphicsItemCommand(diagram, content)); + return true; +} + int QetScriptApi::addFolio() { if (!m_project) return -1; diff --git a/sources/scripting/qetscriptapi.h b/sources/scripting/qetscriptapi.h index ef6673772..816cbb9ad 100644 --- a/sources/scripting/qetscriptapi.h +++ b/sources/scripting/qetscriptapi.h @@ -27,6 +27,8 @@ class DiagramView; class Element; class Terminal; class Conductor; +class IndependentTextItem; +class QetShapeItem; /** @brief The QetScriptApi class @@ -116,6 +118,21 @@ class Conductor; conductor is ambiguous and is refused rather than guessed at -- which in practice means a potential is addressed from one of its leaf terminals, not from the hub several conductors meet at. + - @b Text and @b shapes: the drawing furniture a folio carries beside + its circuit -- a free-standing note, a line, a rectangle, an ellipse + -- added with the same AddGraphicsObjectCommand the corresponding GUI + tools use, and changed through the plainText/color/rotation + properties those items already publish. + + These are addressed by @b index into a listing sorted by position + (top to bottom, then left to right), because unlike an element they + carry no uuid and unlike a conductor they have no terminal to be + named by. Position is the only identity they have, and it persists, + so the ordering is the same after a save and reload -- verified + against exactly that. What it is @b not stable against is adding or + deleting one: indexes after the affected position shift, the way a + list's do. Call texts() or shapes() again rather than holding an + index across an edit that adds or removes one. - @b Navigating and @b messaging: select an element, zoom the active view, and show the user a message. Deliberately narrow: selection and messaging work with no view at all (headless `--run`); zoom is a no-op @@ -211,6 +228,19 @@ class QetScriptApi : public QObject int folioIndexB, const QString &elementUuidB); Q_INVOKABLE bool unlinkElement(int folioIndex, const QString &elementUuid); + // -- independent text and drawing shapes -- + Q_INVOKABLE QStringList texts(int folioIndex) const; + Q_INVOKABLE int addText(int folioIndex, const QString &text, double x, double y); + Q_INVOKABLE bool setTextContent(int folioIndex, int textIndex, const QString &text); + Q_INVOKABLE bool setTextColor(int folioIndex, int textIndex, const QString &color); + Q_INVOKABLE bool setTextRotation(int folioIndex, int textIndex, double angle); + Q_INVOKABLE bool deleteText(int folioIndex, int textIndex); + + Q_INVOKABLE QStringList shapes(int folioIndex) const; + Q_INVOKABLE int addShape(int folioIndex, const QString &type, + double x1, double y1, double x2, double y2); + Q_INVOKABLE bool deleteShape(int folioIndex, int shapeIndex); + // -- folios -- Q_INVOKABLE int addFolio(); Q_INVOKABLE bool setFolioTitle(int folioIndex, const QString &title); @@ -238,6 +268,9 @@ class QetScriptApi : public QObject const QString &caller); Conductor *findConductor(int folioIndex, const QString &elementUuid, int terminalIndex, const QString &caller); + QList sortedTexts(int folioIndex) const; + QList sortedShapes(int folioIndex) const; + IndependentTextItem *findText(int folioIndex, int textIndex, const QString &caller); bool setInfoKey(int folioIndex, const QString &elementUuid, const QString &key, const QString &value, const QString &caller);