From b99f5551d2caf880f8ee822a8c5c5a8af57fa9bd Mon Sep 17 00:00:00 2001 From: ispyisail Date: Mon, 28 Sep 2026 23:56:08 +1300 Subject: [PATCH] Scripting: look up a folio by its uuid Every scripting call names a folio by its index, and the index shifts when a folio is added, removed or moved: a script that removes folio 0 and then edits "folio 2" edits the wrong one. Texts, shapes, pictures, tables and symbol text fields already have a uuid lookup for the same reason; folios had none. - qet.folioUuid(index): the folio's uuid, or "". - qet.folioIndex(uuid): the folio's current index, or -1. A folio saved without a uuid (132 of the 133 in the shipped examples) is given one on load, derived from the file, so it is the same on every load and is written on the next save. No two folios share one: a clash is renewed on load. Tests in misc/qet-mcp's integration suite, which drives these through --run: a folio followed across the removal of the one before it, on a file saved without folio uuids, and a new folio's uuid found in the saved file. Both fail against a build without this change. Co-Authored-By: Claude Opus 5.5 --- misc/qet-mcp/test_qet_mcp.py | 39 +++++++++++++++++++++++++++++- sources/scripting/qetscriptapi.cpp | 31 ++++++++++++++++++++++++ sources/scripting/qetscriptapi.h | 2 ++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/misc/qet-mcp/test_qet_mcp.py b/misc/qet-mcp/test_qet_mcp.py index d9310e6c2..69291ff0a 100644 --- a/misc/qet-mcp/test_qet_mcp.py +++ b/misc/qet-mcp/test_qet_mcp.py @@ -3520,7 +3520,7 @@ class PlcIntegration(unittest.TestCase): @needs_examples class UuidIndexLookups(unittest.TestCase): - """qet.tableIndex() and qet.elementTextIndex(): a uuid in, the index the + """qet.tableIndex(), qet.elementTextIndex() and qet.folioIndex(): a uuid in, the index the other calls take out, as qet.textIndex() does for free texts. The index is what shifts when an earlier item is deleted; the uuid is what holds.""" @@ -3639,6 +3639,43 @@ class UuidIndexLookups(unittest.TestCase): self.assertIn(f"shows='{shows_b}'", out["lb"][out["ib"]]) self.assertEqual((out["bogus"], out["no_element"]), (-1, -1)) + def test_folio_index_follows_the_folio_across_a_removal(self): + """A file saved without folio uuids (tableau_domestique.qet) still has + one per folio once loaded, the same on every load; the index of the + third folio moves when the first is removed, its uuid does not.""" + body = """ + var out = {uuids: []}; + for (var i = 0; i < qet.folioCount(); i++) out.uuids.push(qet.folioUuid(i)); + out.found = out.uuids.map(function (u) { return qet.folioIndex(u); }); + out.bogus = qet.folioIndex("{00000000-0000-4000-8000-000000000000}"); + out.not_uuid = qet.folioIndex("folio 1"); + out.out_of_range = qet.folioUuid(qet.folioCount()); + qet.removeFolio(0); + out.third_after = qet.folioIndex(out.uuids[2]); + out.first_after = qet.folioIndex(out.uuids[0]);""" + project = Path(EXAMPLES) / "tableau_domestique.qet" + self.assertFalse(any(d.get("uuid") for d in ET.parse(project).getroot().iter("diagram"))) + out = self.run_script(project, body) + self.assertEqual(len(out["uuids"]), 5) + self.assertEqual(len(set(out["uuids"])), 5) + self.assertTrue(all(m._UUID_RE.fullmatch(u) for u in out["uuids"])) + self.assertEqual(out["found"], [0, 1, 2, 3, 4]) + self.assertEqual((out["bogus"], out["not_uuid"], out["out_of_range"]), (-1, -1, "")) + self.assertEqual((out["third_after"], out["first_after"]), (1, -1)) + self.assertEqual(self.run_script(project, body)["uuids"], out["uuids"]) + + def test_a_new_folio_keeps_its_uuid_through_a_save(self): + with tempfile.TemporaryDirectory() as tmp: + saved = Path(tmp) / "saved.qet" + out = self.run_script(Path(EXAMPLES) / "tableau_domestique.qet", f""" + var out = {{}}; + out.index = qet.addFolio(); + out.uuid = qet.folioUuid(out.index); + out.saved = qet.save({json.dumps(str(saved))});""") + self.assertTrue(out["saved"]) + in_file = [d.get("uuid") for d in ET.parse(saved).getroot().iter("diagram")] + self.assertEqual(in_file[out["index"]], out["uuid"]) + @needs_examples class CorpusIntegration(unittest.TestCase): diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index b3d083a3f..3c7c4c549 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -123,6 +123,37 @@ QString QetScriptApi::folioTitle(int index) const return diagrams.at(index)->title(); } +/** + @brief QetScriptApi::folioUuid + The uuid of the folio at @p index, or an empty string. + The uuid is what to hold across an edit; the index is what the other + calls take, and it shifts when a folio is added, removed or moved. +*/ +QString QetScriptApi::folioUuid(int index) const +{ + if (!m_project) return QString(); + const QList diagrams = m_project->diagrams(); + if (index < 0 || index >= diagrams.count()) return QString(); + return diagrams.at(index)->uuid().toString(); +} + +/** + @brief QetScriptApi::folioIndex + The current index of the folio carrying @p uuid, or -1. No two folios + of a project carry the same uuid: a clash is renewed on load. +*/ +int QetScriptApi::folioIndex(const QString &uuid) const +{ + if (!m_project) return -1; + const QUuid wanted(uuid); + if (wanted.isNull()) return -1; + const QList diagrams = m_project->diagrams(); + for (int i = 0 ; i < diagrams.count() ; ++i) { + if (diagrams.at(i)->uuid() == wanted) return i; + } + return -1; +} + int QetScriptApi::elementCount(int folioIndex) const { if (!m_project) return 0; diff --git a/sources/scripting/qetscriptapi.h b/sources/scripting/qetscriptapi.h index 533b70830..8aa634f01 100644 --- a/sources/scripting/qetscriptapi.h +++ b/sources/scripting/qetscriptapi.h @@ -353,6 +353,8 @@ class QetScriptApi : public QObject Q_INVOKABLE QString filePath() const; Q_INVOKABLE int folioCount() const; Q_INVOKABLE QString folioTitle(int index) const; + Q_INVOKABLE QString folioUuid(int index) const; + Q_INVOKABLE int folioIndex(const QString &uuid) const; Q_INVOKABLE int elementCount(int folioIndex) const; Q_INVOKABLE int conductorCount(int folioIndex) const;