[education/cantor] src: Track result items by stable IDs

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

Track result items by stable IDs

M  +9    -0    src/animationresultitem.cpp
M  +4    -1    src/animationresultitem.h
M  +176  -30   src/commandentry.cpp
M  +7    -0    src/commandentry.h
M  +16   -1    src/imageresultitem.cpp
M  +4    -0    src/imageresultitem.h
M  +19   -2    src/resultitem.cpp
M  +1    -0    src/resultitem.h

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

diff --git a/src/animationresultitem.cpp b/src/animationresultitem.cpp
index 7d1b858c..7cc4a00a 100644
--- a/src/animationresultitem.cpp
+++ b/src/animationresultitem.cpp
@@ -10,6 +10,7 @@
 #include "lib/animationresult.h"
 
 #include <QFileDialog>
+#include <QGraphicsSceneMouseEvent>
 #include <QMovie>
 
 #include <KLocalizedString>
@@ -131,3 +132,11 @@ void AnimationResultItem::deleteLater()
 {
     WorksheetImageItem::deleteLater();
 }
+
+void AnimationResultItem::mousePressEvent(QGraphicsSceneMouseEvent* event)
+{
+    if (auto* commandEntry = parentEntry())
+        commandEntry->resultItemClicked(m_result);
+
+    WorksheetImageItem::mousePressEvent(event);
+}
diff --git a/src/animationresultitem.h b/src/animationresultitem.h
index a9c1f49a..a8b4a0bd 100644
--- a/src/animationresultitem.h
+++ b/src/animationresultitem.h
@@ -10,6 +10,7 @@
 #include "worksheetimageitem.h"
 
 class QMovie;
+class QGraphicsSceneMouseEvent;
 
 class CommandEntry;
 class WorksheetEntry;
@@ -34,6 +35,9 @@ class AnimationResultItem : public WorksheetImageItem, public ResultItem
     double width() const override;
     double height() const override;
 
+  protected:
+    void mousePressEvent(QGraphicsSceneMouseEvent* event) override;
+
   protected Q_SLOTS:
     void saveResult();
     void stopMovie();
@@ -52,4 +56,3 @@ class AnimationResultItem : public WorksheetImageItem, public ResultItem
 };
 
 #endif //ANIMATIONRESULTITEM_H
-
diff --git a/src/commandentry.cpp b/src/commandentry.cpp
index 8d777453..d0be704b 100644
--- a/src/commandentry.cpp
+++ b/src/commandentry.cpp
@@ -35,10 +35,10 @@
 #include <QTextBlock>
 #include <QTextDocumentFragment>
 #include <QPainter>
+#include <QUuid>
 
 #include <KLocalizedString>
 #include <KColorScheme>
-#include <KSyntaxHighlighting/Repository>
 #include <KSyntaxHighlighting/Definition>
 
 const QString CommandEntry::Prompt     = QLatin1String(">>> ");
@@ -46,42 +46,29 @@ const QString CommandEntry::MidPrompt  = QLatin1String(">>  ");
 const QString CommandEntry::HidePrompt = QLatin1String(">   ");
 const double CommandEntry::VerticalSpacing = 4;
 
-namespace {
-    QString getThemeNameFromIndex (int index)
-    {
-        if (index <= 0)
-            return QString ();
-        const auto& repository = KTextEditor::Editor::instance()->repository();
-        const auto& themes = repository.themes();
-
-        const int themeListIndex = index - 1;
-        if (themeListIndex>= 0 && themeListIndex < themes.count ())
-            return themes.at (themeListIndex).name ();
-
-        return QString ();
-    }
-}
-
 CommandEntry::CommandEntry(Worksheet* worksheet) : WorksheetEntry(worksheet),
     m_promptItem(new WorksheetTextItem(this, Qt::NoTextInteraction)),
+    m_commandId(QUuid::createUuid().toString(QUuid::WithoutBraces)),
     m_commandItem(new WorksheetTextEditorItem(WorksheetTextEditorItem::Editable, this, this)),
     m_resultsCollapsed(false),
     m_errorItem(nullptr),
     m_expression(nullptr),
