From 59cc758b7fe68c91de8dba66f8ac28e081235dbe Mon Sep 17 00:00:00 2001 From: ispyisail Date: Mon, 21 Sep 2026 22:35:16 +1200 Subject: [PATCH] Let a script control the text fields drawn on a symbol elementTexts() addElementText() setElementTextProperty() elementTextProperty() deleteElementText() A symbol arrives with the fields its definition gives it -- coil bobine_ka_a_remanence has A2, A1 and a label -- and until now a script could fill the value the label shows (setElementLabel) but not control the fields themselves: where each sits, its size, whether it is framed, what it is bound to, or add and delete one. A field's source is "text" (a fixed string), "info" (follows one of the element's information keys, so it follows setElementInfo and setElementLabel) or "composite" (a formula). Changes go through QPropertyUndoCommand on the item's own properties, as the element-texts editor does, and adding through AddElementTextCommand. An unknown source, a key that is not an element information key, a non-positive size and a bad index are refused with a reason. Two things called text differ for an information-bound field. The "text" property is the stored string, an unused placeholder (empty, or "Texte" for a newly added field); "shows" is what is drawn. I first believed the displayed text was stale in-session and wrote a helper to read it from the element's information instead. That was wrong: comparing toPlainText() against elementInfo() at seven points across relabel, rebinding, setting and undo found no difference. It was the stored string that looked stale, and the helper and the comment claiming the item "refreshes lazily" are removed. The header now says which is which. Fields are addressed by index in the element's own list, which follows the definition and shifts on delete; undoing a deletion puts the field back at the end. Consecutive info/label changes on one element merge into one undo step (ChangeElementInformationCommand::mergeWith), so one undo can revert several -- noted, not a bug. Verified on a coil: the label field moved, enlarged to 14 pt and framed, a new field bound to "comment" showing "24VDC coil", all through the project, exported to PNG and looked at. The saved file carries the moved position, size, frame and bound value, and a reload reads them back. Co-Authored-By: Claude Sonnet 5 --- sources/scripting/qetscriptapi.cpp | 227 +++++++++++++++++++++++++++++ sources/scripting/qetscriptapi.h | 41 ++++++ 2 files changed, 268 insertions(+) diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index 70df3fd19..92b9cff83 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -32,6 +32,7 @@ #include "../qetresult.h" #include "../qetgraphicsitem/conductor.h" #include "../qetgraphicsitem/diagramimageitem.h" +#include "../qetgraphicsitem/dynamicelementtextitem.h" #include "../qetgraphicsitem/independenttextitem.h" #include "../qetgraphicsitem/qetshapeitem.h" #include "../TerminalStrip/UndoCommand/addterminalstripcommand.h" @@ -45,6 +46,7 @@ #include "../qetinformation.h" #include "../titleblockproperties.h" #include "../undocommand/addgraphicsobjectcommand.h" +#include "../undocommand/addelementtextcommand.h" #include "../undocommand/changeelementinformationcommand.h" #include "../undocommand/changetitleblockcommand.h" #include "../undocommand/deleteqgraphicsitemcommand.h" @@ -56,6 +58,7 @@ #include #include #include +#include #include #include #include @@ -1888,6 +1891,230 @@ bool QetScriptApi::deleteImage(int folioIndex, int imageIndex) return true; } +namespace { +QString textSourceName(DynamicElementTextItem::TextFrom from) +{ + switch (from) { + case DynamicElementTextItem::ElementInfo: return QStringLiteral("info"); + case DynamicElementTextItem::CompositeText: return QStringLiteral("composite"); + default: return QStringLiteral("text"); + } +} + +const QStringList &elementTextPropertyNames() +{ + static const QStringList n{QStringLiteral("text"), QStringLiteral("source"), QStringLiteral("info"), + QStringLiteral("composite"), QStringLiteral("frame"), QStringLiteral("size"), + QStringLiteral("x"), QStringLiteral("y"), QStringLiteral("rotation"), QStringLiteral("width")}; + return n; +} +} // namespace + +DynamicElementTextItem *QetScriptApi::findElementText(int folioIndex, const QString &elementUuid, + int textIndex, const QString &caller) const +{ + Element *element = findElement(folioIndex, elementUuid); + if (!element) return nullptr; + const QList list = element->dynamicTextItems(); + if (textIndex < 0 || textIndex >= list.count()) { + const_cast(this)->log( + QStringLiteral("qet.%1: %2 has %3 text field(s), no index %4") + .arg(caller, element->name()).arg(list.count()).arg(textIndex)); + return nullptr; + } + return list.at(textIndex); +} + +QStringList QetScriptApi::elementTexts(int folioIndex, const QString &elementUuid) const +{ + QStringList out; + Element *element = findElement(folioIndex, elementUuid); + if (!element) return out; + const QList list = element->dynamicTextItems(); + for (int i = 0 ; i < list.count() ; ++i) + { + DynamicElementTextItem *t = list.at(i); + const QString source = textSourceName(t->textFrom()); + QString what; + if (t->textFrom() == DynamicElementTextItem::ElementInfo) what = QStringLiteral(" info='%1'").arg(t->infoName()); + else if (t->textFrom() == DynamicElementTextItem::CompositeText) what = QStringLiteral(" composite='%1'").arg(t->compositeText()); + out << QStringLiteral("%1: source=%2%3 shows='%4' at (%5, %6) size=%7") + .arg(i).arg(source, what, t->toPlainText()) + .arg(t->pos().x()).arg(t->pos().y()).arg(t->font().pointSizeF()); + } + return out; +} + +QString QetScriptApi::elementTextProperty(int folioIndex, const QString &elementUuid, + int textIndex, const QString &property) const +{ + DynamicElementTextItem *t = findElementText(folioIndex, elementUuid, textIndex, + QStringLiteral("elementTextProperty")); + if (!t) return QString(); + if (property == QLatin1String("text")) return t->text(); + if (property == QLatin1String("shows")) return t->toPlainText(); + if (property == QLatin1String("source")) return textSourceName(t->textFrom()); + if (property == QLatin1String("info")) return t->infoName(); + if (property == QLatin1String("composite")) return t->compositeText(); + if (property == QLatin1String("frame")) return t->frame() ? QStringLiteral("true") : QStringLiteral("false"); + if (property == QLatin1String("size")) return QString::number(t->font().pointSizeF()); + if (property == QLatin1String("x")) return QString::number(t->pos().x()); + if (property == QLatin1String("y")) return QString::number(t->pos().y()); + if (property == QLatin1String("rotation")) return QString::number(t->rotation()); + if (property == QLatin1String("width")) return QString::number(t->textWidth()); + return QString(); +} + +/** + @brief QetScriptApi::addElementText + Add a text field to a symbol, through AddElementTextCommand as the + element-texts editor does. + @param source "text" (value is the string shown), "info" (value is an + information key such as "label", and the field then follows that key) or + "composite" (value is a formula) + @return the field's index in elementTexts(), or -1 +*/ +int QetScriptApi::addElementText(int folioIndex, const QString &elementUuid, + const QString &source, const QString &value, + double x, double y) +{ + if (!m_project) return -1; + const QString caller = QStringLiteral("addElementText"); + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.%1: project is read-only").arg(caller)); + return -1; + } + Element *element = findElement(folioIndex, elementUuid); + if (!element) return -1; + + auto *item = new DynamicElementTextItem(element); + if (source == QLatin1String("text")) { + item->setTextFrom(DynamicElementTextItem::UserText); + item->setText(value); + } else if (source == QLatin1String("info")) { + if (!QETInformation::elementInfoKeys().contains(value)) { + log(QStringLiteral("qet.%1: '%2' is not an element information key").arg(caller, value)); + delete item; + return -1; + } + item->setTextFrom(DynamicElementTextItem::ElementInfo); + item->setInfoName(value); + } else if (source == QLatin1String("composite")) { + item->setTextFrom(DynamicElementTextItem::CompositeText); + item->setCompositeText(value); + } else { + log(QStringLiteral("qet.%1: unknown source '%2'; expected text, info or composite").arg(caller, source)); + delete item; + return -1; + } + item->setPos(x, y); + m_project->undoStack()->push(new AddElementTextCommand(element, item)); + return element->dynamicTextItems().indexOf(item); +} + +/** + @brief QetScriptApi::setElementTextProperty + Change one aspect of a symbol's text field through QPropertyUndoCommand on + the item's own properties, the way the element-texts editor does. + Properties: text, source (text|info|composite), info, composite, frame + (true|false), size (points), x, y (in the element's coordinates), + rotation, width. +*/ +bool QetScriptApi::setElementTextProperty(int folioIndex, const QString &elementUuid, + int textIndex, const QString &property, + const QString &value) +{ + if (!m_project) return false; + const QString caller = QStringLiteral("setElementTextProperty"); + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.%1: project is read-only").arg(caller)); + return false; + } + if (!elementTextPropertyNames().contains(property)) { + log(QStringLiteral("qet.%1: unknown property '%2'; expected one of %3") + .arg(caller, property, elementTextPropertyNames().join(QStringLiteral(", ")))); + return false; + } + DynamicElementTextItem *t = findElementText(folioIndex, elementUuid, textIndex, caller); + if (!t) return false; + + auto number = [&](double &out, bool positive) { + bool ok = false; + out = value.toDouble(&ok); + if (!ok || (positive && out <= 0)) { + log(QStringLiteral("qet.%1: '%2' is not a valid %3").arg(caller, value, property)); + return false; + } + return true; + }; + + const char *qt_property = nullptr; + QVariant old_value, new_value; + double d = 0; + + if (property == QLatin1String("text")) { qt_property = "text"; old_value = t->text(); new_value = value; } + else if (property == QLatin1String("info")) { + if (!QETInformation::elementInfoKeys().contains(value)) { + log(QStringLiteral("qet.%1: '%2' is not an element information key").arg(caller, value)); + return false; + } + qt_property = "infoName"; old_value = t->infoName(); new_value = value; + } + else if (property == QLatin1String("composite")) { qt_property = "compositeText"; old_value = t->compositeText(); new_value = value; } + else if (property == QLatin1String("source")) { + DynamicElementTextItem::TextFrom from; + if (value == QLatin1String("text")) from = DynamicElementTextItem::UserText; + else if (value == QLatin1String("info")) from = DynamicElementTextItem::ElementInfo; + else if (value == QLatin1String("composite")) from = DynamicElementTextItem::CompositeText; + else { log(QStringLiteral("qet.%1: unknown source '%2'").arg(caller, value)); return false; } + qt_property = "textFrom"; old_value = QVariant::fromValue(t->textFrom()); new_value = QVariant::fromValue(from); + } + else if (property == QLatin1String("frame")) { + const QString v = value.toLower(); + if (v != QLatin1String("true") && v != QLatin1String("false")) { + log(QStringLiteral("qet.%1: frame is true or false, not '%2'").arg(caller, value)); + return false; + } + qt_property = "frame"; old_value = t->frame(); new_value = (v == QLatin1String("true")); + } + else if (property == QLatin1String("size")) { + if (!number(d, true)) return false; + QFont f = t->font(); f.setPointSizeF(d); + qt_property = "font"; old_value = t->font(); new_value = f; + } + else if (property == QLatin1String("x") || property == QLatin1String("y")) { + if (!number(d, false)) return false; + QPointF p = t->pos(); + (property == QLatin1String("x") ? p.rx() : p.ry()) = d; + qt_property = "pos"; old_value = t->pos(); new_value = p; + } + else if (property == QLatin1String("rotation")) { if (!number(d, false)) return false; qt_property = "rotation"; old_value = t->rotation(); new_value = d; } + else { if (!number(d, false)) return false; qt_property = "textWidth"; old_value = t->textWidth(); new_value = d; } + + if (old_value == new_value) return true; + auto *cmd = new QPropertyUndoCommand(t, qt_property, old_value, new_value); + cmd->setText(QObject::tr("Modifier un texte d'élément")); + m_project->undoStack()->push(cmd); + return true; +} + +bool QetScriptApi::deleteElementText(int folioIndex, const QString &elementUuid, int textIndex) +{ + if (!m_project) return false; + if (m_project->isReadOnly()) { + log(QStringLiteral("qet.deleteElementText: project is read-only")); + return false; + } + DynamicElementTextItem *t = findElementText(folioIndex, elementUuid, textIndex, + QStringLiteral("deleteElementText")); + if (!t) return false; + Diagram *diagram = m_project->diagrams().at(folioIndex); + DiagramContent content; + content.m_element_texts << t; + 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 139d44d44..f9f044d76 100644 --- a/sources/scripting/qetscriptapi.h +++ b/sources/scripting/qetscriptapi.h @@ -31,6 +31,7 @@ class Conductor; class IndependentTextItem; class QetShapeItem; class DiagramImageItem; +class DynamicElementTextItem; /** @brief The QetScriptApi class @@ -184,6 +185,32 @@ class DiagramImageItem; position-sorted listing, like texts and shapes -- by the on-screen bounding box, so scaling or rotating an image, which turns about its centre, can change where it sorts. Re-list after either. + - @b Element @b texts: the text fields drawn on a symbol -- its label, + the names beside its terminals, any value the definition placed there. + A symbol arrives with the fields its definition gives it; setElementLabel() + fills the value one of them shows, and these methods control the fields + themselves: where each sits, its size, whether it draws a frame, what it + shows, and adding or deleting one. Addressed by index in the element's + own list, which follows the definition's order and shifts when one is + deleted -- and undoing a deletion puts the field back at the end, so + list again after either. + + Two things called text, which differ for a field bound to an + information key: the @b "text" property is the field's stored string, + which for an information-bound field is an unused placeholder (empty, + or "Texte" once one has been added), and @b "shows" is what is drawn, + which follows the element's information straight away -- compared + against elementInfo() at seven points across relabel, rebinding, + setting and undo, with no difference. Read "shows". + + Consecutive setElementInfo()/setElementLabel() calls on one element + merge into a single undo step, as ChangeElementInformationCommand + does, so one undo can revert several. + + A field's @b source is "text" (a fixed string), "info" (the value of one + of the element's information keys, so it follows setElementInfo() and + setElementLabel()) or "composite" (a formula over several). Position is in + the element's own coordinates, not the folio's. - @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 @@ -327,6 +354,18 @@ class QetScriptApi : public QObject Q_INVOKABLE bool setImageRotation(int folioIndex, int imageIndex, double angle); Q_INVOKABLE bool deleteImage(int folioIndex, int imageIndex); + // -- the text fields shown on a symbol (label, terminal names, ...) -- + Q_INVOKABLE QStringList elementTexts(int folioIndex, const QString &elementUuid) const; + Q_INVOKABLE int addElementText(int folioIndex, const QString &elementUuid, + const QString &source, const QString &value, + double x, double y); + Q_INVOKABLE bool setElementTextProperty(int folioIndex, const QString &elementUuid, + int textIndex, const QString &property, + const QString &value); + 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); + // -- folios -- Q_INVOKABLE int addFolio(); Q_INVOKABLE bool setFolioTitle(int folioIndex, const QString &title); @@ -357,6 +396,8 @@ class QetScriptApi : public QObject QList sortedTexts(int folioIndex) const; QList sortedShapes(int folioIndex) const; QList sortedImages(int folioIndex) const; + DynamicElementTextItem *findElementText(int folioIndex, const QString &elementUuid, + int textIndex, const QString &caller) 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);