From a9ad6e2f3494ed914b546b0e841363d4ce8e5215 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 27 Sep 2026 09:54:54 +1300 Subject: [PATCH] Give shapes, independent texts and images a persisted uuid Elements, conductors, terminals and tables already carry a uuid. The drawing furniture beside them did not, so a script or the MCP server could only name a line, a box or a note by its index in a position-sorted list, which shifts whenever one is added or removed. - QetShapeItem, IndependentTextItem and DiagramImageItem get uuid(), newUuid() and setUuid(), read from and written to a "uuid" attribute. - A folio loaded from a file written before this (or carrying a duplicate uuid) derives one from the folio uuid, the item kind and its order in the file, so the same file gives the same uuids on every load and a re-save is stable -- the #754 lesson for conductors. - Paste and folio duplication renew them, as they already do for elements and conductors. - Scripting: texts(), shapes() and images() end each line with the uuid; textIndex(), shapeIndex() and imageIndex() turn one back into an index. - misc/qet-mcp: qet_diff keys texts, shapes and images on uuid when both sides have one, so a move or edit reads as a change to that item. Co-Authored-By: Claude Opus 5.5 --- misc/qet-mcp/README.md | 11 +-- misc/qet-mcp/qet_mcp.py | 84 +++++++++++++------ sources/diagram.cpp | 26 ++++++ sources/diagramcommands.cpp | 12 +++ sources/elementspanelwidget.cpp | 15 ++++ sources/qetgraphicsitem/diagramimageitem.cpp | 6 +- sources/qetgraphicsitem/diagramimageitem.h | 5 ++ .../qetgraphicsitem/independenttextitem.cpp | 3 + sources/qetgraphicsitem/independenttextitem.h | 9 +- sources/qetgraphicsitem/qetshapeitem.cpp | 6 ++ sources/qetgraphicsitem/qetshapeitem.h | 5 ++ sources/scripting/qetscriptapi.cpp | 53 ++++++++++-- sources/scripting/qetscriptapi.h | 18 ++-- 13 files changed, 206 insertions(+), 47 deletions(-) diff --git a/misc/qet-mcp/README.md b/misc/qet-mcp/README.md index f4a869f33..1fba73a3e 100644 --- a/misc/qet-mcp/README.md +++ b/misc/qet-mcp/README.md @@ -295,11 +295,12 @@ Python, plus the hang guard on `addConductor` and the database refresh in no-op edit. Where an element predates persisted uuids the end cannot be resolved and keeps a `#`-marked unstable key; the diff then reports `unstable_keys` and says so rather than pretending to be comparable. -- **Texts, shapes and images have no uuid**, so `qet_diff` cannot say "the same - text, edited": an edited text reads as the old one removed and a new one - added, both shown. Shapes and images are keyed by position, so a restyle - or rescale *is* reported as a change to that item, but a move reads as a - removal plus an addition. The folio `version` attribute is left out of the +- **Texts, shapes and images are keyed by uuid** when every one on both sides + has one, so an edit or a move reads as a change to that item. A file saved + before they carried a uuid has none; for such a pair (including a legacy + file against its first re-save) that kind falls back to position, where an + edited text reads as removed plus added and a move as a removal plus an + addition. Each section says which it used in `keyed_by`. The folio `version` attribute is left out of the comparison on purpose: QElectroTech rewrites it on every save, and including it made every folio of any re-saved project look edited. - **Elements** written before persisted uuids fall back to a positional key, diff --git a/misc/qet-mcp/qet_mcp.py b/misc/qet-mcp/qet_mcp.py index c76d5275c..a7254b013 100755 --- a/misc/qet-mcp/qet_mcp.py +++ b/misc/qet-mcp/qet_mcp.py @@ -314,32 +314,42 @@ def _plain_text(html: str) -> str: def _extras(root: ET.Element) -> dict: """Everything a folio holds besides elements and conductors. - Independent texts, shapes and images have no uuid, so nothing here can - say "this is the same text, edited". A change to one therefore reads as - the old one removed and a new one added, with both shown -- accurate, - if less tidy than a move. Position is the identity they have. + Independent texts, shapes and images are records, not a keyed dict: + files written since they carry a uuid are compared by it (see + _diff_items), so an edited or moved text reads as that text, changed. + Older files have only position to go on, and there a change reads as + the old one removed and a new one added, with both shown. """ - folios, texts, shapes, images = {}, {}, {}, {} + folios, texts, shapes, images = {}, [], [], [] + + def record(el, label, value): + return {"uuid": el.get("uuid", ""), "key": tuple(label.values()), + "label": label, "value": value} + for n, d in _folios(root): folios[n] = {f: d.get(f, "") for f in _FOLIO_FIELDS} for t in d.iter("input"): - key = (n, t.get("x", ""), t.get("y", ""), _plain_text(t.get("text", ""))) - texts[key] = {"rotation": t.get("rotation", "0"), - "font": t.get("font", ""), "color": t.get("color", "")} + texts.append(record( + t, {"folio": n, "x": t.get("x", ""), "y": t.get("y", ""), + "text": _plain_text(t.get("text", ""))}, + {"rotation": t.get("rotation", "0"), + "font": t.get("font", ""), "color": t.get("color", "")})) for sh in d.iter("shape"): pen, brush = sh.find("pen"), sh.find("brush") - key = (n, sh.get("type", ""), sh.get("x1", ""), sh.get("y1", ""), - sh.get("x2", ""), sh.get("y2", "")) - shapes[key] = { - "line_color": pen.get("color", "") if pen is not None else "", - "line_style": pen.get("style", "") if pen is not None else "", - "line_width": pen.get("widthF", "") if pen is not None else "", - "fill": (brush.get("color", "") if brush is not None and - brush.get("style", "") != "NoBrush" else "none"), - "rotation": sh.get("rotation", "0")} + shapes.append(record( + sh, {"folio": n, "type": sh.get("type", ""), + "from": [sh.get("x1", ""), sh.get("y1", "")], + "to": [sh.get("x2", ""), sh.get("y2", "")]}, + {"line_color": pen.get("color", "") if pen is not None else "", + "line_style": pen.get("style", "") if pen is not None else "", + "line_width": pen.get("widthF", "") if pen is not None else "", + "fill": (brush.get("color", "") if brush is not None and + brush.get("style", "") != "NoBrush" else "none"), + "rotation": sh.get("rotation", "0")})) for im in d.iter("image"): - key = (n, im.get("x", ""), im.get("y", "")) - images[key] = {"scale": im.get("size", ""), "rotation": im.get("rotation", "")} + images.append(record( + im, {"folio": n, "x": im.get("x", ""), "y": im.get("y", "")}, + {"scale": im.get("size", ""), "rotation": im.get("rotation", "")})) element_texts = {} for n, d in _folios(root): @@ -388,6 +398,32 @@ def _diff_keyed(a: dict, b: dict, label) -> dict: "changed": changed[:50]} +def _diff_items(a: list, b: list) -> dict: + """_diff_keyed() over _extras() records of one kind. + + Keyed on uuid only when every item on both sides has one. A file saved + before these items carried a uuid has none, and the first save by a + current QElectroTech gives them one, so a mixed pair falls back to + position for the whole kind rather than reading as everything removed + and re-added. On uuid, position is part of what is compared, so a move + is a change to that item. + """ + by_uuid = all(r["uuid"] for r in a + b) + + def key(r): + return r["uuid"] if by_uuid else str(r["key"]) + + def keyed(rs): + return {key(r): ({**r["label"], **r["value"]} if by_uuid else r["value"]) + for r in rs} + + labels = {key(r): ({**r["label"], "uuid": r["uuid"]} if by_uuid else r["label"]) + for r in a + b} + out = _diff_keyed(keyed(a), keyed(b), lambda k: labels[k]) + out["keyed_by"] = "uuid" if by_uuid else "position" + return out + + def _diff_extras(before: ET.Element, after: ET.Element) -> dict: a, b = _extras(before), _extras(after) out = {} @@ -407,13 +443,9 @@ def _diff_extras(before: ET.Element, after: ET.Element) -> dict: if len(a["folios"]) != len(b["folios"]) and folio_changes: out["folios"]["note"] = ("the folio count changed, so changes listed here may be " "later folios shifting position rather than edits") - out["texts"] = _diff_keyed(a["texts"], b["texts"], - lambda k: {"folio": k[0], "x": k[1], "y": k[2], "text": k[3]}) - out["shapes"] = _diff_keyed(a["shapes"], b["shapes"], - lambda k: {"folio": k[0], "type": k[1], - "from": [k[2], k[3]], "to": [k[4], k[5]]}) - out["images"] = _diff_keyed(a["images"], b["images"], - lambda k: {"folio": k[0], "x": k[1], "y": k[2]}) + out["texts"] = _diff_items(a["texts"], b["texts"]) + out["shapes"] = _diff_items(a["shapes"], b["shapes"]) + out["images"] = _diff_items(a["images"], b["images"]) # Keyed by element, what the field is bound to, and the nth such field. # A field's own text is also compared ("shows"), so relabelling an # element shows up here as well as in the element's information. diff --git a/sources/diagram.cpp b/sources/diagram.cpp index e8c6b17f9..1bb5d583d 100644 --- a/sources/diagram.cpp +++ b/sources/diagram.cpp @@ -1670,6 +1670,29 @@ bool Diagram::fromXml(QDomElement &document, } } + //Texts, images and shapes written before they carried a uuid (or + //carrying one already used on this folio, from a hand-edited file) + //get one derived from the folio uuid, their kind and their order in + //the file, so that loading the same file twice gives the same uuids. + //A random one would be a different identity on every load, and + //toXml() writes it out, which is what #754 was for conductors. + //Only for a folio being loaded: a paste renews them all anyway. + QSet used_uuids; + auto settle_uuid = [&](auto *item, const QDomElement &xml, + const QString &kind, int index) { + const QUuid persisted(xml.attribute(QStringLiteral("uuid"))); + if (consider_informations + && (persisted.isNull() || used_uuids.contains(persisted))) { + item->setUuid(QUuid::createUuidV5( + m_uuid, + QStringLiteral("%1\n%2\n%3") + .arg(kind) + .arg(index) + .arg(persisted.toString()))); + } + used_uuids.insert(item->uuid()); + }; + // Load text QList added_texts; for (auto text_xml : QET::findInDomElement(root, @@ -1677,6 +1700,7 @@ bool Diagram::fromXml(QDomElement &document, QStringLiteral("input"))) { IndependentTextItem *iti = new IndependentTextItem(); iti -> fromXml(text_xml); + settle_uuid(iti, text_xml, QStringLiteral("input"), added_texts.size()); addItem(iti); added_texts << iti; } @@ -1688,6 +1712,7 @@ bool Diagram::fromXml(QDomElement &document, QStringLiteral("image"))) { DiagramImageItem *dii = new DiagramImageItem (); dii -> fromXml(image_xml); + settle_uuid(dii, image_xml, QStringLiteral("image"), added_images.size()); addItem(dii); added_images << dii; } @@ -1699,6 +1724,7 @@ bool Diagram::fromXml(QDomElement &document, QStringLiteral("shape"))) { QetShapeItem *dii = new QetShapeItem (QPointF(0,0)); dii -> fromXml(shape_xml); + settle_uuid(dii, shape_xml, QStringLiteral("shape"), added_shapes.size()); addItem(dii); added_shapes << dii; } diff --git a/sources/diagramcommands.cpp b/sources/diagramcommands.cpp index fa21ac43c..38c15a2a9 100644 --- a/sources/diagramcommands.cpp +++ b/sources/diagramcommands.cpp @@ -19,9 +19,12 @@ #include "diagram.h" #include "qetgraphicsitem/conductortextitem.h" +#include "qetgraphicsitem/diagramimageitem.h" #include "qetgraphicsitem/dynamicelementtextitem.h" #include "qetgraphicsitem/element.h" #include "qetgraphicsitem/elementtextitemgroup.h" +#include "qetgraphicsitem/independenttextitem.h" +#include "qetgraphicsitem/qetshapeitem.h" #include "qetinformation.h" #include "qgimanager.h" @@ -100,6 +103,15 @@ void PasteDiagramCommand::redo() for (Conductor *c : all_pasted_conductors) { c -> newUuid(); } + for (IndependentTextItem *t : std::as_const(content.m_text_fields)) { + t -> newUuid(); + } + for (DiagramImageItem *i : std::as_const(content.m_images)) { + i -> newUuid(); + } + for (QetShapeItem *s : std::as_const(content.m_shapes)) { + s -> newUuid(); + } //this is the first paste, we do some actions for the new element for (Element *e : elmts_list) diff --git a/sources/elementspanelwidget.cpp b/sources/elementspanelwidget.cpp index ca4178f1d..8b42bb594 100644 --- a/sources/elementspanelwidget.cpp +++ b/sources/elementspanelwidget.cpp @@ -29,6 +29,9 @@ #include #include "qetgraphicsitem/element.h" #include "qetgraphicsitem/dynamicelementtextitem.h" +#include "qetgraphicsitem/diagramimageitem.h" +#include "qetgraphicsitem/independenttextitem.h" +#include "qetgraphicsitem/qetshapeitem.h" #include "qetinformation.h" /* @@ -800,6 +803,18 @@ void ElementsPanelWidget::duplicateDiagram() } } } + else if (auto text = dynamic_cast(item)) { + // Not a database key (yet), but a script or the MCP server + // addresses a text, image or shape by it: a copy must not + // answer to its source's name. + text->newUuid(); + } + else if (auto image = dynamic_cast(item)) { + image->newUuid(); + } + else if (auto shape = dynamic_cast(item)) { + shape->newUuid(); + } else if (Conductor *cond = dynamic_cast(item)) { // Same reasoning for conductors: conductor.uuid is the PRIMARY // KEY of the conductor table, and its insert is a plain INSERT, diff --git a/sources/qetgraphicsitem/diagramimageitem.cpp b/sources/qetgraphicsitem/diagramimageitem.cpp index 25f2fc164..bc76f906c 100644 --- a/sources/qetgraphicsitem/diagramimageitem.cpp +++ b/sources/qetgraphicsitem/diagramimageitem.cpp @@ -1337,7 +1337,10 @@ bool DiagramImageItem::fromXml(const QDomElement &e) if (e.tagName() != "image") { return (false); } - + + const QUuid uuid(e.attribute(QStringLiteral("uuid"))); + if (!uuid.isNull()) m_uuid = uuid; + QDomNode image_node = e.firstChild(); if (!image_node.isText()) { return (false); @@ -1458,6 +1461,7 @@ bool DiagramImageItem::fromXml(const QDomElement &e) QDomElement DiagramImageItem::toXml(QDomDocument &document) const { QDomElement result = document.createElement("image"); + result.setAttribute("uuid", m_uuid.toString()); //write some attribute result.setAttribute("x", QString::number(pos().x())); result.setAttribute("y", QString::number(pos().y())); diff --git a/sources/qetgraphicsitem/diagramimageitem.h b/sources/qetgraphicsitem/diagramimageitem.h index afdbaf7b5..d86854361 100644 --- a/sources/qetgraphicsitem/diagramimageitem.h +++ b/sources/qetgraphicsitem/diagramimageitem.h @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -91,6 +92,9 @@ class DiagramImageItem : public QetGraphicsItem { virtual bool fromXml(const QDomElement &); virtual QDomElement toXml(QDomDocument &) const; + QUuid uuid() const {return m_uuid;} + void setUuid(const QUuid &uuid) {m_uuid = uuid;} + void newUuid() {m_uuid = QUuid::createUuid();} //create new uuid for this item void editProperty() override; void setPixmap(const QPixmap &pixmap); QPixmap pixmap() const { return pixmap_; } @@ -177,6 +181,7 @@ class DiagramImageItem : public QetGraphicsItem { void clearStatusHint() const; protected: + QUuid m_uuid = QUuid::createUuid(); QPixmap pixmap_; // The true, pristine original -- never itself cropped or colour- // keyed. pixmap_ (the displayed result) is always re-derived from diff --git a/sources/qetgraphicsitem/independenttextitem.cpp b/sources/qetgraphicsitem/independenttextitem.cpp index 36c5eb5d8..557861643 100644 --- a/sources/qetgraphicsitem/independenttextitem.cpp +++ b/sources/qetgraphicsitem/independenttextitem.cpp @@ -63,6 +63,8 @@ IndependentTextItem::~IndependentTextItem() @param e L'element XML representant le champ de texte */ void IndependentTextItem::fromXml(const QDomElement &e) { + const QUuid uuid(e.attribute(QStringLiteral("uuid"))); + if (!uuid.isNull()) m_uuid = uuid; setPos(e.attribute("x").toDouble(), e.attribute("y").toDouble()); setHtml(e.attribute("text")); setRotation(e.attribute("rotation").toDouble()); @@ -81,6 +83,7 @@ void IndependentTextItem::fromXml(const QDomElement &e) { QDomElement IndependentTextItem::toXml(QDomDocument &document) const { QDomElement result = document.createElement("input"); + result.setAttribute("uuid", m_uuid.toString()); result.setAttribute("x", QString("%1").arg(pos().x())); result.setAttribute("y", QString("%1").arg(pos().y())); result.setAttribute("text", toHtml()); diff --git a/sources/qetgraphicsitem/independenttextitem.h b/sources/qetgraphicsitem/independenttextitem.h index aa3156e2f..c88a84d63 100644 --- a/sources/qetgraphicsitem/independenttextitem.h +++ b/sources/qetgraphicsitem/independenttextitem.h @@ -20,6 +20,8 @@ #include "diagramtextitem.h" +#include + /** This class represents an independent text field on a particular diagram. It may be moved, edited, and rotated. @@ -41,9 +43,14 @@ class IndependentTextItem : public DiagramTextItem void fromXml(const QDomElement &) override; QDomElement toXml(QDomDocument &) const override; + QUuid uuid() const {return m_uuid;} + void setUuid(const QUuid &uuid) {m_uuid = uuid;} + void newUuid() {m_uuid = QUuid::createUuid();} //create new uuid for this item protected: void focusOutEvent(QFocusEvent *event) override; - + + private: + QUuid m_uuid = QUuid::createUuid(); }; #endif diff --git a/sources/qetgraphicsitem/qetshapeitem.cpp b/sources/qetgraphicsitem/qetshapeitem.cpp index f12c3b3f5..44a1cca02 100644 --- a/sources/qetgraphicsitem/qetshapeitem.cpp +++ b/sources/qetgraphicsitem/qetshapeitem.cpp @@ -2891,6 +2891,11 @@ bool QetShapeItem::fromXml(const QDomElement &e) { if (e.tagName() != "shape") return (false); + //Absent in files written before shapes carried a uuid: keep the + //one this item already has, Diagram::fromXml() settles it. + const QUuid uuid(e.attribute(QStringLiteral("uuid"))); + if (!uuid.isNull()) m_uuid = uuid; + // fromXml() is also used to *restore* an already-displayed item's // state (PromoteShapeCommand's undo/redo), not just to populate a // freshly constructed one -- without this, Qt has no way to know the @@ -3016,6 +3021,7 @@ bool QetShapeItem::fromXml(const QDomElement &e) QDomElement QetShapeItem::toXml(QDomDocument &document) const { QDomElement result = document.createElement("shape"); + result.setAttribute("uuid", m_uuid.toString()); //write some attribute QMetaEnum me = metaObject()->enumerator(metaObject()->indexOfEnumerator("ShapeType")); diff --git a/sources/qetgraphicsitem/qetshapeitem.h b/sources/qetgraphicsitem/qetshapeitem.h index 949b7b86c..c72693067 100644 --- a/sources/qetgraphicsitem/qetshapeitem.h +++ b/sources/qetgraphicsitem/qetshapeitem.h @@ -23,6 +23,7 @@ #include "shapetransform.h" #include +#include #include #include @@ -153,6 +154,9 @@ class QetShapeItem : public QetGraphicsItem virtual bool fromXml (const QDomElement &); virtual QDomElement toXml (QDomDocument &document) const; + QUuid uuid() const {return m_uuid;} + void setUuid(const QUuid &uuid) {m_uuid = uuid;} + void newUuid() {m_uuid = QUuid::createUuid();} //create new uuid for this item virtual bool toDXF (const QString &filepath,const QPen &pen); void editProperty() override; @@ -295,6 +299,7 @@ class QetShapeItem : public QetGraphicsItem ///ATTRIBUTES private: + QUuid m_uuid = QUuid::createUuid(); ShapeType m_shapeType; QPen m_pen; QBrush m_brush; diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index b31d16e1e..4b3dfc760 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -1395,11 +1395,12 @@ QStringList QetScriptApi::texts(int folioIndex) const { IndependentTextItem *t = list.at(i); const QPointF at = t->sceneBoundingRect().topLeft(); - out << QStringLiteral("%1: '%2' at (%3, %4)") + out << QStringLiteral("%1: '%2' at (%3, %4) %5") .arg(i) .arg(t->toPlainText()) .arg(at.x()) - .arg(at.y()); + .arg(at.y()) + .arg(t->uuid().toString()); } return out; } @@ -1518,10 +1519,11 @@ QStringList QetScriptApi::shapes(int folioIndex) const { QetShapeItem *shape = list.at(i); const QRectF r = shape->sceneBoundingRect(); - out << QStringLiteral("%1: %2 (%3, %4) to (%5, %6)") + out << QStringLiteral("%1: %2 (%3, %4) to (%5, %6) %7") .arg(i) .arg(shape->name()) - .arg(r.left()).arg(r.top()).arg(r.right()).arg(r.bottom()); + .arg(r.left()).arg(r.top()).arg(r.right()).arg(r.bottom()) + .arg(shape->uuid().toString()); } return out; } @@ -2836,13 +2838,52 @@ QStringList QetScriptApi::images(int folioIndex) const DiagramImageItem *item = list.at(i); const QPixmap px = item->pixmap(); const QPointF at = item->sceneBoundingRect().topLeft(); - out << QStringLiteral("%1: %2x%3 px at (%4, %5) scale=%6 rotation=%7") + out << QStringLiteral("%1: %2x%3 px at (%4, %5) scale=%6 rotation=%7 %8") .arg(i).arg(px.width()).arg(px.height()).arg(at.x()).arg(at.y()) - .arg(item->scaleFactorX()).arg(item->rotationAngle()); + .arg(item->scaleFactorX()).arg(item->rotationAngle()) + .arg(item->uuid().toString()); } return out; } +namespace { + +template +int indexOfUuid(const QList &sorted, const QString &uuid) +{ + const QUuid wanted(uuid); + if (wanted.isNull()) return -1; + for (int i = 0 ; i < sorted.count() ; ++i) { + if (sorted.at(i)->uuid() == wanted) return i; + } + return -1; +} + +} // namespace + +/** + @brief QetScriptApi::textIndex + The current index in texts() of the text carrying @p uuid, or -1. + The uuid is what to hold across an edit; the index is what the other + text calls take, and it shifts when a text is added or removed. +*/ +int QetScriptApi::textIndex(int folioIndex, const QString &uuid) const +{ + return indexOfUuid(sortedTexts(folioIndex), uuid); +} + +/// @brief QetScriptApi::shapeIndex as textIndex(), for shapes(). +int QetScriptApi::shapeIndex(int folioIndex, const QString &uuid) const +{ + return indexOfUuid(sortedShapes(folioIndex), uuid); +} + +/// @brief QetScriptApi::imageIndex as textIndex(), for images(). +int QetScriptApi::imageIndex(int folioIndex, const QString &uuid) const +{ + return indexOfUuid(sortedImages(folioIndex), uuid); +} + /** @brief QetScriptApi::addImage Place a picture from a file, as the "add image" tool does after its file diff --git a/sources/scripting/qetscriptapi.h b/sources/scripting/qetscriptapi.h index 5426275db..a95ddd2a0 100644 --- a/sources/scripting/qetscriptapi.h +++ b/sources/scripting/qetscriptapi.h @@ -139,14 +139,13 @@ class QetGraphicsTableItem; style editor changes. 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. + (top to bottom, then left to right), and 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. Each listing line ends + with the item's uuid, which is persisted and does not shift: hold + that across an edit, and turn it back into an index with textIndex(), + shapeIndex() or imageIndex() when calling. - @b Querying the project database: run a read-only SELECT against the SQLite database QElectroTech builds from the project, and get rows back as objects. This is not a new door. QET already ships a @@ -432,6 +431,7 @@ class QetScriptApi : public QObject // -- independent text and drawing shapes -- Q_INVOKABLE QStringList texts(int folioIndex) const; + Q_INVOKABLE int textIndex(int folioIndex, const QString &uuid) const; Q_INVOKABLE int addText(int folioIndex, const QString &text, double x, double y); Q_INVOKABLE QString textContent(int folioIndex, int textIndex) const; Q_INVOKABLE bool setTextContent(int folioIndex, int textIndex, const QString &text); @@ -440,6 +440,7 @@ class QetScriptApi : public QObject Q_INVOKABLE bool deleteText(int folioIndex, int textIndex); Q_INVOKABLE QStringList shapes(int folioIndex) const; + Q_INVOKABLE int shapeIndex(int folioIndex, const QString &uuid) 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); @@ -496,6 +497,7 @@ class QetScriptApi : public QObject // -- images, embedded in the project -- Q_INVOKABLE QStringList images(int folioIndex) const; + Q_INVOKABLE int imageIndex(int folioIndex, const QString &uuid) const; Q_INVOKABLE int addImage(int folioIndex, const QString &filePath, double x, double y); Q_INVOKABLE bool setImageScale(int folioIndex, int imageIndex, double factor); Q_INVOKABLE bool setImageRotation(int folioIndex, int imageIndex, double angle);