+    m_dynamicHighlighter(nullptr),
     m_evaluationOption(DoNothing),
+    m_promptItemAnimation(nullptr),
     m_menusInitialized(false),
     m_textColorCustom(false),
     m_backgroundColorCustom(false),
     m_backgroundColorActionGroup(nullptr),
     m_backgroundColorMenu(nullptr),
     m_textColorActionGroup(nullptr),
+    m_themeActionGroup(nullptr),
     m_textColorMenu(nullptr),
     m_fontMenu(nullptr),
-    m_isExecutionEnabled(true),
-    m_dynamicHighlighter(nullptr)
+    m_isExecutionEnabled(true)
 {
     m_promptItem->setPlainText(Prompt);
-    m_promptItem->setItemDragable(true);;
+    m_promptItem->setItemDragable(true);
     m_commandItem->enableCompletion(true);
 
     if(worksheet && worksheet->session() && worksheet->session()->backend())
@@ -430,6 +417,46 @@ QString CommandEntry::command()
     return cmd;
 }
 
+const QString& CommandEntry::commandId() const
+{
+    return m_commandId;
+}
+
+void CommandEntry::regenerateCommandId()
+{
+    m_commandId = QUuid::createUuid().toString(QUuid::WithoutBraces);
+}
+
+int CommandEntry::resultItemCount() const
+{
+    return m_resultItems.size();
+}
+
+ResultItem* CommandEntry::resultItemAt(int index) const
+{
+    if (index < 0 || index >= m_resultItems.size())
+        return nullptr;
+
+    return m_resultItems.at(index);
+}
+
+ResultItem* CommandEntry::resultItemById(const QString& resultId) const
+{
+    if (resultId.isEmpty())
+        return nullptr;
+
+    for (auto* item : m_resultItems)
+    {
+        if (!item || !item->result())
+            continue;
+
+        if (item->result()->resultId() == resultId)
+            return item;
+    }
+
+    return nullptr;
+}
+
 void CommandEntry::setExpression(Cantor::Expression* expr)
 {
     /*
@@ -465,7 +492,12 @@ void CommandEntry::setExpression(Cantor::Expression* expr)
     connect(expr, &Cantor::Expression::resultsCleared, this, &CommandEntry::clearResultItems);
     connect(expr, &Cantor::Expression::resultRemoved, this, &CommandEntry::removeResultItem);
     connect(expr, &Cantor::Expression::resultReplaced, this, &CommandEntry::replaceResultItem);
-    connect(expr, &Cantor::Expression::idChanged, this,  [=]() { updatePrompt();} );
+    connect(expr, &Cantor::Expression::idChanged, this, [=]()
+    {
+        updatePrompt();
+        if (worksheet())
+            worksheet()->refreshTocStructure();
+    });
     connect(expr, &Cantor::Expression::statusChanged, this, &CommandEntry::expressionChangedStatus);
     connect(expr, &Cantor::Expression::needsAdditionalInformation, this, &CommandEntry::showAdditionalInformationPrompt);
     connect(expr, &Cantor::Expression::statusChanged, this,  [=]() { updatePrompt();} );
@@ -499,6 +531,10 @@ void CommandEntry::setContent(const QString& content)
 void CommandEntry::setContent(const QDomElement& content, const KZip& file)
 {
     m_commandItem->setPlainText(content.firstChildElement(QLatin1String("Command")).text());
+    const QString storedCommandId = content.attribute(QLatin1String("command-id"));
+
+    if (!storedCommandId.isEmpty())
+        m_commandId = storedCommandId;
 
     LoadedExpression* expr = new LoadedExpression( worksheet()->session() );
     expr->loadFromXml(content, file);
@@ -556,6 +592,11 @@ void CommandEntry::setContent(const QDomElement& content, const KZip& file)
 void CommandEntry::setContentFromJupyter(const QJsonObject& cell)
 {
     m_commandItem->setPlainText(Cantor::JupyterUtils::getSource(cell));
+    const QJsonObject cantorMetadata = Cantor::JupyterUtils::getCantorMetadata(cell);
+    const QString storedCommandId = cantorMetadata.value(QLatin1String("command-id")).toString();
+
+    if (!storedCommandId.isEmpty())
+        m_commandId = storedCommandId;
 
     LoadedExpression* expr=new LoadedExpression( worksheet()->session() );
     expr->loadFromJupyter(cell);
@@ -597,6 +638,10 @@ QJsonValue CommandEntry::toJupyterJson()
     if (m_resultsCollapsed)
         metadata.insert(QLatin1String("collapsed"), true);
 
+    QJsonObject cantorMetadata = metadata.value(Cantor::JupyterUtils::cantorMetadataKey).toObject();
+    cantorMetadata.insert(QLatin1String("command-id"), m_commandId);
+    metadata.insert(Cantor::JupyterUtils::cantorMetadataKey, cantorMetadata);
+
     entry.insert(QLatin1String("metadata"), metadata);
 
     Cantor::JupyterUtils::setSource(entry, command());
@@ -653,6 +698,7 @@ QString CommandEntry::toPlain(const QString& commandSep, const QString& commentS
 QDomElement CommandEntry::toXml(QDomDocument& doc, KZip* archive)
 {
     QDomElement exprElem = doc.createElement( QLatin1String("Expression") );
+    exprElem.setAttribute(QLatin1String("command-id"), m_commandId);
     QDomElement cmdElem = doc.createElement( QLatin1String("Command") );
     cmdElem.appendChild(doc.createTextNode( command() ));
     exprElem.appendChild(cmdElem);
@@ -832,13 +878,33 @@ void CommandEntry::updateEntry()
         if (m_resultsCollapsed)
             expandResults();
 
+        bool addedResultItem = false;
         for (int i = m_resultItems.size(); i < expr->results().size(); i++)
-            m_resultItems << ResultItem::create(this, expr->results()[i]);
+        {
+            if (auto* resultItem = ResultItem::create(this, expr->results()[i]))
+            {
+                m_resultItems << resultItem;
+                addedResultItem = true;
+            }
+        }
+
+        if (addedResultItem && worksheet())
+            worksheet()->scheduleTocStructureRefresh();
     }
     else
     {
-        for (ResultItem* item: m_resultItems)
+        for (int i = 0; i < m_resultItems.size(); ++i)
+        {
+            auto* item = m_resultItems.at(i);
+            auto* result = i < expr->results().size() ? expr->results().at(i) : nullptr;
+            if (!item || item->result() != result)
+            {
+                replaceResultItem(i);
+                continue;
+            }
+
             item->update();
+        }
     }
 
     m_controlElement.isCollapsable = m_errorItem != nullptr
@@ -999,27 +1065,107 @@ void CommandEntry::removeResult(Cantor::Result* result)
 
 void CommandEntry::removeResultItem(int index)
 {
-    fadeOutItem(m_resultItems[index]->graphicsObject());
-    m_resultItems.remove(index);
+    if (index < 0 || index >= m_resultItems.size())
+        return;
+
+    auto* item = m_resultItems.takeAt(index);
+    if (item)
+    {
+        item->setResult(nullptr);
+        if (auto* object = item->graphicsObject())
+            object->hide();
+        item->deleteLater();
+    }
+
     recalculateSize();
+    if (worksheet())
+        worksheet()->scheduleTocStructureRefresh();
 }
 
 void CommandEntry::clearResultItems()
 {
-    //fade out all result graphic objects
-    for(auto* item : m_resultItems)
-        fadeOutItem(item->graphicsObject());
+    const bool hadResultItems = !m_resultItems.isEmpty();
+
+    for (auto* item : m_resultItems)
+    {
+        if (!item)
+            continue;
+
+        item->setResult(nullptr);
+        if (auto* object = item->graphicsObject())
+            object->hide();
+        item->deleteLater();
+    }
 
     m_resultItems.clear();
     recalculateSize();
+    if (hadResultItems && worksheet())
+        worksheet()->scheduleTocStructureRefresh();
 }
 
 void CommandEntry::replaceResultItem(int index)
 {
+    if (!m_expression)
+        return;
+
+    const auto& results = m_expression->results();
+    if (index < 0 || index >= results.size())
+        return;
+
+    auto* newItem = ResultItem::create(this, results[index]);
+    if (index >= m_resultItems.size())
+    {
+        if (newItem)
+        {
+            if (index == m_resultItems.size())
+                m_resultItems.append(newItem);
+            else
+            {
+                newItem->deleteLater();
+                return;
+            }
+
+            recalculateSize();
+            if (worksheet())
+                worksheet()->scheduleTocStructureRefresh();
+        }
+        return;
+    }
+
     auto* previousItem = m_resultItems[index];
-    m_resultItems[index] = ResultItem::create(this, m_expression->results()[index]);
-    previousItem->deleteLater();
+    if (!newItem)
+    {
+        if (previousItem)
+        {
+            previousItem->setResult(nullptr);
+            if (auto* object = previousItem->graphicsObject())
+                object->hide();
+            previousItem->deleteLater();
+        }
+        m_resultItems.remove(index);
+        recalculateSize();
+        if (worksheet())
+            worksheet()->scheduleTocStructureRefresh();
+        return;
+    }
+
+    m_resultItems[index] = newItem;
+    if (previousItem)
+    {
+        previousItem->setResult(nullptr);
+        if (previousItem->graphicsObject())
+            previousItem->graphicsObject()->hide();
+        previousItem->deleteLater();
+    }
     recalculateSize();
+    if (worksheet())
+        worksheet()->scheduleTocStructureRefresh();
+}
+
+void CommandEntry::resultItemClicked(Cantor::Result* result)
+{
+    if (worksheet())
+        worksheet()->updateCurrentTocNodeFromResult(this, result);
 }
 
 void CommandEntry::updatePrompt(const QString& postfix)
diff --git a/src/commandentry.h b/src/commandentry.h
index 12828a8e..5da8fd6e 100644
--- a/src/commandentry.h
+++ b/src/commandentry.h
@@ -39,6 +39,8 @@ class CommandEntry : public WorksheetEntry
     int type() const override;
 
     QString command();
+    const QString& commandId() const;
+    void regenerateCommandId();
     void setExpression(Cantor::Expression*);
     Cantor::Expression* expression();
 
@@ -47,6 +49,9 @@ class CommandEntry : public WorksheetEntry
     bool isEmpty() override;
     bool isExcludedFromExecution();
     bool isResultCollapsed();
+    int resultItemCount() const;
+    ResultItem* resultItemAt(int index) const;
+    ResultItem* resultItemById(const QString& resultId) const;
 
     void setContent(const QString&) override;
     void setContent(const QDomElement&, const KZip&) override;
@@ -105,6 +110,7 @@ class CommandEntry : public WorksheetEntry
 
     void populateMenu(QMenu*, QPointF) override;
     void updateAfterSettingsChanges() override;
+    void resultItemClicked(Cantor::Result* result);
   protected:
     bool wantToEvaluate() override;
 
@@ -121,6 +127,7 @@ class CommandEntry : public WorksheetEntry
     static const double VerticalSpacing;
 
     WorksheetTextItem* m_promptItem;
+    QString m_commandId;
     WorksheetTextEditorItem* m_commandItem;
     QVector<ResultItem*> m_resultItems;
     bool m_resultsCollapsed;
diff --git a/src/imageresultitem.cpp b/src/imageresultitem.cpp
index 2aa37f44..cffd2e05 100644
--- a/src/imageresultitem.cpp
+++ b/src/imageresultitem.cpp
@@ -8,11 +8,13 @@
 #include "commandentry.h"
 #include "worksheetview.h"
 #include "lib/imageresult.h"
+#include "lib/pdfresult.h"
 
 #include <config-cantor.h>
 
 #include <KLocalizedString>
 #include <QFileDialog>
+#include <QGraphicsSceneMouseEvent>
 #include <QImageReader>
 
 ImageResultItem::ImageResultItem(QGraphicsObject* parent, Cantor::Result* result)
@@ -35,7 +37,7 @@ void ImageResultItem::populateMenu(QMenu* menu, QPointF)
 
 void ImageResultItem::update()
 {
-    Q_ASSERT(m_result->type() == Cantor::ImageResult::Type);
+    Q_ASSERT(m_result->type() == Cantor::ImageResult::Type || m_result->type() == Cantor::PdfResult::Type);
     switch(m_result->type()) {
     case Cantor::ImageResult::Type:
     {
@@ -46,6 +48,9 @@ void ImageResultItem::update()
             setImage(m_result->data().value<QImage>());
     }
         break;
+    case Cantor::PdfResult::Type:
+        setImage(m_result->data().value<QImage>());
+        break;
     default:
         break;
     }
@@ -74,6 +79,8 @@ void ImageResultItem::saveResult()
         auto* imageResult = static_cast<Cantor::ImageResult*>(result());
         format = i18nc("%1 and %2 are file extensions", "%1 files (*.%2)", imageResult->extension().toUpper(), imageResult->extension());
     }
+    else if (m_result->type() == Cantor::PdfResult::Type)
+        format = i18n("PDF files (*.pdf)");
     else
         format = i18n("EPS files (*.eps)");
 
@@ -89,3 +96,11 @@ void ImageResultItem::deleteLater()
 {
     WorksheetImageItem::deleteLater();
 }
+
+void ImageResultItem::mousePressEvent(QGraphicsSceneMouseEvent* event)
+{
+    if (auto* commandEntry = parentEntry())
+        commandEntry->resultItemClicked(m_result);
+
+    WorksheetImageItem::mousePressEvent(event);
+}
diff --git a/src/imageresultitem.h b/src/imageresultitem.h
index 6592863d..c642074e 100644
--- a/src/imageresultitem.h
+++ b/src/imageresultitem.h
@@ -10,6 +10,7 @@
 #include "worksheetimageitem.h"
 
 class CommandEntry;
+class QGraphicsSceneMouseEvent;
 
 class ImageResultItem : public WorksheetImageItem, public ResultItem
 {
@@ -30,6 +31,9 @@ class ImageResultItem : public WorksheetImageItem, public ResultItem
 
     void deleteLater() override;
 
+  protected:
+    void mousePressEvent(QGraphicsSceneMouseEvent* event) override;
+
   protected Q_SLOTS:
     void saveResult();
 };
diff --git a/src/resultitem.cpp b/src/resultitem.cpp
index f04af635..dc49939e 100644
--- a/src/resultitem.cpp
+++ b/src/resultitem.cpp
@@ -14,6 +14,7 @@
 #include "lib/latexresult.h"
 #include "lib/imageresult.h"
 #include "lib/animationresult.h"
+#include "lib/pdfresult.h"
 #include "lib/mimeresult.h"
 #include "lib/htmlresult.h"
 
@@ -26,9 +27,17 @@ ResultItem::ResultItem(Cantor::Result* result):
 
 ResultItem* ResultItem::create(WorksheetEntry* parent, Cantor::Result* result)
 {
-    switch(result->type()) {
+    if (!result)
+        return nullptr;
+
+    if (result->type() == Cantor::LatexResult::Type)
+        return new TextResultItem(parent, result);
+
+    if (dynamic_cast<Cantor::PdfResult*>(result))
+        return new ImageResultItem(parent, result);
+
+    switch (result->type()) {
     case Cantor::TextResult::Type:
-    case Cantor::LatexResult::Type:
     case Cantor::MimeResult::Type:
     case Cantor::HtmlResult::Type:
         return new TextResultItem(parent, result);
@@ -64,7 +73,15 @@ Cantor::Result* ResultItem::result()
     return m_result;
 }
 
+void ResultItem::setResult(Cantor::Result* result)
+{
+    m_result = result;
+}
+
 void ResultItem::needRemove()
 {
+    if (!m_result)
+        return;
+
     parentEntry()->removeResult(m_result);
 }
diff --git a/src/resultitem.h b/src/resultitem.h
index f05d0d95..a4e84779 100644
--- a/src/resultitem.h
+++ b/src/resultitem.h
@@ -48,6 +48,7 @@ class ResultItem
     virtual void updateTheme() {};
     QGraphicsObject* graphicsObject();
     Cantor::Result* result();
+    void setResult(Cantor::Result* result);
     CommandEntry* parentEntry();
 
   protected:
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.