[education/cantor] src: Persist worksheet result metadata

Alexander Semke <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit d646d8b88415271785b307aa80c330a902cc3003 by Alexander Semke, on behalf of Nanhao Lv.
Committed on 26/07/2026 at 19:50.
Pushed by asemke into branch 'master'.

Persist worksheet result metadata

M  +1    -0    src/lib/animationresult.cpp
M  +35   -12   src/lib/expression.cpp
M  +1    -0    src/lib/helpresult.cpp
M  +1    -0    src/lib/htmlresult.cpp
M  +1    -0    src/lib/imageresult.cpp
M  +1    -0    src/lib/mimeresult.cpp
M  +1    -0    src/lib/pdfresult.cpp
M  +108  -3    src/lib/result.cpp
M  +28   -0    src/lib/result.h
M  +1    -0    src/lib/textresult.cpp
M  +37   -15   src/loadedexpression.cpp

https://invent.kde.org/education/cantor/-/commit/d646d8b88415271785b307aa80c330a902cc3003

diff --git a/src/lib/animationresult.cpp b/src/lib/animationresult.cpp
index 06399b9c..0fd5009e 100644
--- a/src/lib/animationresult.cpp
+++ b/src/lib/animationresult.cpp
@@ -75,6 +75,7 @@ QDomElement AnimationResult::toXml(QDomDocument& doc)
     QDomElement e=doc.createElement(QStringLiteral("Result"));
     e.setAttribute(QStringLiteral("type"), QStringLiteral("animation"));
     e.setAttribute(QStringLiteral("filename"), d->url.fileName());
+    applyXmlResultMetadata(e);
     qDebug()<<"done";
 
     return e;
