[education/cantor] src: Add worksheet TOC navigation model

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

Add worksheet TOC navigation model

M  +84   -21   src/hierarchyentry.cpp
M  +8    -4    src/hierarchyentry.h
M  +2504 -1270 src/worksheet.cpp
M  +73   -4    src/worksheet.h
M  +1    -2    src/worksheetentry.cpp
M  +4    -1    src/worksheettexteditoritem.cpp
M  +2    -0    src/worksheettextitem.cpp
M  +24   -14   src/worksheetview.cpp
M  +1    -0    src/worksheetview.h

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

diff --git a/src/hierarchyentry.cpp b/src/hierarchyentry.cpp
index 86acd511..87c4d8a8 100644
--- a/src/hierarchyentry.cpp
+++ b/src/hierarchyentry.cpp
@@ -16,6 +16,7 @@
 #include <QPainter>
 #include <QDebug>
 #include <QActionGroup>
+#include <QUuid>
 
 #include <KLocalizedString>
 
@@ -26,18 +27,25 @@ HierarchyEntry::HierarchyEntry(Worksheet* worksheet) : WorksheetEntry(worksheet)
     , m_textItem(new WorksheetTextItem(this, Qt::TextEditorInteraction))
     , m_depth(HierarchyLevel::Chapter)
     , m_hierarchyNumber(1)
+    , m_hierarchyId(QUuid::createUuid().toString(QUuid::WithoutBraces))
     , m_hidedSubentries(nullptr)
 {
     // Font and sizes should be regulated from future Settings "Styles" option
     m_textItem->enableRichText(false);
+    connect(m_textItem->document(), &QTextDocument::contentsChanged, this, [this]() {
+        auto* ws = this->worksheet();
+        if (!ws || ws->isLoadingFromFile())
+            return;
+
+        ws->updateHierarchyLayout();
+        ws->setModified();
+    });
 
     connect(m_textItem, &WorksheetTextItem::moveToPrevious, this, &HierarchyEntry::moveToPreviousEntry);
     connect(m_textItem, &WorksheetTextItem::moveToNext, this, &HierarchyEntry::moveToNextEntry);
     // Modern syntax of signal/stots don't work on this connection (arguments don't match)
     connect(m_textItem, SIGNAL(execute()), this, SLOT(evaluate()));
 
-    connect(this, &HierarchyEntry::hierarhyEntryNameChange, worksheet, &Worksheet::hierarhyEntryNameChange);
-
     connect(&m_controlElement, &WorksheetControlItem::doubleClick, this, &HierarchyEntry::handleControlElementDoubleClick);
 
     m_setLevelActionGroup = new QActionGroup(this);
@@ -91,6 +99,30 @@ bool HierarchyEntry::focusEntry(int pos, qreal xCoord)
     return true;
 }
 
+bool HierarchyEntry::hasHiddenSubentries() const
+{
+    return m_hidedSubentries != nullptr;
+}
+
+WorksheetEntry* HierarchyEntry::hiddenSubentries() const
+{
+    return m_hidedSubentries;
+}
+
+WorksheetEntry* HierarchyEntry::takeHiddenSubentries()
+{
+    WorksheetEntry* hiddenSubentries = m_hidedSubentries;
+
+    m_hidedSubentries = nullptr;
+
+    if (hiddenSubentries)
+    {
+        m_controlElement.isCollapsed = false;
+        m_controlElement.update();
+    }
+
+    return hiddenSubentries;
+}
 
 void HierarchyEntry::setContent(const QString& content)
 {
@@ -104,6 +136,10 @@ void HierarchyEntry::setContent(const QDomElement& content, const KZip& file)
         return;
 
     m_textItem->setPlainText(content.firstChildElement(QLatin1String("body")).text());
+    const QString storedHierarchyId = content.attribute(QLatin1String("hierarchy-id"));
+
+    if (!storedHierarchyId.isEmpty())
+        m_hierarchyId = storedHierarchyId;
 
     const QDomElement& subentriesMainElem = content.firstChildElement(QLatin1String("HidedSubentries"));
     if (!subentriesMainElem.isNull())
@@ -157,6 +193,10 @@ void HierarchyEntry::setContentFromJupyter(const QJsonObject& cell)
 
         m_depth = (HierarchyLevel)cantorMetadata.value(QLatin1String("level")).toInt();
         m_hierarchyNumber= cantorMetadata.value(QLatin1String("level-number")).toInt();
+        const QString storedHierarchyId = cantorMetadata.value(QLatin1String("hierarchy-id")).toString();
+
+        if (!storedHierarchyId.isEmpty())
+            m_hierarchyId = storedHierarchyId;
 
         updateFonts(true);
     }
@@ -188,6 +228,7 @@ QJsonValue HierarchyEntry::toJupyterJson()
 
     cantorMetadata.insert(QLatin1String("level"), (int)m_depth);
     cantorMetadata.insert(QLatin1String("level-number"), m_hierarchyNumber);
+    cantorMetadata.insert(QLatin1String("hierarchy-id"), m_hierarchyId);
 
     // Don't store subentriesMainElem, because actually too complex
     // Maybe this is a place for future work
@@ -239,6 +280,7 @@ QDomElement HierarchyEntry::toXml(QDomDocument& doc, KZip* archive)
 
     el.setAttribute(QLatin1String("level"), (int)m_depth);
     el.setAttribute(QLatin1String("level-number"), m_hierarchyNumber);
+    el.setAttribute(QLatin1String("hierarchy-id"), m_hierarchyId);
 
     return el;
 }
@@ -259,9 +301,7 @@ QString HierarchyEntry::toPlain(const QString& commandSep, const QString& commen
 
 bool HierarchyEntry::evaluate(EvaluationOption evalOp)
 {
-    Q_EMIT hierarhyEntryNameChange(text(), hierarchyText(), ((int)m_depth)-1);
     evaluateNext(evalOp);
-
     return true;
 }
 
@@ -337,6 +377,16 @@ HierarchyEntry::HierarchyLevel HierarchyEntry::level() const
     return m_depth;
 }
 
+const QString& HierarchyEntry::hierarchyId() const
+{
+    return m_hierarchyId;
+}
+
+void HierarchyEntry::regenerateHierarchyId()
+{
+    m_hierarchyId = QUuid::createUuid().toString(QUuid::WithoutBraces);
+}
+
 void HierarchyEntry::setLevel(HierarchyEntry::HierarchyLevel depth)
 {
     m_depth = depth;
@@ -417,32 +467,40 @@ void HierarchyEntry::recalculateControlGeometry()
 void HierarchyEntry::startDrag(QPointF grabPos)
 {
     // We need reset entry cursor manually, because otherwise the entry cursor will be visible on draggable item
+    if (worksheet()->expandHierarchyForStructureChange(this))
+    {
+        worksheet()->updateHierarchyLayout();
+        worksheet()->updateLayout();
+    }
+
     worksheet()->resetEntryCursor();
 
-    QDrag* drag = new QDrag(worksheetView());
+    auto* drag = new QDrag(worksheetView());
     const qreal scale = worksheet()->renderer()->scale();
+    const QRectF hierarchyBound(boundingRect().x(), boundingRect().y(), boundingRect().width(), m_controlElement.boundingRect().height());
+    const QSizeF hierarchyZoneSize(size().width(), m_controlElement.boundingRect().height());
 
-    QRectF hierarchyBound(boundingRect().x(), boundingRect().y(), boundingRect().width(), m_controlElement.boundingRect().height());
-    QSizeF hierarchyZoneSize(size().width(), m_controlElement.boundingRect().height());
-
-    QPixmap pixmap((hierarchyZoneSize*scale).toSize());
+    QPixmap pixmap((hierarchyZoneSize * scale).toSize());
     pixmap.fill(QColor(255, 255, 255, 0));
 
     QPainter painter(&pixmap);
+
     const QRectF sceneRect = mapRectToScene(hierarchyBound);
     worksheet()->render(&painter, pixmap.rect(), sceneRect);
+
     painter.end();
+    const QBitmap mask = pixmap.createMaskFromColor(QColor(255, 255, 255), Qt::MaskInColor);
 
-    QBitmap mask = pixmap.createMaskFromColor(QColor(255, 255, 255), Qt::MaskInColor);
     pixmap.setMask(mask);
-
     drag->setPixmap(pixmap);
-    if (grabPos.isNull()) {
+
+    if (grabPos.isNull())
+    {
         const QPointF scenePos = worksheetView()->sceneCursorPos();
         drag->setHotSpot((mapFromScene(scenePos) * scale).toPoint());
-    } else {
-        drag->setHotSpot((grabPos * scale).toPoint());
     }
+    else
+        drag->setHotSpot((grabPos * scale).toPoint());
     drag->setMimeData(new QMimeData());
 
     worksheet()->startDragWithHierarchy(this, drag, hierarchyZoneSize);
@@ -527,25 +585,30 @@ void HierarchyEntry::updateFonts(bool force)
     }
 }
 
-
 void HierarchyEntry::handleControlElementDoubleClick()
 {
-    qDebug() << "HierarchyEntry::handleControlElementDoubleClick";
     if (m_controlElement.isCollapsed)
     {
-        worksheet()->insertSubentriesForHierarchy(this, m_hidedSubentries);
-        m_controlElement.isCollapsed = false;
+        WorksheetEntry* hiddenSubentries = takeHiddenSubentries();
+
+        if (hiddenSubentries)
+            worksheet()->insertSubentriesForHierarchy(this, hiddenSubentries);
     }
     else
     {
-        m_hidedSubentries = worksheet()->cutSubentriesForHierarchy(this);
-        m_controlElement.isCollapsed = true;
+        WorksheetEntry* hiddenSubentries = worksheet()->cutSubentriesForHierarchy(this);
+
+        if (hiddenSubentries)
+        {
+            m_hidedSubentries = hiddenSubentries;
+            m_controlElement.isCollapsed = true;
+        }
     }
 
     m_controlElement.update();
 
-    worksheet()->updateLayout();
     worksheet()->updateHierarchyLayout();
+    worksheet()->updateLayout();
 }
 
 void HierarchyEntry::updateAfterSettingsChanges()
diff --git a/src/hierarchyentry.h b/src/hierarchyentry.h
index e263aea0..57047070 100644
--- a/src/hierarchyentry.h
+++ b/src/hierarchyentry.h
@@ -39,8 +39,14 @@ class HierarchyEntry : public WorksheetEntry
     enum {Type = UserType + 9};
     int type() const override;
 
+    bool hasHiddenSubentries() const;
+    WorksheetEntry* hiddenSubentries() const;
+    WorksheetEntry* takeHiddenSubentries();
+
     QString text() const;
     QString hierarchyText() const;
+    const QString& hierarchyId() const;
+    void regenerateHierarchyId();
 
     HierarchyLevel level() const;
     void setLevel(HierarchyLevel);
@@ -76,9 +82,6 @@ class HierarchyEntry : public WorksheetEntry
 
     void startDrag(QPointF grabPos = QPointF()) override;
 
-  Q_SIGNALS:
-    void hierarhyEntryNameChange(QString name, QString searchName, int depth);
-
   public Q_SLOTS:
     bool evaluate(WorksheetEntry::EvaluationOption evalOp = FocusNext) override;
     void updateEntry() override;
@@ -97,14 +100,15 @@ class HierarchyEntry : public WorksheetEntry
     void updateFonts(bool force = false);
 
   private:
-
     WorksheetTextItem* m_hierarchyLevelItem;
     WorksheetTextItem* m_textItem;
     HierarchyLevel m_depth;
     int m_hierarchyNumber;
+    QString m_hierarchyId;
     QActionGroup* m_setLevelActionGroup;
     QMenu* m_setLevelMenu;
     WorksheetEntry* m_hidedSubentries;
+
 };
 
 #endif // HIEARARCHYENTRY_H
diff --git a/src/worksheet.cpp b/src/worksheet.cpp
index 0a60a30d..4aa7a198 100644
--- a/src/worksheet.cpp
+++ b/src/worksheet.cpp
@@ -14,19 +14,25 @@
 #include "markdownentry.h"
 #include "pagebreakentry.h"
 #include "placeholderentry.h"
+#include "resultitem.h"
 #include "settings.h"
 #include "textentry.h"
 #include "worksheetview.h"
-#include "lib/jupyterutils.h"
+#include "lib/animationresult.h"
 #include "lib/backend.h"
 #include "lib/extension.h"
 #include "lib/helpresult.h"
+#include "lib/imageresult.h"
+#include "lib/jupyterutils.h"
+#include "lib/pdfresult.h"
+#include "lib/result.h"
 #include "lib/session.h"
 
 #include <config-cantor.h>
 
 #include <QApplication>
 #include <QBuffer>
+#include <QByteArray>
 #include <QDrag>
 #include <QGraphicsSceneMouseEvent>
 #include <QJsonArray>
@@ -37,6 +43,8 @@
 #include <QTimer>
 #include <QActionGroup>
 #include <QFile>
+#include <QScopedValueRollback>
+#include <QSet>
 
 #include <KMessageBox>
 #include <KActionCollection>
@@ -44,6 +52,7 @@
 #include <KFontSizeAction>
 #include <KToggleAction>
 #include <KLocalizedString>
+
 #include <KZip>
 #include <KSyntaxHighlighting/Repository>
 
@@ -51,6 +60,14 @@
 #include <libxslt/xsltutils.h>
 #include <libxslt/transform.h>
 
+namespace
+{
+    const QLatin1String TocNodeTypeChapter("chapter");
+    const QLatin1String TocNodeTypeSection("section");
+    const QLatin1String TocNodeTypeCommand("command");
+    const QLatin1String TocNodeTypePlot("plot");
+}
+
 const double Worksheet::LeftMargin = 4;
 const double Worksheet::RightMargin = 4;
 const double Worksheet::TopMargin = 12;
@@ -134,7 +151,12 @@ Worksheet::~Worksheet()
     // This is necessary, because a SearchBar might access firstEntry()
     // while the scene is deleted. Maybe there is a better solution to
     // this problem, but I can't seem to find it.
+    if (m_firstEntry)
+        disconnect(m_firstEntry, &WorksheetEntry::aboutToBeDeleted, this, &Worksheet::invalidateFirstEntry);
+    if (m_lastEntry)
+        disconnect(m_lastEntry, &WorksheetEntry::aboutToBeDeleted, this, &Worksheet::invalidateLastEntry);
     m_firstEntry = nullptr;
