[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()));
}
}
}