From ee164f1bf10f6e10b5541fdd1117335fdb6bd205 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sat, 10 Oct 2026 18:05:27 +1300 Subject: [PATCH] Fix a split table losing its data and crashing after a folio is deleted A summary or parts-list table split over several folios saves its data with its first part only; each later part names the part before it. Deleting a folio keeps its tables in memory for undo, still in the chain, so saving then named the deleted part as the previous table of the next one. On reopening, every part after the gap had no data, and closing the project crashed: the first orphaned part, destroyed, handed its missing data on to the next part through a null model. toXml() now names the nearest previous part still in the project, or saves the data if there is none. setPreviousTable() no longer copies a null model, so files already saved this way open and close. Tests (tst_resaveunchanged): missingPreviousTableOpens, deletedFolioKeepsTableChain; both fail without the fix. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019Wga9B92BNurDEtxxtCDxf --- .../ViewItem/qetgraphicstableitem.cpp | 20 ++++-- tests/qttest/tst_resaveunchanged.cpp | 64 +++++++++++++++++++ 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.cpp b/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.cpp index 75660104a..df090c36d 100644 --- a/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.cpp +++ b/sources/qetgraphicsitem/ViewItem/qetgraphicstableitem.cpp @@ -448,7 +448,7 @@ void QetGraphicsTableItem::setPreviousTable(QetGraphicsTableItem *table) m_previous_table->setNextTable(this); setModel(m_previous_table->m_model); } - else //Copie the model of old previous table + else if (old_previous_table->model()) //Copie the model of old previous table { setModel(new ProjectDBModel(*static_cast(old_previous_table->model()))); } @@ -596,11 +596,23 @@ QDomElement QetGraphicsTableItem::toXml(QDomDocument &dom_document) const //Add the header xml dom_table.appendChild(m_header_item->toXml(dom_document)); - //Add previous table, the model is save by the previous table - if (m_previous_table) + //Add previous table, the model is save by the previous table. + //A folio deleted with undo still possible keeps its tables in the + //chain, but it is not saved: name the nearest previous table that + //is, or save the model if none is. + const auto project_ = diagram() ? diagram()->project() : nullptr; + auto in_project = [project_](const QetGraphicsTableItem *table) { + return project_ && table->diagram() + && project_->diagrams().contains(table->diagram()); + }; + QetGraphicsTableItem *previous_ = m_previous_table; + while (previous_ && !in_project(previous_)) { + previous_ = previous_->previousTable(); + } + if (previous_) { auto dom_previous_table = dom_document.createElement("previous_table"); - dom_previous_table.setAttribute("uuid", m_previous_table->m_uuid.toString()); + dom_previous_table.setAttribute("uuid", previous_->m_uuid.toString()); dom_table.appendChild(dom_previous_table); } else if (m_model) //There is not a previous table, we need to save the model diff --git a/tests/qttest/tst_resaveunchanged.cpp b/tests/qttest/tst_resaveunchanged.cpp index e80b4cb50..360d540ba 100644 --- a/tests/qttest/tst_resaveunchanged.cpp +++ b/tests/qttest/tst_resaveunchanged.cpp @@ -160,6 +160,70 @@ private slots: QVERIFY2(sb < sa, "the coil's contacts were saved in another order than the file's"); } + // A table split over several folios whose part names a previous part + // that is not in the file: opening and closing it crashed (the part + // has no data of its own and handed its missing data on when it was + // destroyed). industrial.qet's third is changed. + void missingPreviousTableOpens() + { + QByteArray xml = read(QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet")); + int at = -1; + for (int n = 0 ; n < 3 ; ++n) { + at = xml.indexOf(" 0); + } + at += int(qstrlen(" in the file names a table that is in + // it. It used to name the deleted part, so every part after it opened + // with no data. industrial.qet's folio 46 (index 45) holds such a part. + void deletedFolioKeepsTableChain() + { + const QString out = m_dir.filePath(QStringLiteral("deleted_folio.qet")); + const QString script = m_dir.filePath(QStringLiteral("deleted_folio.js")); + QFile js(script); + QVERIFY(js.open(QIODevice::WriteOnly)); + js.write(QStringLiteral("qet.log('REMOVED ' + qet.removeFolio(45));\n" + "qet.log('SAVED ' + qet.save('%1'));\n").arg(out).toUtf8()); + js.close(); + const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run++)); + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen")); + env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1")); + env.insert(QStringLiteral("HOME"), home); + env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config")); + env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data")); + QProcess proc; + proc.setProcessEnvironment(env); + proc.start(QStringLiteral(QET_TEST_BINARY_PATH), + {QStringLiteral("--run"), script, QStringLiteral(QET_EXAMPLES_DIR "/industrial.qet")}); + QVERIFY(proc.waitForFinished(180000)); + const QString log = QString::fromUtf8(proc.readAllStandardOutput() + proc.readAllStandardError()); + QVERIFY2(log.contains(QStringLiteral("REMOVED true")) && log.contains(QStringLiteral("SAVED true")), + qPrintable(log.right(400))); + + const QString saved = QString::fromUtf8(read(out)); + QSet tables; + for (const auto &m : QRegularExpression(QStringLiteral("]*uuid=\"([^\"]+)\"")).globalMatch(saved)) + tables.insert(m.captured(1)); + int references = 0; + for (const auto &m : QRegularExpression(QStringLiteral(" 0); + } + // A title-block value that is a single space is kept through two saves // (#973), and a value with accents comes back as it went in. void singleSpaceValueKept()