diff --git a/src/lib/expression.cpp b/src/lib/expression.cpp
index b6e2ba83..8dd1868a 100644
--- a/src/lib/expression.cpp
+++ b/src/lib/expression.cpp
@@ -157,34 +157,54 @@ void Expression::addResult(Result* result)
 
 void Expression::clearResults()
 {
-    qDeleteAll(d->results);
+    const auto oldResults = d->results;
     d->results.clear();
     Q_EMIT resultsCleared();
+    qDeleteAll(oldResults);
 }
 
 void Expression::removeResult(Result* result)
 {
     int index = d->results.indexOf(result);
-    d->results.remove(index);
-    delete result;
+    if (index < 0)
+        return;
+
+    Result* removedResult = d->results.takeAt(index);
     Q_EMIT resultRemoved(index);
+    delete removedResult;
 }
 
 void Expression::replaceResult(int index, Result* result)
 {
-    if (result)
-    {
-        //insert the new result
-        d->results.insert(index, result);
+    if (!result)
+        return;
 
-        //delete the previous result
-        Result* oldResult = d->results.at(index+1);
-        d->results.remove(index+1);
-        delete oldResult;
+    if (index < 0 || index >= d->results.size())
+    {
+        delete result;
+        return;
+    }
 
-        //notify about the replacement
+    Result* oldResult = d->results.at(index);
+    if (oldResult == result)
+    {
         Q_EMIT resultReplaced(index);
+        return;
     }
+
+    if (oldResult)
+    {
+        result->setResultId(oldResult->resultId());
+        if (!oldResult->displayName().isEmpty())
+            result->setDisplayName(oldResult->displayName());
+    }
+
+    d->results[index] = result;
+
+    // Notify before releasing the previous Result ownership.
+    Q_EMIT resultReplaced(index);
+
+    delete oldResult;
 }
 
 Result* Expression::result()
@@ -202,6 +222,9 @@ const QVector<Result*>& Expression::results() const
 
 void Expression::setStatus(Expression::Status status)
 {
+    if (d->status == status)
+        return;
+
     d->status=status;
     Q_EMIT statusChanged(status);
 
diff --git a/src/lib/helpresult.cpp b/src/lib/helpresult.cpp
index 3f063f86..78cc7815 100644
--- a/src/lib/helpresult.cpp
+++ b/src/lib/helpresult.cpp
@@ -47,6 +47,7 @@ QDomElement HelpResult::toXml(QDomDocument& doc)
     //No need to save results of a help request
     QDomElement e=doc.createElement(QStringLiteral("Result"));
     e.setAttribute(QStringLiteral("type"), QStringLiteral("help"));
+    applyXmlResultMetadata(e);
     return e;
 }
 
diff --git a/src/lib/htmlresult.cpp b/src/lib/htmlresult.cpp
index fcdd99e8..0fefbc5e 100644
--- a/src/lib/htmlresult.cpp
+++ b/src/lib/htmlresult.cpp
@@ -88,6 +88,7 @@ QDomElement Cantor::HtmlResult::toXml(QDomDocument& doc)
 {
     QDomElement e=doc.createElement(QStringLiteral("Result"));
     e.setAttribute(QStringLiteral("type"), QStringLiteral("html"));
+    applyXmlResultMetadata(e);
     switch(d->format)
     {
         case HtmlResult::HtmlSource:
diff --git a/src/lib/imageresult.cpp b/src/lib/imageresult.cpp
index 027f54a3..4045be50 100644
--- a/src/lib/imageresult.cpp
+++ b/src/lib/imageresult.cpp
@@ -186,6 +186,7 @@ QDomElement ImageResult::toXml(QDomDocument& doc)
     auto e = doc.createElement(QStringLiteral("Result"));
     e.setAttribute(QStringLiteral("type"), QStringLiteral("image"));
     e.setAttribute(QStringLiteral("filename"), d->url.fileName());
+    applyXmlResultMetadata(e);
 
     if (!d->alt.isEmpty())
         e.appendChild(doc.createTextNode(d->alt));
diff --git a/src/lib/mimeresult.cpp b/src/lib/mimeresult.cpp
index f743686f..da759525 100644
--- a/src/lib/mimeresult.cpp
+++ b/src/lib/mimeresult.cpp
@@ -69,6 +69,7 @@ QDomElement MimeResult::toXml(QDomDocument& doc)
     qDebug()<<"saving mime result with types" << d->mimeBundle.keys();
     QDomElement e=doc.createElement(QStringLiteral("Result"));
     e.setAttribute(QStringLiteral("type"), QStringLiteral("mime"));
+    applyXmlResultMetadata(e);
 
     for (const QString& key : d->mimeBundle.keys())
     {
diff --git a/src/lib/pdfresult.cpp b/src/lib/pdfresult.cpp
index 85981abd..60777906 100644
--- a/src/lib/pdfresult.cpp
+++ b/src/lib/pdfresult.cpp
@@ -116,6 +116,7 @@ QDomElement PdfResult::toXml(QDomDocument& doc)
     QDomElement e = doc.createElement(QStringLiteral("Result"));
     e.setAttribute(QStringLiteral("type"), QStringLiteral("pdf"));
     e.setAttribute(QStringLiteral("filename"), d->url.fileName());
+    applyXmlResultMetadata(e);
     return e;
 }
 
diff --git a/src/lib/result.cpp b/src/lib/result.cpp
index 7ac2cba2..3bdaec87 100644
--- a/src/lib/result.cpp
+++ b/src/lib/result.cpp
@@ -4,11 +4,13 @@
 */
 
 #include "result.h"
+#include "jupyterutils.h"
 using namespace Cantor;
 
 #include <QUrl>
 #include <QJsonObject>
 #include <QRegularExpression>
+#include <QUuid>
 
 class Cantor::ResultPrivate
 {
@@ -20,13 +22,15 @@ class Cantor::ResultPrivate
     }
 
     QJsonObject* jupyterMetadata{nullptr};
+    QString resultId;
+    QString displayName;
+    Result::Role role{Result::Role::Generic};
     int executionIndex{-1};
 };
 
-
 Result::Result() : d(new ResultPrivate)
 {
-
+    regenerateResultId();
 }
 
 Result::~Result()
@@ -57,7 +61,18 @@ void Result::saveAdditionalData(KZip* archive)
 
 QJsonObject Cantor::Result::jupyterMetadata() const
 {
-    return d->jupyterMetadata ? *d->jupyterMetadata : QJsonObject();
+    QJsonObject metadata = d->jupyterMetadata ? *d->jupyterMetadata : QJsonObject();
+    QJsonObject cantorMetadata = metadata.value(JupyterUtils::cantorMetadataKey).toObject();
+
+    cantorMetadata.insert(QLatin1String("result-id"), d->resultId);
+    cantorMetadata.insert(QLatin1String("result-role"), roleToString(d->role));
+    if (d->displayName.isEmpty())
+        cantorMetadata.remove(QLatin1String("result-title"));
+    else
+        cantorMetadata.insert(QLatin1String("result-title"), d->displayName);
+
+    metadata.insert(JupyterUtils::cantorMetadataKey, cantorMetadata);
+    return metadata;
 }
 
 void Cantor::Result::setJupyterMetadata(const QJsonObject& metadata)
@@ -65,6 +80,19 @@ void Cantor::Result::setJupyterMetadata(const QJsonObject& metadata)
     if (!d->jupyterMetadata)
         d->jupyterMetadata = new QJsonObject();
     *d->jupyterMetadata = metadata;
+
+    const QJsonObject cantorMetadata = metadata.value(JupyterUtils::cantorMetadataKey).toObject();
+    const QString storedResultId = cantorMetadata.value(QLatin1String("result-id")).toString();
+    if (!storedResultId.isEmpty())
+        d->resultId = storedResultId;
+
+    d->role = roleFromString(cantorMetadata.value(QLatin1String("result-role")).toString());
+
+    const QJsonValue storedTitle = cantorMetadata.value(QLatin1String("result-title"));
+    if (storedTitle.isString())
+        d->displayName = storedTitle.toString().trimmed();
+    else
+        d->displayName.clear();
 }
 
 int Cantor::Result::executionIndex() const
@@ -76,3 +104,80 @@ void Cantor::Result::setExecutionIndex(int index)
 {
     d->executionIndex = index;
 }
+
+QString Cantor::Result::resultId() const
+{
+    return d->resultId;
+}
+
+void Cantor::Result::setResultId(const QString& id)
+{
+    if (id.isEmpty())
+        regenerateResultId();
+    else
+        d->resultId = id;
+}
+
+void Cantor::Result::regenerateResultId()
+{
+    d->resultId = QUuid::createUuid().toString(QUuid::WithoutBraces);
+}
+
+QString Cantor::Result::displayName() const
+{
+    return d->displayName;
+}
+
+void Cantor::Result::setDisplayName(const QString& name)
+{
+    d->displayName = name.trimmed();
+}
+
+Cantor::Result::Role Cantor::Result::role() const
+{
+    return d->role;
+}
+
+void Cantor::Result::setRole(Role role)
+{
+    d->role = role;
+}
+
+QString Cantor::Result::roleToString(Role role)
+{
+    switch (role)
+    {
+        case Role::Plot:
+            return QStringLiteral("plot");
+        case Role::Generic:
+            return QStringLiteral("generic");
+    }
+
+    return QStringLiteral("generic");
+}
+
+Cantor::Result::Role Cantor::Result::roleFromString(const QString& roleName)
+{
+    if (roleName == QLatin1String("plot"))
+        return Role::Plot;
+
+    return Role::Generic;
+}
+
+void Cantor::Result::applyXmlResultMetadata(QDomElement& element) const
+{
+    element.setAttribute(QLatin1String("result-id"), d->resultId);
+    element.setAttribute(QLatin1String("result-role"), roleToString(d->role));
+    if (!d->displayName.isEmpty())
+        element.setAttribute(QLatin1String("result-title"), d->displayName);
+}
+
+void Cantor::Result::loadXmlResultMetadata(const QDomElement& element)
+{
+    const QString storedResultId = element.attribute(QLatin1String("result-id"));
+    if (!storedResultId.isEmpty())
+        d->resultId = storedResultId;
+
+    d->role = roleFromString(element.attribute(QLatin1String("result-role")));
+    d->displayName = element.attribute(QLatin1String("result-title")).trimmed();
+}
diff --git a/src/lib/result.h b/src/lib/result.h
index b4b329c9..b95e36d6 100644
--- a/src/lib/result.h
+++ b/src/lib/result.h
@@ -9,6 +9,7 @@
 #include <QVariant>
 #include <QDomElement>
 #include <QJsonArray>
+#include <QString>
 #include "cantor_export.h"
 
 class KZip;
@@ -24,6 +25,13 @@ class ResultPrivate;
 class CANTOR_EXPORT Result
 {
   public:
+    /** Describes how UI code should treat this result. */
+    enum class Role
+    {
+        Generic,
+        Plot
+    };
+
     /**
      * Default constructor
      */
@@ -95,6 +103,26 @@ class CANTOR_EXPORT Result
      */
     virtual void save(const QString& filename) = 0;
 
+    /** Stable document-local identifier. */
+    QString resultId() const;
+    void setResultId(const QString& id);
+    void regenerateResultId();
+
+    /** Optional user-visible label; empty means use the default label. */
+    QString displayName() const;
+
+    void setDisplayName(const QString& name);
+
+    /** UI role assigned by the backend or loader. */
+    Role role() const;
+    void setRole(Role role);
+
+    static QString roleToString(Role role);
+    static Role roleFromString(const QString& roleName);
+
+    void applyXmlResultMetadata(QDomElement& element) const;
+    void loadXmlResultMetadata(const QDomElement& element);
+
     /**
      * This functions handle Jupyter metadata of
      */
diff --git a/src/lib/textresult.cpp b/src/lib/textresult.cpp
index 1299c66c..a51e2b79 100644
--- a/src/lib/textresult.cpp
+++ b/src/lib/textresult.cpp
@@ -109,6 +109,7 @@ QDomElement TextResult::toXml(QDomDocument& doc)
     QDomElement e = doc.createElement(QStringLiteral("Result"));
     e.setAttribute(QStringLiteral("type"), QStringLiteral("text"));
     e.setAttribute(QStringLiteral("stderr"), d->isStderr);
+    applyXmlResultMetadata(e);
 
     if (d->format == LatexFormat)
         e.setAttribute(QStringLiteral("format"), QStringLiteral("latex"));
diff --git a/src/loadedexpression.cpp b/src/loadedexpression.cpp
index 4a12f010..c68ca2a8 100644
--- a/src/loadedexpression.cpp
+++ b/src/loadedexpression.cpp
@@ -13,6 +13,7 @@
 #include "lib/latexrenderer.h"
 #include "lib/mimeresult.h"
 #include "lib/htmlresult.h"
+#include "lib/pdfresult.h"
 
 #include <QDir>
 #include <QStandardPaths>
@@ -47,9 +48,18 @@ void LoadedExpression::loadFromXml(const QDomElement& xml, const KZip& file)
     for (int i = 0; i < results.size(); i++)
     {
         const QDomElement& resultElement = results.at(i).toElement();
+        const auto addLoadedResult = [this, &resultElement](Cantor::Result* result)
+        {
+            if (!result)
+                return;
+
+            result->loadXmlResultMetadata(resultElement);
+            addResult(result);
+        };
+
         const QString& type = resultElement.attribute(QLatin1String("type"));
         qDebug() << "type" << type;
-        if ( type == QLatin1String("text"))
+        if (type == QLatin1String("text"))
         {
             const QString& format = resultElement.attribute(QLatin1String("format"));
             bool isStderr = resultElement.attribute(QLatin1String("stderr")).toInt();
@@ -57,7 +67,7 @@ void LoadedExpression::loadFromXml(const QDomElement& xml, const KZip& file)
             if (format == QLatin1String("latex"))
                 result->setFormat(Cantor::TextResult::LatexFormat);
             result->setStdErr(isStderr);
-            addResult(result);
+            addLoadedResult(result);
         }
         else if (type == QLatin1String("mime"))
         {
@@ -75,7 +85,7 @@ void LoadedExpression::loadFromXml(const QDomElement& xml, const KZip& file)
                 mimeBundle.insert(mimeType, value);
             }
 
-            addResult(new Cantor::MimeResult(mimeBundle));
+            addLoadedResult(new Cantor::MimeResult(mimeBundle));
         }
         else if (type == QLatin1String("html"))
         {
@@ -104,12 +114,12 @@ void LoadedExpression::loadFromXml(const QDomElement& xml, const KZip& file)
             Cantor::HtmlResult* result = new Cantor::HtmlResult(html, plain, alternatives);
             result->setFormat(format);
 
-            addResult(result);
+            addLoadedResult(result);
         }
-        else if (type == QLatin1String("image") || type == QLatin1String("latex") || type == QLatin1String("animation") || type == QLatin1String("epsimage"))
+        else if (type == QLatin1String("image") || type == QLatin1String("latex") || type == QLatin1String("animation") || type == QLatin1String("epsimage") || type == QLatin1String("pdf"))
         {
-            const KArchiveEntry* imageEntry=file.directory()->entry(resultElement.attribute(QLatin1String("filename")));
-            if (imageEntry&&imageEntry->isFile())
+            const KArchiveEntry* imageEntry = file.directory()->entry(resultElement.attribute(QLatin1String("filename")));
+            if (imageEntry && imageEntry->isFile())
             {
                 const KArchiveFile* imageFile=static_cast<const KArchiveFile*>(imageEntry);
                 QString dir=QStandardPaths::writableLocation(QStandardPaths::TempLocation);
@@ -134,30 +144,42 @@ void LoadedExpression::loadFromXml(const QDomElement& xml, const KZip& file)
                             f.close();
                         }
 
-                        addResult(new Cantor::LatexResult(resultElement.text(), imageUrl, QString(), pdfData));
+                        addLoadedResult(new Cantor::LatexResult(resultElement.text(), imageUrl, QString(), pdfData));
+                    }
+                }
+                else if (type == QLatin1String("pdf"))
+                {
+                    QByteArray pdfData;
+                    QFile f(imageUrl.toLocalFile());
+                    if (f.open(QIODevice::ReadOnly))
+                    {
+                        pdfData = f.readAll();
+                        f.close();
                     }
+
+                    addLoadedResult(new Cantor::PdfResult(imageUrl, pdfData));
                 }
-                else if(type==QLatin1String("animation"))
+                else if (type == QLatin1String("animation"))
                 {
-                    addResult(new Cantor::AnimationResult(imageUrl));
+                    addLoadedResult(new Cantor::AnimationResult(imageUrl));
                 }
-                else if(type==QLatin1String("epsimage"))
+                else if (type == QLatin1String("epsimage"))
                 {
                     const QByteArray& ba = QByteArray::fromBase64(resultElement.attribute(QLatin1String("image")).toLatin1());
                     QImage image;
                     image.loadFromData(ba);
-                    addResult(new Cantor::ImageResult(imageUrl, resultElement.text()));
+                    addLoadedResult(new Cantor::ImageResult(imageUrl, resultElement.text()));
                 }
-                else if(imageFile->name().endsWith(QLatin1String(".eps"), Qt::CaseInsensitive))
+                else if (imageFile->name().endsWith(QLatin1String(".eps"), Qt::CaseInsensitive))
                 {
                     const QByteArray& ba = QByteArray::fromBase64(resultElement.attribute(QLatin1String("image")).toLatin1());
                     QImage image;
                     image.loadFromData(ba);
-                    addResult(new Cantor::ImageResult(imageUrl, resultElement.text()));
+                    addLoadedResult(new Cantor::ImageResult(imageUrl, resultElement.text()));
                 }
                 else
                 {
-                    addResult(new Cantor::ImageResult(imageUrl, resultElement.text()));
+                    addLoadedResult(new Cantor::ImageResult(imageUrl, resultElement.text()));
                 }
             }
         }
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.