+    m_lastEntry = nullptr;
 
     if (m_session)
     {
@@ -245,1191 +267,1841 @@ void Worksheet::setViewSize(qreal w, qreal h, qreal s, bool forceUpdate)
 
 void Worksheet::updateLayout()
 {
+    QScopedValueRollback<bool> layoutGuard(m_layoutUpdateInProgress, true);
     bool cursorRectVisible = false;
     bool atEnd = worksheetView()->isAtEnd();
-    if (currentTextItem()) {
-        QRectF cursorRect = currentTextItem()->sceneCursorRect();
+
+    if (currentTextItem())
+    {
+        const QRectF cursorRect = currentTextItem()->sceneCursorRect();
         cursorRectVisible = worksheetView()->isVisible(cursorRect);
     }
 
     m_maxPromptWidth = 0;
-    if (Settings::useOldCantorEntriesIndent() == false)
+
+    if (!Settings::useOldCantorEntriesIndent())
     {
         for (auto* entry = firstEntry(); entry; entry = entry->next())
+        {
             if (entry->type() == CommandEntry::Type)
                 m_maxPromptWidth = std::max(static_cast<CommandEntry*>(entry)->promptItemWidth(), m_maxPromptWidth);
             else if (entry->type() == HierarchyEntry::Type)
                 m_maxPromptWidth = std::max(static_cast<HierarchyEntry*>(entry)->hierarchyItemWidth(), m_maxPromptWidth);
+        }
     }
 
-   const qreal w = m_viewWidth - LeftMargin - RightMargin;
+    // Hierarchy controls are hidden while printing.
+    const qreal hierarchyControlsWidth = m_isPrinting ? 0.0 : static_cast<qreal>(m_hierarchyMaxDepth) * (WorksheetEntry::ControlElementWidth + WorksheetEntry::ControlElementBorder);
+
+    const qreal w = m_viewWidth - LeftMargin - RightMargin - hierarchyControlsWidth;
+
     qreal y = TopMargin;
     const qreal x = LeftMargin;
+
     for (auto* entry = firstEntry(); entry; entry = entry->next())
-        y += entry->setGeometry(x, x+m_maxPromptWidth, y, w);
+        y += entry->setGeometry(x, x + m_maxPromptWidth, y, w);
 
     updateHierarchyControlsLayout();
 
     setSceneRect(QRectF(0, 0, m_viewWidth, y));
+
     if (cursorRectVisible)
         makeVisible(worksheetCursor());
     else if (atEnd)
         worksheetView()->scrollToEnd();
+
     drawEntryCursor();
 }
 
-void Worksheet::updateHierarchyLayout()
+void Worksheet::refreshTocStructure()
 {
-    QStringList names;
-    QStringList searchStrings;
-    QList<int> depths;
+    if (m_isClosing || m_isLoadingFromFile)
+        return;
 
-    m_hierarchyMaxDepth = 0;
-    std::vector<int> hierarchyNumbers;
-    for (auto* entry = firstEntry(); entry; entry = entry->next())
+    m_tocRefreshScheduled = false;
+    m_tocNodeSnapshot = collectTocNodes();
+
+    if (!m_currentTocNodeId.isEmpty())
     {
-        if (entry->type() == HierarchyEntry::Type)
+        bool currentNodeStillExists = false;
+        for (const QVariant& nodeValue : m_tocNodeSnapshot)
         {
-            auto* hierarchEntry = static_cast<HierarchyEntry*>(entry);
-            hierarchEntry->updateHierarchyLevel(hierarchyNumbers);
-            m_hierarchyMaxDepth = std::max(m_hierarchyMaxDepth, hierarchyNumbers.size());
-
-            names.append(hierarchEntry->text());
-            searchStrings.append(hierarchEntry->hierarchyText());
-            depths.append(static_cast<int>(hierarchyNumbers.size()) - 1);
+            if (nodeValue.toMap().value(QStringLiteral("id")).toString() == m_currentTocNodeId)
+            {
+                currentNodeStillExists = true;
+                break;
+            }
         }
+
+        if (!currentNodeStillExists)
+            setCurrentTocNode(QString());
     }
 
-    Q_EMIT hierarchyChanged(names, searchStrings, depths);
+    Q_EMIT tocNodesChanged(m_tocNodeSnapshot);
 }
 
-void Worksheet::updateHierarchyControlsLayout(WorksheetEntry* startEntry)
+void Worksheet::scheduleTocStructureRefresh()
+{
+    if (m_isClosing || m_isLoadingFromFile || m_tocRefreshScheduled)
+        return;
+
+    m_tocRefreshScheduled = true;
+    QTimer::singleShot(0, this, [this]()
+    {
+        if (!m_tocRefreshScheduled)
+            return;
+
+        refreshTocStructure();
+    });
+}
+
+void Worksheet::emitTocNodeSnapshot()
 {
-    if (startEntry == nullptr)
-        startEntry = firstEntry();
+    if (m_isClosing)
+        return;
 
-     // Update sizes of control elements for hierarchy entries
-    std::vector<HierarchyEntry*> levelsEntries;
-    const int numerationBegin = (int)HierarchyEntry::HierarchyLevel::Chapter;
-    for (int i = numerationBegin; i < (int)HierarchyEntry::HierarchyLevel::EndValue; i++)
-        levelsEntries.push_back(nullptr);
+    if (m_tocNodeSnapshot.isEmpty() && firstEntry())
+        m_tocNodeSnapshot = collectTocNodes();
+
+    Q_EMIT tocNodesChanged(m_tocNodeSnapshot);
+}
+
+QVariantList Worksheet::collectTocNodes()
+{
+    QVariantList nodes;
+    QVector<QString> hierarchyNodeIds;
+    QVector<int> hierarchyDepths;
 
-    for (auto* entry = startEntry; entry; entry = entry->next())
+    visitLogicalEntries([&](WorksheetEntry* entry)
     {
         if (entry->type() == HierarchyEntry::Type)
         {
-            HierarchyEntry* hierarchyEntry = static_cast<HierarchyEntry*>(entry);
-            int idx = (int)hierarchyEntry->level() - numerationBegin;
-            if (levelsEntries[idx] == nullptr)
+            auto* hierarchyEntry = static_cast<HierarchyEntry*>(entry);
+            const int depth = static_cast<int>(hierarchyEntry->level()) - static_cast<int>(HierarchyEntry::HierarchyLevel::Chapter);
+
+            while (!hierarchyDepths.isEmpty() && hierarchyDepths.last() >= depth)
             {
-                levelsEntries[idx] = hierarchyEntry;
+                hierarchyDepths.removeLast();
+                hierarchyNodeIds.removeLast();
             }
-            else
+
+            const QString parentNodeId = hierarchyNodeIds.isEmpty() ? QString() : hierarchyNodeIds.last();
+            const QString hierarchyId = hierarchyEntry->hierarchyId();
+            const QString title = hierarchyEntry->text();
+            const QString displayText = hierarchyEntry->hierarchyText().isEmpty()
+                ? hierarchyEntry->text()
+                : hierarchyEntry->hierarchyText() + QLatin1Char(' ') + hierarchyEntry->text();
+
+            QVariantMap node;
+            node.insert(QStringLiteral("id"), hierarchyId);
+            node.insert(QStringLiteral("parentId"), parentNodeId);
+            node.insert(QStringLiteral("type"), depth == 0 ? QString(TocNodeTypeChapter) : QString(TocNodeTypeSection));
+            node.insert(QStringLiteral("title"), title);
+            node.insert(QStringLiteral("displayText"), displayText);
+            node.insert(QStringLiteral("hierarchyText"), hierarchyEntry->hierarchyText());
+            node.insert(QStringLiteral("depth"), depth);
+            node.insert(QStringLiteral("editable"), true);
+            node.insert(QStringLiteral("navigable"), true);
+            node.insert(QStringLiteral("hierarchyId"), hierarchyId);
+            node.insert(QStringLiteral("resultIndex"), -1);
+            node.insert(QStringLiteral("entryId"), hierarchyId);
+            nodes.append(node);
+
+            hierarchyNodeIds.append(hierarchyId);
+            hierarchyDepths.append(depth);
+        }
+        else if (entry->type() == CommandEntry::Type)
+        {
+            auto* commandEntry = static_cast<CommandEntry*>(entry);
+            const QString parentNodeId = hierarchyNodeIds.isEmpty() ? QString() : hierarchyNodeIds.last();
+            const QString commandNodeId = buildCommandNodeId(commandEntry);
+
+            QVariantMap commandNode;
+            commandNode.insert(QStringLiteral("id"), commandNodeId);
+            commandNode.insert(QStringLiteral("parentId"), parentNodeId);
+            commandNode.insert(QStringLiteral("type"), QString(TocNodeTypeCommand));
+            commandNode.insert(QStringLiteral("title"), commandTocTitle());
+            commandNode.insert(QStringLiteral("displayText"), commandTocDisplayText(commandEntry));
+            commandNode.insert(QStringLiteral("hierarchyText"), QString());
+            commandNode.insert(QStringLiteral("depth"), hierarchyDepths.isEmpty() ? 0 : hierarchyDepths.last() + 1);
+            commandNode.insert(QStringLiteral("editable"), false);
+            commandNode.insert(QStringLiteral("navigable"), true);
+            commandNode.insert(QStringLiteral("hierarchyId"), QString());
+            commandNode.insert(QStringLiteral("resultIndex"), -1);
+            commandNode.insert(QStringLiteral("entryId"), commandEntry->commandId());
+            nodes.append(commandNode);
+
+            if (auto* expression = commandEntry->expression())
             {
-                for (int i = idx; i < (int)levelsEntries.size(); i++)
-                    if (levelsEntries[i] != nullptr)
-                    {
-                        bool haveSubelements = levelsEntries[i]->next() ? levelsEntries[i]->next() != entry : false;
-                        levelsEntries[i]->updateControlElementForHierarchy(hierarchyEntry->y() - WorksheetEntry::VerticalMargin, m_hierarchyMaxDepth, haveSubelements);
-                        levelsEntries[i] = nullptr;
-                    }
-                levelsEntries[idx] = hierarchyEntry;
+                const auto& results = expression->results();
+                int plotCount = 0;
+                for (auto* result : results)
+                {
+                    if (isPlotResult(result))
+                        ++plotCount;
+                }
+
+                int plotOrdinal = 0;
+                for (int index = 0; index < results.size(); ++index)
+                {
+                    auto* result = results.at(index);
+                    if (!isPlotResult(result))
+                        continue;
+
+                    ++plotOrdinal;
+
+                    QVariantMap plotNode;
+                    const QString customTitle = result->displayName();
+                    plotNode.insert(QStringLiteral("id"), buildPlotNodeId(commandEntry->commandId(), result->resultId()));
+                    plotNode.insert(QStringLiteral("parentId"), commandNodeId);
+                    plotNode.insert(QStringLiteral("type"), QString(TocNodeTypePlot));
+                    plotNode.insert(QStringLiteral("title"), plotTocTitle(result));
+                    plotNode.insert(QStringLiteral("customTitle"), customTitle);
+                    plotNode.insert(QStringLiteral("displayText"), plotTocDisplayText(commandEntry, result, plotOrdinal, plotCount));
+                    plotNode.insert(QStringLiteral("hierarchyText"), QString());
+                    plotNode.insert(QStringLiteral("depth"), hierarchyDepths.isEmpty() ? 1 : hierarchyDepths.last() + 2);
+                    plotNode.insert(QStringLiteral("editable"), true);
+                    plotNode.insert(QStringLiteral("navigable"), true);
+                    plotNode.insert(QStringLiteral("hierarchyId"), QString());
+                    plotNode.insert(QStringLiteral("resultIndex"), index);
+                    plotNode.insert(QStringLiteral("resultId"), result->resultId());
+                    plotNode.insert(QStringLiteral("entryId"), commandEntry->commandId());
+                    nodes.append(plotNode);
+                }
             }
         }
-    }
+        return true;
+    });
 
-    if (lastEntry())
-        for (int i = 0; i < (int)levelsEntries.size(); i++)
-            if (levelsEntries[i] != nullptr)
-            {
-                bool haveSubelements = levelsEntries[i] != lastEntry();
-                levelsEntries[i]->updateControlElementForHierarchy(lastEntry()->y() + lastEntry()->size().height() - WorksheetEntry::VerticalMargin, m_hierarchyMaxDepth, haveSubelements);
-                levelsEntries[i] = nullptr;
-            }
+    return nodes;
 }
 
-std::vector<WorksheetEntry*> Worksheet::hierarchySubelements(HierarchyEntry* hierarchyEntry) const
+QString Worksheet::buildCommandNodeId(CommandEntry* entry)
 {
-    std::vector<WorksheetEntry*> subentries;
+    if (!entry)
+        return QString();
 
-    Q_ASSERT(hierarchyEntry);
+    const QString& commandId = entry->commandId();
+    if (commandId.isEmpty())
+        return QString();
 
-    bool subentriesEnd = false;
-    const int level = (int)hierarchyEntry->level();
-    for (auto* entry = hierarchyEntry->next(); entry && !subentriesEnd; entry = entry->next())
-    {
-        if (entry->type() == HierarchyEntry::Type)
-        {
-            if ((int)(static_cast<HierarchyEntry*>(entry)->level()) <= level)
-                subentriesEnd = true;
-            else
-                subentries.push_back(entry);
-        }
-        else
-            subentries.push_back(entry);
-    }
-    return subentries;
+    return QStringLiteral("command:") + commandId;
 }
 
-void Worksheet::updateEntrySize(WorksheetEntry* entry)
+QString Worksheet::buildPlotNodeId(const QString& commandId, const QString& resultId) const
 {
-    bool cursorRectVisible = false;
-    bool atEnd = worksheetView()->isAtEnd();
-    if (currentTextItem()) {
-        QRectF cursorRect = currentTextItem()->sceneCursorRect();
-        cursorRectVisible = worksheetView()->isVisible(cursorRect);
-    }
+    if (commandId.isEmpty() || resultId.isEmpty())
+        return QString();
 
-    if (Settings::useOldCantorEntriesIndent() == false)
-    {
-        qreal newMaxPromptWidth = m_maxPromptWidth;
-        if (entry->type() == CommandEntry::Type)
-            newMaxPromptWidth = static_cast<CommandEntry*>(entry)->promptItemWidth();
-        else if (entry->type() == HierarchyEntry::Type)
-            newMaxPromptWidth = static_cast<HierarchyEntry*>(entry)->hierarchyItemWidth();
+    return QStringLiteral("plot:%1:%2").arg(commandId, resultId);
+}
 
-        // If width of prompt (if presence) of the entry more, is less than that of current
-        // maximum, then we need full layout update
-        if (newMaxPromptWidth > m_maxPromptWidth)
-        {
-            updateLayout();
-            return;
-        }
-    }
+bool Worksheet::parseCommandNodeId(const QString& nodeId, QString* commandId) const
+{
+    const QString prefix = QStringLiteral("command:");
+    if (!nodeId.startsWith(prefix))
+        return false;
 
-    qreal y = entry->y() + entry->size().height();
-    for (entry = entry->next(); entry; entry = entry->next()) {
-        entry->setY(y);
-        y += entry->size().height();
-    }
+    const QString parsedCommandId = nodeId.mid(prefix.size());
+    if (parsedCommandId.isEmpty() || parsedCommandId.contains(QLatin1Char(':')))
+        return false;
 
-    if (!m_isLoadingFromFile)
-        updateHierarchyControlsLayout(entry);
+    if (commandId)
+        *commandId = parsedCommandId;
 
-    setSceneRect(QRectF(0, 0, sceneRect().width(), y));
-    if (cursorRectVisible)
-        makeVisible(worksheetCursor());
-    else if (atEnd)
-        worksheetView()->scrollToEnd();
-    drawEntryCursor();
+    return true;
 }
 
-void Worksheet::setRequestedWidth(QGraphicsObject* object, qreal width)
+bool Worksheet::parsePlotNodeId(const QString& nodeId, QString* commandId, QString* resultId) const
 {
-    qreal oldWidth = m_itemWidths[object];
-    m_itemWidths[object] = width;
-
-    if (width > m_maxWidth || oldWidth == m_maxWidth)
-    {
-        m_maxWidth = width;
-        qreal y = lastEntry() ? lastEntry()->size().height() + lastEntry()->y() : 0;
-        setSceneRect(QRectF(0, 0, m_maxWidth + LeftMargin + RightMargin, y));
-    }
-}
+    const QString prefix = QStringLiteral("plot:");
+    if (!nodeId.startsWith(prefix))
+        return false;
 
-void Worksheet::removeRequestedWidth(QGraphicsObject* object)
-{
-    if (!m_itemWidths.contains(object))
-        return;
+    const QStringList parts = nodeId.mid(prefix.size()).split(QLatin1Char(':'));
+    if (parts.size() != 2 || parts.at(0).isEmpty() || parts.at(1).isEmpty())
+        return false;
 
-    qreal width = m_itemWidths[object];
-    m_itemWidths.remove(object);
+    if (commandId)
+        *commandId = parts.at(0);
+    if (resultId)
+        *resultId = parts.at(1);
 
-    if (width == m_maxWidth)
-    {
-        m_maxWidth = 0;
-        for (qreal width : m_itemWidths.values())
-            if (width > m_maxWidth)
-                m_maxWidth = width;
-        qreal y = lastEntry() ? lastEntry()->size().height() + lastEntry()->y() : 0;
-        setSceneRect(QRectF(0, 0, m_maxWidth + LeftMargin + RightMargin, y));
-    }
+    return true;
 }
 
-bool Worksheet::isEmpty()
+QString Worksheet::commandTocTitle() const
 {
-    return !m_firstEntry;
+    return i18n("Command");
 }
 
-bool Worksheet::isLoadingFromFile()
+QString Worksheet::commandTocDisplayText(CommandEntry* entry) const
 {
-    return m_isLoadingFromFile;
+    auto* expression = entry ? entry->expression() : nullptr;
+    if (m_showExpressionIds && expression && expression->id() != -1)
+        return i18n("Command %1", expression->id());
+
+    return commandTocTitle();
 }
 
-void Worksheet::makeVisible(WorksheetEntry* entry)
+QString Worksheet::plotTocTitle(Cantor::Result* result) const
 {
-    QRectF r = entry->boundingRect();
-    r = entry->mapRectToScene(r);
-    r.adjust(0, -10, 0, 10);
-    worksheetView()->makeVisible(r);
+    if (result && !result->displayName().isEmpty())
+        return result->displayName();
+
+    return i18n("Plot");
 }
 
-void Worksheet::makeVisible(const KWorksheetCursor& cursor)
+QString Worksheet::plotTocDisplayText(CommandEntry* entry, Cantor::Result* result, int plotOrdinal, int plotCount) const
 {
-    if(!cursor.cursor().isValid())
+    const QString title = plotTocTitle(result);
+    auto* expression = entry ? entry->expression() : nullptr;
+    if (m_showExpressionIds && expression && expression->id() != -1)
     {
-        if(cursor.entry())
-            makeVisible(cursor.entry());
-        return;
+        if (plotCount > 1)
+            return i18n("%1 %2.%3", title, expression->id(), plotOrdinal);
+
+        return i18n("%1 %2", title, expression->id());
     }
-    QRectF r = cursor.textItem()->sceneCursorRect();
-    QRectF er = cursor.entry()->boundingRect();
-    er = cursor.entry()->mapRectToScene(er);
-    er.adjust(0, -10, 0, 10);
-    r.adjust(0, qMax(qreal(-100.0), er.top() - r.top()),
-             0, qMin(qreal(100.0), er.bottom() - r.bottom()));
-    worksheetView()->makeVisible(r);
+
+    if (plotCount > 1)
+        return i18n("%1 %2", title, plotOrdinal);
+
+    return title;
 }
 
-void Worksheet::makeVisible(const WorksheetCursor& cursor)
+bool Worksheet::isPlotResult(Cantor::Result* result)
 {
-    if (cursor.textCursor().isNull()) {
-        if (cursor.entry())
-            makeVisible(cursor.entry());
-        return;
-    }
-    QRectF r = cursor.textItem()->sceneCursorRect(cursor.textCursor());
-    QRectF er = cursor.entry()->boundingRect();
-    er = cursor.entry()->mapRectToScene(er);
-    er.adjust(0, -10, 0, 10);
-    r.adjust(0, qMax(qreal(-100.0), er.top() - r.top()),
-             0, qMin(qreal(100.0), er.bottom() - r.bottom()));
-    worksheetView()->makeVisible(r);
+    if (!result || result->role() != Cantor::Result::Role::Plot)
+        return false;
+
+    if (result->type() == Cantor::ImageResult::Type)
+        return true;
+
+    if (result->type() == Cantor::AnimationResult::Type)
+        return true;
+
+    return result->type() == Cantor::PdfResult::Type && dynamic_cast<Cantor::PdfResult*>(result);
 }
 
-WorksheetView* Worksheet::worksheetView()
+bool Worksheet::visitLogicalEntries(WorksheetEntry* first, const std::function<bool(WorksheetEntry*)>& visitor)
 {
-    return static_cast<WorksheetView*>(views().first());
+    for (auto* entry = first; entry; entry = entry->next())
+    {
+        if (!visitor(entry))
+            return false;
+
+        if (entry->type() != HierarchyEntry::Type)
+            continue;
+
+        auto* hierarchyEntry = static_cast<HierarchyEntry*>(entry);
+        if (auto* hiddenEntry = hierarchyEntry->hiddenSubentries())
+        {
+            if (!visitLogicalEntries(hiddenEntry, visitor))
+                return false;
+        }
+    }
+
+    return true;
 }
 
-void Worksheet::setModified()
+bool Worksheet::visitLogicalEntries(const std::function<bool(WorksheetEntry*)>& visitor)
 {
-    if (!m_isClosing && !m_isLoadingFromFile)
-        Q_EMIT modified();
+    return visitLogicalEntries(firstEntry(), visitor);
 }
 
-KWorksheetCursor Worksheet::worksheetCursor()
+bool Worksheet::findHierarchyEntryById(WorksheetEntry* first, const QString& hierarchyId, QVector<HierarchyEntry*> collapsedAncestors, HierarchySearchResult& result)
 {
-    auto* entry = currentEntry();
-    auto* item = currentTextItem();
+    for (auto* entry = first; entry; entry = entry->next())
+    {
+        if (entry->type() != HierarchyEntry::Type)
+            continue;
 
-    if (!entry || !item)
-        return KWorksheetCursor();
-    return KWorksheetCursor(entry, item, item->view()->cursorPosition());
-}
+        auto* hierarchyEntry = static_cast<HierarchyEntry*>(entry);
 
-void Worksheet::setWorksheetCursor(const WorksheetCursor& cursor)
-{
-    if (!cursor.isValid() || !cursor.textItem())
-        return;
+        if (hierarchyEntry->hierarchyId() == hierarchyId)
+        {
+            result.entry = hierarchyEntry;
+            result.collapsedAncestors = collapsedAncestors;
+            return true;
+        }
 
-    if (m_lastFocusedTextItem)
-        m_lastFocusedTextItem->clearSelection();
-    if (m_legacylastFocusedTextItem)
-        m_legacylastFocusedTextItem->clearSelection();
+        if (auto* hiddenEntry = hierarchyEntry->hiddenSubentries())
+        {
+            auto childAncestors = collapsedAncestors;
+            childAncestors.append(hierarchyEntry);
 
-    m_legacylastFocusedTextItem = cursor.textItem();
-    m_lastFocusedTextItem = nullptr;
+            if (findHierarchyEntryById(hiddenEntry, hierarchyId, childAncestors, result))
+                return true;
+        }
+    }
 
-    cursor.textItem()->setTextCursor(cursor.textCursor());
+    return false;
 }
 
-void Worksheet::setWorksheetCursor(const KWorksheetCursor& cursor)
+Worksheet::HierarchySearchResult Worksheet::findHierarchyEntryById(const QString& hierarchyId)
 {
-    if(!cursor.isValid())
-        return;
-
-    if (m_lastFocusedTextItem)
-        m_lastFocusedTextItem->clearSelection();
-    if (m_legacylastFocusedTextItem)
-        m_legacylastFocusedTextItem->clearSelection();
+    HierarchySearchResult result;
 
-    m_lastFocusedTextItem = cursor.textItem();
-    m_legacylastFocusedTextItem = nullptr;
+    if (hierarchyId.isEmpty())
+        return result;
 
-    cursor.textItem()->view()->setSelection(cursor.foundRange());
+    findHierarchyEntryById(firstEntry(), hierarchyId, {}, result);
+    return result;
 }
 
-
-WorksheetEntry* Worksheet::currentEntry()
+bool Worksheet::findCommandEntryById(WorksheetEntry* first, const QString& commandId, QVector<HierarchyEntry*> collapsedAncestors, CommandSearchResult& result)
 {
-    // Entry cursor activate
-    if (m_choosenCursorEntry || m_isCursorEntryAfterLastEntry)
-        return nullptr;
+    for (auto* entry = first; entry; entry = entry->next())
+    {
+        if (entry->type() == CommandEntry::Type)
+        {
+            auto* commandEntry = static_cast<CommandEntry*>(entry);
+            if (commandEntry->commandId() == commandId)
+            {
+                result.entry = commandEntry;
+                result.collapsedAncestors = collapsedAncestors;
+                return true;
+            }
+        }
 
-    auto* item = focusItem();
-    if (!item /*&& !hasFocus()*/)
-        item = m_lastFocusedTextItem;
-    /*else
-      m_focusItem = item;*/
-    while (item && (item->type() < QGraphicsItem::UserType ||
-                    item->type() >= QGraphicsItem::UserType + 100))
-        item = item->parentItem();
-    if (item) {
-        auto* entry = qobject_cast<WorksheetEntry*>(item->toGraphicsObject());
-        if (entry && entry->aboutToBeRemoved()) {
-            if (entry->isAncestorOf(m_lastFocusedTextItem))
-                m_lastFocusedTextItem = nullptr;
-            return nullptr;
+        if (entry->type() != HierarchyEntry::Type)
+            continue;
+
+        auto* hierarchyEntry = static_cast<HierarchyEntry*>(entry);
+        if (auto* hiddenEntry = hierarchyEntry->hiddenSubentries())
+        {
+            auto childAncestors = collapsedAncestors;
+            childAncestors.append(hierarchyEntry);
+
+            if (findCommandEntryById(hiddenEntry, commandId, childAncestors, result))
+                return true;
         }
-        return entry;
     }
-    return nullptr;
-}
 
-WorksheetEntry* Worksheet::firstEntry()
-{
-    return m_firstEntry;
+    return false;
 }
 
-WorksheetEntry* Worksheet::lastEntry()
+Worksheet::CommandSearchResult Worksheet::findCommandEntryById(const QString& commandId)
 {
-    return m_lastEntry;
-}
+    CommandSearchResult result;
 
-void Worksheet::setFirstEntry(WorksheetEntry* entry)
-{
-    if (m_firstEntry)
-        disconnect(m_firstEntry, &WorksheetEntry::aboutToBeDeleted,
-                   this, &Worksheet::invalidateFirstEntry);
-    m_firstEntry = entry;
-    if (m_firstEntry)
-        connect(m_firstEntry, &WorksheetEntry::aboutToBeDeleted,
-                this, &Worksheet::invalidateFirstEntry, Qt::DirectConnection);
-}
+    if (commandId.isEmpty())
+        return result;
 
-void Worksheet::setLastEntry(WorksheetEntry* entry)
-{
-    if (m_lastEntry)
-        disconnect(m_lastEntry, &WorksheetEntry::aboutToBeDeleted,
-                   this, &Worksheet::invalidateLastEntry);
-    m_lastEntry = entry;
-    if (m_lastEntry)
-        connect(m_lastEntry, &WorksheetEntry::aboutToBeDeleted,
-                this, &Worksheet::invalidateLastEntry, Qt::DirectConnection);
+    findCommandEntryById(firstEntry(), commandId, {}, result);
+    return result;
 }
 
-void Worksheet::invalidateFirstEntry()
+bool Worksheet::expandHierarchyAncestors(const QVector<HierarchyEntry*>& ancestors)
 {
-    if (m_firstEntry)
-        setFirstEntry(m_firstEntry->next());
-}
+    bool expanded = false;
 
-void Worksheet::invalidateLastEntry()
-{
-    if (m_lastEntry)
-        setLastEntry(m_lastEntry->previous());
-}
+    for (auto* ancestor : ancestors)
+    {
+        if (!ancestor || !ancestor->hasHiddenSubentries())
+            continue;
 
-WorksheetEntry* Worksheet::entryAt(qreal x, qreal y)
-{
-    auto* item = itemAt(x, y, QTransform());
-    while (item && (item->type() <= QGraphicsItem::UserType ||
-                    item->type() >= QGraphicsItem::UserType + 100))
-        item = item->parentItem();
-    if (item)
-        return qobject_cast<WorksheetEntry*>(item->toGraphicsObject());
-    return nullptr;
-}
+        WorksheetEntry* hiddenSubentries = ancestor->takeHiddenSubentries();
 
-WorksheetEntry* Worksheet::entryAt(QPointF p)
-{
-    return entryAt(p.x(), p.y());
-}
+        if (!hiddenSubentries)
+            continue;
 
-void Worksheet::focusEntry(WorksheetEntry* entry)
-{
-    if (!entry)
-        return;
-    entry->focusEntry();
-    resetEntryCursor();
-    //bool rt = entry->acceptRichText();
-    //setActionsEnabled(rt);
-    //setAcceptRichText(rt);
-    //ensureCursorVisible();
+        insertSubentriesForHierarchy(ancestor, hiddenSubentries);
+        expanded = true;
+    }
+
+    return expanded;
 }
 
-void Worksheet::startDrag(WorksheetEntry* entry, QDrag* drag)
+void Worksheet::updateHierarchyLayout()
 {
-    if (m_readOnly)
-        return;
+    QSet<QString> usedHierarchyIds;
+    QSet<QString> usedCommandIds;
+    QSet<QString> usedResultIds;
 
-    resetEntryCursor();
-    m_dragEntry = entry;
-    auto* prev = entry->previous();
-    auto* next = entry->next();
-    m_placeholderEntry = new PlaceHolderEntry(this, entry->size());
-    m_placeholderEntry->setPrevious(prev);
-    m_placeholderEntry->setNext(next);
-    if (prev)
-        prev->setNext(m_placeholderEntry);
-    else
-        setFirstEntry(m_placeholderEntry);
-    if (next)
-        next->setPrevious(m_placeholderEntry);
-    else
-        setLastEntry(m_placeholderEntry);
-    m_dragEntry->hide();
-    Qt::DropAction action = drag->exec();
+    m_hierarchyMaxDepth = 0;
+    std::vector<int> hierarchyNumbers;
 
-    qDebug() << action;
-    if (action == Qt::MoveAction && m_placeholderEntry) {
-        qDebug() << "insert in new position";
-        prev = m_placeholderEntry->previous();
-        next = m_placeholderEntry->next();
-    }
-    m_dragEntry->setPrevious(prev);
-    m_dragEntry->setNext(next);
-    if (prev)
-        prev->setNext(m_dragEntry);
-    else
-        setFirstEntry(m_dragEntry);
-    if (next)
-        next->setPrevious(m_dragEntry);
-    else
-        setLastEntry(m_dragEntry);
-    m_dragEntry->show();
-    if (m_dragEntry->type() == HierarchyEntry::Type)
-        updateHierarchyLayout();
-    m_dragEntry->focusEntry();
-    const QPointF scenePos = worksheetView()->sceneCursorPos();
-    if (entryAt(scenePos) != m_dragEntry)
-        m_dragEntry->hideActionBar();
-    updateLayout();
-    if (m_placeholderEntry) {
-        m_placeholderEntry->setPrevious(nullptr);
-        m_placeholderEntry->setNext(nullptr);
-        m_placeholderEntry->hide();
-        m_placeholderEntry->deleteLater();
-        m_placeholderEntry = nullptr;
-    }
-    m_dragEntry = nullptr;
+    visitLogicalEntries([&](WorksheetEntry* entry)
+    {
+        if (entry->type() == HierarchyEntry::Type)
+        {
+            auto* hierarchyEntry = static_cast<HierarchyEntry*>(entry);
+
+            hierarchyEntry->updateHierarchyLevel(hierarchyNumbers);
+
+            m_hierarchyMaxDepth = std::max(m_hierarchyMaxDepth, hierarchyNumbers.size());
+
+            // Keep IDs valid for old files and duplicated entries.
+            QString hierarchyId = hierarchyEntry->hierarchyId();
+
+            if (hierarchyId.isEmpty() || usedHierarchyIds.contains(hierarchyId))
+            {
+                hierarchyEntry->regenerateHierarchyId();
+                hierarchyId = hierarchyEntry->hierarchyId();
+            }
+
+            usedHierarchyIds.insert(hierarchyId);
+        }
+        else if (entry->type() == CommandEntry::Type)
+        {
+            auto* commandEntry = static_cast<CommandEntry*>(entry);
+            QString commandId = commandEntry->commandId();
+
+            if (commandId.isEmpty() || usedCommandIds.contains(commandId))
+            {
+                commandEntry->regenerateCommandId();
+                commandId = commandEntry->commandId();
+            }
+
+            usedCommandIds.insert(commandId);
+
+            if (auto* expression = commandEntry->expression())
+            {
+                const auto& results = expression->results();
+                for (auto* result : results)
+                {
+                    if (!result)
+                        continue;
+
+                    QString resultId = result->resultId();
+                    if (resultId.isEmpty() || usedResultIds.contains(resultId))
+                    {
+                        result->regenerateResultId();
+                        resultId = result->resultId();
+                    }
+
+                    usedResultIds.insert(resultId);
+                }
+            }
+        }
+
+        return true;
+    });
+
+    refreshTocStructure();
 }
 
-void Worksheet::startDragWithHierarchy(HierarchyEntry* entry, QDrag* drag, QSizeF responsibleZoneSize)
+void Worksheet::updateHierarchyControlsLayout(WorksheetEntry* startEntry)
 {
-    if (m_readOnly)
-        return;
+    Q_UNUSED(startEntry);
 
-    resetEntryCursor();
-    m_dragEntry = entry;
-    auto* prev = entry->previous();
-    m_hierarchySubentriesDrag = hierarchySubelements(entry);
+    std::vector<HierarchyEntry*> levelEntries;
+    const int numerationBegin = static_cast<int>(HierarchyEntry::HierarchyLevel::Chapter);
+    const int numerationEnd = static_cast<int>(HierarchyEntry::HierarchyLevel::EndValue);
 
-    WorksheetEntry* next;
-    if (m_hierarchySubentriesDrag.size() != 0)
-        next = m_hierarchySubentriesDrag.back()->next();
-    else
-        next = entry->next();
+    for (int i = numerationBegin; i < numerationEnd; ++i)
+        levelEntries.push_back(nullptr);
 
-    m_placeholderEntry = new PlaceHolderEntry(this, responsibleZoneSize);
-    m_hierarchyDragSize = responsibleZoneSize;
-    m_placeholderEntry->setPrevious(prev);
-    m_placeholderEntry->setNext(next);
-    if (prev)
-        prev->setNext(m_placeholderEntry);
-    else
-        setFirstEntry(m_placeholderEntry);
-    if (next)
-        next->setPrevious(m_placeholderEntry);
-    else
-        setLastEntry(m_placeholderEntry);
+    WorksheetEntry* lastRealEntry = nullptr;
 
-    m_dragEntry->hide();
-    for(auto* subEntry : m_hierarchySubentriesDrag)
-        subEntry->hide();
+    for (auto* entry = firstEntry(); entry; entry = entry->next())
+    {
+        if (entry->type() == PlaceHolderEntry::Type || entry->aboutToBeRemoved())
+            continue;
 
-    Qt::DropAction action = drag->exec();
+        lastRealEntry = entry;
 
-    qDebug() << action;
-    if (action == Qt::MoveAction && m_placeholderEntry) {
-        qDebug() << "insert in new position";
-        prev = m_placeholderEntry->previous();
-        next = m_placeholderEntry->next();
-    }
-    m_dragEntry->setPrevious(prev);
+        if (entry->type() != HierarchyEntry::Type)
+            continue;
 
-    WorksheetEntry* lastDraggingEntry;
-    if (m_hierarchySubentriesDrag.size() != 0)
-        lastDraggingEntry = m_hierarchySubentriesDrag.back();
-    else
-        lastDraggingEntry = entry;
+        auto* hierarchyEntry = static_cast<HierarchyEntry*>(entry);
 
-    lastDraggingEntry->setNext(next);
+        const int index = static_cast<int>(hierarchyEntry->level()) - numerationBegin;
 
-    if (prev)
-        prev->setNext(m_dragEntry);
-    else
-        setFirstEntry(m_dragEntry);
+        if (index < 0 || index >= static_cast<int>(levelEntries.size()))
+            continue;
 
-    if (next)
-        next->setPrevious(lastDraggingEntry);
-    else
-        setLastEntry(lastDraggingEntry);
+        if (!levelEntries[index])
+        {
+            levelEntries[index] = hierarchyEntry;
+            continue;
+        }
 
-    m_dragEntry->show();
-     for(auto* subEntry : m_hierarchySubentriesDrag)
-        subEntry->show();
+        // Close previous controls at this level and below.
+        for (int i = index; i < static_cast<int>(levelEntries.size()); ++i)
+        {
+            auto* openEntry = levelEntries[i];
 
-    updateHierarchyLayout();
-    m_dragEntry->focusEntry();
-    const QPointF scenePos = worksheetView()->sceneCursorPos();
-    if (entryAt(scenePos) != m_dragEntry)
-        m_dragEntry->hideActionBar();
-    updateLayout();
+            if (!openEntry)
+                continue;
+
+            const qreal ownBottom = openEntry->y() + openEntry->size().height() - WorksheetEntry::VerticalMargin;
+            const qreal controlEnd = hierarchyEntry->y() - WorksheetEntry::VerticalMargin;
+            const bool hasSubelements = controlEnd > ownBottom;
+
+            openEntry->updateControlElementForHierarchy(qMax(controlEnd, ownBottom), m_hierarchyMaxDepth, hasSubelements);
 
-    if (m_placeholderEntry) {
-        m_placeholderEntry->setPrevious(nullptr);
-        m_placeholderEntry->setNext(nullptr);
-        m_placeholderEntry->hide();
-        m_placeholderEntry->deleteLater();
-        m_placeholderEntry = nullptr;
+            levelEntries[i] = nullptr;
+        }
+
+        levelEntries[index] = hierarchyEntry;
     }
-    m_dragEntry = nullptr;
-    m_hierarchySubentriesDrag.clear();
-}
 
-void Worksheet::evaluate()
-{
-    qDebug()<<"evaluate worksheet";
-    // login if not done yet
-    if (!m_readOnly && m_session && m_session->status() == Cantor::Session::Disable)
-        loginToSession();
+    if (!lastRealEntry)
+        return;
 
-    // evaluate the worksheet if the login was successful
-    if (m_session && m_session->status() == Cantor::Session::Done) {
-        firstEntry()->evaluate(WorksheetEntry::EvaluateNext);
-        setModified();
+    const qreal documentEnd = lastRealEntry->y() + lastRealEntry->size().height() - WorksheetEntry::VerticalMargin;
+
+    for (auto* openEntry : levelEntries)
+    {
+        if (!openEntry)
+            continue;
+
+        const qreal ownBottom = openEntry->y() + openEntry->size().height() - WorksheetEntry::VerticalMargin;
+        const qreal controlEnd = qMax(documentEnd, ownBottom);
+        const bool hasSubelements = controlEnd > ownBottom;
+
+        openEntry->updateControlElementForHierarchy(controlEnd, m_hierarchyMaxDepth, hasSubelements);
     }
 }
 
-void Worksheet::evaluateCurrentEntry()
+std::vector<WorksheetEntry*> Worksheet::hierarchySubelements(HierarchyEntry* hierarchyEntry) const
 {
-    // login if not done yet
-    if (!m_readOnly && m_session && m_session->status() == Cantor::Session::Disable)
-        loginToSession();
+    std::vector<WorksheetEntry*> subentries;
 
-    // evaluate the current entry if the login was successful
-    if (!m_session)
-        return;
+    Q_ASSERT(hierarchyEntry);
 
-    // the current status of the session should be Done or Running - the later is the case when we're
-    // waiting for the additional input like Maxima's help requests or assumptions for integrate(), etc.
-    if (m_session->status() == Cantor::Session::Done || m_session->status() == Cantor::Session::Running)
+    bool subentriesEnd = false;
+    const int level = (int)hierarchyEntry->level();
+    for (auto* entry = hierarchyEntry->next(); entry && !subentriesEnd; entry = entry->next())
     {
-        if(auto* entry = currentEntry())
-            entry->evaluateCurrentItem();
+        if (entry->type() == HierarchyEntry::Type)
+        {
+            if ((int)(static_cast<HierarchyEntry*>(entry)->level()) <= level)
+                subentriesEnd = true;
+            else
+                subentries.push_back(entry);
+        }
+        else
+            subentries.push_back(entry);
     }
+    return subentries;
 }
 
-bool Worksheet::completionEnabled()
+void Worksheet::updateCurrentHierarchy(WorksheetEntry* entry)
 {
-    return m_completionEnabled;
+    QString nodeId;
+
+    if (entry && entry->type() == CommandEntry::Type)
+        nodeId = buildCommandNodeId(static_cast<CommandEntry*>(entry));
+    else
+        nodeId = hierarchyIdForEntry(entry);
+
+    setCurrentTocNode(nodeId);
 }
 
-void Worksheet::showCompletion()
+QString Worksheet::hierarchyIdForEntry(WorksheetEntry* entry) const
 {
-    auto* current = currentEntry();
-    if (current)
-        current->showCompletion();
+    for (auto* current = entry; current; current = current->previous())
+    {
+        if (current->type() == HierarchyEntry::Type)
+            return static_cast<HierarchyEntry*>(current)->hierarchyId();
+    }
+
+    return QString();
 }
 
-WorksheetEntry* Worksheet::appendEntry(const int type, bool focus)
+void Worksheet::setCurrentTocNode(const QString& nodeId)
 {
-    auto* entry = WorksheetEntry::create(type, this);
+    if (m_currentTocNodeId == nodeId)
+        return;
 
-    if (entry)
+    m_currentTocNodeId = nodeId;
+
+    Q_EMIT currentTocNodeChanged(nodeId);
+}
+
+void Worksheet::normalizeDraggedHierarchyLevels(HierarchyEntry* rootEntry, WorksheetEntry* previousEntry, const std::vector<WorksheetEntry*>& subentries)
+{
+    if (!rootEntry)
+        return;
+
+    HierarchyEntry* previousHierarchyEntry = nullptr;
+
+    for (auto* entry = previousEntry; entry; entry = entry->previous())
     {
-        qDebug() << "Entry Appended";
-        entry->setPrevious(lastEntry());
-        if (lastEntry())
-            lastEntry()->setNext(entry);
-        if (!firstEntry())
-            setFirstEntry(entry);
-        setLastEntry(entry);
-        if (!m_isLoadingFromFile)
-        {
-            if (type == HierarchyEntry::Type)
-                updateHierarchyLayout();
-            updateLayout();
-            if (focus)
-            {
-                makeVisible(entry);
-                focusEntry(entry);
-            }
-            setModified();
-        }
+        if (entry->type() != HierarchyEntry::Type)
+            continue;
+
+        previousHierarchyEntry = static_cast<HierarchyEntry*>(entry);
+        break;
     }
-    return entry;
-}
 
-WorksheetEntry* Worksheet::appendCommandEntry()
-{
-   return appendEntry(CommandEntry::Type);
-}
+    const int minimumLevel = static_cast<int>(HierarchyEntry::HierarchyLevel::Chapter);
+    const int maximumLevel = static_cast<int>(HierarchyEntry::HierarchyLevel::Subparagraph);
 
-WorksheetEntry* Worksheet::appendTextEntry()
-{
-   return appendEntry(TextEntry::Type);
-}
+    int maximumAllowedRootLevel = minimumLevel;
 
-WorksheetEntry* Worksheet::appendMarkdownEntry()
-{
-   return appendEntry(MarkdownEntry::Type);
-}
+    if (previousHierarchyEntry)
+        maximumAllowedRootLevel = qMin(maximumLevel, static_cast<int>(previousHierarchyEntry->level()) + 1);
 
-WorksheetEntry* Worksheet::appendPageBreakEntry()
-{
-    return appendEntry(PageBreakEntry::Type);
-}
+    const int oldRootLevel = static_cast<int>(rootEntry->level());
+    if (oldRootLevel <= maximumAllowedRootLevel)
+        return;
 
-WorksheetEntry* Worksheet::appendImageEntry()
-{
-   return appendEntry(ImageEntry::Type);
-}
+    const int levelOffset = maximumAllowedRootLevel - oldRootLevel;
 
-WorksheetEntry* Worksheet::appendLatexEntry()
-{
-    return appendEntry(LatexEntry::Type);
-}
+    const auto shiftLevel = [levelOffset, minimumLevel, maximumLevel](HierarchyEntry* hierarchyEntry)
+    {
+        if (!hierarchyEntry)
+            return;
 
-void Worksheet::appendCommandEntry(const QString& text)
-{
-    auto* entry = lastEntry();
-    if(!entry->isEmpty())
-        entry = appendCommandEntry();
+        const int newLevel = qBound(minimumLevel, static_cast<int>(hierarchyEntry->level()) + levelOffset, maximumLevel);
 
-    if (entry)
+        hierarchyEntry->setLevel(static_cast<HierarchyEntry::HierarchyLevel>(newLevel));
+    };
+
+    shiftLevel(rootEntry);
+
+    for (auto* entry : subentries)
     {
-        focusEntry(entry);
-        entry->setContent(text);
-        evaluateCurrentEntry();
-    }
-}
+        if (entry->type() != HierarchyEntry::Type)
+            continue;
 
-WorksheetEntry* Worksheet::appendHorizontalRuleEntry()
-{
-    return appendEntry(HorizontalRuleEntry::Type);
+        shiftLevel(static_cast<HierarchyEntry*>(entry));
+    }
 }
 
-WorksheetEntry* Worksheet::appendHierarchyEntry()
+void Worksheet::updateCurrentHierarchyFromView(const QRectF& viewRect)
 {
-    return appendEntry(HierarchyEntry::Type);
-}
+    if (m_hierarchyTrackingSource != HierarchyTrackingSource::Viewport || m_layoutUpdateInProgress || m_isLoadingFromFile || viewRect.isEmpty())
+        return;
 
-WorksheetEntry* Worksheet::insertEntry(const int type, WorksheetEntry* current)
-{
-    if (!current)
-        current = currentEntry();
+    const qreal activationOffset = qMin<qreal>(48.0, viewRect.height() * 0.15);
 
-    if (!current)
-        return appendEntry(type);
+    const qreal activationY = viewRect.top() + activationOffset;
 
-    auto* next = current->next();
-    WorksheetEntry* entry = nullptr;
+    WorksheetEntry* activeEntry = nullptr;
 
-    if (!next || next->type() != type || !next->isEmpty())
+    for (auto* entry = firstEntry(); entry; entry = entry->next())
     {
-        entry = WorksheetEntry::create(type, this);
-        entry->setPrevious(current);
-        entry->setNext(next);
-        current->setNext(entry);
-        if (next)
-            next->setPrevious(entry);
-        else
-            setLastEntry(entry);
-        if (type == HierarchyEntry::Type)
-            updateHierarchyLayout();
-        updateLayout();
-        setModified();
-    } else {
-        entry = next;
-    }
-
-    focusEntry(entry);
-    makeVisible(entry);
-
-    return entry;
-}
+        if (!entry->isVisible())
+            continue;
 
-WorksheetEntry* Worksheet::insertTextEntry(WorksheetEntry* current)
-{
-    return insertEntry(TextEntry::Type, current);
-}
+        if (entry->scenePos().y() > activationY)
+            break;
 
-WorksheetEntry* Worksheet::insertMarkdownEntry(WorksheetEntry* current)
-{
-    return insertEntry(MarkdownEntry::Type, current);
-}
+        activeEntry = entry;
+    }
 
-WorksheetEntry* Worksheet::insertCommandEntry(WorksheetEntry* current)
-{
-    return insertEntry(CommandEntry::Type, current);
+    updateCurrentHierarchy(activeEntry);
 }
 
-WorksheetEntry* Worksheet::insertImageEntry(WorksheetEntry* current)
+void Worksheet::updateCurrentHierarchyFromEntry(WorksheetEntry* entry)
 {
-    auto* entry = insertEntry(ImageEntry::Type, current);
-    auto* imageEntry = static_cast<ImageEntry*>(entry);
-    QTimer::singleShot(0, this, [=] () {imageEntry->startConfigDialog();});
-    return entry;
-}
+    m_hierarchyTrackingSource = HierarchyTrackingSource::FocusedEntry;
 
-WorksheetEntry* Worksheet::insertPageBreakEntry(WorksheetEntry* current)
-{
-    return insertEntry(PageBreakEntry::Type, current);
+    updateCurrentHierarchy(entry);
 }
 
-WorksheetEntry* Worksheet::insertLatexEntry(WorksheetEntry* current)
+void Worksheet::followHierarchyFromView()
 {
-    return insertEntry(LatexEntry::Type, current);
-}
+    m_hierarchyTrackingSource = HierarchyTrackingSource::Viewport;
 
-WorksheetEntry* Worksheet::insertHorizontalRuleEntry(WorksheetEntry* current)
-{
-    return insertEntry(HorizontalRuleEntry::Type, current);
-}
+    // Defer until viewRect() reflects the final scroll position.
+    QTimer::singleShot(0, this, [this]() {
+        if (m_hierarchyTrackingSource != HierarchyTrackingSource::Viewport)
+            return;
 
-WorksheetEntry* Worksheet::insertHierarchyEntry(WorksheetEntry* current)
-{
-    return insertEntry(HierarchyEntry::Type, current);
+        updateCurrentHierarchyFromView(worksheetView()->viewRect());
+    });
 }
 
-WorksheetEntry* Worksheet::insertEntryBefore(int type, WorksheetEntry* current)
+void Worksheet::updateEntrySize(WorksheetEntry* entry)
 {
-    if (!current)
-        current = currentEntry();
+    QScopedValueRollback<bool> layoutGuard(m_layoutUpdateInProgress, true);
+    bool cursorRectVisible = false;
+    bool atEnd = worksheetView()->isAtEnd();
+    if (currentTextItem()) {
+        QRectF cursorRect = currentTextItem()->sceneCursorRect();
+        cursorRectVisible = worksheetView()->isVisible(cursorRect);
+    }
 
-    if (!current)
-        return nullptr;
+    if (Settings::useOldCantorEntriesIndent() == false)
+    {
+        qreal newMaxPromptWidth = m_maxPromptWidth;
+        if (entry->type() == CommandEntry::Type)
+            newMaxPromptWidth = static_cast<CommandEntry*>(entry)->promptItemWidth();
+        else if (entry->type() == HierarchyEntry::Type)
+            newMaxPromptWidth = static_cast<HierarchyEntry*>(entry)->hierarchyItemWidth();
 
-    auto* prev = current->previous();
-    WorksheetEntry* entry = nullptr;
+        // If width of prompt (if presence) of the entry more, is less than that of current
+        // maximum, then we need full layout update
+        if (newMaxPromptWidth > m_maxPromptWidth)
+        {
+            updateLayout();
+            return;
+        }
+    }
 
-    if(!prev || prev->type() != type || !prev->isEmpty())
-    {
-        entry = WorksheetEntry::create(type, this);
-        entry->setNext(current);
-        entry->setPrevious(prev);
-        current->setPrevious(entry);
-        if (prev)
-            prev->setNext(entry);
-        else
-            setFirstEntry(entry);
-        if (type == HierarchyEntry::Type)
-            updateHierarchyLayout();
-        updateLayout();
-        setModified();
+    qreal y = entry->y() + entry->size().height();
+    for (entry = entry->next(); entry; entry = entry->next()) {
+        entry->setY(y);
+        y += entry->size().height();
     }
-    else
-        entry = prev;
 
-    focusEntry(entry);
-    return entry;
-}
+    if (!m_isLoadingFromFile)
+        updateHierarchyControlsLayout(entry);
 
-WorksheetEntry* Worksheet::insertTextEntryBefore(WorksheetEntry* current)
-{
-    return insertEntryBefore(TextEntry::Type, current);
+    setSceneRect(QRectF(0, 0, sceneRect().width(), y));
+    if (cursorRectVisible)
+        makeVisible(worksheetCursor());
+    else if (atEnd)
+        worksheetView()->scrollToEnd();
+    drawEntryCursor();
 }
 
-WorksheetEntry* Worksheet::insertMarkdownEntryBefore(WorksheetEntry* current)
+void Worksheet::setRequestedWidth(QGraphicsObject* object, qreal width)
 {
-    return insertEntryBefore(MarkdownEntry::Type, current);
-}
+    qreal oldWidth = m_itemWidths[object];
+    m_itemWidths[object] = width;
 
-WorksheetEntry* Worksheet::insertCommandEntryBefore(WorksheetEntry* current)
-{
-    return insertEntryBefore(CommandEntry::Type, current);
+    if (width > m_maxWidth || oldWidth == m_maxWidth)
+    {
+        m_maxWidth = width;
+        qreal y = lastEntry() ? lastEntry()->size().height() + lastEntry()->y() : 0;
+        setSceneRect(QRectF(0, 0, m_maxWidth + LeftMargin + RightMargin, y));
+    }
 }
 
-WorksheetEntry* Worksheet::insertPageBreakEntryBefore(WorksheetEntry* current)
+void Worksheet::removeRequestedWidth(QGraphicsObject* object)
 {
-    return insertEntryBefore(PageBreakEntry::Type, current);
-}
+    if (!m_itemWidths.contains(object))
+        return;
 
-WorksheetEntry* Worksheet::insertImageEntryBefore(WorksheetEntry* current)
-{
-    auto* entry = insertEntryBefore(ImageEntry::Type, current);
-    auto* imageEntry = static_cast<ImageEntry*>(entry);
-    QTimer::singleShot(0, this, [=] () {imageEntry->startConfigDialog();});
-    return entry;
+    qreal width = m_itemWidths[object];
+    m_itemWidths.remove(object);
+
+    if (width == m_maxWidth)
+    {
+        m_maxWidth = 0;
+        for (qreal width : m_itemWidths.values())
+            if (width > m_maxWidth)
+                m_maxWidth = width;
+        qreal y = lastEntry() ? lastEntry()->size().height() + lastEntry()->y() : 0;
+        setSceneRect(QRectF(0, 0, m_maxWidth + LeftMargin + RightMargin, y));
+    }
 }
 
-WorksheetEntry* Worksheet::insertLatexEntryBefore(WorksheetEntry* current)
+bool Worksheet::isEmpty()
 {
-    return insertEntryBefore(LatexEntry::Type, current);
+    return !m_firstEntry;
 }
 
-WorksheetEntry* Worksheet::insertHorizontalRuleEntryBefore(WorksheetEntry* current)
+bool Worksheet::isLoadingFromFile()
 {
-    return insertEntryBefore(HorizontalRuleEntry::Type, current);
+    return m_isLoadingFromFile;
 }
 
-WorksheetEntry* Worksheet::insertHierarchyEntryBefore(WorksheetEntry* current)
+void Worksheet::makeVisible(WorksheetEntry* entry)
 {
-    return insertEntryBefore(HierarchyEntry::Type, current);
+    QRectF r = entry->boundingRect();
+    r = entry->mapRectToScene(r);
+    r.adjust(0, -10, 0, 10);
+    worksheetView()->makeVisible(r);
 }
 
-void Worksheet::interrupt()
+void Worksheet::makeVisible(const KWorksheetCursor& cursor)
 {
-    if (m_session->status() == Cantor::Session::Running)
+    if(!cursor.cursor().isValid())
     {
-        m_session->interrupt();
-        Q_EMIT updatePrompt();
+        if(cursor.entry())
+            makeVisible(cursor.entry());
+        return;
     }
+    QRectF r = cursor.textItem()->sceneCursorRect();
+    QRectF er = cursor.entry()->boundingRect();
+    er = cursor.entry()->mapRectToScene(er);
+    er.adjust(0, -10, 0, 10);
+    r.adjust(0, qMax(qreal(-100.0), er.top() - r.top()),
+             0, qMin(qreal(100.0), er.bottom() - r.bottom()));
+    worksheetView()->makeVisible(r);
 }
 
-void Worksheet::interruptCurrentEntryEvaluation()
+void Worksheet::makeVisible(const WorksheetCursor& cursor)
 {
-    currentEntry()->interruptEvaluation();
+    if (cursor.textCursor().isNull()) {
+        if (cursor.entry())
+            makeVisible(cursor.entry());
+        return;
+    }
+    QRectF r = cursor.textItem()->sceneCursorRect(cursor.textCursor());
+    QRectF er = cursor.entry()->boundingRect();
+    er = cursor.entry()->mapRectToScene(er);
+    er.adjust(0, -10, 0, 10);
+    r.adjust(0, qMax(qreal(-100.0), er.top() - r.top()),
+             0, qMin(qreal(100.0), er.bottom() - r.bottom()));
+    worksheetView()->makeVisible(r);
 }
 
+WorksheetView* Worksheet::worksheetView()
+{
+    return static_cast<WorksheetView*>(views().first());
+}
 
-bool Worksheet::variableHighlightingEnabled() const
+void Worksheet::setModified()
 {
-    return m_variableHighlightingEnabled;
+    if (!m_isClosing && !m_isLoadingFromFile)
+        Q_EMIT modified();
 }
 
-void Worksheet::setVariableHighlightingEnabled(bool enabled)
+KWorksheetCursor Worksheet::worksheetCursor()
 {
-    if (m_variableHighlightingEnabled == enabled)
-    {
-        return;
-    }
-    m_variableHighlightingEnabled = enabled;
+    auto* entry = currentEntry();
+    auto* item = currentTextItem();
 
-    for (auto* entry = firstEntry(); entry; entry = entry->next())
-    {
-        if (entry->type() == CommandEntry::Type)
-            static_cast<CommandEntry*>(entry)->setVariableHighlightingEnabled(enabled);
-    }
+    if (!entry || !item)
+        return KWorksheetCursor();
+    return KWorksheetCursor(entry, item, item->view()->cursorPosition());
 }
 
-void Worksheet::enableCompletion(bool enable)
+void Worksheet::setWorksheetCursor(const WorksheetCursor& cursor)
 {
-    m_completionEnabled=enable;
+    if (!cursor.isValid() || !cursor.textItem())
+        return;
+
+    if (m_lastFocusedTextItem)
+        m_lastFocusedTextItem->clearSelection();
+    if (m_legacylastFocusedTextItem)
+        m_legacylastFocusedTextItem->clearSelection();
+
+    m_legacylastFocusedTextItem = cursor.textItem();
+    m_lastFocusedTextItem = nullptr;
+
+    cursor.textItem()->setTextCursor(cursor.textCursor());
 }
 
-Cantor::Session* Worksheet::session() const
+void Worksheet::setWorksheetCursor(const KWorksheetCursor& cursor)
 {
-    return m_session;
+    if(!cursor.isValid())
+        return;
+
+    if (m_lastFocusedTextItem)
+        m_lastFocusedTextItem->clearSelection();
+    if (m_legacylastFocusedTextItem)
+        m_legacylastFocusedTextItem->clearSelection();
+
+    m_lastFocusedTextItem = cursor.textItem();
+    m_legacylastFocusedTextItem = nullptr;
+
+    cursor.textItem()->view()->setSelection(cursor.foundRange());
 }
 
-bool Worksheet::isRunning()
+
+WorksheetEntry* Worksheet::currentEntry()
 {
-    return m_session && m_session->status()==Cantor::Session::Running;
+    // Entry cursor activate
+    if (m_choosenCursorEntry || m_isCursorEntryAfterLastEntry)
+        return nullptr;
+
+    auto* item = focusItem();
+    if (!item /*&& !hasFocus()*/)
+        item = m_lastFocusedTextItem;
+    /*else
+      m_focusItem = item;*/
+    while (item && (item->type() < QGraphicsItem::UserType ||
+                    item->type() >= QGraphicsItem::UserType + 100))
+        item = item->parentItem();
+    if (item) {
+        auto* entry = qobject_cast<WorksheetEntry*>(item->toGraphicsObject());
+        if (entry && entry->aboutToBeRemoved()) {
+            if (entry->isAncestorOf(m_lastFocusedTextItem))
+                m_lastFocusedTextItem = nullptr;
+            return nullptr;
+        }
+        return entry;
+    }
+    return nullptr;
 }
 
-bool Worksheet::isReadOnly()
+WorksheetEntry* Worksheet::firstEntry()
 {
-    return m_readOnly;
+    return m_firstEntry;
 }
 
-bool Worksheet::showExpressionIds()
+WorksheetEntry* Worksheet::lastEntry()
 {
-    return m_showExpressionIds;
+    return m_lastEntry;
 }
 
-bool Worksheet::animationsEnabled()
+void Worksheet::setFirstEntry(WorksheetEntry* entry)
 {
-    return m_animationsEnabled;
+    if (m_firstEntry)
+        disconnect(m_firstEntry, &WorksheetEntry::aboutToBeDeleted,
+                   this, &Worksheet::invalidateFirstEntry);
+    m_firstEntry = entry;
+    if (m_firstEntry)
+        connect(m_firstEntry, &WorksheetEntry::aboutToBeDeleted,
+                this, &Worksheet::invalidateFirstEntry, Qt::DirectConnection);
 }
 
-void Worksheet::enableAnimations(bool enable)
+void Worksheet::setLastEntry(WorksheetEntry* entry)
 {
-    m_animationsEnabled = enable;
+    if (m_lastEntry)
+        disconnect(m_lastEntry, &WorksheetEntry::aboutToBeDeleted,
+                   this, &Worksheet::invalidateLastEntry);
+    m_lastEntry = entry;
+    if (m_lastEntry)
+        connect(m_lastEntry, &WorksheetEntry::aboutToBeDeleted,
+                this, &Worksheet::invalidateLastEntry, Qt::DirectConnection);
 }
 
-bool Worksheet::embeddedMathEnabled()
+void Worksheet::invalidateFirstEntry()
 {
-    return m_embeddedMathEnabled && m_mathRenderer.mathRenderAvailable();
+    if (m_firstEntry)
+        setFirstEntry(m_firstEntry->next());
 }
 
-void Worksheet::enableEmbeddedMath(bool enable)
+void Worksheet::invalidateLastEntry()
 {
-    m_embeddedMathEnabled = enable;
+    if (m_lastEntry)
+        setLastEntry(m_lastEntry->previous());
 }
 
-void Worksheet::enableExpressionNumbering(bool enable)
+WorksheetEntry* Worksheet::entryAt(qreal x, qreal y)
 {
-    m_showExpressionIds=enable;
-    Q_EMIT updatePrompt();
-    if (views().size() != 0)
-        updateLayout();
+    auto* item = itemAt(x, y, QTransform());
+    while (item && (item->type() <= QGraphicsItem::UserType ||
+                    item->type() >= QGraphicsItem::UserType + 100))
+        item = item->parentItem();
+    if (item)
+        return qobject_cast<WorksheetEntry*>(item->toGraphicsObject());
+    return nullptr;
 }
 
-QDomDocument Worksheet::toXML(KZip* archive)
+WorksheetEntry* Worksheet::entryAt(QPointF p)
 {
-    QDomDocument doc( QLatin1String("CantorWorksheet") );
-    QDomElement root = doc.createElement( QLatin1String("Worksheet") );
-    root.setAttribute(QLatin1String("backend"), (m_session ? m_session->backend()->name(): m_backendName));
-    doc.appendChild(root);
+    return entryAt(p.x(), p.y());
+}
 
-    for( auto* entry = firstEntry(); entry; entry = entry->next())
-    {
-        QDomElement el = entry->toXml(doc, archive);
-        root.appendChild( el );
-    }
-    return doc;
+void Worksheet::focusEntry(WorksheetEntry* entry)
+{
+    if (!entry)
+        return;
+    entry->focusEntry();
+    resetEntryCursor();
+    //bool rt = entry->acceptRichText();
+    //setActionsEnabled(rt);
+    //setAcceptRichText(rt);
+    //ensureCursorVisible();
 }
 
-QJsonDocument Worksheet::toJupyterJson()
+void Worksheet::startDrag(WorksheetEntry* entry, QDrag* drag)
 {
-    QJsonDocument doc;
-    QJsonObject root;
+    if (m_readOnly || !entry || !drag)
+        return;
 
-    QJsonObject metadata(m_jupyterMetadata ? *m_jupyterMetadata : QJsonObject());
+    resetEntryCursor();
 
-    QJsonObject kernalInfo;
-    if (m_session && m_session->backend())
-        kernalInfo = Cantor::JupyterUtils::getKernelspec(m_session->backend());
-    else
-        kernalInfo.insert(QLatin1String("name"), m_backendName);
-    metadata.insert(QLatin1String("kernelspec"), kernalInfo);
+    m_dragEntry = entry;
 
-    root.insert(QLatin1String("metadata"), metadata);
+    WorksheetEntry* originalPrevious = entry->previous();
+    WorksheetEntry* originalNext = entry->next();
+    WorksheetEntry* previous = originalPrevious;
+    WorksheetEntry* next = originalNext;
 
-    // Not sure, but it looks like we support nbformat version 4.5
-    root.insert(QLatin1String("nbformat"), 4);
-    root.insert(QLatin1String("nbformat_minor"), 5);
+    m_placeholderEntry = new PlaceHolderEntry(this, entry->size());
+    m_placeholderEntry->setPrevious(previous);
+    m_placeholderEntry->setNext(next);
 
-    QJsonArray cells;
-    for( auto* entry = firstEntry(); entry; entry = entry->next())
-    {
-        const QJsonValue entryJson = entry->toJupyterJson();
+    if (previous)
+        previous->setNext(m_placeholderEntry);
+    else
+        setFirstEntry(m_placeholderEntry);
 
-        if (!entryJson.isNull())
-            cells.append(entryJson);
-    }
-    root.insert(QLatin1String("cells"), cells);
+    if (next)
+        next->setPrevious(m_placeholderEntry);
+    else
+        setLastEntry(m_placeholderEntry);
 
-    doc.setObject(root);
-    return doc;
-}
+    m_dragEntry->hide();
 
-void Worksheet::save( const QString& filename )
-{
-    QFile file(filename);
-    if ( !file.open(QIODevice::WriteOnly) )
+    const Qt::DropAction action = drag->exec();
+
+    bool positionChanged = false;
+
+    if (action == Qt::MoveAction && m_placeholderEntry)
     {
-        KMessageBox::error( worksheetView(),
-                            i18n( "Cannot write file %1." , filename ),
-                            i18n( "Error - Cantor" ));
-        return;
+        previous = m_placeholderEntry->previous();
+        next = m_placeholderEntry->next();
+        positionChanged = previous != originalPrevious || next != originalNext;
     }
 
-    save(&file);
-}
+    removeDragPlaceholder();
 
-QByteArray Worksheet::saveToByteArray()
-{
-    QBuffer buffer;
-    save(&buffer);
+    m_dragEntry->setPrevious(previous);
+    m_dragEntry->setNext(next);
 
-    return buffer.buffer();
-}
+    if (previous)
+        previous->setNext(m_dragEntry);
+    else
+        setFirstEntry(m_dragEntry);
 
-void Worksheet::save( QIODevice* device)
-{
-    qDebug()<<"saving to filename";
-    switch (m_type)
-    {
-        case CantorWorksheet:
-        {
-            KZip zipFile( device );
+    if (next)
+        next->setPrevious(m_dragEntry);
+    else
+        setLastEntry(m_dragEntry);
 
-            if ( !zipFile.open(QIODevice::WriteOnly) )
-            {
-                KMessageBox::error( worksheetView(),
-                                    i18n( "Cannot write file." ),
-                                    i18n( "Error - Cantor" ));
-                return;
-            }
+    m_dragEntry->show();
+    const bool hierarchyMoved = m_dragEntry->type() == HierarchyEntry::Type;
 
-            QByteArray content = toXML(&zipFile).toByteArray();
-            zipFile.writeFile( QLatin1String("content.xml"), content.data());
-            break;
-        }
+    m_dragEntry->focusEntry();
+    const QPointF scenePosition = worksheetView()->sceneCursorPos();
 
-        case JupyterNotebook:
-        {
-            if (!device->isWritable())
-            {
-                KMessageBox::error( worksheetView(),
-                                    i18n( "Cannot write file." ),
-                                    i18n( "Error - Cantor" ));
-                return;
-            }
+    if (entryAt(scenePosition) != m_dragEntry)
+        m_dragEntry->hideActionBar();
 
-            const QJsonDocument& doc = toJupyterJson();
-            device->write(doc.toJson(QJsonDocument::Indented));
-            break;
-        }
-    }
+    m_dragEntry = nullptr;
+
+    if (hierarchyMoved && positionChanged)
+        updateHierarchyLayout();
+
+    updateLayout();
+
+    if (positionChanged)
+        setModified();
 }
 
-void Worksheet::savePlain(const QString& filename)
+void Worksheet::startDragWithHierarchy(HierarchyEntry* entry, QDrag* drag, QSizeF responsibleZoneSize)
 {
-    QFile file(filename);
-    if(!file.open(QIODevice::WriteOnly))
-    {
-        KMessageBox::error(worksheetView(), i18n("Error saving file %1", filename), i18n("Error - Cantor"));
+    if (m_readOnly || !entry || !drag)
         return;
-    }
 
-    QString cmdSep=QLatin1String(";\n");
-    QString commentStartingSeq = QLatin1String("");
-    QString commentEndingSeq = QLatin1String("");
+    resetEntryCursor();
 
-    if (!m_readOnly)
-    {
-        Cantor::Backend * const backend=session()->backend();
-        if (backend->extensions().contains(QLatin1String("ScriptExtension")))
-        {
-            Cantor::ScriptExtension* e=dynamic_cast<Cantor::ScriptExtension*>(backend->extension(QLatin1String(("ScriptExtension"))));
-            if (e)
-            {
-                cmdSep=e->commandSeparator();
-                commentStartingSeq = e->commentStartingSequence();
-                commentEndingSeq = e->commentEndingSequence();
-            }
-        }
-    }
+    m_dragEntry = entry;
+    m_hierarchySubentriesDrag = hierarchySubelements(entry);
+    m_hierarchyDragSize = responsibleZoneSize;
+
+    WorksheetEntry* originalPrevious = entry->previous();
+    WorksheetEntry* originalNext = nullptr;
+
+    if (!m_hierarchySubentriesDrag.empty())
+        originalNext = m_hierarchySubentriesDrag.back()->next();
     else
-        KMessageBox::information(worksheetView(), i18n("In read-only mode Cantor couldn't guarantee, that the export will be valid for %1", m_backendName), i18n("Cantor"));
+        originalNext = entry->next();
 
-    QTextStream stream(&file);
+    WorksheetEntry* previous = originalPrevious;
+    WorksheetEntry* next = originalNext;
 
-    for(auto* entry = firstEntry(); entry; entry = entry->next())
-    {
-        const QString& str=entry->toPlain(cmdSep, commentStartingSeq, commentEndingSeq);
-        if(!str.isEmpty())
-            stream << str + QLatin1Char('\n');
-    }
+    m_placeholderEntry = new PlaceHolderEntry(this, responsibleZoneSize);
+    m_placeholderEntry->setPrevious(previous);
+    m_placeholderEntry->setNext(next);
 
-    file.close();
-}
+    if (previous)
+        previous->setNext(m_placeholderEntry);
+    else
+        setFirstEntry(m_placeholderEntry);
 
-void Worksheet::saveLatex(const QString& filename)
-{
-    qDebug()<<"exporting to Latex: " <<filename;
+    if (next)
+        next->setPrevious(m_placeholderEntry);
+    else
+        setLastEntry(m_placeholderEntry);
 
-    QFile file(filename);
-    if(!file.open(QIODevice::WriteOnly))
+    m_dragEntry->hide();
+
+    for (auto* subentry : m_hierarchySubentriesDrag)
+        subentry->hide();
+
+    const Qt::DropAction action = drag->exec();
+
+    bool positionChanged = false;
+
+    if (action == Qt::MoveAction && m_placeholderEntry)
     {
-        KMessageBox::error(worksheetView(), i18n("Error saving file %1", filename), i18n("Export to LaTeX"));
-        return;
+        previous = m_placeholderEntry->previous();
+        next = m_placeholderEntry->next();
+        positionChanged = previous != originalPrevious || next != originalNext;
     }
 
-    QString stylesheet = QStandardPaths::locate(QStandardPaths::AppDataLocation, QLatin1String("xslt/latex.xsl"));
-    if (stylesheet.isEmpty()) {
-        KMessageBox::error(worksheetView(), i18n("Error loading latex.xsl stylesheet"), i18n("Export to LaTeX"));
-        return;
-    }
+    removeDragPlaceholder();
 
-    xsltStylesheetPtr xsltStyleSheet = xsltParseStylesheetFile((const xmlChar *)stylesheet.toLocal8Bit().constData());
-    xmlDocPtr output = xmlReadDoc((const xmlChar *)toXML().toString().toStdString().c_str(), nullptr, "utf-8", XML_PARSE_RECOVER | XML_PARSE_NOENT | XML_PARSE_DTDLOAD);
+    m_dragEntry->setPrevious(previous);
 
-    const char *params[16+1];
-    params[0] = nullptr;
-    auto res = xsltApplyStylesheet(xsltStyleSheet, output, params);
-    if (res) {
-        xmlChar *xmlResultBuffer = nullptr;
-        int xmlResultLength = 0;
-        int res = xsltSaveResultToString(&xmlResultBuffer, &xmlResultLength, output, xsltStyleSheet);
-        if (res != -1) {
-            QString outString = QString::fromUtf8((char *)xmlResultBuffer);
+    if (previous)
+        previous->setNext(m_dragEntry);
+    else
+        setFirstEntry(m_dragEntry);
 
-            // Transform HTML escaped special characters to valid LaTeX characters (&, <, >)
-            QTextStream stream(&file);
-            stream << outString.replace(QLatin1String("&amp;"), QLatin1String("&"))
-                         .replace(QLatin1String("&gt;"), QLatin1String(">"))
-                         .replace(QLatin1String("&lt;"), QLatin1String("<"));
-            file.close();
-        }
+    WorksheetEntry* lastDraggingEntry = m_hierarchySubentriesDrag.empty() ? static_cast<WorksheetEntry*>(entry) : m_hierarchySubentriesDrag.back();
 
-        xmlFree(xmlResultBuffer);
-    }
+    lastDraggingEntry->setNext(next);
 
-    xsltFreeStylesheet(xsltStyleSheet);
-    xmlFreeDoc(res);
-    xmlFreeDoc(output);
+    if (next)
+        next->setPrevious(lastDraggingEntry);
+    else
+        setLastEntry(lastDraggingEntry);
 
-    xsltCleanupGlobals();
-    xmlCleanupParser();
+    if (positionChanged)
+        normalizeDraggedHierarchyLevels(entry, previous, m_hierarchySubentriesDrag);
+
+    m_dragEntry->show();
+
+    for (auto* subentry : m_hierarchySubentriesDrag)
+        subentry->show();
+
+    m_dragEntry->focusEntry();
+
+    const QPointF scenePosition = worksheetView()->sceneCursorPos();
+    if (entryAt(scenePosition) != m_dragEntry)
+        m_dragEntry->hideActionBar();
+
+#ifndef NDEBUG
+    for (auto* current = firstEntry(); current; current = current->next())
+        Q_ASSERT(current->type() != PlaceHolderEntry::Type);
+#endif
+
+    m_hierarchySubentriesDrag.clear();
+    m_dragEntry = nullptr;
+
+    updateHierarchyLayout();
+    updateLayout();
+
+    if (positionChanged)
+        setModified();
 }
 
-bool Worksheet::load(const QString& filename )
+void Worksheet::evaluate()
 {
-    qDebug() << "loading worksheet" << filename;
-    QFile file(filename);
-    if (!file.open(QIODevice::ReadOnly)) {
-        KMessageBox::error(worksheetView(), i18n("Couldn't open the file %1.", filename), i18n("Open File"));
-        return false;
+    qDebug()<<"evaluate worksheet";
+    // login if not done yet
+    if (!m_readOnly && m_session && m_session->status() == Cantor::Session::Disable)
+        loginToSession();
+
+    // evaluate the worksheet if the login was successful
+    if (m_session && m_session->status() == Cantor::Session::Done) {
+        firstEntry()->evaluate(WorksheetEntry::EvaluateNext);
+        setModified();
     }
+}
 
-    bool rc = load(&file);
-    if (rc && !m_readOnly)
-        m_session->setWorksheetPath(filename);
+void Worksheet::evaluateCurrentEntry()
+{
+    // login if not done yet
+    if (!m_readOnly && m_session && m_session->status() == Cantor::Session::Disable)
+        loginToSession();
 
-    return rc;
+    // evaluate the current entry if the login was successful
+    if (!m_session)
+        return;
+
+    // the current status of the session should be Done or Running - the later is the case when we're
+    // waiting for the additional input like Maxima's help requests or assumptions for integrate(), etc.
+    if (m_session->status() == Cantor::Session::Done || m_session->status() == Cantor::Session::Running)
+    {
+        if(auto* entry = currentEntry())
+            entry->evaluateCurrentItem();
+    }
 }
 
-void Worksheet::load(QByteArray* data)
+bool Worksheet::completionEnabled()
 {
-    QBuffer buf(data);
-    buf.open(QIODevice::ReadOnly);
-    load(&buf);
+    return m_completionEnabled;
 }
 
-bool Worksheet::load(QIODevice* device)
+void Worksheet::showCompletion()
 {
-    if (!device->isReadable())
-    {
-        QApplication::restoreOverrideCursor();
-        KMessageBox::error(worksheetView(), i18n("Couldn't open the selected file for reading."), i18n("Open File"));
-        return false;
-    }
+    auto* current = currentEntry();
+    if (current)
+        current->showCompletion();
+}
 
-    KZip archive(device);
+WorksheetEntry* Worksheet::appendEntry(const int type, bool focus)
+{
+    auto* entry = WorksheetEntry::create(type, this);
 
-    if (archive.open(QIODevice::ReadOnly))
-        return loadCantorWorksheet(archive);
-    else
+    if (entry)
     {
-        qDebug() <<"not a zip file";
-        // Go to begin of data, we need read all data in second time
-        device->seek(0);
-
-        QJsonParseError error;
-        const QJsonDocument& doc = QJsonDocument::fromJson(device->readAll(), &error);
-        if (error.error != QJsonParseError::NoError)
+        qDebug() << "Entry Appended";
+        entry->setPrevious(lastEntry());
+        if (lastEntry())
+            lastEntry()->setNext(entry);
+        if (!firstEntry())
+            setFirstEntry(entry);
+        setLastEntry(entry);
+        if (!m_isLoadingFromFile)
         {
-            qDebug()<<"not a json file, parsing failed with error: " << error.errorString();
-            QApplication::restoreOverrideCursor();
-            KMessageBox::error(worksheetView(), i18n("The selected file is not a valid Cantor or Jupyter project file."), i18n("Open File"));
-            return false;
+            updateHierarchyLayout();
+            updateLayout();
+            if (focus)
+            {
+                makeVisible(entry);
+                focusEntry(entry);
+            }
+            setModified();
         }
-        else
-            return loadJupyterNotebook(doc);
     }
+    return entry;
 }
 
-bool Worksheet::loadCantorWorksheet(const KZip& archive)
+WorksheetEntry* Worksheet::appendCommandEntry()
 {
-    m_type = Type::CantorWorksheet;
+   return appendEntry(CommandEntry::Type);
+}
 
-    const KArchiveEntry* contentEntry=archive.directory()->entry(QLatin1String("content.xml"));
-    if (!contentEntry->isFile())
-    {
-        qDebug()<<"content.xml file not found in the zip archive";
-        QApplication::restoreOverrideCursor();
-        KMessageBox::error(worksheetView(), i18n("The selected file is not a valid Cantor project file."), i18n("Open File"));
-        return false;
-    }
+WorksheetEntry* Worksheet::appendTextEntry()
+{
+   return appendEntry(TextEntry::Type);
+}
 
-    const KArchiveFile* content = static_cast<const KArchiveFile*>(contentEntry);
+WorksheetEntry* Worksheet::appendMarkdownEntry()
+{
+   return appendEntry(MarkdownEntry::Type);
+}
+
+WorksheetEntry* Worksheet::appendPageBreakEntry()
+{
+    return appendEntry(PageBreakEntry::Type);
+}
+
+WorksheetEntry* Worksheet::appendImageEntry()
+{
+   return appendEntry(ImageEntry::Type);
+}
+
+WorksheetEntry* Worksheet::appendLatexEntry()
+{
+    return appendEntry(LatexEntry::Type);
+}
+
+void Worksheet::appendCommandEntry(const QString& text)
+{
+    auto* entry = lastEntry();
+    if(!entry->isEmpty())
+        entry = appendCommandEntry();
+
+    if (entry)
+    {
+        focusEntry(entry);
+        entry->setContent(text);
+        evaluateCurrentEntry();
+    }
+}
+
+WorksheetEntry* Worksheet::appendHorizontalRuleEntry()
+{
+    return appendEntry(HorizontalRuleEntry::Type);
+}
+
+WorksheetEntry* Worksheet::appendHierarchyEntry()
+{
+    return appendEntry(HierarchyEntry::Type);
+}
+
+WorksheetEntry* Worksheet::insertEntry(const int type, WorksheetEntry* current)
+{
+    if (!current)
+        current = currentEntry();
+
+    if (!current)
+        return appendEntry(type);
+
+    auto* next = current->next();
+    WorksheetEntry* entry = nullptr;
+
+    if (!next || next->type() != type || !next->isEmpty())
+    {
+        entry = WorksheetEntry::create(type, this);
+        entry->setPrevious(current);
+        entry->setNext(next);
+        current->setNext(entry);
+        if (next)
+            next->setPrevious(entry);
+        else
+            setLastEntry(entry);
+        updateHierarchyLayout();
+        updateLayout();
+        setModified();
+    } else {
+        entry = next;
+    }
+
+    focusEntry(entry);
+    makeVisible(entry);
+
+    return entry;
+}
+
+WorksheetEntry* Worksheet::insertTextEntry(WorksheetEntry* current)
+{
+    return insertEntry(TextEntry::Type, current);
+}
+
+WorksheetEntry* Worksheet::insertMarkdownEntry(WorksheetEntry* current)
+{
+    return insertEntry(MarkdownEntry::Type, current);
+}
+
+WorksheetEntry* Worksheet::insertCommandEntry(WorksheetEntry* current)
+{
+    return insertEntry(CommandEntry::Type, current);
+}
+
+WorksheetEntry* Worksheet::insertImageEntry(WorksheetEntry* current)
+{
+    auto* entry = insertEntry(ImageEntry::Type, current);
+    auto* imageEntry = static_cast<ImageEntry*>(entry);
+    QTimer::singleShot(0, this, [=] () {imageEntry->startConfigDialog();});
+    return entry;
+}
+
+WorksheetEntry* Worksheet::insertPageBreakEntry(WorksheetEntry* current)
+{
+    return insertEntry(PageBreakEntry::Type, current);
+}
+
+WorksheetEntry* Worksheet::insertLatexEntry(WorksheetEntry* current)
+{
+    return insertEntry(LatexEntry::Type, current);
+}
+
+WorksheetEntry* Worksheet::insertHorizontalRuleEntry(WorksheetEntry* current)
+{
+    return insertEntry(HorizontalRuleEntry::Type, current);
+}
+
+WorksheetEntry* Worksheet::insertHierarchyEntry(WorksheetEntry* current)
+{
+    return insertEntry(HierarchyEntry::Type, current);
+}
+
+WorksheetEntry* Worksheet::insertEntryBefore(int type, WorksheetEntry* current)
+{
+    if (!current)
+        current = currentEntry();
+
+    if (!current)
+        return nullptr;
+
+    auto* prev = current->previous();
+    WorksheetEntry* entry = nullptr;
+
+    if(!prev || prev->type() != type || !prev->isEmpty())
+    {
+        entry = WorksheetEntry::create(type, this);
+        entry->setNext(current);
+        entry->setPrevious(prev);
+        current->setPrevious(entry);
+        if (prev)
+            prev->setNext(entry);
+        else
+            setFirstEntry(entry);
+        updateHierarchyLayout();
+        updateLayout();
+        setModified();
+    }
+    else
+        entry = prev;
+
+    focusEntry(entry);
+    return entry;
+}
+
+WorksheetEntry* Worksheet::insertTextEntryBefore(WorksheetEntry* current)
+{
+    return insertEntryBefore(TextEntry::Type, current);
+}
+
+WorksheetEntry* Worksheet::insertMarkdownEntryBefore(WorksheetEntry* current)
+{
+    return insertEntryBefore(MarkdownEntry::Type, current);
+}
+
+WorksheetEntry* Worksheet::insertCommandEntryBefore(WorksheetEntry* current)
+{
+    return insertEntryBefore(CommandEntry::Type, current);
+}
+
+WorksheetEntry* Worksheet::insertPageBreakEntryBefore(WorksheetEntry* current)
+{
+    return insertEntryBefore(PageBreakEntry::Type, current);
+}
+
+WorksheetEntry* Worksheet::insertImageEntryBefore(WorksheetEntry* current)
+{
+    auto* entry = insertEntryBefore(ImageEntry::Type, current);
+    auto* imageEntry = static_cast<ImageEntry*>(entry);
+    QTimer::singleShot(0, this, [=] () {imageEntry->startConfigDialog();});
+    return entry;
+}
+
+WorksheetEntry* Worksheet::insertLatexEntryBefore(WorksheetEntry* current)
+{
+    return insertEntryBefore(LatexEntry::Type, current);
+}
+
+WorksheetEntry* Worksheet::insertHorizontalRuleEntryBefore(WorksheetEntry* current)
+{
+    return insertEntryBefore(HorizontalRuleEntry::Type, current);
+}
+
+WorksheetEntry* Worksheet::insertHierarchyEntryBefore(WorksheetEntry* current)
+{
+    return insertEntryBefore(HierarchyEntry::Type, current);
+}
+
+void Worksheet::interrupt()
+{
+    if (m_session->status() == Cantor::Session::Running)
+    {
+        m_session->interrupt();
+        Q_EMIT updatePrompt();
+    }
+}
+
+void Worksheet::interruptCurrentEntryEvaluation()
+{
+    currentEntry()->interruptEvaluation();
+}
+
+
+bool Worksheet::variableHighlightingEnabled() const
+{
+    return m_variableHighlightingEnabled;
+}
+
+void Worksheet::setVariableHighlightingEnabled(bool enabled)
+{
+    if (m_variableHighlightingEnabled == enabled)
+    {
+        return;
+    }
+    m_variableHighlightingEnabled = enabled;
+
+    for (auto* entry = firstEntry(); entry; entry = entry->next())
+    {
+        if (entry->type() == CommandEntry::Type)
+            static_cast<CommandEntry*>(entry)->setVariableHighlightingEnabled(enabled);
+    }
+}
+
+void Worksheet::enableCompletion(bool enable)
+{
+    m_completionEnabled=enable;
+}
+
+Cantor::Session* Worksheet::session() const
+{
+    return m_session;
+}
+
+bool Worksheet::isRunning()
+{
+    return m_session && m_session->status()==Cantor::Session::Running;
+}
+
+bool Worksheet::isReadOnly()
+{
+    return m_readOnly;
+}
+
+bool Worksheet::showExpressionIds()
+{
+    return m_showExpressionIds;
+}
+
+bool Worksheet::animationsEnabled()
+{
+    return m_animationsEnabled;
+}
+
+void Worksheet::enableAnimations(bool enable)
+{
+    m_animationsEnabled = enable;
+}
+
+bool Worksheet::embeddedMathEnabled()
+{
+    return m_embeddedMathEnabled && m_mathRenderer.mathRenderAvailable();
+}
+
+void Worksheet::enableEmbeddedMath(bool enable)
+{
+    m_embeddedMathEnabled = enable;
+}
+
+void Worksheet::enableExpressionNumbering(bool enable)
+{
+    m_showExpressionIds=enable;
+    Q_EMIT updatePrompt();
+    refreshTocStructure();
+    if (views().size() != 0)
+        updateLayout();
+}
+
+QDomDocument Worksheet::toXML(KZip* archive)
+{
+    QDomDocument doc( QLatin1String("CantorWorksheet") );
+    QDomElement root = doc.createElement( QLatin1String("Worksheet") );
+    root.setAttribute(QLatin1String("backend"), (m_session ? m_session->backend()->name(): m_backendName));
+    doc.appendChild(root);
+
+    for( auto* entry = firstEntry(); entry; entry = entry->next())
+    {
+        QDomElement el = entry->toXml(doc, archive);
+        root.appendChild( el );
+    }
+    return doc;
+}
+
+QJsonDocument Worksheet::toJupyterJson()
+{
+    QJsonDocument doc;
+    QJsonObject root;
+
+    QJsonObject metadata(m_jupyterMetadata ? *m_jupyterMetadata : QJsonObject());
+
+    QJsonObject kernalInfo;
+    if (m_session && m_session->backend())
+        kernalInfo = Cantor::JupyterUtils::getKernelspec(m_session->backend());
+    else
+        kernalInfo.insert(QLatin1String("name"), m_backendName);
+    metadata.insert(QLatin1String("kernelspec"), kernalInfo);
+
+    root.insert(QLatin1String("metadata"), metadata);
+
+    // Not sure, but it looks like we support nbformat version 4.5
+    root.insert(QLatin1String("nbformat"), 4);
+    root.insert(QLatin1String("nbformat_minor"), 5);
+
+    QJsonArray cells;
+    for( auto* entry = firstEntry(); entry; entry = entry->next())
+    {
+        const QJsonValue entryJson = entry->toJupyterJson();
+
+        if (!entryJson.isNull())
+            cells.append(entryJson);
+    }
+    root.insert(QLatin1String("cells"), cells);
+
+    doc.setObject(root);
+    return doc;
+}
+
+void Worksheet::save( const QString& filename )
+{
+    QFile file(filename);
+    if ( !file.open(QIODevice::WriteOnly) )
+    {
+        KMessageBox::error( worksheetView(),
+                            i18n( "Cannot write file %1." , filename ),
+                            i18n( "Error - Cantor" ));
+        return;
+    }
+
+    save(&file);
+}
+
+QByteArray Worksheet::saveToByteArray()
+{
+    QBuffer buffer;
+    save(&buffer);
+
+    return buffer.buffer();
+}
+
+void Worksheet::save( QIODevice* device)
+{
+    qDebug()<<"saving to filename";
+    switch (m_type)
+    {
+        case CantorWorksheet:
+        {
+            KZip zipFile( device );
+
+            if ( !zipFile.open(QIODevice::WriteOnly) )
+            {
+                KMessageBox::error( worksheetView(),
+                                    i18n( "Cannot write file." ),
+                                    i18n( "Error - Cantor" ));
+                return;
+            }
+
+            QByteArray content = toXML(&zipFile).toByteArray();
+            zipFile.writeFile( QLatin1String("content.xml"), content.data());
+            break;
+        }
+
+        case JupyterNotebook:
+        {
+            if (!device->isWritable())
+            {
+                KMessageBox::error( worksheetView(),
+                                    i18n( "Cannot write file." ),
+                                    i18n( "Error - Cantor" ));
+                return;
+            }
+
+            const QJsonDocument& doc = toJupyterJson();
+            device->write(doc.toJson(QJsonDocument::Indented));
+            break;
+        }
+    }
+}
+
+void Worksheet::savePlain(const QString& filename)
+{
+    QFile file(filename);
+    if(!file.open(QIODevice::WriteOnly))
+    {
+        KMessageBox::error(worksheetView(), i18n("Error saving file %1", filename), i18n("Error - Cantor"));
+        return;
+    }
+
+    QString cmdSep=QLatin1String(";\n");
+    QString commentStartingSeq = QLatin1String("");
+    QString commentEndingSeq = QLatin1String("");
+
+    if (!m_readOnly)
+    {
+        Cantor::Backend * const backend=session()->backend();
+        if (backend->extensions().contains(QLatin1String("ScriptExtension")))
+        {
+            Cantor::ScriptExtension* e=dynamic_cast<Cantor::ScriptExtension*>(backend->extension(QLatin1String(("ScriptExtension"))));
+            if (e)
+            {
+                cmdSep=e->commandSeparator();
+                commentStartingSeq = e->commentStartingSequence();
+                commentEndingSeq = e->commentEndingSequence();
+            }
+        }
+    }
+    else
+        KMessageBox::information(worksheetView(), i18n("In read-only mode Cantor couldn't guarantee, that the export will be valid for %1", m_backendName), i18n("Cantor"));
+
+    QTextStream stream(&file);
+
+    for(auto* entry = firstEntry(); entry; entry = entry->next())
+    {
+        const QString& str=entry->toPlain(cmdSep, commentStartingSeq, commentEndingSeq);
+        if(!str.isEmpty())
+            stream << str + QLatin1Char('\n');
+    }
+
+    file.close();
+}
+
+void Worksheet::saveLatex(const QString& filename)
+{
+    qDebug()<<"exporting to Latex: " <<filename;
+
+    QFile file(filename);
+    if(!file.open(QIODevice::WriteOnly))
+    {
+        KMessageBox::error(worksheetView(), i18n("Error saving file %1", filename), i18n("Export to LaTeX"));
+        return;
+    }
+
+    QString stylesheet = QStandardPaths::locate(QStandardPaths::AppDataLocation, QLatin1String("xslt/latex.xsl"));
+    if (stylesheet.isEmpty()) {
+        KMessageBox::error(worksheetView(), i18n("Error loading latex.xsl stylesheet"), i18n("Export to LaTeX"));
+        return;
+    }
+
+    xsltStylesheetPtr xsltStyleSheet = xsltParseStylesheetFile((const xmlChar *)stylesheet.toLocal8Bit().constData());
+    xmlDocPtr output = xmlReadDoc((const xmlChar *)toXML().toString().toStdString().c_str(), nullptr, "utf-8", XML_PARSE_RECOVER | XML_PARSE_NOENT | XML_PARSE_DTDLOAD);
+
+    const char *params[16+1];
+    params[0] = nullptr;
+    auto res = xsltApplyStylesheet(xsltStyleSheet, output, params);
+    if (res) {
+        xmlChar *xmlResultBuffer = nullptr;
+        int xmlResultLength = 0;
+        int res = xsltSaveResultToString(&xmlResultBuffer, &xmlResultLength, output, xsltStyleSheet);
+        if (res != -1) {
+            QString outString = QString::fromUtf8((char *)xmlResultBuffer);
+
+            // Transform HTML escaped special characters to valid LaTeX characters (&, <, >)
+            QTextStream stream(&file);
+            stream << outString.replace(QLatin1String("&amp;"), QLatin1String("&"))
+                         .replace(QLatin1String("&gt;"), QLatin1String(">"))
+                         .replace(QLatin1String("&lt;"), QLatin1String("<"));
+            file.close();
+        }
+
+        xmlFree(xmlResultBuffer);
+    }
+
+    xsltFreeStylesheet(xsltStyleSheet);
+    xmlFreeDoc(res);
+    xmlFreeDoc(output);
+
+    xsltCleanupGlobals();
+    xmlCleanupParser();
+}
+
+bool Worksheet::load(const QString& filename )
+{
+    qDebug() << "loading worksheet" << filename;
+    QFile file(filename);
+    if (!file.open(QIODevice::ReadOnly)) {
+        KMessageBox::error(worksheetView(), i18n("Couldn't open the file %1.", filename), i18n("Open File"));
+        return false;
+    }
+
+    bool rc = load(&file);
+    if (rc && !m_readOnly)
+        m_session->setWorksheetPath(filename);
+
+    return rc;
+}
+
+void Worksheet::load(QByteArray* data)
+{
+    QBuffer buf(data);
+    buf.open(QIODevice::ReadOnly);
+    load(&buf);
+}
+
+bool Worksheet::load(QIODevice* device)
+{
+    if (!device->isReadable())
+    {
+        QApplication::restoreOverrideCursor();
+        KMessageBox::error(worksheetView(), i18n("Couldn't open the selected file for reading."), i18n("Open File"));
+        return false;
+    }
+
+    KZip archive(device);
+
+    if (archive.open(QIODevice::ReadOnly))
+        return loadCantorWorksheet(archive);
+    else
+    {
+        qDebug() <<"not a zip file";
+        // Go to begin of data, we need read all data in second time
+        device->seek(0);
+
+        QJsonParseError error;
+        const QJsonDocument& doc = QJsonDocument::fromJson(device->readAll(), &error);
+        if (error.error != QJsonParseError::NoError)
+        {
+            qDebug()<<"not a json file, parsing failed with error: " << error.errorString();
+            QApplication::restoreOverrideCursor();
+            KMessageBox::error(worksheetView(), i18n("The selected file is not a valid Cantor or Jupyter project file."), i18n("Open File"));
+            return false;
+        }
+        else
+            return loadJupyterNotebook(doc);
+    }
+}
+
+bool Worksheet::loadCantorWorksheet(const KZip& archive)
+{
+    m_type = Type::CantorWorksheet;
+
+    const KArchiveEntry* contentEntry=archive.directory()->entry(QLatin1String("content.xml"));
+    if (!contentEntry->isFile())
+    {
+        qDebug()<<"content.xml file not found in the zip archive";
+        QApplication::restoreOverrideCursor();
+        KMessageBox::error(worksheetView(), i18n("The selected file is not a valid Cantor project file."), i18n("Open File"));
+        return false;
+    }
+
+    const KArchiveFile* content = static_cast<const KArchiveFile*>(contentEntry);
     QByteArray data = content->data();
 
     QDomDocument doc;
@@ -1794,6 +2466,7 @@ void Worksheet::gotResult(Cantor::Expression* expr)
             break;
         }
     }
+
 }
 
 void Worksheet::removeCurrentEntry()
@@ -2281,6 +2954,14 @@ WorksheetTextItem* Worksheet::lastFocusedTextItem()
 
 void Worksheet::updateFocusedTextItem(WorksheetTextItem* newItem)
 {
+    if (newItem)
+    {
+        auto* entry = qobject_cast<WorksheetEntry*>(newItem->parentObject());
+
+        if (entry && isValidEntry(entry))
+            updateCurrentHierarchyFromEntry(entry);
+    }
+
     if (m_lastFocusedTextItem) {
         disconnect(m_lastFocusedTextItem, &WorksheetTextEditorItem::undoAvailable, this, &Worksheet::undoAvailable);
         disconnect(m_lastFocusedTextItem, &WorksheetTextEditorItem::redoAvailable, this, &Worksheet::redoAvailable);
@@ -2340,6 +3021,13 @@ void Worksheet::updateFocusedTextItem(WorksheetTextItem* newItem)
 
 void Worksheet::updateFocusedTextItem(WorksheetTextEditorItem* newItem)
 {
+    if (newItem)
+    {
+        auto* entry = qobject_cast<WorksheetEntry*>(newItem->parentObject());
+
+        if (entry && isValidEntry(entry))
+            updateCurrentHierarchyFromEntry(entry);
+    }
     if (m_legacylastFocusedTextItem) {
         disconnect(m_legacylastFocusedTextItem, &WorksheetTextItem::undoAvailable, this, &Worksheet::undoAvailable);
         disconnect(m_legacylastFocusedTextItem, &WorksheetTextItem::redoAvailable, this, &Worksheet::redoAvailable);
@@ -2427,8 +3115,8 @@ void Worksheet::paste() {
 
 void Worksheet::setRichTextInformation(const RichTextInfo& info)
 {
-    // if (!m_boldAction)
-    //     initActions();
+    if (!m_boldAction)
+        return;
 
     m_boldAction->setChecked(info.bold);
     m_italicAction->setChecked(info.italic);
@@ -2517,570 +3205,1064 @@ void Worksheet::setTextStrikeOut(bool b)
         m_legacylastFocusedTextItem->setTextStrikeOut(b);
 }
 
-void Worksheet::setAlignLeft()
+void Worksheet::setAlignLeft()
+{
+    if (m_lastFocusedTextItem)
+        m_lastFocusedTextItem->setAlignment(Qt::AlignLeft);
+    else if (m_legacylastFocusedTextItem)
+        m_legacylastFocusedTextItem->setAlignment(Qt::AlignLeft);
+}
+
+void Worksheet::setAlignRight()
+{
+    if (m_lastFocusedTextItem)
+        m_lastFocusedTextItem->setAlignment(Qt::AlignRight);
+    else if (m_legacylastFocusedTextItem)
+        m_legacylastFocusedTextItem->setAlignment(Qt::AlignRight);
+}
+
+void Worksheet::setAlignCenter()
+{
+    if (m_lastFocusedTextItem)
+        m_lastFocusedTextItem->setAlignment(Qt::AlignCenter);
+    else if (m_legacylastFocusedTextItem)
+        m_legacylastFocusedTextItem->setAlignment(Qt::AlignCenter);
+}
+
+void Worksheet::setAlignJustify()
+{
+    if (m_lastFocusedTextItem)
+        m_lastFocusedTextItem->setAlignment(Qt::AlignJustify);
+    else if (m_legacylastFocusedTextItem)
+        m_legacylastFocusedTextItem->setAlignment(Qt::AlignJustify);
+}
+
+void Worksheet::setFontFamily(const QString& font)
+{
+    if (m_lastFocusedTextItem)
+        m_lastFocusedTextItem->setFontFamily(font);
+    else if (m_legacylastFocusedTextItem)
+        m_legacylastFocusedTextItem->setFontFamily(font);
+}
+
+void Worksheet::setFontSize(int size)
+{
+    if (m_lastFocusedTextItem)
+        m_lastFocusedTextItem->setFontSize(size);
+    else if (m_legacylastFocusedTextItem)
+        m_legacylastFocusedTextItem->setFontSize(size);
+}
+
+
+bool Worksheet::isShortcut(const QKeySequence& sequence)
+{
+    return m_shortcuts.contains(sequence);
+}
+
+void Worksheet::registerShortcut(QAction* action)
+{
+    for (auto& shortcut : action->shortcuts())
+        m_shortcuts.insert(shortcut, action);
+
+    connect(action, &QAction::changed, this, &Worksheet::updateShortcut);
+}
+
+void Worksheet::updateShortcut()
+{
+    QAction* action = qobject_cast<QAction*>(sender());
+    if (!action)
+        return;
+
+    // delete the old shortcuts of this action
+    QList<QKeySequence> shortcuts = m_shortcuts.keys(action);
+    for (auto& shortcut : shortcuts)
+        m_shortcuts.remove(shortcut);
+
+    // add the new shortcuts
+    for (auto& shortcut : action->shortcuts())
+        m_shortcuts.insert(shortcut, action);
+}
+
+void Worksheet::dragEnterEvent(QGraphicsSceneDragDropEvent* event)
+{
+    if (m_dragEntry)
+        event->accept();
+    else
+        QGraphicsScene::dragEnterEvent(event);
+}
+
+void Worksheet::dragLeaveEvent(QGraphicsSceneDragDropEvent* event)
+{
+    if (!m_dragEntry)
+    {
+        QGraphicsScene::dragLeaveEvent(event);
+        return;
+    }
+
+    event->accept();
+
+    removeDragPlaceholder();
+    updateLayout();
+}
+
+void Worksheet::dragMoveEvent(QGraphicsSceneDragDropEvent* event)
+{
+    if (!m_dragEntry) {
+        QGraphicsScene::dragMoveEvent(event);
+        return;
+    }
+
+    QPointF pos = event->scenePos();
+    auto* entry = entryAt(pos);
+    WorksheetEntry* prev = nullptr;
+    WorksheetEntry* next = nullptr;
+    if (entry) {
+        if (pos.y() < entry->y() + entry->size().height()/2) {
+            prev = entry->previous();
+            next = entry;
+        } else if (pos.y() >= entry->y() + entry->size().height()/2) {
+            prev = entry;
+            next = entry->next();
+        }
+    } else {
+        auto* last = lastEntry();
+        if (last && pos.y() > last->y() + last->size().height()) {
+            prev = last;
+            next = nullptr;
+        }
+    }
+
+    const bool dragWithHierarchy = !m_hierarchySubentriesDrag.empty();
+
+    if (m_placeholderEntry)
+    {
+        if (prev == m_placeholderEntry)
+            prev = m_placeholderEntry->previous();
+
+        if (next == m_placeholderEntry)
+            next = m_placeholderEntry->next();
+    }
+
+    if (prev || next)
+    {
+        const QSizeF placeholderSize = dragWithHierarchy ? m_hierarchyDragSize : m_dragEntry->size();
+
+        if (!m_placeholderEntry)
+            m_placeholderEntry = new PlaceHolderEntry(this, QSizeF(0, 0));
+
+        const bool positionChanged = m_placeholderEntry->previous() != prev || m_placeholderEntry->next() != next;
+
+        if (positionChanged)
+        {
+            auto* oldPrevious = m_placeholderEntry->previous();
+            auto* oldNext = m_placeholderEntry->next();
+
+            if (oldPrevious && oldPrevious->next() == m_placeholderEntry)
+                oldPrevious->setNext(oldNext);
+            else if (firstEntry() == m_placeholderEntry)
+                setFirstEntry(oldNext);
+
+            if (oldNext && oldNext->previous() == m_placeholderEntry)
+                oldNext->setPrevious(oldPrevious);
+            else if (lastEntry() == m_placeholderEntry)
+                setLastEntry(oldPrevious);
+
+            m_placeholderEntry->setPrevious(prev);
+            m_placeholderEntry->setNext(next);
+
+            if (prev)
+                prev->setNext(m_placeholderEntry);
+            else
+                setFirstEntry(m_placeholderEntry);
+
+            if (next)
+                next->setPrevious(m_placeholderEntry);
+            else
+                setLastEntry(m_placeholderEntry);
+        }
+
+        m_placeholderEntry->changeSize(placeholderSize);
+
+        updateLayout();
+    }
+
+    const QPoint viewPos = worksheetView()->mapFromScene(pos);
+    const int viewHeight = worksheetView()->viewport()->height();
+    if ((viewPos.y() < 10 || viewPos.y() > viewHeight - 10) &&
+        !m_dragScrollTimer) {
+        m_dragScrollTimer = new QTimer(this);
+        m_dragScrollTimer->setSingleShot(true);
+        m_dragScrollTimer->setInterval(100);
+        connect(m_dragScrollTimer, SIGNAL(timeout()), this,
+                SLOT(updateDragScrollTimer()));
+        m_dragScrollTimer->start();
+    }
+
+    event->accept();
+}
+
+void Worksheet::dropEvent(QGraphicsSceneDragDropEvent* event)
+{
+    if (!m_dragEntry)
+        QGraphicsScene::dropEvent(event);
+    event->accept();
+}
+
+void Worksheet::updateDragScrollTimer()
+{
+    if (!m_dragScrollTimer)
+        return;
+
+    const QPoint viewPos = worksheetView()->viewCursorPos();
+    const QWidget* viewport = worksheetView()->viewport();
+    const int viewHeight = viewport->height();
+    if (!m_dragEntry || !(viewport->rect().contains(viewPos)) ||
+        (viewPos.y() >= 10 && viewPos.y() <= viewHeight - 10)) {
+        delete m_dragScrollTimer;
+        m_dragScrollTimer = nullptr;
+        return;
+    }
+
+    if (viewPos.y() < 10)
+        worksheetView()->scrollBy(-10*(10 - viewPos.y()));
+    else
+        worksheetView()->scrollBy(10*(viewHeight - viewPos.y()));
+
+    m_dragScrollTimer->start();
+}
+
+void Worksheet::removeDragPlaceholder()
+{
+    if (!m_placeholderEntry)
+        return;
+
+    auto* placeholder = m_placeholderEntry;
+    auto* previous = placeholder->previous();
+    auto* next = placeholder->next();
+
+    if (previous && previous->next() == placeholder)
+        previous->setNext(next);
+    else if (firstEntry() == placeholder)
+        setFirstEntry(next);
+
+    if (next && next->previous() == placeholder)
+        next->setPrevious(previous);
+    else if (lastEntry() == placeholder)
+        setLastEntry(previous);
+
+    placeholder->setPrevious(nullptr);
+    placeholder->setNext(nullptr);
+    placeholder->hide();
+    placeholder->deleteLater();
+
+    m_placeholderEntry = nullptr;
+}
+
+void Worksheet::updateEntryCursor(QGraphicsSceneMouseEvent* event)
+{
+    // determine the worksheet entry near which the entry cursor will be shown
+    resetEntryCursor();
+    if (event->button() == Qt::LeftButton && !focusItem())
+    {
+        const qreal y = event->scenePos().y();
+        for (auto* entry = firstEntry(); entry; entry = entry->next())
+        {
+            if (entry == firstEntry() && y < entry->y() )
+            {
+                m_choosenCursorEntry = firstEntry();
+                break;
+            }
+            else if (entry->y() < y && (entry->next() && y < entry->next()->y()))
+            {
+                m_choosenCursorEntry = entry->next();
+                break;
+            }
+            else if (entry->y() < y && entry == lastEntry())
+            {
+                m_isCursorEntryAfterLastEntry = true;
+                break;
+            }
+        }
+    }
+
+    if (m_choosenCursorEntry || m_isCursorEntryAfterLastEntry)
+        drawEntryCursor();
+}
+
+void Worksheet::addEntryFromEntryCursor()
+{
+    qDebug() << "Add new entry from entry cursor";
+    if (m_isCursorEntryAfterLastEntry)
+        insertCommandEntry(lastEntry());
+    else
+        insertCommandEntryBefore(m_choosenCursorEntry);
+    resetEntryCursor();
+}
+
+void Worksheet::animateEntryCursor()
 {
-    if (m_lastFocusedTextItem)
-        m_lastFocusedTextItem->setAlignment(Qt::AlignLeft);
-    else if (m_legacylastFocusedTextItem)
-        m_legacylastFocusedTextItem->setAlignment(Qt::AlignLeft);
+    if ((m_choosenCursorEntry || m_isCursorEntryAfterLastEntry) && m_entryCursorItem)
+        m_entryCursorItem->setVisible(!m_entryCursorItem->isVisible());
 }
 
-void Worksheet::setAlignRight()
+void Worksheet::resetEntryCursor()
 {
-    if (m_lastFocusedTextItem)
-        m_lastFocusedTextItem->setAlignment(Qt::AlignRight);
-    else if (m_legacylastFocusedTextItem)
-        m_legacylastFocusedTextItem->setAlignment(Qt::AlignRight);
+    m_choosenCursorEntry = nullptr;
+    m_isCursorEntryAfterLastEntry = false;
+    m_entryCursorItem->hide();
 }
 
-void Worksheet::setAlignCenter()
+void Worksheet::drawEntryCursor()
 {
-    if (m_lastFocusedTextItem)
-        m_lastFocusedTextItem->setAlignment(Qt::AlignCenter);
-    else if (m_legacylastFocusedTextItem)
-        m_legacylastFocusedTextItem->setAlignment(Qt::AlignCenter);
+    if (m_entryCursorItem && (m_choosenCursorEntry || (m_isCursorEntryAfterLastEntry && lastEntry())))
+    {
+        qreal x;
+        qreal y;
+        if (m_isCursorEntryAfterLastEntry)
+        {
+            x = lastEntry()->x();
+            y = lastEntry()->y() + lastEntry()->size().height() - (EntryCursorWidth - 1);
+        }
+        else
+        {
+            x = m_choosenCursorEntry->x();
+            y = m_choosenCursorEntry->y();
+        }
+        m_entryCursorItem->setLine(x,y,x+EntryCursorLength,y);
+        m_entryCursorItem->show();
+    }
 }
 
-void Worksheet::setAlignJustify()
+void Worksheet::setType(Worksheet::Type type)
 {
-    if (m_lastFocusedTextItem)
-        m_lastFocusedTextItem->setAlignment(Qt::AlignJustify);
-    else if (m_legacylastFocusedTextItem)
-        m_legacylastFocusedTextItem->setAlignment(Qt::AlignJustify);
+    m_type = type;
 }
 
-void Worksheet::setFontFamily(const QString& font)
+Worksheet::Type Worksheet::type() const
 {
-    if (m_lastFocusedTextItem)
-        m_lastFocusedTextItem->setFontFamily(font);
-    else if (m_legacylastFocusedTextItem)
-        m_legacylastFocusedTextItem->setFontFamily(font);
+    return m_type;
 }
 
-void Worksheet::setFontSize(int size)
+void Worksheet::changeEntryType(WorksheetEntry* target, int newType)
 {
-    if (m_lastFocusedTextItem)
-        m_lastFocusedTextItem->setFontSize(size);
-    else if (m_legacylastFocusedTextItem)
-        m_legacylastFocusedTextItem->setFontSize(size);
-}
+    if (target && target->type() != newType)
+    {
+        bool animation_state = m_animationsEnabled;
+        m_animationsEnabled = false;
 
+        QString content;
 
-bool Worksheet::isShortcut(const QKeySequence& sequence)
-{
-    return m_shortcuts.contains(sequence);
-}
+        int targetEntryType = target->type();
+        switch(targetEntryType)
+        {
+            case CommandEntry::Type:
+                content = static_cast<CommandEntry*>(target)->command();
+                break;
+            case MarkdownEntry::Type:
+                content = static_cast<MarkdownEntry*>(target)->plainText();
+                break;
+            case TextEntry::Type:
+                content = static_cast<TextEntry*>(target)->text();
+                break;
+            case LatexEntry::Type:
+                content = static_cast<LatexEntry*>(target)->plain();
+        }
 
-void Worksheet::registerShortcut(QAction* action)
-{
-    for (auto& shortcut : action->shortcuts())
-        m_shortcuts.insert(shortcut, action);
+        auto* newEntry = WorksheetEntry::create(newType, this);
+        if (newEntry)
+        {
+            newEntry->setContent(content);
+            auto* tmp = target;
 
-    connect(action, &QAction::changed, this, &Worksheet::updateShortcut);
-}
+            newEntry->setPrevious(tmp->previous());
+            newEntry->setNext(tmp->next());
 
-void Worksheet::updateShortcut()
-{
-    QAction* action = qobject_cast<QAction*>(sender());
-    if (!action)
-        return;
+            tmp->setPrevious(nullptr);
+            tmp->setNext(nullptr);
+            tmp->clearFocus();
+            tmp->forceRemove();
 
-    // delete the old shortcuts of this action
-    QList<QKeySequence> shortcuts = m_shortcuts.keys(action);
-    for (auto& shortcut : shortcuts)
-        m_shortcuts.remove(shortcut);
+            if (newEntry->previous())
+                newEntry->previous()->setNext(newEntry);
+            else
+                setFirstEntry(newEntry);
 
-    // add the new shortcuts
-    for (auto& shortcut : action->shortcuts())
-        m_shortcuts.insert(shortcut, action);
+            if (newEntry->next())
+                newEntry->next()->setPrevious(newEntry);
+            else
+                setLastEntry(newEntry);
+
+            if (newType == HierarchyEntry::Type || targetEntryType == HierarchyEntry::Type)
+                updateHierarchyLayout();
+            updateLayout();
+            makeVisible(newEntry);
+            focusEntry(newEntry);
+            setModified();
+            newEntry->focusEntry();
+        }
+        m_animationsEnabled = animation_state;
+    }
 }
 
-void Worksheet::dragEnterEvent(QGraphicsSceneDragDropEvent* event)
+bool Worksheet::isValidEntry(WorksheetEntry* entry)
 {
-    if (m_dragEntry)
-        event->accept();
-    else
-        QGraphicsScene::dragEnterEvent(event);
+    for (auto* iter = firstEntry(); iter; iter = iter->next())
+        if (entry == iter)
+            return true;
+
+    return false;
 }
 
-void Worksheet::dragLeaveEvent(QGraphicsSceneDragDropEvent* event)
+void Worksheet::selectionRemove()
 {
-    if (!m_dragEntry) {
-        QGraphicsScene::dragLeaveEvent(event);
+    if (m_selectedEntries.isEmpty())
         return;
+
+    if (Settings::warnAboutEntryDelete())
+    {
+        const auto result = KMessageBox::warningTwoActions(worksheetView(),
+                i18n("This step cannot be undone. "
+                     "Do you really want to delete "
+                     "the selected entries?"),
+                i18n("Delete Entries"),
+                KStandardGuiItem::remove(),
+                KStandardGuiItem::cancel());
+
+        if (result != KMessageBox::PrimaryAction)
+            return;
     }
 
-    event->accept();
-    if (m_placeholderEntry) {
-        m_placeholderEntry->startRemoving();
-        m_placeholderEntry = nullptr;
+    for (auto* entry : m_selectedEntries)
+    {
+        if (isValidEntry(entry))
+            entry->startRemoving(false);
     }
+
+    m_selectedEntries.clear();
 }
 
-void Worksheet::dragMoveEvent(QGraphicsSceneDragDropEvent* event)
+void Worksheet::selectionEvaluate()
 {
-    if (!m_dragEntry) {
-        QGraphicsScene::dragMoveEvent(event);
-        return;
-    }
-
-    QPointF pos = event->scenePos();
-    auto* entry = entryAt(pos);
-    WorksheetEntry* prev = nullptr;
-    WorksheetEntry* next = nullptr;
-    if (entry) {
-        if (pos.y() < entry->y() + entry->size().height()/2) {
-            prev = entry->previous();
-            next = entry;
-        } else if (pos.y() >= entry->y() + entry->size().height()/2) {
-            prev = entry;
-            next = entry->next();
-        }
-    } else {
-        auto* last = lastEntry();
-        if (last && pos.y() > last->y() + last->size().height()) {
-            prev = last;
-            next = nullptr;
-        }
-    }
+    // run entries in worksheet order: from top to down
+    for (auto* entry = firstEntry(); entry; entry = entry->next())
+        if (m_selectedEntries.indexOf(entry) != -1)
+            entry->evaluate();
+}
 
-    bool dragWithHierarchy = m_hierarchySubentriesDrag.size() != 0;
+void Worksheet::selectionMoveUp()
+{
+    bool moveHierarchyEntry = false;
+    // movement up should have an order from top to down.
+    for(auto* entry = firstEntry(); entry; entry = entry->next())
+        if(m_selectedEntries.indexOf(entry) != -1)
+            if (entry->previous() && m_selectedEntries.indexOf(entry->previous()) == -1)
+            {
+                entry->moveToPrevious(false);
+                if (entry->type() == HierarchyEntry::Type)
+                    moveHierarchyEntry = true;
+            }
+    if (moveHierarchyEntry)
+        updateHierarchyLayout();
+    updateLayout();
+}
 
-    if (prev || next) {
-        auto* oldPlaceHolder = m_placeholderEntry;
-        if (prev && prev->type() == PlaceHolderEntry::Type &&
-            (!prev->aboutToBeRemoved() || prev->stopRemoving())) {
-            m_placeholderEntry = qgraphicsitem_cast<PlaceHolderEntry*>(prev);
-            if (dragWithHierarchy)
-                m_placeholderEntry->changeSize(m_hierarchyDragSize);
-            else
-                m_placeholderEntry->changeSize(m_dragEntry->size());
-        } else if (next && next->type() == PlaceHolderEntry::Type &&
-                   (!next->aboutToBeRemoved() || next->stopRemoving())) {
-            m_placeholderEntry = qgraphicsitem_cast<PlaceHolderEntry*>(next);
-            if (dragWithHierarchy)
-                m_placeholderEntry->changeSize(m_hierarchyDragSize);
-            else
-                m_placeholderEntry->changeSize(m_dragEntry->size());
-        } else {
-            m_placeholderEntry = new PlaceHolderEntry(this, QSizeF(0,0));
-            m_placeholderEntry->setPrevious(prev);
-            m_placeholderEntry->setNext(next);
-            if (prev)
-                prev->setNext(m_placeholderEntry);
-            else
-                setFirstEntry(m_placeholderEntry);
-            if (next)
-                next->setPrevious(m_placeholderEntry);
-            else
-                setLastEntry(m_placeholderEntry);
-            if (dragWithHierarchy)
-                m_placeholderEntry->changeSize(m_hierarchyDragSize);
-            else
-                m_placeholderEntry->changeSize(m_dragEntry->size());
-        }
-        if (oldPlaceHolder && oldPlaceHolder != m_placeholderEntry)
-            oldPlaceHolder->startRemoving();
-        updateLayout();
-    }
+void Worksheet::selectionMoveDown()
+{
+    bool moveHierarchyEntry = false;
+    // movement up should have an order from down to top.
+    for(auto* entry = lastEntry(); entry; entry = entry->previous())
+        if(m_selectedEntries.indexOf(entry) != -1)
+            if (entry->next() && m_selectedEntries.indexOf(entry->next()) == -1)
+            {
+                entry->moveToNext(false);
+                if (entry->type() == HierarchyEntry::Type)
+                    moveHierarchyEntry = true;
+            }
+    if (moveHierarchyEntry)
+        updateHierarchyLayout();
+    updateLayout();
+}
 
-    const QPoint viewPos = worksheetView()->mapFromScene(pos);
-    const int viewHeight = worksheetView()->viewport()->height();
-    if ((viewPos.y() < 10 || viewPos.y() > viewHeight - 10) &&
-        !m_dragScrollTimer) {
-        m_dragScrollTimer = new QTimer(this);
-        m_dragScrollTimer->setSingleShot(true);
-        m_dragScrollTimer->setInterval(100);
-        connect(m_dragScrollTimer, SIGNAL(timeout()), this,
-                SLOT(updateDragScrollTimer()));
-        m_dragScrollTimer->start();
+void Worksheet::notifyEntryFocus(WorksheetEntry* entry)
+{
+    if (entry)
+    {
+        m_circularFocusBuffer.enqueue(entry);
+
+        if (m_circularFocusBuffer.size() > 2)
+            m_circularFocusBuffer.dequeue();
     }
+    else
+        m_circularFocusBuffer.clear();
+}
 
-    event->accept();
+void Worksheet::collapseAllResults()
+{
+    for (auto* entry = firstEntry(); entry; entry = entry->next())
+        if (entry->type() == CommandEntry::Type)
+            static_cast<CommandEntry*>(entry)->collapseResults();
 }
 
-void Worksheet::dropEvent(QGraphicsSceneDragDropEvent* event)
+void Worksheet::uncollapseAllResults()
 {
-    if (!m_dragEntry)
-        QGraphicsScene::dropEvent(event);
-    event->accept();
+    for (auto* entry = firstEntry(); entry; entry = entry->next())
+        if (entry->type() == CommandEntry::Type)
+            static_cast<CommandEntry*>(entry)->expandResults();
 }
 
-void Worksheet::updateDragScrollTimer()
+void Worksheet::removeAllResults()
 {
-    if (!m_dragScrollTimer)
-        return;
+    bool remove = false;
 
-    const QPoint viewPos = worksheetView()->viewCursorPos();
-    const QWidget* viewport = worksheetView()->viewport();
-    const int viewHeight = viewport->height();
-    if (!m_dragEntry || !(viewport->rect().contains(viewPos)) ||
-        (viewPos.y() >= 10 && viewPos.y() <= viewHeight - 10)) {
-        delete m_dragScrollTimer;
-        m_dragScrollTimer = nullptr;
-        return;
+    if (KMessageBox::shouldBeShownContinue(QLatin1String("WarnAboutAllResultsRemoving")))
+    {
+        KMessageBox::ButtonCode btn = KMessageBox::warningContinueCancel(
+            views().first(),
+            i18n("This step cannot be undone. Do you really want to delete all results?"),
+            i18n("Delete all results"),
+            KStandardGuiItem::cont(),
+            KStandardGuiItem::cancel(),
+            QLatin1String("WarnAboutAllResultsRemoving")
+        );
+        remove = (btn == KMessageBox::Continue);
     }
-
-    if (viewPos.y() < 10)
-        worksheetView()->scrollBy(-10*(10 - viewPos.y()));
     else
-        worksheetView()->scrollBy(10*(viewHeight - viewPos.y()));
+        remove = true;
 
-    m_dragScrollTimer->start();
+    if (remove)
+    {
+        for (auto *entry = firstEntry(); entry; entry = entry->next())
+            if (entry->type() == CommandEntry::Type)
+                static_cast<CommandEntry*>(entry)->removeResults();
+    }
 }
 
-void Worksheet::updateEntryCursor(QGraphicsSceneMouseEvent* event)
+void Worksheet::addToExectuionSelection()
 {
-    // determine the worksheet entry near which the entry cursor will be shown
-    resetEntryCursor();
-    if (event->button() == Qt::LeftButton && !focusItem())
-    {
-        const qreal y = event->scenePos().y();
-        for (auto* entry = firstEntry(); entry; entry = entry->next())
-        {
-            if (entry == firstEntry() && y < entry->y() )
-            {
-                m_choosenCursorEntry = firstEntry();
-                break;
-            }
-            else if (entry->y() < y && (entry->next() && y < entry->next()->y()))
-            {
-                m_choosenCursorEntry = entry->next();
-                break;
-            }
-            else if (entry->y() < y && entry == lastEntry())
-            {
-                m_isCursorEntryAfterLastEntry = true;
-                break;
-            }
-        }
-    }
+    for (auto* entry : m_selectedEntries)
+        if (entry->type() == CommandEntry::Type)
+            static_cast<CommandEntry*>(entry)->addToExecution();
+}
 
-    if (m_choosenCursorEntry || m_isCursorEntryAfterLastEntry)
-        drawEntryCursor();
+void Worksheet::excludeFromExecutionSelection()
+{
+    for (auto* entry : m_selectedEntries)
+        if (entry->type() == CommandEntry::Type)
+            static_cast<CommandEntry*>(entry)->excludeFromExecution();
 }
 
-void Worksheet::addEntryFromEntryCursor()
+void Worksheet::collapseSelectionResults()
 {
-    qDebug() << "Add new entry from entry cursor";
-    if (m_isCursorEntryAfterLastEntry)
-        insertCommandEntry(lastEntry());
-    else
-        insertCommandEntryBefore(m_choosenCursorEntry);
-    resetEntryCursor();
+    for (auto* entry : m_selectedEntries)
+        if (entry->type() == CommandEntry::Type)
+            static_cast<CommandEntry*>(entry)->collapseResults();
 }
 
-void Worksheet::animateEntryCursor()
+void Worksheet::uncollapseSelectionResults()
 {
-    if ((m_choosenCursorEntry || m_isCursorEntryAfterLastEntry) && m_entryCursorItem)
-        m_entryCursorItem->setVisible(!m_entryCursorItem->isVisible());
+    for (auto* entry : m_selectedEntries)
+        if (entry->type() == CommandEntry::Type)
+            static_cast<CommandEntry*>(entry)->expandResults();
 }
 
-void Worksheet::resetEntryCursor()
+void Worksheet::removeSelectionResults()
 {
-    m_choosenCursorEntry = nullptr;
-    m_isCursorEntryAfterLastEntry = false;
-    m_entryCursorItem->hide();
+    for (auto* entry : m_selectedEntries)
+        if (entry->type() == CommandEntry::Type)
+            static_cast<CommandEntry*>(entry)->removeResults();
 }
 
-void Worksheet::drawEntryCursor()
+void Worksheet::navigateToTocNode(QString nodeId)
 {
-    if (m_entryCursorItem && (m_choosenCursorEntry || (m_isCursorEntryAfterLastEntry && lastEntry())))
+    QString plotCommandId;
+    QString plotResultId;
+    if (parsePlotNodeId(nodeId, &plotCommandId, &plotResultId))
     {
-        qreal x;
-        qreal y;
-        if (m_isCursorEntryAfterLastEntry)
+        const CommandSearchResult commandSearch = findCommandEntryById(plotCommandId);
+        if (!commandSearch.entry)
         {
-            x = lastEntry()->x();
-            y = lastEntry()->y() + lastEntry()->size().height() - (EntryCursorWidth - 1);
+            scheduleTocStructureRefresh();
+            return;
         }
-        else
+
+        const bool expanded = expandHierarchyAncestors(commandSearch.collapsedAncestors);
+        if (expanded)
         {
-            x = m_choosenCursorEntry->x();
-            y = m_choosenCursorEntry->y();
+            updateHierarchyLayout();
+            updateLayout();
         }
-        m_entryCursorItem->setLine(x,y,x+EntryCursorLength,y);
-        m_entryCursorItem->show();
+
+        if (!navigateToPlotResult(commandSearch.entry, plotResultId))
+            scheduleTocStructureRefresh();
+
+        return;
+    }
+
+    QString commandId;
+    if (parseCommandNodeId(nodeId, &commandId))
+    {
+        const CommandSearchResult commandSearch = findCommandEntryById(commandId);
+        if (!commandSearch.entry)
+            return;
+
+
+        const bool expanded = expandHierarchyAncestors(commandSearch.collapsedAncestors);
+        if (expanded)
+        {
+            updateHierarchyLayout();
+            updateLayout();
+        }
+
+        auto* commandEntry = commandSearch.entry;
+        updateCurrentHierarchyFromEntry(commandEntry);
+
+        worksheetView()->scrollTo(qRound(commandEntry->scenePos().y()));
+
+        worksheetView()->setFocus();
+
+        commandEntry->focusEntry(WorksheetTextItem::TopLeft);
+
+        resetEntryCursor();
+        return;
+    }
+
+    const HierarchySearchResult hierarchySearch = findHierarchyEntryById(nodeId);
+    if (hierarchySearch.entry)
+    {
+        const bool expanded = expandHierarchyAncestors(hierarchySearch.collapsedAncestors);
+
+        if (expanded)
+        {
+            updateHierarchyLayout();
+            updateLayout();
+        }
+
+        auto* hierarchyEntry = hierarchySearch.entry;
+        updateCurrentHierarchyFromEntry(hierarchyEntry);
+
+        worksheetView()->scrollTo(qRound(hierarchyEntry->scenePos().y()));
+
+        worksheetView()->setFocus();
+
+        hierarchyEntry->focusEntry(WorksheetTextItem::BottomRight);
+
+        resetEntryCursor();
     }
 }
 
-void Worksheet::setType(Worksheet::Type type)
+bool Worksheet::navigateToPlotResult(CommandEntry* commandEntry, const QString& resultId)
 {
-    m_type = type;
+    if (!commandEntry || resultId.isEmpty())
+        return false;
+
+    if (commandEntry->isResultCollapsed())
+    {
+        commandEntry->expandResults();
+        updateLayout();
+    }
+
+    auto* resultItem = commandEntry->resultItemById(resultId);
+    if (!resultItem || !resultItem->result() || !isPlotResult(resultItem->result()))
+        return false;
+
+    auto* object = resultItem->graphicsObject();
+    if (!object)
+        return false;
+
+    const QString nodeId = buildPlotNodeId(commandEntry->commandId(), resultId);
+
+    worksheetView()->scrollTo(qRound(object->sceneBoundingRect().top()));
+    worksheetView()->setFocus();
+    object->setFocus();
+
+    m_hierarchyTrackingSource = HierarchyTrackingSource::FocusedEntry;
+    setCurrentTocNode(nodeId);
+
+    resetEntryCursor();
+    return true;
 }
 
-Worksheet::Type Worksheet::type() const
+void Worksheet::updateCurrentTocNodeFromResult(CommandEntry* commandEntry, Cantor::Result* result)
 {
-    return m_type;
+    if (!commandEntry || !result || !isValidEntry(commandEntry))
+        return;
+
+    m_hierarchyTrackingSource = HierarchyTrackingSource::FocusedEntry;
+
+    if (isPlotResult(result))
+        setCurrentTocNode(buildPlotNodeId(commandEntry->commandId(), result->resultId()));
+    else
+        updateCurrentHierarchyFromEntry(commandEntry);
 }
 
-void Worksheet::changeEntryType(WorksheetEntry* target, int newType)
+void Worksheet::renamePlot(const QString& commandId, const QString& resultId, const QString& newTitle)
 {
-    if (target && target->type() != newType)
+    if (m_readOnly || commandId.isEmpty() || resultId.isEmpty())
+        return;
+
+    QString normalizedTitle = newTitle;
+    normalizedTitle.replace(QLatin1Char('\r'), QLatin1Char(' '));
+    normalizedTitle.replace(QLatin1Char('\n'), QLatin1Char(' '));
+    normalizedTitle = normalizedTitle.trimmed();
+
+    const CommandSearchResult commandSearch = findCommandEntryById(commandId);
+    auto* commandEntry = commandSearch.entry;
+    auto* expression = commandEntry ? commandEntry->expression() : nullptr;
+
+    if (!commandEntry || !expression)
+        return;
+
+    for (auto* result : expression->results())
     {
-        bool animation_state = m_animationsEnabled;
-        m_animationsEnabled = false;
+        if (!result || result->resultId() != resultId || !isPlotResult(result))
+            continue;
 
-        QString content;
+        if (result->displayName() == normalizedTitle)
+            return;
 
-        int targetEntryType = target->type();
-        switch(targetEntryType)
-        {
-            case CommandEntry::Type:
-                content = static_cast<CommandEntry*>(target)->command();
-                break;
-            case MarkdownEntry::Type:
-                content = static_cast<MarkdownEntry*>(target)->plainText();
-                break;
-            case TextEntry::Type:
-                content = static_cast<TextEntry*>(target)->text();
-                break;
-            case LatexEntry::Type:
-                content = static_cast<LatexEntry*>(target)->plain();
-        }
+        result->setDisplayName(normalizedTitle);
+        setModified();
+        scheduleTocStructureRefresh();
+        return;
+    }
+}
 
-        auto* newEntry = WorksheetEntry::create(newType, this);
-        if (newEntry)
-        {
-            newEntry->setContent(content);
-            auto* tmp = target;
+void Worksheet::deletePlot(const QString& commandId, const QString& resultId)
+{
+    if (m_readOnly || commandId.isEmpty() || resultId.isEmpty())
+        return;
 
-            newEntry->setPrevious(tmp->previous());
-            newEntry->setNext(tmp->next());
+    const CommandSearchResult commandSearch = findCommandEntryById(commandId);
+    auto* commandEntry = commandSearch.entry;
+    auto* expression = commandEntry ? commandEntry->expression() : nullptr;
 
-            tmp->setPrevious(nullptr);
-            tmp->setNext(nullptr);
-            tmp->clearFocus();
-            tmp->forceRemove();
+    if (!commandEntry || !expression)
+        return;
 
-            if (newEntry->previous())
-                newEntry->previous()->setNext(newEntry);
-            else
-                setFirstEntry(newEntry);
+    for (auto* result : expression->results())
+    {
+        if (!result || result->resultId() != resultId || !isPlotResult(result))
+            continue;
 
-            if (newEntry->next())
-                newEntry->next()->setPrevious(newEntry);
-            else
-                setLastEntry(newEntry);
+        const QString plotNodeId = buildPlotNodeId(commandId, resultId);
+        const bool wasCurrentNode = m_currentTocNodeId == plotNodeId;
 
-            if (newType == HierarchyEntry::Type || targetEntryType == HierarchyEntry::Type)
-                updateHierarchyLayout();
-            updateLayout();
-            makeVisible(newEntry);
-            focusEntry(newEntry);
-            setModified();
-            newEntry->focusEntry();
-        }
-        m_animationsEnabled = animation_state;
+        expression->removeResult(result);
+
+        if (wasCurrentNode)
+            setCurrentTocNode(buildCommandNodeId(commandEntry));
+
+        setModified();
+        scheduleTocStructureRefresh();
+        return;
     }
 }
 
-bool Worksheet::isValidEntry(WorksheetEntry* entry)
+void Worksheet::renameHierarchyEntry(const QString& hierarchyId, const QString& newName)
 {
-    for (auto* iter = firstEntry(); iter; iter = iter->next())
-        if (entry == iter)
-            return true;
+    if (m_readOnly || hierarchyId.isEmpty())
+        return;
 
-    return false;
+    QString normalizedName = newName;
+
+    normalizedName.replace(QLatin1Char('\r'), QLatin1Char(' '));
+    normalizedName.replace(QLatin1Char('\n'), QLatin1Char(' '));
+
+    normalizedName = normalizedName.trimmed();
+
+    const HierarchySearchResult hierarchySearch = findHierarchyEntryById(hierarchyId);
+    auto* hierarchyEntry = hierarchySearch.entry;
+
+    if (!hierarchyEntry || hierarchyEntry->text() == normalizedName)
+        return;
+
+    hierarchyEntry->setContent(normalizedName);
 }
 
-void Worksheet::selectionRemove()
+void Worksheet::changeHierarchyLevel(QString hierarchyId, int levelDelta)
 {
-    if (m_selectedEntries.isEmpty())
+    if (m_readOnly || hierarchyId.isEmpty() || (levelDelta != -1 && levelDelta != 1))
         return;
 
-    if (Settings::warnAboutEntryDelete())
+    const HierarchySearchResult hierarchySearch = findHierarchyEntryById(hierarchyId);
+    HierarchyEntry* targetEntry = hierarchySearch.entry;
+
+    if (!targetEntry)
+        return;
+
+    bool expanded = false;
+
+    const int minimumLevel = static_cast<int>(HierarchyEntry::HierarchyLevel::Chapter);
+
+    const int maximumLevel = static_cast<int>(HierarchyEntry::HierarchyLevel::Subparagraph);
+
+    const int currentRootLevel = static_cast<int>(targetEntry->level());
+
+    if (levelDelta < 0)
     {
-        int rc = KMessageBox::warningContinueCancel(nullptr,
-                                                i18n("This step cannot be undone. Do you really want to delete the selected entries?"),
-                                                i18n("Delete Entries"));
-        if (rc == KMessageBox::SecondaryAction)
+        if (currentRootLevel <= minimumLevel)
             return;
+
+        expanded = expandHierarchyAncestors(hierarchySearch.collapsedAncestors);
     }
+    else
+    {
+        if (currentRootLevel >= maximumLevel)
+            return;
 
-    for (auto* entry : m_selectedEntries)
-        if (isValidEntry(entry))
-            entry->startRemoving(false);
+        expanded = expandHierarchyAncestors(hierarchySearch.collapsedAncestors);
 
-    m_selectedEntries.clear();
-}
+        bool hasPreviousSibling = false;
 
-void Worksheet::selectionEvaluate()
-{
-    // run entries in worksheet order: from top to down
-    for (auto* entry = firstEntry(); entry; entry = entry->next())
-        if (m_selectedEntries.indexOf(entry) != -1)
-            entry->evaluate();
-}
+        for (auto* entry = targetEntry->previous(); entry; entry = entry->previous())
+        {
+            if (entry->type() != HierarchyEntry::Type)
+                continue;
 
-void Worksheet::selectionMoveUp()
-{
-    bool moveHierarchyEntry = false;
-    // movement up should have an order from top to down.
-    for(auto* entry = firstEntry(); entry; entry = entry->next())
-        if(m_selectedEntries.indexOf(entry) != -1)
-            if (entry->previous() && m_selectedEntries.indexOf(entry->previous()) == -1)
+            const int entryLevel = static_cast<int>(static_cast<HierarchyEntry*>(entry)->level());
+
+            if (entryLevel < currentRootLevel)
+                break;
+
+            if (entryLevel == currentRootLevel)
             {
-                entry->moveToPrevious(false);
-                if (entry->type() == HierarchyEntry::Type)
-                    moveHierarchyEntry = true;
+                hasPreviousSibling = true;
+                break;
             }
-    if (moveHierarchyEntry)
-        updateHierarchyLayout();
-    updateLayout();
-}
+        }
 
-void Worksheet::selectionMoveDown()
-{
-    bool moveHierarchyEntry = false;
-    // movement up should have an order from down to top.
-    for(auto* entry = lastEntry(); entry; entry = entry->previous())
-        if(m_selectedEntries.indexOf(entry) != -1)
-            if (entry->next() && m_selectedEntries.indexOf(entry->next()) == -1)
+        if (!hasPreviousSibling)
+            return;
+    }
+
+    expanded = expandHierarchyForStructureChange(targetEntry) || expanded;
+    const std::vector<WorksheetEntry*>subentries = hierarchySubelements(targetEntry);
+
+    if (levelDelta > 0)
+    {
+        for (auto* entry : subentries)
+        {
+            if (!entry || entry->type() != HierarchyEntry::Type)
+                continue;
+
+            const int entryLevel = static_cast<int>(static_cast<HierarchyEntry*>(entry)->level());
+
+            if (entryLevel >= maximumLevel)
             {
-                entry->moveToNext(false);
-                if (entry->type() == HierarchyEntry::Type)
-                    moveHierarchyEntry = true;
+                updateHierarchyLayout();
+                if (expanded)
+                    updateLayout();
+                return;
             }
-    if (moveHierarchyEntry)
-        updateHierarchyLayout();
-    updateLayout();
-}
+        }
+    }
 
-void Worksheet::notifyEntryFocus(WorksheetEntry* entry)
-{
-    if (entry)
+    const auto shiftHierarchyEntry = [levelDelta](HierarchyEntry* hierarchyEntry)
     {
-        m_circularFocusBuffer.enqueue(entry);
+        if (!hierarchyEntry)
+            return;
 
-        if (m_circularFocusBuffer.size() > 2)
-            m_circularFocusBuffer.dequeue();
+        const int newLevel = static_cast<int>(hierarchyEntry->level()) + levelDelta;
+
+        hierarchyEntry->setLevel(static_cast<HierarchyEntry::HierarchyLevel>(newLevel));
+    };
+
+    shiftHierarchyEntry(targetEntry);
+
+    for (auto* entry : subentries)
+    {
+        if (!entry || entry->type() != HierarchyEntry::Type)
+            continue;
+
+        shiftHierarchyEntry(static_cast<HierarchyEntry*>(entry));
     }
-    else
-        m_circularFocusBuffer.clear();
-}
 
-void Worksheet::collapseAllResults()
-{
-    for (auto* entry = firstEntry(); entry; entry = entry->next())
-        if (entry->type() == CommandEntry::Type)
-            static_cast<CommandEntry*>(entry)->collapseResults();
-}
+    updateHierarchyLayout();
+    updateLayout();
 
-void Worksheet::uncollapseAllResults()
-{
-    for (auto* entry = firstEntry(); entry; entry = entry->next())
-        if (entry->type() == CommandEntry::Type)
-            static_cast<CommandEntry*>(entry)->expandResults();
+    setModified();
 }
 
-void Worksheet::removeAllResults()
+void Worksheet::deleteHierarchyEntry(const QString& hierarchyId, bool deleteContents)
 {
-    bool remove = false;
+    if (m_readOnly || hierarchyId.isEmpty())
+        return;
 
-    if (KMessageBox::shouldBeShownContinue(QLatin1String("WarnAboutAllResultsRemoving")))
+    const HierarchySearchResult hierarchySearch = findHierarchyEntryById(hierarchyId);
+    HierarchyEntry* targetEntry = hierarchySearch.entry;
+
+    if (!targetEntry)
+        return;
+
+    QString headingLabel = targetEntry->text().trimmed();
+
+    if (headingLabel.isEmpty())
+        headingLabel = targetEntry->hierarchyText();
+
+    QString warningText;
+    QString dialogTitle;
+
+    if (deleteContents)
     {
-        KMessageBox::ButtonCode btn = KMessageBox::warningContinueCancel(
-            views().first(),
-            i18n("This step cannot be undone. Do you really want to delete all results?"),
-            i18n("Delete all results"),
-            KStandardGuiItem::cont(),
-            KStandardGuiItem::cancel(),
-            QLatin1String("WarnAboutAllResultsRemoving")
-        );
-        remove = (btn == KMessageBox::Continue);
+        warningText = i18n("Do you really want to delete "
+                 "the heading \"%1\" and all "
+                 "entries in this section? "
+                 "This action cannot be undone.",
+                 headingLabel);
+
+        dialogTitle = i18n("Delete Section");
     }
     else
-        remove = true;
-
-    if (remove)
     {
-        for (auto *entry = firstEntry(); entry; entry = entry->next())
-            if (entry->type() == CommandEntry::Type)
-                static_cast<CommandEntry*>(entry)->removeResults();
+        warningText = i18n("Do you really want to delete "
+                 "only the heading \"%1\"? "
+                 "The contents of the section "
+                 "will remain in the worksheet. "
+                 "This action cannot be undone.",
+                 headingLabel);
+
+        dialogTitle = i18n("Delete Heading");
     }
-}
 
-void Worksheet::addToExectuionSelection()
-{
-    for (auto* entry : m_selectedEntries)
-        if (entry->type() == CommandEntry::Type)
-            static_cast<CommandEntry*>(entry)->addToExecution();
-}
+    if (Settings::warnAboutEntryDelete())
+    {
+        const auto result = KMessageBox::warningTwoActions(
+                worksheetView(),
+                warningText,
+                dialogTitle,
+                KStandardGuiItem::remove(),
+                KStandardGuiItem::cancel());
+
+        if (result != KMessageBox::PrimaryAction)
+            return;
+    }
 
-void Worksheet::excludeFromExecutionSelection()
-{
-    for (auto* entry : m_selectedEntries)
-        if (entry->type() == CommandEntry::Type)
-            static_cast<CommandEntry*>(entry)->excludeFromExecution();
-}
+    expandHierarchyAncestors(hierarchySearch.collapsedAncestors);
+    expandHierarchyForStructureChange(targetEntry);
 
-void Worksheet::collapseSelectionResults()
-{
-    for (auto* entry : m_selectedEntries)
-        if (entry->type() == CommandEntry::Type)
-            static_cast<CommandEntry*>(entry)->collapseResults();
-}
+    const std::vector<WorksheetEntry*>subentries = hierarchySubelements(targetEntry);
 
-void Worksheet::uncollapseSelectionResults()
-{
-    for (auto* entry : m_selectedEntries)
-        if (entry->type() == CommandEntry::Type)
-            static_cast<CommandEntry*>(entry)->expandResults();
-}
+    clearAllSelections();
+    notifyEntryFocus(nullptr);
 
-void Worksheet::removeSelectionResults()
-{
-    for (auto* entry : m_selectedEntries)
-        if (entry->type() == CommandEntry::Type)
-            static_cast<CommandEntry*>(entry)->removeResults();
-}
+    QList<WorksheetEntry*> entriesToRemove;
+    entriesToRemove.append(targetEntry);
 
-void Worksheet::requestScrollToHierarchyEntry(QString hierarchyText)
-{
-    for (auto* entry = firstEntry(); entry; entry = entry->next())
+    if (deleteContents)
     {
-        if (entry->type() == HierarchyEntry::Type)
+        for (auto* entry : subentries)
+            entriesToRemove.append(entry);
+    }
+    else
+    {
+        const int minimumLevel = static_cast<int>(HierarchyEntry::HierarchyLevel::Chapter);
+
+        for (auto* entry : subentries)
         {
-            auto* hierarchEntry = static_cast<HierarchyEntry*>(entry);
-            if (hierarchEntry->hierarchyText() == hierarchyText)
-                worksheetView()->scrollTo(hierarchEntry->y());
+            if (!entry || entry->type() != HierarchyEntry::Type)
+                continue;
+
+            auto* childHeading = static_cast<HierarchyEntry*>(entry);
+            const int newLevel = qMax(minimumLevel, static_cast<int>(childHeading->level()) - 1);
+            childHeading->setLevel(static_cast<HierarchyEntry::HierarchyLevel>(newLevel));
         }
     }
-}
 
-WorksheetEntry* Worksheet::cutSubentriesForHierarchy(HierarchyEntry* hierarchyEntry)
-{
-    Q_ASSERT(hierarchyEntry->next());
-    auto* cutBegin = hierarchyEntry->next();
-    auto* cutEnd = cutBegin;
+    WorksheetEntry* previousEntry = targetEntry->previous();
+    WorksheetEntry* lastRemovedEntry = entriesToRemove.constLast();
+    WorksheetEntry* nextEntry = lastRemovedEntry->next();
+
+    if (previousEntry)
+        previousEntry->setNext(nextEntry);
+    else
+        setFirstEntry(nextEntry);
+
+    if (nextEntry)
+        nextEntry->setPrevious(previousEntry);
+    else
+        setLastEntry(previousEntry);
+
+    clearFocus();
+
+    updateFocusedTextItem(static_cast<WorksheetTextItem*>(nullptr));
+    updateFocusedTextItem(static_cast<WorksheetTextEditorItem*>(nullptr));
 
-    bool isCutEnd = false;
-    int level = (int)hierarchyEntry->level();
-    while (!isCutEnd && cutEnd && cutEnd->next())
+    for (auto* entry : entriesToRemove)
     {
-        auto* next = cutEnd->next();
-        if (next->type() == HierarchyEntry::Type && (int)static_cast<HierarchyEntry*>(next)->level() <= level)
-            isCutEnd = true;
-        else
-            cutEnd = next;
+        if (!entry)
+            continue;
+
+        entry->setPrevious(nullptr);
+        entry->setNext(nullptr);
+        entry->clearFocus();
+        entry->hide();
+        entry->deleteLater();
     }
 
-    //cutEnd not an end of all entries
-    if (cutEnd->next())
+    WorksheetEntry* focusTarget = nextEntry ? nextEntry : previousEntry;
+
+    if (!firstEntry())
+        focusTarget = appendCommandEntry();
+
+    updateHierarchyLayout();
+    updateLayout();
+
+    if (focusTarget)
     {
-        hierarchyEntry->setNext(cutEnd->next());
-        cutEnd->setNext(nullptr);
+        focusTarget->focusEntry();
+        makeVisible(focusTarget);
+        updateCurrentHierarchyFromEntry(focusTarget);
     }
     else
     {
-        hierarchyEntry->setNext(nullptr);
-        setLastEntry(hierarchyEntry);
+        m_hierarchyTrackingSource = HierarchyTrackingSource::FocusedEntry;
+        updateCurrentHierarchy(nullptr);
+    }
+
+    resetEntryCursor();
+    setModified();
+}
+
+WorksheetEntry* Worksheet::cutSubentriesForHierarchy(HierarchyEntry* hierarchyEntry)
+{
+    if (!hierarchyEntry || !hierarchyEntry->next())
+        return nullptr;
+
+    WorksheetEntry* cutBegin = hierarchyEntry->next();
+    if (cutBegin->type() == HierarchyEntry::Type && static_cast<int>(static_cast<HierarchyEntry*>(cutBegin)->level()) <= static_cast<int>(hierarchyEntry->level()))
+        return nullptr;
+    WorksheetEntry* cutEnd = cutBegin;
+
+    const int hierarchyLevel = static_cast<int>(hierarchyEntry->level());
+
+    while (cutEnd->next())
+    {
+        WorksheetEntry* candidate = cutEnd->next();
+
+        if (candidate->type() == HierarchyEntry::Type)
+        {
+            const int candidateLevel = static_cast<int>(static_cast<HierarchyEntry*>(candidate)->level());
+
+            if (candidateLevel <= hierarchyLevel)
+                break;
+        }
+
+        cutEnd = candidate;
     }
 
+    WorksheetEntry* entryAfterSection = cutEnd->next();
+
+    hierarchyEntry->setNext(entryAfterSection);
+
+    if (entryAfterSection)
+        entryAfterSection->setPrevious(hierarchyEntry);
+    else
+        setLastEntry(hierarchyEntry);
+
     cutBegin->setPrevious(nullptr);
+    cutEnd->setNext(nullptr);
 
-    for(auto* entry = cutBegin; entry; entry = entry->next())
+    for (auto* entry = cutBegin; entry; entry = entry->next())
         entry->hide();
 
     return cutBegin;
@@ -3088,21 +4270,73 @@ WorksheetEntry* Worksheet::cutSubentriesForHierarchy(HierarchyEntry* hierarchyEn
 
 void Worksheet::insertSubentriesForHierarchy(HierarchyEntry* hierarchyEntry, WorksheetEntry* storedSubentriesBegin)
 {
-    auto* previousNext = hierarchyEntry->next();
+    if (!hierarchyEntry || !storedSubentriesBegin)
+        return;
+
+    WorksheetEntry* previousNext = hierarchyEntry->next();
+
     hierarchyEntry->setNext(storedSubentriesBegin);
-    storedSubentriesBegin->show();
+    storedSubentriesBegin->setPrevious(hierarchyEntry);
 
-    auto* storedEnd = storedSubentriesBegin;
-    while(storedEnd->next())
+    WorksheetEntry* storedEnd = storedSubentriesBegin;
+
+    for (auto* entry = storedSubentriesBegin; entry; entry = entry->next())
     {
-        storedEnd = storedEnd->next();
-        storedEnd->show();
+        entry->show();
+        storedEnd = entry;
     }
+
     storedEnd->setNext(previousNext);
-    if (!previousNext)
+
+    if (previousNext)
+        previousNext->setPrevious(storedEnd);
+    else
         setLastEntry(storedEnd);
 }
 
+bool Worksheet::expandHierarchyForStructureChange(HierarchyEntry* hierarchyEntry)
+{
+    if (!hierarchyEntry)
+        return false;
+
+    bool hierarchyExpanded = false;
+    const auto expandEntry = [this, &hierarchyExpanded](HierarchyEntry* entry)
+    {
+        if (!entry || !entry->hasHiddenSubentries())
+            return;
+
+        WorksheetEntry* hiddenSubentries = entry->takeHiddenSubentries();
+
+        if (!hiddenSubentries)
+            return;
+
+        insertSubentriesForHierarchy(entry, hiddenSubentries);
+
+        hierarchyExpanded = true;
+    };
+
+    expandEntry(hierarchyEntry);
+
+    const int rootLevel = static_cast<int>(hierarchyEntry->level());
+    for (auto* entry = hierarchyEntry->next(); entry;)
+    {
+        if (entry->type() == HierarchyEntry::Type)
+        {
+            auto* childHierarchy = static_cast<HierarchyEntry*>(entry);
+            const int childLevel = static_cast<int>(childHierarchy->level());
+
+            if (childLevel <= rootLevel)
+                break;
+
+            expandEntry(childHierarchy);
+        }
+
+        entry = entry->next();
+    }
+
+    return hierarchyExpanded;
+}
+
 void Worksheet::handleSettingsChanges()
 {
     const QString themeSetting = Settings::self()->defaultTheme();
diff --git a/src/worksheet.h b/src/worksheet.h
index c36819f4..6fb142da 100644
--- a/src/worksheet.h
+++ b/src/worksheet.h
@@ -15,6 +15,9 @@
 #include <QDomDocument>
 #include <QGraphicsScene>
 #include <QQueue>
+#include <QVariantList>
+
+#include <functional>
 
 #include "lib/renderer.h"
 #include "mathrender.h"
@@ -24,9 +27,11 @@ namespace Cantor {
     class Backend;
     class Session;
     class Expression;
+    class Result;
 }
 
 class WorksheetEntry;
+class CommandEntry;
 class WorksheetView;
 class HierarchyEntry;
 class PlaceHolderEntry;
@@ -48,11 +53,18 @@ class Worksheet : public QGraphicsScene
 {
   Q_OBJECT
   public:
-    enum Type {
+    enum Type
+    {
       CantorWorksheet,
       JupyterNotebook
     };
 
+    enum class HierarchyTrackingSource
+    {
+        Viewport,
+        FocusedEntry
+    };
+
     Worksheet(Cantor::Backend*, QWidget*, bool useDeafultWorksheetParameters = true);
     ~Worksheet() override;
 
@@ -99,6 +111,7 @@ class Worksheet : public QGraphicsScene
 
     WorksheetEntry* cutSubentriesForHierarchy(HierarchyEntry*);
     void insertSubentriesForHierarchy(HierarchyEntry*, WorksheetEntry*);
+    bool expandHierarchyForStructureChange(HierarchyEntry*);
 
     KWorksheetCursor worksheetCursor();
     void setWorksheetCursor(const KWorksheetCursor&);
@@ -183,6 +196,9 @@ class Worksheet : public QGraphicsScene
     void updateLayout();
     void updateHierarchyLayout();
     void updateHierarchyControlsLayout(WorksheetEntry* startEntry = nullptr);
+    void updateCurrentHierarchyFromView(const QRectF& viewRect);
+    void updateCurrentHierarchyFromEntry(WorksheetEntry* entry);
+    void followHierarchyFromView();
     void updateEntrySize(WorksheetEntry*);
 
     void print(QPrinter*);
@@ -217,6 +233,9 @@ class Worksheet : public QGraphicsScene
     bool load(const QString&);
 
     void gotResult(Cantor::Expression* expr = nullptr);
+    void refreshTocStructure();
+    void scheduleTocStructureRefresh();
+    void emitTocNodeSnapshot();
 
     void removeCurrentEntry();
 
@@ -262,7 +281,13 @@ class Worksheet : public QGraphicsScene
     void addToExectuionSelection();
     void excludeFromExecutionSelection();
 
-    void requestScrollToHierarchyEntry(QString);
+    void renameHierarchyEntry(const QString& hierarchyId, const QString& newName);
+    void changeHierarchyLevel(QString hierarchyId, int levelDelta);
+    void deleteHierarchyEntry(const QString& hierarchyId, bool deleteContents);
+    void renamePlot(const QString& commandId, const QString& resultId, const QString& newTitle);
+    void deletePlot(const QString& commandId, const QString& resultId);
+    void navigateToTocNode(QString nodeId);
+    void updateCurrentTocNodeFromResult(CommandEntry* commandEntry, Cantor::Result* result);
     void clearAllSelections();
     void handleSettingsChanges();
 
@@ -270,8 +295,8 @@ class Worksheet : public QGraphicsScene
     void modified();
     void loaded();
     void showHelp(const QString&);
-    void hierarchyChanged(const QStringList&, const QStringList&, const QList<int>&);
-    void hierarhyEntryNameChange(QString name, QString searchName, int depth);
+    void tocNodesChanged(QVariantList nodes);
+    void currentTocNodeChanged(QString nodeId);
     void updatePrompt();
     void undoAvailable(bool);
     void redoAvailable(bool);
@@ -299,6 +324,40 @@ class Worksheet : public QGraphicsScene
 
     bool isValidEntry(WorksheetEntry*);
 
+  private:
+    struct HierarchySearchResult
+    {
+        HierarchyEntry* entry{nullptr};
+        QVector<HierarchyEntry*> collapsedAncestors;
+    };
+
+    struct CommandSearchResult
+    {
+        CommandEntry* entry{nullptr};
+        QVector<HierarchyEntry*> collapsedAncestors;
+    };
+
+    QVariantList collectTocNodes();
+    bool visitLogicalEntries(WorksheetEntry* first, const std::function<bool(WorksheetEntry*)>& visitor);
+    bool visitLogicalEntries(const std::function<bool(WorksheetEntry*)>& visitor);
+    bool findHierarchyEntryById(WorksheetEntry* first, const QString& hierarchyId, QVector<HierarchyEntry*> collapsedAncestors, HierarchySearchResult& result);
+    HierarchySearchResult findHierarchyEntryById(const QString& hierarchyId);
+    bool findCommandEntryById(WorksheetEntry* first, const QString& commandId, QVector<HierarchyEntry*> collapsedAncestors, CommandSearchResult& result);
+    CommandSearchResult findCommandEntryById(const QString& commandId);
+    bool expandHierarchyAncestors(const QVector<HierarchyEntry*>& ancestors);
+    QString buildCommandNodeId(CommandEntry* entry);
+    QString buildPlotNodeId(const QString& commandId, const QString& resultId) const;
+    bool parseCommandNodeId(const QString& nodeId, QString* commandId) const;
+    bool parsePlotNodeId(const QString& nodeId, QString* commandId, QString* resultId) const;
+    bool navigateToPlotResult(CommandEntry* commandEntry, const QString& resultId);
+    void setCurrentTocNode(const QString& nodeId);
+    QString hierarchyIdForEntry(WorksheetEntry* entry) const;
+    QString commandTocTitle() const;
+    QString commandTocDisplayText(CommandEntry* entry) const;
+    QString plotTocTitle(Cantor::Result* result) const;
+    QString plotTocDisplayText(CommandEntry* entry, Cantor::Result* result, int plotOrdinal, int plotCount) const;
+    bool isPlotResult(Cantor::Result* result);
+
   private Q_SLOTS:
     //void checkEntriesForSanity();
 
@@ -318,6 +377,7 @@ class Worksheet : public QGraphicsScene
     WorksheetEntry* entryAt(qreal x, qreal y);
     WorksheetEntry* entryAt(QPointF);
     WorksheetEntry* entryAt(int row);
+    void removeDragPlaceholder();
     void updateEntryCursor(QGraphicsSceneMouseEvent*);
     void addEntryFromEntryCursor();
     void drawEntryCursor();
@@ -327,6 +387,13 @@ class Worksheet : public QGraphicsScene
     void showInvalidNotebookSchemeError(QString additionalInfo = QString());
     void initSession(Cantor::Backend*);
     std::vector<WorksheetEntry*> hierarchySubelements(HierarchyEntry*) const;
+    void updateCurrentHierarchy(WorksheetEntry* entry);
+    void normalizeDraggedHierarchyLevels(HierarchyEntry* rootEntry, WorksheetEntry* previousEntry, const std::vector<WorksheetEntry*>& subentries);
+
+    QString m_currentTocNodeId;
+    HierarchyTrackingSource m_hierarchyTrackingSource{HierarchyTrackingSource::Viewport};
+
+    bool m_layoutUpdateInProgress{false};
 
     static const double LeftMargin;
     static const double RightMargin;
@@ -395,6 +462,8 @@ class Worksheet : public QGraphicsScene
     QQueue<WorksheetEntry*> m_circularFocusBuffer;
 
     size_t m_hierarchyMaxDepth{0};
+    QVariantList m_tocNodeSnapshot;
+    bool m_tocRefreshScheduled{false};
 };
 
 #endif // WORKSHEET_H
diff --git a/src/worksheetentry.cpp b/src/worksheetentry.cpp
index 1a27cc9f..cac97243 100644
--- a/src/worksheetentry.cpp
+++ b/src/worksheetentry.cpp
@@ -828,8 +828,7 @@ void WorksheetEntry::remove()
     else
         worksheet()->setLastEntry(previous());
 
-    if (type() == HierarchyEntry::Type)
-        worksheet()->updateHierarchyLayout();
+    worksheet()->updateHierarchyLayout();
 
     // make the entry invisible to QGraphicsScene's itemAt() function
     forceRemove();
diff --git a/src/worksheettexteditoritem.cpp b/src/worksheettexteditoritem.cpp
index b0a78405..31713322 100644
--- a/src/worksheettexteditoritem.cpp
+++ b/src/worksheettexteditoritem.cpp
@@ -89,7 +89,8 @@ WorksheetTextEditorItem::WorksheetTextEditorItem(EditorMode initialMode, Workshe
         m_view->registerCompletionModel(m_completionModel);
         connect(m_completionModel, &CantorCompletionModel::modelIsReady, this, &WorksheetTextEditorItem::showCustomCompleter);
 
-        auto* model = session()->variableModel();
+        auto* currentSession = session();
+        auto* model = currentSession ? currentSession->variableModel() : nullptr;
         if (model) {
             connect(model, &Cantor::DefaultVariableModel::initialModelPopulated, this, [this]() {
                 if (m_view && m_document)
@@ -987,6 +988,7 @@ bool WorksheetTextEditorItem::eventFilter(QObject* object, QEvent* event)
 
 void WorksheetTextEditorItem::keyPressEvent(QKeyEvent* event)
 {
+    worksheet()->updateFocusedTextItem(this);
     const int key = event->key();
     const auto modifiers = event->modifiers();
     worksheet()->resetEntryCursor();
@@ -1122,6 +1124,7 @@ void WorksheetTextEditorItem::focusOutEvent(QFocusEvent* event)
 
 void WorksheetTextEditorItem::mousePressEvent(QGraphicsSceneMouseEvent* event)
 {
+    worksheet()->updateFocusedTextItem(this);
     QGraphicsProxyWidget::mousePressEvent(event);
     KTextEditor::View* view = m_view;
 
diff --git a/src/worksheettextitem.cpp b/src/worksheettextitem.cpp
index 4d93f196..9fb9fcb0 100644
--- a/src/worksheettextitem.cpp
+++ b/src/worksheettextitem.cpp
@@ -403,6 +403,7 @@ Cantor::Session* WorksheetTextItem::session()
 
 void WorksheetTextItem::keyPressEvent(QKeyEvent* event)
 {
+    worksheet()->updateFocusedTextItem(this);
     switch (event->key()) {
     case Qt::Key_Left:
         if (event->modifiers() == Qt::NoModifier && textCursor().atStart()) {
@@ -521,6 +522,7 @@ void WorksheetTextItem::focusOutEvent(QFocusEvent* event)
 
 void WorksheetTextItem::mousePressEvent(QGraphicsSceneMouseEvent* event)
 {
+    worksheet()->updateFocusedTextItem(this);
     int p = textCursor().position();
     bool b = textCursor().hasSelection();
 
diff --git a/src/worksheetview.cpp b/src/worksheetview.cpp
index dd8f6af0..13760b0a 100644
--- a/src/worksheetview.cpp
+++ b/src/worksheetview.cpp
@@ -25,6 +25,14 @@ WorksheetView::WorksheetView(Worksheet* scene, QWidget* parent) : QGraphicsView(
     setRenderHint(QPainter::Antialiasing, true);
     setRenderHint(QPainter::TextAntialiasing, true);
     setRenderHint(QPainter::SmoothPixmapTransform, true);
+
+    connect(verticalScrollBar(), &QScrollBar::sliderMoved, this, [this](int) {
+        Q_EMIT userScrollStarted();
+    });
+
+    connect(verticalScrollBar(), &QScrollBar::actionTriggered, this, [this](int) {
+        Q_EMIT userScrollStarted();
+    });
 }
 
 void WorksheetView::makeVisible(const QRectF& sceneRect)
@@ -128,11 +136,9 @@ void WorksheetView::makeVisible(const QRectF& sceneRect)
 
 void WorksheetView::scrollTo(int y)
 {
-    if (!verticalScrollBar())
-        return;
-
-    qreal dy = y - verticalScrollBar()->value();
-    scrollBy(dy);
+    QRectF targetRect = viewRect();
+    targetRect.moveTop(y);
+    makeVisible(targetRect);
 }
 
 
@@ -220,11 +226,10 @@ QPointF WorksheetView::sceneCursorPos() const
 
 QRectF WorksheetView::viewRect() const
 {
-    const qreal w = viewport()->width() / m_scale;
-    const qreal h = viewport()->height() / m_scale;
-    qreal y = verticalScrollBar()->value();
-    qreal x = horizontalScrollBar() ? horizontalScrollBar()->value() : 0;
-    return QRectF(x, y, w, h);
+    if (!viewport())
+        return {};
+
+    return mapToScene(viewport()->rect()).boundingRect();
 }
 
 void WorksheetView::resizeEvent(QResizeEvent* event)
@@ -250,11 +255,16 @@ void WorksheetView::wheelEvent(QWheelEvent* event)
 {
     if ((QApplication::keyboardModifiers() & Qt::ControlModifier)) {
         //https://wiki.qt.io/Smooth_Zoom_In_QGraphicsView
-        QPoint numDegrees = event->angleDelta() / 8;
-        int numSteps = numDegrees.y() / 15; // see QWheelEvent documentation
+        const QPoint numDegrees = event->angleDelta() / 8;
+        const int numSteps = numDegrees.y() / 15;
         zoom(numSteps);
-    } else
-        QGraphicsView::wheelEvent(event);
+        return;
+    }
+    const bool hasVerticalMovement = event->angleDelta().y() != 0 || event->pixelDelta().y() != 0;
+    if (hasVerticalMovement)
+        Q_EMIT userScrollStarted();
+
+    QGraphicsView::wheelEvent(event);
 }
 
 void WorksheetView::zoom(int numSteps)
diff --git a/src/worksheetview.h b/src/worksheetview.h
index b93a6ece..80ea0f8c 100644
--- a/src/worksheetview.h
+++ b/src/worksheetview.h
@@ -37,6 +37,7 @@ public:
 Q_SIGNALS:
     void viewRectChanged(QRectF) const;
     void scaleFactorChanged(double scale);
+    void userScrollStarted();
 
 public Q_SLOTS:
     void applyThemeToBackground();
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.