[education/cantor] src: Move worksheet hierarchy handling into a manager
Alexander Semke <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit 59ae70e88806c14b7c69fb4ac174d009f1bd9fad by Alexander Semke, on behalf of Nanhao Lv.
Committed on 26/07/2026 at 19:50.
Pushed by asemke into branch 'master'.
Move worksheet hierarchy handling into a manager
M +1 -0 src/CMakeLists.txt
M +1 -0 src/test/CMakeLists.txt
M +2169 -3335 src/worksheet.cpp
M +4 -46 src/worksheet.h
A +1308 -0 src/worksheethierarchymanager.cpp *
A +110 -0 src/worksheethierarchymanager.h *
The files marked with a * at the end have a non valid license. Please read: https://community.kde.org/Policies/Licensing_Policy and use the headers which are listed at that page.
https://invent.kde.org/education/cantor/-/commit/59ae70e88806c14b7c69fb4ac174d009f1bd9fad
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index e5914920..86ea33a0 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -63,6 +63,7 @@ set(cantor_PART_SRCS
cantor_part.cpp
cantorcompletionmodel.cpp
worksheet.cpp
+ worksheethierarchymanager.cpp
worksheetview.cpp
worksheetentry.cpp
worksheettexteditoritem.cpp
diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt
index d27d31e4..1b3e7f70 100644
--- a/src/test/CMakeLists.txt
+++ b/src/test/CMakeLists.txt
@@ -1,5 +1,6 @@
set(worksheettest_SRCS
../worksheet.cpp
+ ../worksheethierarchymanager.cpp
../worksheetview.cpp
../worksheetentry.cpp
../worksheettexteditoritem.cpp
diff --git a/src/worksheet.cpp b/src/worksheet.cpp
index 4aa7a198..d8f566a3 100644
--- a/src/worksheet.cpp
+++ b/src/worksheet.cpp
@@ -14,17 +14,14 @@
#include "markdownentry.h"
#include "pagebreakentry.h"
#include "placeholderentry.h"
-#include "resultitem.h"
#include "settings.h"
#include "textentry.h"
+#include "worksheethierarchymanager.h"
#include "worksheetview.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"
@@ -44,7 +41,6 @@
#include <QActionGroup>
#include <QFile>
#include <QScopedValueRollback>
-#include <QSet>
#include <KMessageBox>
#include <KActionCollection>
@@ -60,14 +56,6 @@
#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;
@@ -79,6 +67,8 @@ Worksheet::Worksheet(Cantor::Backend* backend, QWidget* parent, bool useDefaultW
m_cursorItemTimer(new QTimer(this)),
m_useDefaultWorksheetParameters(useDefaultWorksheetParameters)
{
+ m_hierarchyManager = new WorksheetHierarchyManager(this);
+
m_entryCursorItem = addLine(0,0,0,0);
const QColor& color = (palette().color(QPalette::Base).lightness() < 128) ? Qt::white : Qt::black;
QPen pen(color);
@@ -291,7 +281,7 @@ void Worksheet::updateLayout()
}
// Hierarchy controls are hidden while printing.
- const qreal hierarchyControlsWidth = m_isPrinting ? 0.0 : static_cast<qreal>(m_hierarchyMaxDepth) * (WorksheetEntry::ControlElementWidth + WorksheetEntry::ControlElementBorder);
+ const qreal hierarchyControlsWidth = m_isPrinting ? 0.0 : static_cast<qreal>(m_hierarchyManager->hierarchyMaxDepth()) * (WorksheetEntry::ControlElementWidth + WorksheetEntry::ControlElementBorder);
const qreal w = m_viewWidth - LeftMargin - RightMargin - hierarchyControlsWidth;
@@ -315,3821 +305,2675 @@ void Worksheet::updateLayout()
void Worksheet::refreshTocStructure()
{
- if (m_isClosing || m_isLoadingFromFile)
- return;
-
- m_tocRefreshScheduled = false;
- m_tocNodeSnapshot = collectTocNodes();
-
- if (!m_currentTocNodeId.isEmpty())
- {
- bool currentNodeStillExists = false;
- for (const QVariant& nodeValue : m_tocNodeSnapshot)
- {
- if (nodeValue.toMap().value(QStringLiteral("id")).toString() == m_currentTocNodeId)
- {
- currentNodeStillExists = true;
- break;
- }
- }
-
- if (!currentNodeStillExists)
- setCurrentTocNode(QString());
- }
-
- Q_EMIT tocNodesChanged(m_tocNodeSnapshot);
+ m_hierarchyManager->refreshTocStructure();
}
void Worksheet::scheduleTocStructureRefresh()
{
- if (m_isClosing || m_isLoadingFromFile || m_tocRefreshScheduled)
- return;
-
- m_tocRefreshScheduled = true;
- QTimer::singleShot(0, this, [this]()
- {
- if (!m_tocRefreshScheduled)
- return;
-
- refreshTocStructure();
- });
+ m_hierarchyManager->scheduleTocStructureRefresh();
}
void Worksheet::emitTocNodeSnapshot()
{
- if (m_isClosing)
- return;
-
- if (m_tocNodeSnapshot.isEmpty() && firstEntry())
- m_tocNodeSnapshot = collectTocNodes();
-
- Q_EMIT tocNodesChanged(m_tocNodeSnapshot);
+ m_hierarchyManager->emitTocNodeSnapshot();
}
-QVariantList Worksheet::collectTocNodes()
+void Worksheet::updateHierarchyLayout()
{
- QVariantList nodes;
- QVector<QString> hierarchyNodeIds;
- QVector<int> hierarchyDepths;
-
- visitLogicalEntries([&](WorksheetEntry* entry)
- {
- if (entry->type() == HierarchyEntry::Type)
- {
- 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)
- {
- hierarchyDepths.removeLast();
- hierarchyNodeIds.removeLast();
- }
-
- 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())
- {
- 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;
- });
-
- return nodes;
+ m_hierarchyManager->updateHierarchyLayout();
}
-QString Worksheet::buildCommandNodeId(CommandEntry* entry)
+void Worksheet::updateHierarchyControlsLayout(WorksheetEntry* startEntry)
{
- if (!entry)
- return QString();
-
- const QString& commandId = entry->commandId();
- if (commandId.isEmpty())
- return QString();
-
- return QStringLiteral("command:") + commandId;
+ m_hierarchyManager->updateHierarchyControlsLayout(startEntry);
}
-QString Worksheet::buildPlotNodeId(const QString& commandId, const QString& resultId) const
+std::vector<WorksheetEntry*> Worksheet::hierarchySubelements(HierarchyEntry* hierarchyEntry) const
{
- if (commandId.isEmpty() || resultId.isEmpty())
- return QString();
-
- return QStringLiteral("plot:%1:%2").arg(commandId, resultId);
+ return m_hierarchyManager->hierarchySubelements(hierarchyEntry);
}
-bool Worksheet::parseCommandNodeId(const QString& nodeId, QString* commandId) const
+void Worksheet::updateCurrentHierarchy(WorksheetEntry* entry)
{
- const QString prefix = QStringLiteral("command:");
- if (!nodeId.startsWith(prefix))
- return false;
-
- const QString parsedCommandId = nodeId.mid(prefix.size());
- if (parsedCommandId.isEmpty() || parsedCommandId.contains(QLatin1Char(':')))
- return false;
-
- if (commandId)
- *commandId = parsedCommandId;
-
- return true;
+ m_hierarchyManager->updateCurrentHierarchy(entry);
}
-bool Worksheet::parsePlotNodeId(const QString& nodeId, QString* commandId, QString* resultId) const
+void Worksheet::normalizeDraggedHierarchyLevels(HierarchyEntry* rootEntry, WorksheetEntry* previousEntry, const std::vector<WorksheetEntry*>& subentries)
{
- const QString prefix = QStringLiteral("plot:");
- if (!nodeId.startsWith(prefix))
- return false;
-
- const QStringList parts = nodeId.mid(prefix.size()).split(QLatin1Char(':'));
- if (parts.size() != 2 || parts.at(0).isEmpty() || parts.at(1).isEmpty())
- return false;
-
- if (commandId)
- *commandId = parts.at(0);
- if (resultId)
- *resultId = parts.at(1);
-
- return true;
+ m_hierarchyManager->normalizeDraggedHierarchyLevels(rootEntry, previousEntry, subentries);
}
-QString Worksheet::commandTocTitle() const
+void Worksheet::updateCurrentHierarchyFromView(const QRectF& viewRect)
{
- return i18n("Command");
+ m_hierarchyManager->updateCurrentHierarchyFromView(viewRect);
}
-QString Worksheet::commandTocDisplayText(CommandEntry* entry) const
+void Worksheet::updateCurrentHierarchyFromEntry(WorksheetEntry* entry)
{
- auto* expression = entry ? entry->expression() : nullptr;
- if (m_showExpressionIds && expression && expression->id() != -1)
- return i18n("Command %1", expression->id());
-
- return commandTocTitle();
+ m_hierarchyManager->updateCurrentHierarchyFromEntry(entry);
}
-QString Worksheet::plotTocTitle(Cantor::Result* result) const
+void Worksheet::followHierarchyFromView()
{
- if (result && !result->displayName().isEmpty())
- return result->displayName();
-
- return i18n("Plot");
+ m_hierarchyManager->followHierarchyFromView();
}
-QString Worksheet::plotTocDisplayText(CommandEntry* entry, Cantor::Result* result, int plotOrdinal, int plotCount) const
+void Worksheet::updateEntrySize(WorksheetEntry* entry)
{
- const QString title = plotTocTitle(result);
- auto* expression = entry ? entry->expression() : nullptr;
- if (m_showExpressionIds && expression && expression->id() != -1)
+ QScopedValueRollback<bool> layoutGuard(m_layoutUpdateInProgress, true);
+ bool cursorRectVisible = false;
+ bool atEnd = worksheetView()->isAtEnd();
+ if (currentTextItem()) {
+ QRectF cursorRect = currentTextItem()->sceneCursorRect();
+ cursorRectVisible = worksheetView()->isVisible(cursorRect);
+ }
+
+ if (Settings::useOldCantorEntriesIndent() == false)
{
- if (plotCount > 1)
- return i18n("%1 %2.%3", title, expression->id(), plotOrdinal);
+ 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();
+
+ // 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;
+ }
+ }
- return i18n("%1 %2", title, expression->id());
+ qreal y = entry->y() + entry->size().height();
+ for (entry = entry->next(); entry; entry = entry->next()) {
+ entry->setY(y);
+ y += entry->size().height();
}
- if (plotCount > 1)
- return i18n("%1 %2", title, plotOrdinal);
+ if (!m_isLoadingFromFile)
+ updateHierarchyControlsLayout(entry);
- return title;
+ setSceneRect(QRectF(0, 0, sceneRect().width(), y));
+ if (cursorRectVisible)
+ makeVisible(worksheetCursor());
+ else if (atEnd)
+ worksheetView()->scrollToEnd();
+ drawEntryCursor();
}
-bool Worksheet::isPlotResult(Cantor::Result* result)
+void Worksheet::setRequestedWidth(QGraphicsObject* object, qreal width)
{
- 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;
+ qreal oldWidth = m_itemWidths[object];
+ m_itemWidths[object] = width;
- return result->type() == Cantor::PdfResult::Type && dynamic_cast<Cantor::PdfResult*>(result);
+ 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));
+ }
}
-bool Worksheet::visitLogicalEntries(WorksheetEntry* first, const std::function<bool(WorksheetEntry*)>& visitor)
+void Worksheet::removeRequestedWidth(QGraphicsObject* object)
{
- for (auto* entry = first; entry; entry = entry->next())
- {
- if (!visitor(entry))
- return false;
+ if (!m_itemWidths.contains(object))
+ return;
- if (entry->type() != HierarchyEntry::Type)
- continue;
+ qreal width = m_itemWidths[object];
+ m_itemWidths.remove(object);
- auto* hierarchyEntry = static_cast<HierarchyEntry*>(entry);
- if (auto* hiddenEntry = hierarchyEntry->hiddenSubentries())
- {
- if (!visitLogicalEntries(hiddenEntry, visitor))
- return false;
- }
+ 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::visitLogicalEntries(const std::function<bool(WorksheetEntry*)>& visitor)
+bool Worksheet::isEmpty()
{
- return visitLogicalEntries(firstEntry(), visitor);
+ return !m_firstEntry;
}
-bool Worksheet::findHierarchyEntryById(WorksheetEntry* first, const QString& hierarchyId, QVector<HierarchyEntry*> collapsedAncestors, HierarchySearchResult& result)
+bool Worksheet::isLoadingFromFile()
{
- for (auto* entry = first; entry; entry = entry->next())
- {
- if (entry->type() != HierarchyEntry::Type)
- continue;
-
- auto* hierarchyEntry = static_cast<HierarchyEntry*>(entry);
-
- if (hierarchyEntry->hierarchyId() == hierarchyId)
- {
- result.entry = hierarchyEntry;
- result.collapsedAncestors = collapsedAncestors;
- return true;
- }
-
- if (auto* hiddenEntry = hierarchyEntry->hiddenSubentries())
- {
- auto childAncestors = collapsedAncestors;
- childAncestors.append(hierarchyEntry);
-
- if (findHierarchyEntryById(hiddenEntry, hierarchyId, childAncestors, result))
- return true;
- }
- }
-
- return false;
+ return m_isLoadingFromFile;
}
-Worksheet::HierarchySearchResult Worksheet::findHierarchyEntryById(const QString& hierarchyId)
+void Worksheet::makeVisible(WorksheetEntry* entry)
{
- HierarchySearchResult result;
-
- if (hierarchyId.isEmpty())
- return result;
-
- findHierarchyEntryById(firstEntry(), hierarchyId, {}, result);
- return result;
+ QRectF r = entry->boundingRect();
+ r = entry->mapRectToScene(r);
+ r.adjust(0, -10, 0, 10);
+ worksheetView()->makeVisible(r);
}
-bool Worksheet::findCommandEntryById(WorksheetEntry* first, const QString& commandId, QVector<HierarchyEntry*> collapsedAncestors, CommandSearchResult& result)
+void Worksheet::makeVisible(const KWorksheetCursor& cursor)
{
- for (auto* entry = first; entry; entry = entry->next())
+ if(!cursor.cursor().isValid())
{
- if (entry->type() == CommandEntry::Type)
- {
- auto* commandEntry = static_cast<CommandEntry*>(entry);
- if (commandEntry->commandId() == commandId)
- {
- result.entry = commandEntry;
- result.collapsedAncestors = collapsedAncestors;
- return true;
- }
- }
-
- 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;
- }
+ if(cursor.entry())
+ makeVisible(cursor.entry());
+ return;
}
-
- return false;
+ 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);
}
-Worksheet::CommandSearchResult Worksheet::findCommandEntryById(const QString& commandId)
+void Worksheet::makeVisible(const WorksheetCursor& cursor)
{
- CommandSearchResult result;
-
- if (commandId.isEmpty())
- return result;
-
- findCommandEntryById(firstEntry(), commandId, {}, result);
- return 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);
}
-bool Worksheet::expandHierarchyAncestors(const QVector<HierarchyEntry*>& ancestors)
+WorksheetView* Worksheet::worksheetView()
{
- bool expanded = false;
-
- for (auto* ancestor : ancestors)
- {
- if (!ancestor || !ancestor->hasHiddenSubentries())
- continue;
-
- WorksheetEntry* hiddenSubentries = ancestor->takeHiddenSubentries();
+ return static_cast<WorksheetView*>(views().first());
+}
- if (!hiddenSubentries)
- continue;
+void Worksheet::setModified()
+{
+ if (!m_isClosing && !m_isLoadingFromFile)
+ Q_EMIT modified();
+}
- insertSubentriesForHierarchy(ancestor, hiddenSubentries);
- expanded = true;
- }
+KWorksheetCursor Worksheet::worksheetCursor()
+{
+ auto* entry = currentEntry();
+ auto* item = currentTextItem();
- return expanded;
+ if (!entry || !item)
+ return KWorksheetCursor();
+ return KWorksheetCursor(entry, item, item->view()->cursorPosition());
}
-void Worksheet::updateHierarchyLayout()
+void Worksheet::setWorksheetCursor(const WorksheetCursor& cursor)
{
- QSet<QString> usedHierarchyIds;
- QSet<QString> usedCommandIds;
- QSet<QString> usedResultIds;
+ if (!cursor.isValid() || !cursor.textItem())
+ return;
- m_hierarchyMaxDepth = 0;
- std::vector<int> hierarchyNumbers;
+ if (m_lastFocusedTextItem)
+ m_lastFocusedTextItem->clearSelection();
+ if (m_legacylastFocusedTextItem)
+ m_legacylastFocusedTextItem->clearSelection();
- visitLogicalEntries([&](WorksheetEntry* entry)
- {
- if (entry->type() == HierarchyEntry::Type)
- {
- auto* hierarchyEntry = static_cast<HierarchyEntry*>(entry);
+ m_legacylastFocusedTextItem = cursor.textItem();
+ m_lastFocusedTextItem = nullptr;
- hierarchyEntry->updateHierarchyLevel(hierarchyNumbers);
+ cursor.textItem()->setTextCursor(cursor.textCursor());
+}
- m_hierarchyMaxDepth = std::max(m_hierarchyMaxDepth, hierarchyNumbers.size());
+void Worksheet::setWorksheetCursor(const KWorksheetCursor& cursor)
+{
+ if(!cursor.isValid())
+ return;
- // Keep IDs valid for old files and duplicated entries.
- QString hierarchyId = hierarchyEntry->hierarchyId();
+ if (m_lastFocusedTextItem)
+ m_lastFocusedTextItem->clearSelection();
+ if (m_legacylastFocusedTextItem)
+ m_legacylastFocusedTextItem->clearSelection();
- if (hierarchyId.isEmpty() || usedHierarchyIds.contains(hierarchyId))
- {
- hierarchyEntry->regenerateHierarchyId();
- hierarchyId = hierarchyEntry->hierarchyId();
- }
+ m_lastFocusedTextItem = cursor.textItem();
+ m_legacylastFocusedTextItem = nullptr;
- usedHierarchyIds.insert(hierarchyId);
- }
- else if (entry->type() == CommandEntry::Type)
- {
- auto* commandEntry = static_cast<CommandEntry*>(entry);
- QString commandId = commandEntry->commandId();
+ cursor.textItem()->view()->setSelection(cursor.foundRange());
+}
- if (commandId.isEmpty() || usedCommandIds.contains(commandId))
- {
- commandEntry->regenerateCommandId();
- commandId = commandEntry->commandId();
- }
- usedCommandIds.insert(commandId);
+WorksheetEntry* Worksheet::currentEntry()
+{
+ // Entry cursor activate
+ if (m_choosenCursorEntry || m_isCursorEntryAfterLastEntry)
+ return nullptr;
- 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);
- }
- }
+ 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 true;
- });
-
- refreshTocStructure();
+ return entry;
+ }
+ return nullptr;
}
-void Worksheet::updateHierarchyControlsLayout(WorksheetEntry* startEntry)
+WorksheetEntry* Worksheet::firstEntry()
{
- Q_UNUSED(startEntry);
+ return m_firstEntry;
+}
- std::vector<HierarchyEntry*> levelEntries;
- const int numerationBegin = static_cast<int>(HierarchyEntry::HierarchyLevel::Chapter);
- const int numerationEnd = static_cast<int>(HierarchyEntry::HierarchyLevel::EndValue);
+WorksheetEntry* Worksheet::lastEntry()
+{
+ return m_lastEntry;
+}
- for (int i = numerationBegin; i < numerationEnd; ++i)
- levelEntries.push_back(nullptr);
+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);
+}
- WorksheetEntry* lastRealEntry = nullptr;
+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);
+}
- for (auto* entry = firstEntry(); entry; entry = entry->next())
- {
- if (entry->type() == PlaceHolderEntry::Type || entry->aboutToBeRemoved())
- continue;
+void Worksheet::invalidateFirstEntry()
+{
+ if (m_firstEntry)
+ setFirstEntry(m_firstEntry->next());
+}
- lastRealEntry = entry;
+void Worksheet::invalidateLastEntry()
+{
+ if (m_lastEntry)
+ setLastEntry(m_lastEntry->previous());
+}
- if (entry->type() != HierarchyEntry::Type)
- 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;
+}
- auto* hierarchyEntry = static_cast<HierarchyEntry*>(entry);
+WorksheetEntry* Worksheet::entryAt(QPointF p)
+{
+ return entryAt(p.x(), p.y());
+}
- const int index = static_cast<int>(hierarchyEntry->level()) - numerationBegin;
+void Worksheet::focusEntry(WorksheetEntry* entry)
+{
+ if (!entry)
+ return;
+ entry->focusEntry();
+ resetEntryCursor();
+ //bool rt = entry->acceptRichText();
+ //setActionsEnabled(rt);
+ //setAcceptRichText(rt);
+ //ensureCursorVisible();
+}
- if (index < 0 || index >= static_cast<int>(levelEntries.size()))
- continue;
+void Worksheet::startDrag(WorksheetEntry* entry, QDrag* drag)
+{
+ if (m_readOnly || !entry || !drag)
+ return;
- if (!levelEntries[index])
- {
- levelEntries[index] = hierarchyEntry;
- continue;
- }
+ resetEntryCursor();
- // Close previous controls at this level and below.
- for (int i = index; i < static_cast<int>(levelEntries.size()); ++i)
- {
- auto* openEntry = levelEntries[i];
+ m_dragEntry = entry;
- if (!openEntry)
- continue;
+ WorksheetEntry* originalPrevious = entry->previous();
+ WorksheetEntry* originalNext = entry->next();
+ WorksheetEntry* previous = originalPrevious;
+ WorksheetEntry* next = originalNext;
- const qreal ownBottom = openEntry->y() + openEntry->size().height() - WorksheetEntry::VerticalMargin;
- const qreal controlEnd = hierarchyEntry->y() - WorksheetEntry::VerticalMargin;
- const bool hasSubelements = controlEnd > ownBottom;
+ m_placeholderEntry = new PlaceHolderEntry(this, entry->size());
+ m_placeholderEntry->setPrevious(previous);
+ m_placeholderEntry->setNext(next);
- openEntry->updateControlElementForHierarchy(qMax(controlEnd, ownBottom), m_hierarchyMaxDepth, hasSubelements);
+ if (previous)
+ previous->setNext(m_placeholderEntry);
+ else
+ setFirstEntry(m_placeholderEntry);
- levelEntries[i] = nullptr;
- }
+ if (next)
+ next->setPrevious(m_placeholderEntry);
+ else
+ setLastEntry(m_placeholderEntry);
- levelEntries[index] = hierarchyEntry;
- }
+ m_dragEntry->hide();
- if (!lastRealEntry)
- return;
+ const Qt::DropAction action = drag->exec();
- const qreal documentEnd = lastRealEntry->y() + lastRealEntry->size().height() - WorksheetEntry::VerticalMargin;
+ bool positionChanged = false;
- for (auto* openEntry : levelEntries)
+ if (action == Qt::MoveAction && m_placeholderEntry)
{
- 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);
+ previous = m_placeholderEntry->previous();
+ next = m_placeholderEntry->next();
+ positionChanged = previous != originalPrevious || next != originalNext;
}
-}
-std::vector<WorksheetEntry*> Worksheet::hierarchySubelements(HierarchyEntry* hierarchyEntry) const
-{
- std::vector<WorksheetEntry*> subentries;
-
- Q_ASSERT(hierarchyEntry);
+ removeDragPlaceholder();
- 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;
-}
+ m_dragEntry->setPrevious(previous);
+ m_dragEntry->setNext(next);
-void Worksheet::updateCurrentHierarchy(WorksheetEntry* entry)
-{
- QString nodeId;
+ if (previous)
+ previous->setNext(m_dragEntry);
+ else
+ setFirstEntry(m_dragEntry);
- if (entry && entry->type() == CommandEntry::Type)
- nodeId = buildCommandNodeId(static_cast<CommandEntry*>(entry));
+ if (next)
+ next->setPrevious(m_dragEntry);
else
- nodeId = hierarchyIdForEntry(entry);
+ setLastEntry(m_dragEntry);
- setCurrentTocNode(nodeId);
-}
+ m_dragEntry->show();
+ const bool hierarchyMoved = m_dragEntry->type() == HierarchyEntry::Type;
-QString Worksheet::hierarchyIdForEntry(WorksheetEntry* entry) const
-{
- for (auto* current = entry; current; current = current->previous())
- {
- if (current->type() == HierarchyEntry::Type)
- return static_cast<HierarchyEntry*>(current)->hierarchyId();
- }
+ m_dragEntry->focusEntry();
+ const QPointF scenePosition = worksheetView()->sceneCursorPos();
- return QString();
-}
+ if (entryAt(scenePosition) != m_dragEntry)
+ m_dragEntry->hideActionBar();
-void Worksheet::setCurrentTocNode(const QString& nodeId)
-{
- if (m_currentTocNodeId == nodeId)
- return;
+ m_dragEntry = nullptr;
- m_currentTocNodeId = nodeId;
+ if (hierarchyMoved && positionChanged)
+ updateHierarchyLayout();
+
+ updateLayout();
- Q_EMIT currentTocNodeChanged(nodeId);
+ if (positionChanged)
+ setModified();
}
-void Worksheet::normalizeDraggedHierarchyLevels(HierarchyEntry* rootEntry, WorksheetEntry* previousEntry, const std::vector<WorksheetEntry*>& subentries)
+void Worksheet::startDragWithHierarchy(HierarchyEntry* entry, QDrag* drag, QSizeF responsibleZoneSize)
{
- if (!rootEntry)
+ if (m_readOnly || !entry || !drag)
return;
- HierarchyEntry* previousHierarchyEntry = nullptr;
+ resetEntryCursor();
- for (auto* entry = previousEntry; entry; entry = entry->previous())
- {
- if (entry->type() != HierarchyEntry::Type)
- continue;
+ m_dragEntry = entry;
+ m_hierarchySubentriesDrag = hierarchySubelements(entry);
+ m_hierarchyDragSize = responsibleZoneSize;
- previousHierarchyEntry = static_cast<HierarchyEntry*>(entry);
- break;
- }
+ WorksheetEntry* originalPrevious = entry->previous();
+ WorksheetEntry* originalNext = nullptr;
- const int minimumLevel = static_cast<int>(HierarchyEntry::HierarchyLevel::Chapter);
- const int maximumLevel = static_cast<int>(HierarchyEntry::HierarchyLevel::Subparagraph);
+ if (!m_hierarchySubentriesDrag.empty())
+ originalNext = m_hierarchySubentriesDrag.back()->next();
+ else
+ originalNext = entry->next();
- int maximumAllowedRootLevel = minimumLevel;
+ WorksheetEntry* previous = originalPrevious;
+ WorksheetEntry* next = originalNext;
- if (previousHierarchyEntry)
- maximumAllowedRootLevel = qMin(maximumLevel, static_cast<int>(previousHierarchyEntry->level()) + 1);
+ m_placeholderEntry = new PlaceHolderEntry(this, responsibleZoneSize);
+ m_placeholderEntry->setPrevious(previous);
+ m_placeholderEntry->setNext(next);
- const int oldRootLevel = static_cast<int>(rootEntry->level());
- if (oldRootLevel <= maximumAllowedRootLevel)
- return;
+ if (previous)
+ previous->setNext(m_placeholderEntry);
+ else
+ setFirstEntry(m_placeholderEntry);
- const int levelOffset = maximumAllowedRootLevel - oldRootLevel;
+ if (next)
+ next->setPrevious(m_placeholderEntry);
+ else
+ setLastEntry(m_placeholderEntry);
- const auto shiftLevel = [levelOffset, minimumLevel, maximumLevel](HierarchyEntry* hierarchyEntry)
- {
- if (!hierarchyEntry)
- return;
+ m_dragEntry->hide();
- const int newLevel = qBound(minimumLevel, static_cast<int>(hierarchyEntry->level()) + levelOffset, maximumLevel);
+ for (auto* subentry : m_hierarchySubentriesDrag)
+ subentry->hide();
- hierarchyEntry->setLevel(static_cast<HierarchyEntry::HierarchyLevel>(newLevel));
- };
+ const Qt::DropAction action = drag->exec();
- shiftLevel(rootEntry);
+ bool positionChanged = false;
- for (auto* entry : subentries)
+ if (action == Qt::MoveAction && m_placeholderEntry)
{
- if (entry->type() != HierarchyEntry::Type)
- continue;
-
- shiftLevel(static_cast<HierarchyEntry*>(entry));
+ previous = m_placeholderEntry->previous();
+ next = m_placeholderEntry->next();
+ positionChanged = previous != originalPrevious || next != originalNext;
}
-}
-void Worksheet::updateCurrentHierarchyFromView(const QRectF& viewRect)
-{
- if (m_hierarchyTrackingSource != HierarchyTrackingSource::Viewport || m_layoutUpdateInProgress || m_isLoadingFromFile || viewRect.isEmpty())
- return;
+ removeDragPlaceholder();
- const qreal activationOffset = qMin<qreal>(48.0, viewRect.height() * 0.15);
+ m_dragEntry->setPrevious(previous);
- const qreal activationY = viewRect.top() + activationOffset;
+ if (previous)
+ previous->setNext(m_dragEntry);
+ else
+ setFirstEntry(m_dragEntry);
- WorksheetEntry* activeEntry = nullptr;
+ WorksheetEntry* lastDraggingEntry = m_hierarchySubentriesDrag.empty() ? static_cast<WorksheetEntry*>(entry) : m_hierarchySubentriesDrag.back();
- for (auto* entry = firstEntry(); entry; entry = entry->next())
- {
- if (!entry->isVisible())
- continue;
+ lastDraggingEntry->setNext(next);
- if (entry->scenePos().y() > activationY)
- break;
+ if (next)
+ next->setPrevious(lastDraggingEntry);
+ else
+ setLastEntry(lastDraggingEntry);
- activeEntry = entry;
- }
+ if (positionChanged)
+ normalizeDraggedHierarchyLevels(entry, previous, m_hierarchySubentriesDrag);
- updateCurrentHierarchy(activeEntry);
-}
+ m_dragEntry->show();
-void Worksheet::updateCurrentHierarchyFromEntry(WorksheetEntry* entry)
-{
- m_hierarchyTrackingSource = HierarchyTrackingSource::FocusedEntry;
+ for (auto* subentry : m_hierarchySubentriesDrag)
+ subentry->show();
- updateCurrentHierarchy(entry);
-}
+ m_dragEntry->focusEntry();
-void Worksheet::followHierarchyFromView()
-{
- m_hierarchyTrackingSource = HierarchyTrackingSource::Viewport;
+ const QPointF scenePosition = worksheetView()->sceneCursorPos();
+ if (entryAt(scenePosition) != m_dragEntry)
+ m_dragEntry->hideActionBar();
- // Defer until viewRect() reflects the final scroll position.
- QTimer::singleShot(0, this, [this]() {
- if (m_hierarchyTrackingSource != HierarchyTrackingSource::Viewport)
- return;
+#ifndef NDEBUG
+ for (auto* current = firstEntry(); current; current = current->next())
+ Q_ASSERT(current->type() != PlaceHolderEntry::Type);
+#endif
- updateCurrentHierarchyFromView(worksheetView()->viewRect());
- });
-}
+ m_hierarchySubentriesDrag.clear();
+ m_dragEntry = nullptr;
-void Worksheet::updateEntrySize(WorksheetEntry* entry)
-{
- QScopedValueRollback<bool> layoutGuard(m_layoutUpdateInProgress, true);
- bool cursorRectVisible = false;
- bool atEnd = worksheetView()->isAtEnd();
- if (currentTextItem()) {
- QRectF cursorRect = currentTextItem()->sceneCursorRect();
- cursorRectVisible = worksheetView()->isVisible(cursorRect);
- }
-
- 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();
-
- // 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;
- }
- }
-
- qreal y = entry->y() + entry->size().height();
- for (entry = entry->next(); entry; entry = entry->next()) {
- entry->setY(y);
- y += entry->size().height();
- }
-
- if (!m_isLoadingFromFile)
- updateHierarchyControlsLayout(entry);
+ updateHierarchyLayout();
+ updateLayout();
- setSceneRect(QRectF(0, 0, sceneRect().width(), y));
- if (cursorRectVisible)
- makeVisible(worksheetCursor());
- else if (atEnd)
- worksheetView()->scrollToEnd();
- drawEntryCursor();
+ if (positionChanged)
+ setModified();
}
-void Worksheet::setRequestedWidth(QGraphicsObject* object, qreal width)
+void Worksheet::evaluate()
{
- qreal oldWidth = m_itemWidths[object];
- m_itemWidths[object] = width;
+ qDebug()<<"evaluate worksheet";
+ // login if not done yet
+ if (!m_readOnly && m_session && m_session->status() == Cantor::Session::Disable)
+ loginToSession();
- 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));
+ // evaluate the worksheet if the login was successful
+ if (m_session && m_session->status() == Cantor::Session::Done) {
+ firstEntry()->evaluate(WorksheetEntry::EvaluateNext);
+ setModified();
}
}
-void Worksheet::removeRequestedWidth(QGraphicsObject* object)
+void Worksheet::evaluateCurrentEntry()
{
- if (!m_itemWidths.contains(object))
- return;
+ // login if not done yet
+ if (!m_readOnly && m_session && m_session->status() == Cantor::Session::Disable)
+ loginToSession();
- qreal width = m_itemWidths[object];
- m_itemWidths.remove(object);
+ // evaluate the current entry if the login was successful
+ if (!m_session)
+ return;
- if (width == m_maxWidth)
+ // 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)
{
- 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));
+ if(auto* entry = currentEntry())
+ entry->evaluateCurrentItem();
}
}
-bool Worksheet::isEmpty()
+bool Worksheet::completionEnabled()
{
- return !m_firstEntry;
+ return m_completionEnabled;
}
-bool Worksheet::isLoadingFromFile()
+void Worksheet::showCompletion()
{
- return m_isLoadingFromFile;
+ auto* current = currentEntry();
+ if (current)
+ current->showCompletion();
}
-void Worksheet::makeVisible(WorksheetEntry* entry)
+WorksheetEntry* Worksheet::appendEntry(const int type, bool focus)
{
- QRectF r = entry->boundingRect();
- r = entry->mapRectToScene(r);
- r.adjust(0, -10, 0, 10);
- worksheetView()->makeVisible(r);
-}
+ auto* entry = WorksheetEntry::create(type, this);
-void Worksheet::makeVisible(const KWorksheetCursor& cursor)
-{
- if(!cursor.cursor().isValid())
+ if (entry)
{
- if(cursor.entry())
- makeVisible(cursor.entry());
- return;
+ qDebug() << "Entry Appended";
+ entry->setPrevious(lastEntry());
+ if (lastEntry())
+ lastEntry()->setNext(entry);
+ if (!firstEntry())
+ setFirstEntry(entry);
+ setLastEntry(entry);
+ if (!m_isLoadingFromFile)
+ {
+ updateHierarchyLayout();
+ updateLayout();
+ if (focus)
+ {
+ makeVisible(entry);
+ focusEntry(entry);
+ }
+ setModified();
+ }
}
- 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);
+ return entry;
}
-void Worksheet::makeVisible(const WorksheetCursor& cursor)
+WorksheetEntry* Worksheet::appendCommandEntry()
{
- 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);
+ return appendEntry(CommandEntry::Type);
}
-WorksheetView* Worksheet::worksheetView()
+WorksheetEntry* Worksheet::appendTextEntry()
{
- return static_cast<WorksheetView*>(views().first());
+ return appendEntry(TextEntry::Type);
}
-void Worksheet::setModified()
+WorksheetEntry* Worksheet::appendMarkdownEntry()
{
- if (!m_isClosing && !m_isLoadingFromFile)
- Q_EMIT modified();
+ return appendEntry(MarkdownEntry::Type);
}
-KWorksheetCursor Worksheet::worksheetCursor()
+WorksheetEntry* Worksheet::appendPageBreakEntry()
{
- auto* entry = currentEntry();
- auto* item = currentTextItem();
-
- if (!entry || !item)
- return KWorksheetCursor();
- return KWorksheetCursor(entry, item, item->view()->cursorPosition());
+ return appendEntry(PageBreakEntry::Type);
}
-void Worksheet::setWorksheetCursor(const WorksheetCursor& cursor)
+WorksheetEntry* Worksheet::appendImageEntry()
{
- 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());
+ return appendEntry(ImageEntry::Type);
}
-void Worksheet::setWorksheetCursor(const KWorksheetCursor& cursor)
+WorksheetEntry* Worksheet::appendLatexEntry()
{
- 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());
+ return appendEntry(LatexEntry::Type);
}
-
-WorksheetEntry* Worksheet::currentEntry()
+void Worksheet::appendCommandEntry(const QString& text)
{
- // Entry cursor activate
- if (m_choosenCursorEntry || m_isCursorEntryAfterLastEntry)
- return nullptr;
+ auto* entry = lastEntry();
+ if(!entry->isEmpty())
+ entry = appendCommandEntry();
- 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;
+ if (entry)
+ {
+ focusEntry(entry);
+ entry->setContent(text);
+ evaluateCurrentEntry();
}
- return nullptr;
}
-WorksheetEntry* Worksheet::firstEntry()
+WorksheetEntry* Worksheet::appendHorizontalRuleEntry()
{
- return m_firstEntry;
+ return appendEntry(HorizontalRuleEntry::Type);
}
-WorksheetEntry* Worksheet::lastEntry()
+WorksheetEntry* Worksheet::appendHierarchyEntry()
{
- return m_lastEntry;
+ return appendEntry(HierarchyEntry::Type);
}
-void Worksheet::setFirstEntry(WorksheetEntry* entry)
+WorksheetEntry* Worksheet::insertEntry(const int type, WorksheetEntry* current)
{
- 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 (!current)
+ current = currentEntry();
-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);
+ 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;
}
-void Worksheet::invalidateFirstEntry()
+WorksheetEntry* Worksheet::insertTextEntry(WorksheetEntry* current)
{
- if (m_firstEntry)
- setFirstEntry(m_firstEntry->next());
+ return insertEntry(TextEntry::Type, current);
}
-void Worksheet::invalidateLastEntry()
+WorksheetEntry* Worksheet::insertMarkdownEntry(WorksheetEntry* current)
{
- if (m_lastEntry)
- setLastEntry(m_lastEntry->previous());
+ return insertEntry(MarkdownEntry::Type, current);
}
-WorksheetEntry* Worksheet::entryAt(qreal x, qreal y)
+WorksheetEntry* Worksheet::insertCommandEntry(WorksheetEntry* current)
{
- 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;
+ return insertEntry(CommandEntry::Type, current);
}
-WorksheetEntry* Worksheet::entryAt(QPointF p)
+WorksheetEntry* Worksheet::insertImageEntry(WorksheetEntry* current)
{
- return entryAt(p.x(), p.y());
+ auto* entry = insertEntry(ImageEntry::Type, current);
+ auto* imageEntry = static_cast<ImageEntry*>(entry);
+ QTimer::singleShot(0, this, [=] () {imageEntry->startConfigDialog();});
+ return entry;
}
-void Worksheet::focusEntry(WorksheetEntry* entry)
+WorksheetEntry* Worksheet::insertPageBreakEntry(WorksheetEntry* current)
{
- if (!entry)
- return;
- entry->focusEntry();
- resetEntryCursor();
- //bool rt = entry->acceptRichText();
- //setActionsEnabled(rt);
- //setAcceptRichText(rt);
- //ensureCursorVisible();
+ return insertEntry(PageBreakEntry::Type, current);
}
-void Worksheet::startDrag(WorksheetEntry* entry, QDrag* drag)
+WorksheetEntry* Worksheet::insertLatexEntry(WorksheetEntry* current)
{
- if (m_readOnly || !entry || !drag)
- return;
+ return insertEntry(LatexEntry::Type, current);
+}
- resetEntryCursor();
+WorksheetEntry* Worksheet::insertHorizontalRuleEntry(WorksheetEntry* current)
+{
+ return insertEntry(HorizontalRuleEntry::Type, current);
+}
- m_dragEntry = entry;
+WorksheetEntry* Worksheet::insertHierarchyEntry(WorksheetEntry* current)
+{
+ return insertEntry(HierarchyEntry::Type, current);
+}
- WorksheetEntry* originalPrevious = entry->previous();
- WorksheetEntry* originalNext = entry->next();
- WorksheetEntry* previous = originalPrevious;
- WorksheetEntry* next = originalNext;
+WorksheetEntry* Worksheet::insertEntryBefore(int type, WorksheetEntry* current)
+{
+ if (!current)
+ current = currentEntry();
- m_placeholderEntry = new PlaceHolderEntry(this, entry->size());
- m_placeholderEntry->setPrevious(previous);
- m_placeholderEntry->setNext(next);
+ if (!current)
+ return nullptr;
- if (previous)
- previous->setNext(m_placeholderEntry);
- else
- setFirstEntry(m_placeholderEntry);
+ auto* prev = current->previous();
+ WorksheetEntry* entry = nullptr;
- if (next)
- next->setPrevious(m_placeholderEntry);
+ 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
- setLastEntry(m_placeholderEntry);
-
- m_dragEntry->hide();
-
- const Qt::DropAction action = drag->exec();
+ entry = prev;
- bool positionChanged = false;
+ focusEntry(entry);
+ return entry;
+}
- if (action == Qt::MoveAction && m_placeholderEntry)
- {
- previous = m_placeholderEntry->previous();
- next = m_placeholderEntry->next();
- positionChanged = previous != originalPrevious || next != originalNext;
- }
+WorksheetEntry* Worksheet::insertTextEntryBefore(WorksheetEntry* current)
+{
+ return insertEntryBefore(TextEntry::Type, current);
+}
- removeDragPlaceholder();
+WorksheetEntry* Worksheet::insertMarkdownEntryBefore(WorksheetEntry* current)
+{
+ return insertEntryBefore(MarkdownEntry::Type, current);
+}
- m_dragEntry->setPrevious(previous);
- m_dragEntry->setNext(next);
+WorksheetEntry* Worksheet::insertCommandEntryBefore(WorksheetEntry* current)
+{
+ return insertEntryBefore(CommandEntry::Type, current);
+}
- if (previous)
- previous->setNext(m_dragEntry);
- else
- setFirstEntry(m_dragEntry);
+WorksheetEntry* Worksheet::insertPageBreakEntryBefore(WorksheetEntry* current)
+{
+ return insertEntryBefore(PageBreakEntry::Type, current);
+}
- if (next)
- next->setPrevious(m_dragEntry);
- else
- setLastEntry(m_dragEntry);
+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;
+}
- m_dragEntry->show();
- const bool hierarchyMoved = m_dragEntry->type() == HierarchyEntry::Type;
+WorksheetEntry* Worksheet::insertLatexEntryBefore(WorksheetEntry* current)
+{
+ return insertEntryBefore(LatexEntry::Type, current);
+}
- m_dragEntry->focusEntry();
- const QPointF scenePosition = worksheetView()->sceneCursorPos();
+WorksheetEntry* Worksheet::insertHorizontalRuleEntryBefore(WorksheetEntry* current)
+{
+ return insertEntryBefore(HorizontalRuleEntry::Type, current);
+}
- if (entryAt(scenePosition) != m_dragEntry)
- m_dragEntry->hideActionBar();
+WorksheetEntry* Worksheet::insertHierarchyEntryBefore(WorksheetEntry* current)
+{
+ return insertEntryBefore(HierarchyEntry::Type, current);
+}
- m_dragEntry = nullptr;
+void Worksheet::interrupt()
+{
+ if (m_session->status() == Cantor::Session::Running)
+ {
+ m_session->interrupt();
+ Q_EMIT updatePrompt();
+ }
+}
- if (hierarchyMoved && positionChanged)
- updateHierarchyLayout();
+void Worksheet::interruptCurrentEntryEvaluation()
+{
+ currentEntry()->interruptEvaluation();
+}
- updateLayout();
- if (positionChanged)
- setModified();
+bool Worksheet::variableHighlightingEnabled() const
+{
+ return m_variableHighlightingEnabled;
}
-void Worksheet::startDragWithHierarchy(HierarchyEntry* entry, QDrag* drag, QSizeF responsibleZoneSize)
+void Worksheet::setVariableHighlightingEnabled(bool enabled)
{
- if (m_readOnly || !entry || !drag)
+ if (m_variableHighlightingEnabled == enabled)
+ {
return;
+ }
+ m_variableHighlightingEnabled = enabled;
- resetEntryCursor();
+ for (auto* entry = firstEntry(); entry; entry = entry->next())
+ {
+ if (entry->type() == CommandEntry::Type)
+ static_cast<CommandEntry*>(entry)->setVariableHighlightingEnabled(enabled);
+ }
+}
- m_dragEntry = entry;
- m_hierarchySubentriesDrag = hierarchySubelements(entry);
- m_hierarchyDragSize = responsibleZoneSize;
+void Worksheet::enableCompletion(bool enable)
+{
+ m_completionEnabled=enable;
+}
- WorksheetEntry* originalPrevious = entry->previous();
- WorksheetEntry* originalNext = nullptr;
+Cantor::Session* Worksheet::session() const
+{
+ return m_session;
+}
- if (!m_hierarchySubentriesDrag.empty())
- originalNext = m_hierarchySubentriesDrag.back()->next();
- else
- originalNext = entry->next();
+bool Worksheet::isRunning()
+{
+ return m_session && m_session->status()==Cantor::Session::Running;
+}
- WorksheetEntry* previous = originalPrevious;
- WorksheetEntry* next = originalNext;
+bool Worksheet::isReadOnly()
+{
+ return m_readOnly;
+}
- m_placeholderEntry = new PlaceHolderEntry(this, responsibleZoneSize);
- m_placeholderEntry->setPrevious(previous);
- m_placeholderEntry->setNext(next);
+bool Worksheet::showExpressionIds()
+{
+ return m_showExpressionIds;
+}
- if (previous)
- previous->setNext(m_placeholderEntry);
- else
- setFirstEntry(m_placeholderEntry);
+bool Worksheet::animationsEnabled()
+{
+ return m_animationsEnabled;
+}
- if (next)
- next->setPrevious(m_placeholderEntry);
- else
- setLastEntry(m_placeholderEntry);
+void Worksheet::enableAnimations(bool enable)
+{
+ m_animationsEnabled = enable;
+}
- m_dragEntry->hide();
+bool Worksheet::embeddedMathEnabled()
+{
+ return m_embeddedMathEnabled && m_mathRenderer.mathRenderAvailable();
+}
- for (auto* subentry : m_hierarchySubentriesDrag)
- subentry->hide();
+void Worksheet::enableEmbeddedMath(bool enable)
+{
+ m_embeddedMathEnabled = enable;
+}
- const Qt::DropAction action = drag->exec();
+void Worksheet::enableExpressionNumbering(bool enable)
+{
+ m_showExpressionIds=enable;
+ Q_EMIT updatePrompt();
+ refreshTocStructure();
+ if (views().size() != 0)
+ updateLayout();
+}
- bool positionChanged = false;
+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);
- if (action == Qt::MoveAction && m_placeholderEntry)
+ for( auto* entry = firstEntry(); entry; entry = entry->next())
{
- previous = m_placeholderEntry->previous();
- next = m_placeholderEntry->next();
- positionChanged = previous != originalPrevious || next != originalNext;
+ QDomElement el = entry->toXml(doc, archive);
+ root.appendChild( el );
}
+ return doc;
+}
- removeDragPlaceholder();
-
- m_dragEntry->setPrevious(previous);
-
- if (previous)
- previous->setNext(m_dragEntry);
- else
- setFirstEntry(m_dragEntry);
-
- WorksheetEntry* lastDraggingEntry = m_hierarchySubentriesDrag.empty() ? static_cast<WorksheetEntry*>(entry) : m_hierarchySubentriesDrag.back();
+QJsonDocument Worksheet::toJupyterJson()
+{
+ QJsonDocument doc;
+ QJsonObject root;
- lastDraggingEntry->setNext(next);
+ QJsonObject metadata(m_jupyterMetadata ? *m_jupyterMetadata : QJsonObject());
- if (next)
- next->setPrevious(lastDraggingEntry);
+ QJsonObject kernalInfo;
+ if (m_session && m_session->backend())
+ kernalInfo = Cantor::JupyterUtils::getKernelspec(m_session->backend());
else
- setLastEntry(lastDraggingEntry);
-
- if (positionChanged)
- normalizeDraggedHierarchyLevels(entry, previous, m_hierarchySubentriesDrag);
-
- m_dragEntry->show();
+ kernalInfo.insert(QLatin1String("name"), m_backendName);
+ metadata.insert(QLatin1String("kernelspec"), kernalInfo);
- for (auto* subentry : m_hierarchySubentriesDrag)
- subentry->show();
+ root.insert(QLatin1String("metadata"), metadata);
- m_dragEntry->focusEntry();
+ // Not sure, but it looks like we support nbformat version 4.5
+ root.insert(QLatin1String("nbformat"), 4);
+ root.insert(QLatin1String("nbformat_minor"), 5);
- const QPointF scenePosition = worksheetView()->sceneCursorPos();
- if (entryAt(scenePosition) != m_dragEntry)
- m_dragEntry->hideActionBar();
+ QJsonArray cells;
+ for( auto* entry = firstEntry(); entry; entry = entry->next())
+ {
+ const QJsonValue entryJson = entry->toJupyterJson();
-#ifndef NDEBUG
- for (auto* current = firstEntry(); current; current = current->next())
- Q_ASSERT(current->type() != PlaceHolderEntry::Type);
-#endif
+ if (!entryJson.isNull())
+ cells.append(entryJson);
+ }
+ root.insert(QLatin1String("cells"), cells);
- m_hierarchySubentriesDrag.clear();
- m_dragEntry = nullptr;
+ doc.setObject(root);
+ return doc;
+}
- updateHierarchyLayout();
- updateLayout();
+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;
+ }
- if (positionChanged)
- setModified();
+ save(&file);
}
-void Worksheet::evaluate()
+QByteArray Worksheet::saveToByteArray()
{
- qDebug()<<"evaluate worksheet";
- // login if not done yet
- if (!m_readOnly && m_session && m_session->status() == Cantor::Session::Disable)
- loginToSession();
+ QBuffer buffer;
+ save(&buffer);
- // evaluate the worksheet if the login was successful
- if (m_session && m_session->status() == Cantor::Session::Done) {
- firstEntry()->evaluate(WorksheetEntry::EvaluateNext);
- setModified();
- }
+ return buffer.buffer();
}
-void Worksheet::evaluateCurrentEntry()
+void Worksheet::save( QIODevice* device)
{
- // login if not done yet
- if (!m_readOnly && m_session && m_session->status() == Cantor::Session::Disable)
- loginToSession();
+ qDebug()<<"saving to filename";
+ switch (m_type)
+ {
+ case CantorWorksheet:
+ {
+ KZip zipFile( device );
- // evaluate the current entry if the login was successful
- if (!m_session)
- return;
+ if ( !zipFile.open(QIODevice::WriteOnly) )
+ {
+ KMessageBox::error( worksheetView(),
+ i18n( "Cannot write file." ),
+ i18n( "Error - Cantor" ));
+ 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();
- }
-}
+ QByteArray content = toXML(&zipFile).toByteArray();
+ zipFile.writeFile( QLatin1String("content.xml"), content.data());
+ break;
+ }
-bool Worksheet::completionEnabled()
-{
- return m_completionEnabled;
-}
+ case JupyterNotebook:
+ {
+ if (!device->isWritable())
+ {
+ KMessageBox::error( worksheetView(),
+ i18n( "Cannot write file." ),
+ i18n( "Error - Cantor" ));
+ return;
+ }
-void Worksheet::showCompletion()
-{
- auto* current = currentEntry();
- if (current)
- current->showCompletion();
+ const QJsonDocument& doc = toJupyterJson();
+ device->write(doc.toJson(QJsonDocument::Indented));
+ break;
+ }
+ }
}
-WorksheetEntry* Worksheet::appendEntry(const int type, bool focus)
+void Worksheet::savePlain(const QString& filename)
{
- auto* entry = WorksheetEntry::create(type, this);
+ QFile file(filename);
+ if(!file.open(QIODevice::WriteOnly))
+ {
+ KMessageBox::error(worksheetView(), i18n("Error saving file %1", filename), i18n("Error - Cantor"));
+ return;
+ }
- if (entry)
+ QString cmdSep=QLatin1String(";\n");
+ QString commentStartingSeq = QLatin1String("");
+ QString commentEndingSeq = QLatin1String("");
+
+ if (!m_readOnly)
{
- qDebug() << "Entry Appended";
- entry->setPrevious(lastEntry());
- if (lastEntry())
- lastEntry()->setNext(entry);
- if (!firstEntry())
- setFirstEntry(entry);
- setLastEntry(entry);
- if (!m_isLoadingFromFile)
+ Cantor::Backend * const backend=session()->backend();
+ if (backend->extensions().contains(QLatin1String("ScriptExtension")))
{
- updateHierarchyLayout();
- updateLayout();
- if (focus)
+ Cantor::ScriptExtension* e=dynamic_cast<Cantor::ScriptExtension*>(backend->extension(QLatin1String(("ScriptExtension"))));
+ if (e)
{
- makeVisible(entry);
- focusEntry(entry);
+ cmdSep=e->commandSeparator();
+ commentStartingSeq = e->commentStartingSequence();
+ commentEndingSeq = e->commentEndingSequence();
}
- setModified();
}
}
- return entry;
-}
+ 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"));
-WorksheetEntry* Worksheet::appendCommandEntry()
-{
- return appendEntry(CommandEntry::Type);
-}
+ QTextStream stream(&file);
-WorksheetEntry* Worksheet::appendTextEntry()
-{
- return appendEntry(TextEntry::Type);
-}
+ for(auto* entry = firstEntry(); entry; entry = entry->next())
+ {
+ const QString& str=entry->toPlain(cmdSep, commentStartingSeq, commentEndingSeq);
+ if(!str.isEmpty())
+ stream << str + QLatin1Char('\n');
+ }
-WorksheetEntry* Worksheet::appendMarkdownEntry()
-{
- return appendEntry(MarkdownEntry::Type);
+ file.close();
}
-WorksheetEntry* Worksheet::appendPageBreakEntry()
+void Worksheet::saveLatex(const QString& filename)
{
- return appendEntry(PageBreakEntry::Type);
-}
+ qDebug()<<"exporting to Latex: " <<filename;
-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)
+ QFile file(filename);
+ if(!file.open(QIODevice::WriteOnly))
{
- focusEntry(entry);
- entry->setContent(text);
- evaluateCurrentEntry();
+ KMessageBox::error(worksheetView(), i18n("Error saving file %1", filename), i18n("Export to LaTeX"));
+ return;
}
-}
-
-WorksheetEntry* Worksheet::appendHorizontalRuleEntry()
-{
- return appendEntry(HorizontalRuleEntry::Type);
-}
-WorksheetEntry* Worksheet::appendHierarchyEntry()
-{
- return appendEntry(HierarchyEntry::Type);
-}
+ 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;
+ }
-WorksheetEntry* Worksheet::insertEntry(const int type, WorksheetEntry* current)
-{
- if (!current)
- current = currentEntry();
+ 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);
- if (!current)
- return appendEntry(type);
+ 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);
- auto* next = current->next();
- WorksheetEntry* entry = nullptr;
+ // Transform HTML escaped special characters to valid LaTeX characters (&, <, >)
+ QTextStream stream(&file);
+ stream << outString.replace(QLatin1String("&"), QLatin1String("&"))
+ .replace(QLatin1String(">"), QLatin1String(">"))
+ .replace(QLatin1String("<"), QLatin1String("<"));
+ file.close();
+ }
- 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;
+ xmlFree(xmlResultBuffer);
}
- focusEntry(entry);
- makeVisible(entry);
+ xsltFreeStylesheet(xsltStyleSheet);
+ xmlFreeDoc(res);
+ xmlFreeDoc(output);
- return entry;
+ xsltCleanupGlobals();
+ xmlCleanupParser();
}
-WorksheetEntry* Worksheet::insertTextEntry(WorksheetEntry* current)
+bool Worksheet::load(const QString& filename )
{
- return insertEntry(TextEntry::Type, current);
-}
+ 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;
+ }
-WorksheetEntry* Worksheet::insertMarkdownEntry(WorksheetEntry* current)
-{
- return insertEntry(MarkdownEntry::Type, current);
-}
+ bool rc = load(&file);
+ if (rc && !m_readOnly)
+ m_session->setWorksheetPath(filename);
-WorksheetEntry* Worksheet::insertCommandEntry(WorksheetEntry* current)
-{
- return insertEntry(CommandEntry::Type, current);
+ return rc;
}
-WorksheetEntry* Worksheet::insertImageEntry(WorksheetEntry* current)
+void Worksheet::load(QByteArray* data)
{
- auto* entry = insertEntry(ImageEntry::Type, current);
- auto* imageEntry = static_cast<ImageEntry*>(entry);
- QTimer::singleShot(0, this, [=] () {imageEntry->startConfigDialog();});
- return entry;
+ QBuffer buf(data);
+ buf.open(QIODevice::ReadOnly);
+ load(&buf);
}
-WorksheetEntry* Worksheet::insertPageBreakEntry(WorksheetEntry* current)
+bool Worksheet::load(QIODevice* device)
{
- return insertEntry(PageBreakEntry::Type, current);
-}
+ if (!device->isReadable())
+ {
+ QApplication::restoreOverrideCursor();
+ KMessageBox::error(worksheetView(), i18n("Couldn't open the selected file for reading."), i18n("Open File"));
+ return false;
+ }
-WorksheetEntry* Worksheet::insertLatexEntry(WorksheetEntry* current)
-{
- return insertEntry(LatexEntry::Type, current);
-}
+ KZip archive(device);
-WorksheetEntry* Worksheet::insertHorizontalRuleEntry(WorksheetEntry* current)
-{
- return insertEntry(HorizontalRuleEntry::Type, current);
-}
+ 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);
-WorksheetEntry* Worksheet::insertHierarchyEntry(WorksheetEntry* current)
-{
- return insertEntry(HierarchyEntry::Type, current);
+ 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);
+ }
}
-WorksheetEntry* Worksheet::insertEntryBefore(int type, WorksheetEntry* current)
+bool Worksheet::loadCantorWorksheet(const KZip& archive)
{
- if (!current)
- current = currentEntry();
+ m_type = Type::CantorWorksheet;
- if (!current)
- return nullptr;
+ 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;
+ }
- auto* prev = current->previous();
- WorksheetEntry* entry = nullptr;
+ const KArchiveFile* content = static_cast<const KArchiveFile*>(contentEntry);
+ QByteArray data = content->data();
- if(!prev || prev->type() != type || !prev->isEmpty())
+ QDomDocument doc;
+ doc.setContent(data);
+ QDomElement root = doc.documentElement();
+
+ m_backendName = root.attribute(QLatin1String("backend"));
+
+ //There is "Python" only now, replace "Python 3" by "Python"
+ if (m_backendName == QLatin1String("Python 3"))
+ m_backendName = QLatin1String("Python");
+
+ //"Python 2" in older projects not supported anymore, switch to Python (=Python3)
+ if (m_backendName == QLatin1String("Python 2"))
{
- 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();
+ QApplication::restoreOverrideCursor();
+ KMessageBox::information(worksheetView(),
+ i18n("This worksheet was created using Python2 which is not supported anymore. Python3 will be used."),
+ i18n("Python2 not supported anymore"));
+ QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
+ m_backendName = QLatin1String("Python");
+ }
+
+ auto* b = Cantor::Backend::getBackend(m_backendName);
+ if (!b)
+ {
+ QApplication::restoreOverrideCursor();
+ KMessageBox::information(worksheetView(), i18n("%1 backend was not found. Editing and executing entries is not possible.", m_backendName), i18n("Open File"));
+ m_readOnly = true;
}
else
- entry = prev;
+ m_readOnly = false;
- focusEntry(entry);
- return entry;
-}
+ if(!m_readOnly && !b->isEnabled())
+ {
+ QApplication::restoreOverrideCursor();
+ KMessageBox::information(worksheetView(), i18n("There are some problems with the %1 backend,\n"\
+ "please check your configuration or install the needed packages.\n"
+ "You will only be able to view this worksheet.", m_backendName), i18n("Open File"));
+ m_readOnly = true;
+ }
-WorksheetEntry* Worksheet::insertTextEntryBefore(WorksheetEntry* current)
-{
- return insertEntryBefore(TextEntry::Type, current);
-}
+ m_isLoadingFromFile = true;
-WorksheetEntry* Worksheet::insertMarkdownEntryBefore(WorksheetEntry* current)
-{
- return insertEntryBefore(MarkdownEntry::Type, current);
-}
+ //cleanup the worksheet and all it contains
+ delete m_session;
+ m_session = nullptr;
-WorksheetEntry* Worksheet::insertCommandEntryBefore(WorksheetEntry* current)
-{
- return insertEntryBefore(CommandEntry::Type, current);
-}
+ //file can only be loaded in a worksheet that was not edited/modified yet (s.a. CantorShell::load())
+ //in this case on the default "first entry" is available -> delete it.
+ if (m_firstEntry) {
+ delete m_firstEntry;
+ m_firstEntry = nullptr;
+ }
-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();
-}
+ resetEntryCursor();
+ m_itemWidths.clear();
+ m_maxWidth = 0;
+ if (!m_readOnly)
+ initSession(b);
-bool Worksheet::variableHighlightingEnabled() const
-{
- return m_variableHighlightingEnabled;
-}
+ qDebug()<<"loading entries";
+ QDomElement expressionChild = root.firstChildElement();
+ while (!expressionChild.isNull()) {
+ QString tag = expressionChild.tagName();
+ // Don't add focus on load
+ auto* entry = appendEntry(typeForTagName(tag), false);
+ if (entry)
+ {
+ entry->setContent(expressionChild, archive);
+ if (m_readOnly)
+ entry->setAcceptHoverEvents(false);
+ }
-void Worksheet::setVariableHighlightingEnabled(bool enabled)
-{
- if (m_variableHighlightingEnabled == enabled)
- {
- return;
+ expressionChild = expressionChild.nextSiblingElement();
}
- m_variableHighlightingEnabled = enabled;
- for (auto* entry = firstEntry(); entry; entry = entry->next())
- {
- if (entry->type() == CommandEntry::Type)
- static_cast<CommandEntry*>(entry)->setVariableHighlightingEnabled(enabled);
- }
-}
+ if (m_readOnly)
+ clearFocus();
-void Worksheet::enableCompletion(bool enable)
-{
- m_completionEnabled=enable;
-}
+ m_isLoadingFromFile = false;
+ updateHierarchyLayout();
+ updateLayout();
-Cantor::Session* Worksheet::session() const
-{
- return m_session;
-}
+ //Set the Highlighting, depending on the current state
+ //If the session isn't logged in, use the default
-bool Worksheet::isRunning()
-{
- return m_session && m_session->status()==Cantor::Session::Running;
+ Q_EMIT loaded();
+ return true;
}
-bool Worksheet::isReadOnly()
+int Worksheet::typeForTagName(const QString& tag)
{
- return m_readOnly;
-}
+ if (tag == QLatin1String("Expression"))
+ return CommandEntry::Type;
+ else if (tag == QLatin1String("Text"))
+ return TextEntry::Type;
+ else if (tag == QLatin1String("Markdown"))
+ return MarkdownEntry::Type;
+ else if (tag == QLatin1String("Latex"))
+ return LatexEntry::Type;
+ else if (tag == QLatin1String("PageBreak"))
+ return PageBreakEntry::Type;
+ else if (tag == QLatin1String("Image"))
+ return ImageEntry::Type;
+ else if (tag == QLatin1String("HorizontalRule"))
+ return HorizontalRuleEntry::Type;
+ else if (tag == QLatin1String("Hierarchy"))
+ return HierarchyEntry::Type;
-bool Worksheet::showExpressionIds()
-{
- return m_showExpressionIds;
+ return 0;
}
-bool Worksheet::animationsEnabled()
-{
- return m_animationsEnabled;
-}
-void Worksheet::enableAnimations(bool enable)
+void Worksheet::initSession(Cantor::Backend* backend)
{
- m_animationsEnabled = enable;
+ m_session = backend->createSession();
+ if (m_useDefaultWorksheetParameters)
+ {
+ if (Cantor::LatexRenderer::isLatexAvailable())
+ m_session->setTypesettingEnabled(Settings::self()->typesetDefault());
+ enableCompletion(Settings::self()->completionDefault());
+ enableExpressionNumbering(Settings::self()->expressionNumberingDefault());
+ enableAnimations(Settings::self()->animationDefault());
+ enableEmbeddedMath(Settings::self()->embeddedMathDefault());
+ }
}
-bool Worksheet::embeddedMathEnabled()
+bool Worksheet::loadJupyterNotebook(const QJsonDocument& doc)
{
- return m_embeddedMathEnabled && m_mathRenderer.mathRenderAvailable();
-}
+ m_type = Type::JupyterNotebook;
-void Worksheet::enableEmbeddedMath(bool enable)
-{
- m_embeddedMathEnabled = enable;
-}
+ int nbformatMajor, nbformatMinor;
+ if (!Cantor::JupyterUtils::isJupyterNotebook(doc))
+ {
+ // Two possibilities: old jupyter notebook (version <= 4.0.0 and a another scheme) or just not a notebook at all
+ std::tie(nbformatMajor, nbformatMinor) = Cantor::JupyterUtils::getNbformatVersion(doc.object());
+ if (nbformatMajor == 0 && nbformatMinor == 0)
+ {
+ QApplication::restoreOverrideCursor();
+ showInvalidNotebookSchemeError();
+ }
+ else
+ {
+ KMessageBox::error(worksheetView(),
+ i18n("Jupyter notebooks with versions lower than 4.5 (detected version %1.%2) are not supported.", nbformatMajor, nbformatMinor ),
+ i18n("Open File"));
+ }
-void Worksheet::enableExpressionNumbering(bool enable)
-{
- m_showExpressionIds=enable;
- Q_EMIT updatePrompt();
- refreshTocStructure();
- if (views().size() != 0)
- updateLayout();
-}
+ return false;
+ }
-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);
+ QJsonObject notebookObject = doc.object();
+ std::tie(nbformatMajor, nbformatMinor) = Cantor::JupyterUtils::getNbformatVersion(notebookObject);
- for( auto* entry = firstEntry(); entry; entry = entry->next())
+ if (QT_VERSION_CHECK(nbformatMajor, nbformatMinor, 0) > QT_VERSION_CHECK(4,5,0))
{
- QDomElement el = entry->toXml(doc, archive);
- root.appendChild( el );
+ QApplication::restoreOverrideCursor();
+ KMessageBox::error(
+ worksheetView(),
+ i18n("Jupyter notebooks with versions higher than 4.5 (detected version %1.%2) are not supported.", nbformatMajor, nbformatMinor),
+ i18n("Open File")
+ );
+ return false;
}
- 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);
+ const QJsonArray& cells = Cantor::JupyterUtils::getCells(notebookObject);
+ const QJsonObject& metadata = Cantor::JupyterUtils::getMetadata(notebookObject);
+ if (m_jupyterMetadata)
+ delete m_jupyterMetadata;
+ m_jupyterMetadata = new QJsonObject(metadata);
- root.insert(QLatin1String("metadata"), metadata);
+ const QJsonObject& kernalspec = metadata.value(QLatin1String("kernelspec")).toObject();
+ m_backendName = Cantor::JupyterUtils::getKernelName(kernalspec);
- // Not sure, but it looks like we support nbformat version 4.5
- root.insert(QLatin1String("nbformat"), 4);
- root.insert(QLatin1String("nbformat_minor"), 5);
+ //There is "Python" only now, replace "python3" by "Python"
+ if (m_backendName == QLatin1String("python3"))
+ m_backendName = QLatin1String("Python");
- QJsonArray cells;
- for( auto* entry = firstEntry(); entry; entry = entry->next())
+ //"python 2" in older projects not supported anymore, switch to Python (=Python3)
+ if (m_backendName == QLatin1String("python2"))
{
- const QJsonValue entryJson = entry->toJupyterJson();
-
- if (!entryJson.isNull())
- cells.append(entryJson);
+ QApplication::restoreOverrideCursor();
+ KMessageBox::information(worksheetView(),
+ i18n("This notebook was created using Python2 which is not supported anymore. Python3 will be used."),
+ i18n("Python2 not supported anymore"));
+ QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
+ m_backendName = QLatin1String("Python");
}
- root.insert(QLatin1String("cells"), cells);
- doc.setObject(root);
- return doc;
-}
+ if (kernalspec.isEmpty() || m_backendName.isEmpty())
+ {
+ QApplication::restoreOverrideCursor();
+ showInvalidNotebookSchemeError();
+ return false;
+ }
-void Worksheet::save( const QString& filename )
-{
- QFile file(filename);
- if ( !file.open(QIODevice::WriteOnly) )
+ Cantor::Backend* backend = Cantor::Backend::getBackend(m_backendName);
+ if (!backend)
{
- KMessageBox::error( worksheetView(),
- i18n( "Cannot write file %1." , filename ),
- i18n( "Error - Cantor" ));
- return;
+ QApplication::restoreOverrideCursor();
+ KMessageBox::information(worksheetView(),
+ i18n("%1 backend was not found. Editing and executing entries is not possible.", m_backendName),
+ i18n("Open File"));
+ m_readOnly = true;
}
+ else
+ m_readOnly = false;
- save(&file);
-}
+ if(!m_readOnly && !backend->isEnabled())
+ {
+ QApplication::restoreOverrideCursor();
+ KMessageBox::information(worksheetView(), i18n("There are some problems with the %1 backend,\n"\
+ "please check your configuration or install the needed packages.\n"
+ "You will only be able to view this worksheet.", m_backendName), i18n("Open File"));
+ m_readOnly = true;
+ }
-QByteArray Worksheet::saveToByteArray()
-{
- QBuffer buffer;
- save(&buffer);
+ if (m_readOnly)
+ {
+ for (QAction* action : m_richTextActionList)
+ action->setEnabled(false);
+ }
- return buffer.buffer();
-}
-void Worksheet::save( QIODevice* device)
-{
- qDebug()<<"saving to filename";
- switch (m_type)
- {
- case CantorWorksheet:
- {
- KZip zipFile( device );
+ m_isLoadingFromFile = true;
- if ( !zipFile.open(QIODevice::WriteOnly) )
- {
- KMessageBox::error( worksheetView(),
- i18n( "Cannot write file." ),
- i18n( "Error - Cantor" ));
- return;
- }
+ if (m_session)
+ delete m_session;
+ m_session = nullptr;
- QByteArray content = toXML(&zipFile).toByteArray();
- zipFile.writeFile( QLatin1String("content.xml"), content.data());
- break;
- }
+ if (m_firstEntry) {
+ delete m_firstEntry;
+ m_firstEntry = nullptr;
+ }
- case JupyterNotebook:
+ resetEntryCursor();
+ m_itemWidths.clear();
+ m_maxWidth = 0;
+
+ if (!m_readOnly)
+ initSession(backend);
+
+ qDebug() << "loading jupyter entries";
+
+ WorksheetEntry* entry = nullptr;
+ for (QJsonArray::const_iterator iter = cells.begin(); iter != cells.end(); ++iter) {
+ if (!Cantor::JupyterUtils::isJupyterCell(*iter))
{
- if (!device->isWritable())
- {
- KMessageBox::error( worksheetView(),
- i18n( "Cannot write file." ),
- i18n( "Error - Cantor" ));
- return;
- }
+ QApplication::restoreOverrideCursor();
+ QString explanation;
+ if (iter->isObject())
+ explanation = i18n("an object with keys: %1", iter->toObject().keys().join(QLatin1String(", ")));
+ else
+ explanation = i18n("non object JSON value");
- const QJsonDocument& doc = toJupyterJson();
- device->write(doc.toJson(QJsonDocument::Indented));
- break;
+ m_isLoadingFromFile = false;
+ showInvalidNotebookSchemeError(i18n("found incorrect data (%1) that is not Jupyter cell", explanation));
+ return false;
}
- }
-}
-
-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("");
+ const QJsonObject& cell = iter->toObject();
+ QString cellType = Cantor::JupyterUtils::getCellType(cell);
- if (!m_readOnly)
- {
- Cantor::Backend * const backend=session()->backend();
- if (backend->extensions().contains(QLatin1String("ScriptExtension")))
+ if (cellType == QLatin1String("code"))
{
- Cantor::ScriptExtension* e=dynamic_cast<Cantor::ScriptExtension*>(backend->extension(QLatin1String(("ScriptExtension"))));
- if (e)
+ if (LatexEntry::isConvertableToLatexEntry(cell))
{
- cmdSep=e->commandSeparator();
- commentStartingSeq = e->commentStartingSequence();
- commentEndingSeq = e->commentEndingSequence();
+ entry = appendEntry(LatexEntry::Type, false);
+ entry->setContentFromJupyter(cell);
+ entry->evaluate(WorksheetEntry::InternalEvaluation);
+ }
+ else
+ {
+ entry = appendEntry(CommandEntry::Type, false);
+ entry->setContentFromJupyter(cell);
+ }
+ }
+ else if (cellType == QLatin1String("markdown"))
+ {
+ if (TextEntry::isConvertableToTextEntry(cell))
+ {
+ entry = appendEntry(TextEntry::Type, false);
+ entry->setContentFromJupyter(cell);
+ }
+ else if (HorizontalRuleEntry::isConvertableToHorizontalRuleEntry(cell))
+ {
+ entry = appendEntry(HorizontalRuleEntry::Type, false);
+ entry->setContentFromJupyter(cell);
}
+ else if (HierarchyEntry::isConvertableToHierarchyEntry(cell))
+ {
+ entry = appendEntry(HierarchyEntry::Type, false);
+ entry->setContentFromJupyter(cell);
+ }
+ else
+ {
+ entry = appendEntry(MarkdownEntry::Type, false);
+ entry->setContentFromJupyter(cell);
+ entry->evaluate(WorksheetEntry::InternalEvaluation);
+ }
+ }
+ else if (cellType == QLatin1String("raw"))
+ {
+ if (PageBreakEntry::isConvertableToPageBreakEntry(cell))
+ entry = appendEntry(PageBreakEntry::Type, false);
+ else
+ entry = appendEntry(TextEntry::Type, false);
+ entry->setContentFromJupyter(cell);
+ }
+
+ if (m_readOnly && entry)
+ {
+ entry->setAcceptHoverEvents(false);
+ entry = nullptr;
}
}
- 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);
+ if (m_readOnly)
+ clearFocus();
- for(auto* entry = firstEntry(); entry; entry = entry->next())
- {
- const QString& str=entry->toPlain(cmdSep, commentStartingSeq, commentEndingSeq);
- if(!str.isEmpty())
- stream << str + QLatin1Char('\n');
- }
+ m_isLoadingFromFile = false;
+ updateHierarchyLayout();
+ updateLayout();
- file.close();
+
+ Q_EMIT loaded();
+ return true;
}
-void Worksheet::saveLatex(const QString& filename)
+void Worksheet::showInvalidNotebookSchemeError(QString additionalInfo)
{
- qDebug()<<"exporting to Latex: " <<filename;
+ if (additionalInfo.isEmpty())
+ KMessageBox::error(worksheetView(), i18n("The file is not valid Jupyter notebook"), i18n("Open File"));
+ else
+ KMessageBox::error(worksheetView(), i18n("Invalid Jupyter notebook scheme: %1", additionalInfo), i18n("Open File"));
+}
- QFile file(filename);
- if(!file.open(QIODevice::WriteOnly))
- {
- KMessageBox::error(worksheetView(), i18n("Error saving file %1", filename), i18n("Export to LaTeX"));
- return;
- }
+void Worksheet::gotResult(Cantor::Expression* expr)
+{
+ if(expr==nullptr)
+ expr=qobject_cast<Cantor::Expression*>(sender());
- 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"));
+ if(expr==nullptr)
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);
+ //We're only interested in help results, others are handled by the WorksheetEntry
+ for (auto* result : expr->results())
+ {
+ if(result && result->type()==Cantor::HelpResult::Type)
+ {
+ QString help = result->toHtml();
+ //Do some basic LaTeX replacing
+ //TODO: what for? relevant for sage only?
+ help.replace(QRegularExpression(QStringLiteral("\\\\code\\{([^\\}]*)\\}")), QStringLiteral("<b>\\1</b>"));
+ help.replace(QRegularExpression(QStringLiteral("\\$([^\\$])\\$")), QStringLiteral("<i>\\1</i>"));
- 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);
+ Q_EMIT showHelp(help);
- // Transform HTML escaped special characters to valid LaTeX characters (&, <, >)
- QTextStream stream(&file);
- stream << outString.replace(QLatin1String("&"), QLatin1String("&"))
- .replace(QLatin1String(">"), QLatin1String(">"))
- .replace(QLatin1String("<"), QLatin1String("<"));
- file.close();
+ //TODO: break after the first help result found, not clear yet how to handle multiple requests for help within one single command (e.g. ??ev;??int).
+ break;
}
-
- xmlFree(xmlResultBuffer);
}
- xsltFreeStylesheet(xsltStyleSheet);
- xmlFreeDoc(res);
- xmlFreeDoc(output);
-
- xsltCleanupGlobals();
- xmlCleanupParser();
}
-bool Worksheet::load(const QString& filename )
+void Worksheet::removeCurrentEntry()
{
- 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;
- }
+ auto* entry = currentEntry();
+ if(!entry)
+ return;
- bool rc = load(&file);
- if (rc && !m_readOnly)
- m_session->setWorksheetPath(filename);
+ // In case we just removed this
+ if (entry->isAncestorOf(m_lastFocusedTextItem))
+ m_lastFocusedTextItem = nullptr;
- return rc;
+ entry->startRemoving();
}
-void Worksheet::load(QByteArray* data)
+Cantor::Renderer* Worksheet::renderer()
{
- QBuffer buf(data);
- buf.open(QIODevice::ReadOnly);
- load(&buf);
+ return &m_renderer;
}
-bool Worksheet::load(QIODevice* device)
+MathRenderer* Worksheet::mathRenderer()
{
- 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);
+ return &m_mathRenderer;
+}
- 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);
+QMenu* Worksheet::createContextMenu()
+{
+ auto* menu = new QMenu(worksheetView());
+ connect(menu, SIGNAL(aboutToHide()), menu, SLOT(deleteLater()));
- 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);
- }
+ return menu;
}
-bool Worksheet::loadCantorWorksheet(const KZip& archive)
+void Worksheet::populateMenu(QMenu* menu, QPointF pos)
{
- m_type = Type::CantorWorksheet;
-
- const KArchiveEntry* contentEntry=archive.directory()->entry(QLatin1String("content.xml"));
- if (!contentEntry->isFile())
+ // Two different context menus - 1. for the current entry, 2. for multiple selected entries
+ if (m_selectedEntries.isEmpty())
{
- 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();
+ auto* entry = entryAt(pos);
+ if (entry && !entry->isAncestorOf(m_lastFocusedTextItem)) {
+ auto* item =
+ qgraphicsitem_cast<WorksheetTextEditorItem*>(itemAt(pos, QTransform()));
+ if (item && item->isEditable())
+ m_lastFocusedTextItem = item;
+ }
- QDomDocument doc;
- doc.setContent(data);
- QDomElement root = doc.documentElement();
+ if (entry) {
+ //"Convert To" menu
+ QMenu* convertTo = new QMenu(i18n("Convert To"));
+ convertTo->setIcon(QIcon::fromTheme(QLatin1String("gtk-convert")));
+ menu->addMenu(convertTo);
- m_backendName = root.attribute(QLatin1String("backend"));
+ if (entry->type() != CommandEntry::Type)
+ convertTo->addAction(QIcon::fromTheme(QLatin1String("run-build")), i18n("Command"), entry, &WorksheetEntry::convertToCommandEntry);
- //There is "Python" only now, replace "Python 3" by "Python"
- if (m_backendName == QLatin1String("Python 3"))
- m_backendName = QLatin1String("Python");
+ if (entry->type() != TextEntry::Type)
+ convertTo->addAction(QIcon::fromTheme(QLatin1String("draw-text")), i18n("Text"), entry, &WorksheetEntry::convertToTextEntry);
- //"Python 2" in older projects not supported anymore, switch to Python (=Python3)
- if (m_backendName == QLatin1String("Python 2"))
- {
- QApplication::restoreOverrideCursor();
- KMessageBox::information(worksheetView(),
- i18n("This worksheet was created using Python2 which is not supported anymore. Python3 will be used."),
- i18n("Python2 not supported anymore"));
- QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
- m_backendName = QLatin1String("Python");
- }
+ #ifdef Discount_FOUND
+ if (entry->type() != MarkdownEntry::Type)
+ convertTo->addAction(QIcon::fromTheme(QLatin1String("text-x-markdown")), i18n("Markdown"), entry, &WorksheetEntry::convertToMarkdownEntry);
+ #endif
+ if (entry->type() != LatexEntry::Type)
+ convertTo->addAction(QIcon::fromTheme(QLatin1String("text-x-tex")), i18n("LaTeX"), entry, &WorksheetEntry::convertToLatexEntry);
+ if (entry->type() != ImageEntry::Type)
+ convertTo->addAction(QIcon::fromTheme(QLatin1String("image-x-generic")), i18n("Image"), entry, &WorksheetEntry::convertToImageEntry);
- auto* b = Cantor::Backend::getBackend(m_backendName);
- if (!b)
- {
- QApplication::restoreOverrideCursor();
- KMessageBox::information(worksheetView(), i18n("%1 backend was not found. Editing and executing entries is not possible.", m_backendName), i18n("Open File"));
- m_readOnly = true;
- }
- else
- m_readOnly = false;
+ if (entry->type() != PageBreakEntry::Type)
+ convertTo->addAction(QIcon::fromTheme(QLatin1String("insert-page-break")), i18n("Page Break"), entry, &WorksheetEntry::converToPageBreakEntry);
- if(!m_readOnly && !b->isEnabled())
- {
- QApplication::restoreOverrideCursor();
- KMessageBox::information(worksheetView(), i18n("There are some problems with the %1 backend,\n"\
- "please check your configuration or install the needed packages.\n"
- "You will only be able to view this worksheet.", m_backendName), i18n("Open File"));
- m_readOnly = true;
- }
+ if (entry->type() != HorizontalRuleEntry::Type)
+ convertTo->addAction(QIcon::fromTheme(QLatin1String("newline")), i18n("Horizontal Line"), entry, &WorksheetEntry::convertToHorizontalRuleEntry);
- m_isLoadingFromFile = true;
+ if (entry->type() != HierarchyEntry::Type)
+ convertTo->addAction(QIcon::fromTheme(QLatin1String("view-list-tree")), i18n("Hierarchy Entry"), entry, &WorksheetEntry::convertToHierarchyEntry);
- //cleanup the worksheet and all it contains
- delete m_session;
- m_session = nullptr;
+ //"Insert After" menu
+ QMenu* insert = new QMenu(i18n("Insert After"), menu);
+ insert->setIcon(QIcon::fromTheme(QLatin1String("edit-table-insert-row-below")));
+ menu->addSeparator();
+ menu->addMenu(insert);
- //file can only be loaded in a worksheet that was not edited/modified yet (s.a. CantorShell::load())
- //in this case on the default "first entry" is available -> delete it.
- if (m_firstEntry) {
- delete m_firstEntry;
- m_firstEntry = nullptr;
- }
+ insert->addAction(QIcon::fromTheme(QLatin1String("run-build")), i18n("Command"), entry, SLOT(insertCommandEntry()));
+ insert->addAction(QIcon::fromTheme(QLatin1String("draw-text")), i18n("Text"), entry, SLOT(insertTextEntry()));
+ #ifdef Discount_FOUND
+ insert->addAction(QIcon::fromTheme(QLatin1String("text-x-markdown")), i18n("Markdown"), entry, SLOT(insertMarkdownEntry()));
+ #endif
+ insert->addAction(QIcon::fromTheme(QLatin1String("text-x-tex")), i18n("LaTeX"), entry, SLOT(insertLatexEntry()));
+ insert->addAction(QIcon::fromTheme(QLatin1String("image-x-generic")), i18n("Image"), entry, SLOT(insertImageEntry()));
+ insert->addSeparator();
+ insert->addAction(QIcon::fromTheme(QLatin1String("newline")), i18n("Horizontal Line"), entry, SLOT(insertHorizontalRuleEntry()));
+ insert->addAction(QIcon::fromTheme(QLatin1String("insert-page-break")), i18n("Page Break"), entry, SLOT(insertPageBreakEntry()));
+ insert->addSeparator();
+ insert->addAction(QIcon::fromTheme(QLatin1String("view-list-tree")), i18n("Hierarchy Entry"), entry, SLOT(insertHierarchyEntry()));
- resetEntryCursor();
- m_itemWidths.clear();
- m_maxWidth = 0;
+ //"Insert Before" menu
+ QMenu* insertBefore = new QMenu(i18n("Insert Before"), menu);
+ insertBefore->setIcon(QIcon::fromTheme(QLatin1String("edit-table-insert-row-above")));
+ menu->addMenu(insertBefore);
- if (!m_readOnly)
- initSession(b);
+ insertBefore->addAction(QIcon::fromTheme(QLatin1String("run-build")), i18n("Command"), entry, SLOT(insertCommandEntryBefore()));
+ insertBefore->addAction(QIcon::fromTheme(QLatin1String("draw-text")), i18n("Text"), entry, SLOT(insertTextEntryBefore()));
+ #ifdef Discount_FOUND
+ insertBefore->addAction(QIcon::fromTheme(QLatin1String("text-x-markdown")), i18n("Markdown"), entry, SLOT(insertMarkdownEntryBefore()));
+ #endif
+ insertBefore->addAction(QIcon::fromTheme(QLatin1String("text-x-tex")), i18n("LaTeX"), entry, SLOT(insertLatexEntryBefore()));
+ insertBefore->addAction(QIcon::fromTheme(QLatin1String("image-x-generic")), i18n("Image"), entry, SLOT(insertImageEntryBefore()));
+ insertBefore->addSeparator();
+ insertBefore->addAction(QIcon::fromTheme(QLatin1String("newline")), i18n("Horizontal Line"), entry, SLOT(insertHorizontalRuleEntryBefore()));
+ insertBefore->addAction(QIcon::fromTheme(QLatin1String("insert-page-break")), i18n("Page Break"), entry, SLOT(insertPageBreakEntryBefore()));
+ insertBefore->addSeparator();
+ insertBefore->addAction(QIcon::fromTheme(QLatin1String("view-list-tree")), i18n("Hierarchy Entry"), entry, SLOT(insertHierarchyEntryBefore()));
+ } else {
+ QMenu* insertMenu = new QMenu(i18n("Insert"));
+ insertMenu->setIcon(QIcon::fromTheme(QLatin1String("insert-table-row")));
- qDebug()<<"loading entries";
- QDomElement expressionChild = root.firstChildElement();
- while (!expressionChild.isNull()) {
- QString tag = expressionChild.tagName();
- // Don't add focus on load
- auto* entry = appendEntry(typeForTagName(tag), false);
- if (entry)
- {
- entry->setContent(expressionChild, archive);
- if (m_readOnly)
- entry->setAcceptHoverEvents(false);
- }
+ insertMenu->addAction(QIcon::fromTheme(QLatin1String("run-build")), i18n("Command"), this, SLOT(appendCommandEntry()));
+ insertMenu->addAction(QIcon::fromTheme(QLatin1String("draw-text")), i18n("Text"), this, &Worksheet::appendTextEntry);
+ #ifdef Discount_FOUND
+ insertMenu->addAction(QIcon::fromTheme(QLatin1String("text-x-markdown")), i18n("Markdown"), this, &Worksheet::appendMarkdownEntry);
+ #endif
- expressionChild = expressionChild.nextSiblingElement();
- }
+ insertMenu->addAction(QIcon::fromTheme(QLatin1String("text-x-tex")), i18n("LaTeX"), this, &Worksheet::appendLatexEntry);
+ insertMenu->addAction(QIcon::fromTheme(QLatin1String("image-x-generic")), i18n("Image"), this, &Worksheet::appendImageEntry);
+ insertMenu->addSeparator();
+ insertMenu->addAction(QIcon::fromTheme(QLatin1String("newline")), i18n("Horizontal Line"), this, &Worksheet::appendHorizontalRuleEntry);
+ insertMenu->addAction(QIcon::fromTheme(QLatin1String("insert-page-break")), i18n("Page Break"), this, &Worksheet::appendPageBreakEntry);
+ insertMenu->addSeparator();
+ insertMenu->addAction(QIcon::fromTheme(QLatin1String("view-list-tree")), i18n("Hierarchy Entry"), this, &Worksheet::appendHierarchyEntry);
- if (m_readOnly)
- clearFocus();
+ menu->addMenu(insertMenu);
- m_isLoadingFromFile = false;
- updateHierarchyLayout();
- updateLayout();
+ //"Show help" for backend's documentation
+#ifdef HAVE_EMBEDDED_DOCUMENTATION
+ menu->addSeparator();
+ menu->addAction(QIcon::fromTheme(QLatin1String("help-hint")), i18n("Show Help"), this,
+ [=] () { requestDocumentation(QString()); });
+#endif
+ }
- //Set the Highlighting, depending on the current state
- //If the session isn't logged in, use the default
+ //evaluate the whole worksheet or interrupt the current calculation
+ menu->addSeparator();
+ if (!isRunning())
+ menu->addAction(QIcon::fromTheme(QLatin1String("system-run")), i18n("Evaluate Worksheet"),
+ this, &Worksheet::evaluate);
+ else
+ menu->addAction(QIcon::fromTheme(QLatin1String("process-stop")), i18n("Interrupt"), this,
+ &Worksheet::interrupt);
- Q_EMIT loaded();
- return true;
-}
+ //zooming
+ menu->addSeparator();
+ auto* zoomMenu = new QMenu(i18n("Zoom"));
+ zoomMenu->setIcon(QIcon::fromTheme(QLatin1String("zoom-draw")));
+ auto* view = worksheetView();
-int Worksheet::typeForTagName(const QString& tag)
-{
- if (tag == QLatin1String("Expression"))
- return CommandEntry::Type;
- else if (tag == QLatin1String("Text"))
- return TextEntry::Type;
- else if (tag == QLatin1String("Markdown"))
- return MarkdownEntry::Type;
- else if (tag == QLatin1String("Latex"))
- return LatexEntry::Type;
- else if (tag == QLatin1String("PageBreak"))
- return PageBreakEntry::Type;
- else if (tag == QLatin1String("Image"))
- return ImageEntry::Type;
- else if (tag == QLatin1String("HorizontalRule"))
- return HorizontalRuleEntry::Type;
- else if (tag == QLatin1String("Hierarchy"))
- return HierarchyEntry::Type;
+ auto* action = zoomMenu->addAction(QIcon::fromTheme(QLatin1String("zoom-in")), i18n("Zoom In"), view, &WorksheetView::zoomIn);
+ action->setShortcut(Qt::CTRL | Qt::Key_Plus);
- return 0;
-}
+ action = zoomMenu->addAction(QIcon::fromTheme(QLatin1String("zoom-out")), i18n("Zoom Out"), view, &WorksheetView::zoomOut);
+ action->setShortcut(Qt::CTRL | Qt::Key_Minus);
+ zoomMenu->addSeparator();
+ action = zoomMenu->addAction(QIcon::fromTheme(QLatin1String("zoom-original")), i18n("Original Size"), view, &WorksheetView::actualSize);
+ action->setShortcut(Qt::CTRL | Qt::Key_1);
-void Worksheet::initSession(Cantor::Backend* backend)
-{
- m_session = backend->createSession();
- if (m_useDefaultWorksheetParameters)
- {
- if (Cantor::LatexRenderer::isLatexAvailable())
- m_session->setTypesettingEnabled(Settings::self()->typesetDefault());
- enableCompletion(Settings::self()->completionDefault());
- enableExpressionNumbering(Settings::self()->expressionNumberingDefault());
- enableAnimations(Settings::self()->animationDefault());
- enableEmbeddedMath(Settings::self()->embeddedMathDefault());
+ menu->addMenu(zoomMenu);
}
-}
+ else
+ {
+ menu->clear();
+ menu->addAction(QIcon::fromTheme(QLatin1String("go-up")), i18n("Move Entries Up"), this, &Worksheet::selectionMoveUp);
+ menu->addAction(QIcon::fromTheme(QLatin1String("go-down")), i18n("Move Entries Down"), this, &Worksheet::selectionMoveDown);
+ menu->addAction(QIcon::fromTheme(QLatin1String("media-playback-start")), i18n("Evaluate Entries"), this, &Worksheet::selectionEvaluate);
+ menu->addSeparator();
+ menu->addAction(QIcon::fromTheme(QLatin1String("edit-delete")), i18n("Delete Entries"), this, &Worksheet::selectionRemove);
-bool Worksheet::loadJupyterNotebook(const QJsonDocument& doc)
-{
- m_type = Type::JupyterNotebook;
+ bool isAnyCommandEntryInSelection = false;
+ for (auto* entry : m_selectedEntries)
+ if (entry->type() == CommandEntry::Type)
+ {
+ isAnyCommandEntryInSelection = true;
+ break;
+ }
- int nbformatMajor, nbformatMinor;
- if (!Cantor::JupyterUtils::isJupyterNotebook(doc))
- {
- // Two possibilities: old jupyter notebook (version <= 4.0.0 and a another scheme) or just not a notebook at all
- std::tie(nbformatMajor, nbformatMinor) = Cantor::JupyterUtils::getNbformatVersion(doc.object());
- if (nbformatMajor == 0 && nbformatMinor == 0)
- {
- QApplication::restoreOverrideCursor();
- showInvalidNotebookSchemeError();
- }
- else
+ if (isAnyCommandEntryInSelection)
{
- KMessageBox::error(worksheetView(),
- i18n("Jupyter notebooks with versions lower than 4.5 (detected version %1.%2) are not supported.", nbformatMajor, nbformatMinor ),
- i18n("Open File"));
+ menu->addSeparator();
+ menu->addAction(QIcon(), i18n("Collapse Command Entry Results"), this, &Worksheet::collapseSelectionResults);
+ menu->addAction(QIcon(), i18n("Expand Command Entry Results"), this, &Worksheet::uncollapseSelectionResults);
+ menu->addSeparator();
+ menu->addAction(QIcon(), i18n("Delete Command Entry Results"), this, &Worksheet::removeSelectionResults);
+ menu->addSeparator();
+ menu->addAction(QIcon(), i18n("Exclude Command Entry From Execution"), this, &Worksheet::excludeFromExecutionSelection);
+ menu->addAction(QIcon(), i18n("Add Command Entry To Execution"), this, &Worksheet::addToExectuionSelection);
}
-
- return false;
}
+}
- QJsonObject notebookObject = doc.object();
- std::tie(nbformatMajor, nbformatMinor) = Cantor::JupyterUtils::getNbformatVersion(notebookObject);
+void Worksheet::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
+{
+ if (m_readOnly)
+ return;
- if (QT_VERSION_CHECK(nbformatMajor, nbformatMinor, 0) > QT_VERSION_CHECK(4,5,0))
- {
- QApplication::restoreOverrideCursor();
- KMessageBox::error(
- worksheetView(),
- i18n("Jupyter notebooks with versions higher than 4.5 (detected version %1.%2) are not supported.", nbformatMajor, nbformatMinor),
- i18n("Open File")
- );
- return false;
- }
+ // forward the event to the items
+ QGraphicsScene::contextMenuEvent(event);
- const QJsonArray& cells = Cantor::JupyterUtils::getCells(notebookObject);
- const QJsonObject& metadata = Cantor::JupyterUtils::getMetadata(notebookObject);
- if (m_jupyterMetadata)
- delete m_jupyterMetadata;
- m_jupyterMetadata = new QJsonObject(metadata);
+ if (!event->isAccepted()) {
+ event->accept();
+ QMenu* menu = createContextMenu();
+ populateMenu(menu, event->scenePos());
- const QJsonObject& kernalspec = metadata.value(QLatin1String("kernelspec")).toObject();
- m_backendName = Cantor::JupyterUtils::getKernelName(kernalspec);
+ menu->popup(event->screenPos());
+ }
+}
- //There is "Python" only now, replace "python3" by "Python"
- if (m_backendName == QLatin1String("python3"))
- m_backendName = QLatin1String("Python");
+void Worksheet::mousePressEvent(QGraphicsSceneMouseEvent* event)
+{
+ /*
+ if (event->button() == Qt::LeftButton && !focusItem() && lastEntry() &&
+ event->scenePos().y() > lastEntry()->y() + lastEntry()->size().height())
+ lastEntry()->focusEntry(WorksheetTextItem::BottomRight);
+ */
+ QGraphicsScene::mousePressEvent(event);
- //"python 2" in older projects not supported anymore, switch to Python (=Python3)
- if (m_backendName == QLatin1String("python2"))
+ if (!m_readOnly && event->buttons() & Qt::LeftButton)
{
- QApplication::restoreOverrideCursor();
- KMessageBox::information(worksheetView(),
- i18n("This notebook was created using Python2 which is not supported anymore. Python3 will be used."),
- i18n("Python2 not supported anymore"));
- QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
- m_backendName = QLatin1String("Python");
- }
+ auto* selectedEntry = entryAt(event->scenePos());
+ if (event->modifiers() & Qt::ControlModifier)
+ {
+ clearFocus();
+ resetEntryCursor();
- if (kernalspec.isEmpty() || m_backendName.isEmpty())
- {
- QApplication::restoreOverrideCursor();
- showInvalidNotebookSchemeError();
- return false;
- }
+ if (selectedEntry)
+ {
+ selectedEntry->setCellSelected(!selectedEntry->isCellSelected());
+ selectedEntry->update();
- Cantor::Backend* backend = Cantor::Backend::getBackend(m_backendName);
- if (!backend)
- {
- QApplication::restoreOverrideCursor();
- KMessageBox::information(worksheetView(),
- i18n("%1 backend was not found. Editing and executing entries is not possible.", m_backendName),
- i18n("Open File"));
- m_readOnly = true;
- }
- else
- m_readOnly = false;
+ auto* lastSelectedEntry = m_circularFocusBuffer.size() > 0 ? m_circularFocusBuffer.last() : nullptr;
+ if (lastSelectedEntry)
+ {
+ lastSelectedEntry->setCellSelected(!lastSelectedEntry->isCellSelected());
+ lastSelectedEntry->update();
+ m_circularFocusBuffer.clear();
+ }
- if(!m_readOnly && !backend->isEnabled())
- {
- QApplication::restoreOverrideCursor();
- KMessageBox::information(worksheetView(), i18n("There are some problems with the %1 backend,\n"\
- "please check your configuration or install the needed packages.\n"
- "You will only be able to view this worksheet.", m_backendName), i18n("Open File"));
- m_readOnly = true;
+ for (auto* entry : {selectedEntry, lastSelectedEntry})
+ if (entry)
+ {
+ if (entry->isCellSelected())
+ m_selectedEntries.append(entry);
+ else if (!entry->isCellSelected())
+ m_selectedEntries.removeOne(entry);
+ }
+ }
+ }
+ else
+ {
+ for (auto* entry : m_selectedEntries)
+ {
+ if(isValidEntry(entry))
+ {
+ entry->setCellSelected(false);
+ entry->update();
+ }
+ }
+ m_selectedEntries.clear();
+
+ if (selectedEntry)
+ notifyEntryFocus(selectedEntry);
+
+ updateEntryCursor(event);
+ }
}
+}
+void Worksheet::keyPressEvent(QKeyEvent* event)
+{
if (m_readOnly)
- {
- for (QAction* action : m_richTextActionList)
- action->setEnabled(false);
- }
+ return;
+ if ((event->modifiers() & Qt::ControlModifier) && (event->key() == Qt::Key_1))
+ worksheetView()->actualSize();
+ else if ((m_choosenCursorEntry || m_isCursorEntryAfterLastEntry) && !event->text().isEmpty())
+ addEntryFromEntryCursor(); //add new entry when entry cursor is actived when user starts typing text
- m_isLoadingFromFile = true;
+ QGraphicsScene::keyPressEvent(event);
+}
- if (m_session)
- delete m_session;
- m_session = nullptr;
+void Worksheet::setActionCollection(KActionCollection* collection)
+{
+ m_collection = collection;
+}
- if (m_firstEntry) {
- delete m_firstEntry;
- m_firstEntry = nullptr;
- }
+void Worksheet::initActions()
+{
+ // Mostly copied from KRichTextWidget::createActions(KActionCollection*)
+ // It would be great if this wasn't necessary.
- resetEntryCursor();
- m_itemWidths.clear();
- m_maxWidth = 0;
+ // Text color
+ /* This is "format-stroke-color" in KRichTextWidget */
+ auto* action = new QAction(QIcon::fromTheme(QLatin1String("format-text-color")),
+ i18nc("@action", "Text &Color..."), m_collection);
+ action->setIconText(i18nc("@label text color", "Color"));
+ action->setPriority(QAction::LowPriority);
+ m_richTextActionList.append(action);
+ connect(action, &QAction::triggered, this, &Worksheet::setTextForegroundColor);
- if (!m_readOnly)
- initSession(backend);
+ // Text color
+ action = new QAction(QIcon::fromTheme(QLatin1String("format-fill-color")),
+ i18nc("@action", "Text &Highlight..."), m_collection);
+ action->setPriority(QAction::LowPriority);
+ m_richTextActionList.append(action);
+ connect(action, &QAction::triggered, this, &Worksheet::setTextBackgroundColor);
- qDebug() << "loading jupyter entries";
+ // Font Family
+ m_fontAction = new KFontAction(i18nc("@action", "&Font"), m_collection);
+ m_richTextActionList.append(m_fontAction);
+ connect(m_fontAction, &KFontAction::textTriggered, this, &Worksheet::setFontFamily);
- WorksheetEntry* entry = nullptr;
- for (QJsonArray::const_iterator iter = cells.begin(); iter != cells.end(); ++iter) {
- if (!Cantor::JupyterUtils::isJupyterCell(*iter))
- {
- QApplication::restoreOverrideCursor();
- QString explanation;
- if (iter->isObject())
- explanation = i18n("an object with keys: %1", iter->toObject().keys().join(QLatin1String(", ")));
- else
- explanation = i18n("non object JSON value");
+ // Font Size
+ m_fontSizeAction = new KFontSizeAction(i18nc("@action", "Font &Size"), m_collection);
+ m_richTextActionList.append(m_fontSizeAction);
+ connect(m_fontSizeAction, &KFontSizeAction::fontSizeChanged, this, &Worksheet::setFontSize);
- m_isLoadingFromFile = false;
- showInvalidNotebookSchemeError(i18n("found incorrect data (%1) that is not Jupyter cell", explanation));
- return false;
- }
+ // Bold
+ m_boldAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-text-bold")),
+ i18nc("@action boldify selected text", "&Bold"),
+ m_collection);
+ m_boldAction->setPriority(QAction::LowPriority);
+ QFont bold;
+ bold.setBold(true);
+ m_boldAction->setFont(bold);
+ m_richTextActionList.append(m_boldAction);
+ connect(m_boldAction, &QAction::triggered, this, &Worksheet::setTextBold);
- const QJsonObject& cell = iter->toObject();
- QString cellType = Cantor::JupyterUtils::getCellType(cell);
-
- if (cellType == QLatin1String("code"))
- {
- if (LatexEntry::isConvertableToLatexEntry(cell))
- {
- entry = appendEntry(LatexEntry::Type, false);
- entry->setContentFromJupyter(cell);
- entry->evaluate(WorksheetEntry::InternalEvaluation);
- }
- else
- {
- entry = appendEntry(CommandEntry::Type, false);
- entry->setContentFromJupyter(cell);
- }
- }
- else if (cellType == QLatin1String("markdown"))
- {
- if (TextEntry::isConvertableToTextEntry(cell))
- {
- entry = appendEntry(TextEntry::Type, false);
- entry->setContentFromJupyter(cell);
- }
- else if (HorizontalRuleEntry::isConvertableToHorizontalRuleEntry(cell))
- {
- entry = appendEntry(HorizontalRuleEntry::Type, false);
- entry->setContentFromJupyter(cell);
- }
- else if (HierarchyEntry::isConvertableToHierarchyEntry(cell))
- {
- entry = appendEntry(HierarchyEntry::Type, false);
- entry->setContentFromJupyter(cell);
- }
- else
- {
- entry = appendEntry(MarkdownEntry::Type, false);
- entry->setContentFromJupyter(cell);
- entry->evaluate(WorksheetEntry::InternalEvaluation);
- }
- }
- else if (cellType == QLatin1String("raw"))
- {
- if (PageBreakEntry::isConvertableToPageBreakEntry(cell))
- entry = appendEntry(PageBreakEntry::Type, false);
- else
- entry = appendEntry(TextEntry::Type, false);
- entry->setContentFromJupyter(cell);
- }
-
- if (m_readOnly && entry)
- {
- entry->setAcceptHoverEvents(false);
- entry = nullptr;
- }
- }
-
- if (m_readOnly)
- clearFocus();
-
- m_isLoadingFromFile = false;
- updateHierarchyLayout();
- updateLayout();
-
-
- Q_EMIT loaded();
- return true;
-}
-
-void Worksheet::showInvalidNotebookSchemeError(QString additionalInfo)
-{
- if (additionalInfo.isEmpty())
- KMessageBox::error(worksheetView(), i18n("The file is not valid Jupyter notebook"), i18n("Open File"));
- else
- KMessageBox::error(worksheetView(), i18n("Invalid Jupyter notebook scheme: %1", additionalInfo), i18n("Open File"));
-}
-
-void Worksheet::gotResult(Cantor::Expression* expr)
-{
- if(expr==nullptr)
- expr=qobject_cast<Cantor::Expression*>(sender());
-
- if(expr==nullptr)
- return;
-
- //We're only interested in help results, others are handled by the WorksheetEntry
- for (auto* result : expr->results())
- {
- if(result && result->type()==Cantor::HelpResult::Type)
- {
- QString help = result->toHtml();
- //Do some basic LaTeX replacing
- //TODO: what for? relevant for sage only?
- help.replace(QRegularExpression(QStringLiteral("\\\\code\\{([^\\}]*)\\}")), QStringLiteral("<b>\\1</b>"));
- help.replace(QRegularExpression(QStringLiteral("\\$([^\\$])\\$")), QStringLiteral("<i>\\1</i>"));
-
- Q_EMIT showHelp(help);
-
- //TODO: break after the first help result found, not clear yet how to handle multiple requests for help within one single command (e.g. ??ev;??int).
- break;
- }
- }
-
-}
-
-void Worksheet::removeCurrentEntry()
-{
- auto* entry = currentEntry();
- if(!entry)
- return;
-
- // In case we just removed this
- if (entry->isAncestorOf(m_lastFocusedTextItem))
- m_lastFocusedTextItem = nullptr;
-
- entry->startRemoving();
-}
-
-Cantor::Renderer* Worksheet::renderer()
-{
- return &m_renderer;
-}
-
-MathRenderer* Worksheet::mathRenderer()
-{
- return &m_mathRenderer;
-}
-
-QMenu* Worksheet::createContextMenu()
-{
- auto* menu = new QMenu(worksheetView());
- connect(menu, SIGNAL(aboutToHide()), menu, SLOT(deleteLater()));
-
- return menu;
-}
-
-void Worksheet::populateMenu(QMenu* menu, QPointF pos)
-{
- // Two different context menus - 1. for the current entry, 2. for multiple selected entries
- if (m_selectedEntries.isEmpty())
- {
- auto* entry = entryAt(pos);
- if (entry && !entry->isAncestorOf(m_lastFocusedTextItem)) {
- auto* item =
- qgraphicsitem_cast<WorksheetTextEditorItem*>(itemAt(pos, QTransform()));
- if (item && item->isEditable())
- m_lastFocusedTextItem = item;
- }
-
- if (entry) {
- //"Convert To" menu
- QMenu* convertTo = new QMenu(i18n("Convert To"));
- convertTo->setIcon(QIcon::fromTheme(QLatin1String("gtk-convert")));
- menu->addMenu(convertTo);
-
- if (entry->type() != CommandEntry::Type)
- convertTo->addAction(QIcon::fromTheme(QLatin1String("run-build")), i18n("Command"), entry, &WorksheetEntry::convertToCommandEntry);
-
- if (entry->type() != TextEntry::Type)
- convertTo->addAction(QIcon::fromTheme(QLatin1String("draw-text")), i18n("Text"), entry, &WorksheetEntry::convertToTextEntry);
-
- #ifdef Discount_FOUND
- if (entry->type() != MarkdownEntry::Type)
- convertTo->addAction(QIcon::fromTheme(QLatin1String("text-x-markdown")), i18n("Markdown"), entry, &WorksheetEntry::convertToMarkdownEntry);
- #endif
- if (entry->type() != LatexEntry::Type)
- convertTo->addAction(QIcon::fromTheme(QLatin1String("text-x-tex")), i18n("LaTeX"), entry, &WorksheetEntry::convertToLatexEntry);
- if (entry->type() != ImageEntry::Type)
- convertTo->addAction(QIcon::fromTheme(QLatin1String("image-x-generic")), i18n("Image"), entry, &WorksheetEntry::convertToImageEntry);
-
- if (entry->type() != PageBreakEntry::Type)
- convertTo->addAction(QIcon::fromTheme(QLatin1String("insert-page-break")), i18n("Page Break"), entry, &WorksheetEntry::converToPageBreakEntry);
-
- if (entry->type() != HorizontalRuleEntry::Type)
- convertTo->addAction(QIcon::fromTheme(QLatin1String("newline")), i18n("Horizontal Line"), entry, &WorksheetEntry::convertToHorizontalRuleEntry);
-
- if (entry->type() != HierarchyEntry::Type)
- convertTo->addAction(QIcon::fromTheme(QLatin1String("view-list-tree")), i18n("Hierarchy Entry"), entry, &WorksheetEntry::convertToHierarchyEntry);
-
- //"Insert After" menu
- QMenu* insert = new QMenu(i18n("Insert After"), menu);
- insert->setIcon(QIcon::fromTheme(QLatin1String("edit-table-insert-row-below")));
- menu->addSeparator();
- menu->addMenu(insert);
-
- insert->addAction(QIcon::fromTheme(QLatin1String("run-build")), i18n("Command"), entry, SLOT(insertCommandEntry()));
- insert->addAction(QIcon::fromTheme(QLatin1String("draw-text")), i18n("Text"), entry, SLOT(insertTextEntry()));
- #ifdef Discount_FOUND
- insert->addAction(QIcon::fromTheme(QLatin1String("text-x-markdown")), i18n("Markdown"), entry, SLOT(insertMarkdownEntry()));
- #endif
- insert->addAction(QIcon::fromTheme(QLatin1String("text-x-tex")), i18n("LaTeX"), entry, SLOT(insertLatexEntry()));
- insert->addAction(QIcon::fromTheme(QLatin1String("image-x-generic")), i18n("Image"), entry, SLOT(insertImageEntry()));
- insert->addSeparator();
- insert->addAction(QIcon::fromTheme(QLatin1String("newline")), i18n("Horizontal Line"), entry, SLOT(insertHorizontalRuleEntry()));
- insert->addAction(QIcon::fromTheme(QLatin1String("insert-page-break")), i18n("Page Break"), entry, SLOT(insertPageBreakEntry()));
- insert->addSeparator();
- insert->addAction(QIcon::fromTheme(QLatin1String("view-list-tree")), i18n("Hierarchy Entry"), entry, SLOT(insertHierarchyEntry()));
-
- //"Insert Before" menu
- QMenu* insertBefore = new QMenu(i18n("Insert Before"), menu);
- insertBefore->setIcon(QIcon::fromTheme(QLatin1String("edit-table-insert-row-above")));
- menu->addMenu(insertBefore);
-
- insertBefore->addAction(QIcon::fromTheme(QLatin1String("run-build")), i18n("Command"), entry, SLOT(insertCommandEntryBefore()));
- insertBefore->addAction(QIcon::fromTheme(QLatin1String("draw-text")), i18n("Text"), entry, SLOT(insertTextEntryBefore()));
- #ifdef Discount_FOUND
- insertBefore->addAction(QIcon::fromTheme(QLatin1String("text-x-markdown")), i18n("Markdown"), entry, SLOT(insertMarkdownEntryBefore()));
- #endif
- insertBefore->addAction(QIcon::fromTheme(QLatin1String("text-x-tex")), i18n("LaTeX"), entry, SLOT(insertLatexEntryBefore()));
- insertBefore->addAction(QIcon::fromTheme(QLatin1String("image-x-generic")), i18n("Image"), entry, SLOT(insertImageEntryBefore()));
- insertBefore->addSeparator();
- insertBefore->addAction(QIcon::fromTheme(QLatin1String("newline")), i18n("Horizontal Line"), entry, SLOT(insertHorizontalRuleEntryBefore()));
- insertBefore->addAction(QIcon::fromTheme(QLatin1String("insert-page-break")), i18n("Page Break"), entry, SLOT(insertPageBreakEntryBefore()));
- insertBefore->addSeparator();
- insertBefore->addAction(QIcon::fromTheme(QLatin1String("view-list-tree")), i18n("Hierarchy Entry"), entry, SLOT(insertHierarchyEntryBefore()));
- } else {
- QMenu* insertMenu = new QMenu(i18n("Insert"));
- insertMenu->setIcon(QIcon::fromTheme(QLatin1String("insert-table-row")));
-
- insertMenu->addAction(QIcon::fromTheme(QLatin1String("run-build")), i18n("Command"), this, SLOT(appendCommandEntry()));
- insertMenu->addAction(QIcon::fromTheme(QLatin1String("draw-text")), i18n("Text"), this, &Worksheet::appendTextEntry);
- #ifdef Discount_FOUND
- insertMenu->addAction(QIcon::fromTheme(QLatin1String("text-x-markdown")), i18n("Markdown"), this, &Worksheet::appendMarkdownEntry);
- #endif
-
- insertMenu->addAction(QIcon::fromTheme(QLatin1String("text-x-tex")), i18n("LaTeX"), this, &Worksheet::appendLatexEntry);
- insertMenu->addAction(QIcon::fromTheme(QLatin1String("image-x-generic")), i18n("Image"), this, &Worksheet::appendImageEntry);
- insertMenu->addSeparator();
- insertMenu->addAction(QIcon::fromTheme(QLatin1String("newline")), i18n("Horizontal Line"), this, &Worksheet::appendHorizontalRuleEntry);
- insertMenu->addAction(QIcon::fromTheme(QLatin1String("insert-page-break")), i18n("Page Break"), this, &Worksheet::appendPageBreakEntry);
- insertMenu->addSeparator();
- insertMenu->addAction(QIcon::fromTheme(QLatin1String("view-list-tree")), i18n("Hierarchy Entry"), this, &Worksheet::appendHierarchyEntry);
-
- menu->addMenu(insertMenu);
-
- //"Show help" for backend's documentation
-#ifdef HAVE_EMBEDDED_DOCUMENTATION
- menu->addSeparator();
- menu->addAction(QIcon::fromTheme(QLatin1String("help-hint")), i18n("Show Help"), this,
- [=] () { requestDocumentation(QString()); });
-#endif
- }
-
- //evaluate the whole worksheet or interrupt the current calculation
- menu->addSeparator();
- if (!isRunning())
- menu->addAction(QIcon::fromTheme(QLatin1String("system-run")), i18n("Evaluate Worksheet"),
- this, &Worksheet::evaluate);
- else
- menu->addAction(QIcon::fromTheme(QLatin1String("process-stop")), i18n("Interrupt"), this,
- &Worksheet::interrupt);
-
- //zooming
- menu->addSeparator();
- auto* zoomMenu = new QMenu(i18n("Zoom"));
- zoomMenu->setIcon(QIcon::fromTheme(QLatin1String("zoom-draw")));
- auto* view = worksheetView();
-
- auto* action = zoomMenu->addAction(QIcon::fromTheme(QLatin1String("zoom-in")), i18n("Zoom In"), view, &WorksheetView::zoomIn);
- action->setShortcut(Qt::CTRL | Qt::Key_Plus);
-
- action = zoomMenu->addAction(QIcon::fromTheme(QLatin1String("zoom-out")), i18n("Zoom Out"), view, &WorksheetView::zoomOut);
- action->setShortcut(Qt::CTRL | Qt::Key_Minus);
- zoomMenu->addSeparator();
-
- action = zoomMenu->addAction(QIcon::fromTheme(QLatin1String("zoom-original")), i18n("Original Size"), view, &WorksheetView::actualSize);
- action->setShortcut(Qt::CTRL | Qt::Key_1);
-
- menu->addMenu(zoomMenu);
- }
- else
- {
- menu->clear();
- menu->addAction(QIcon::fromTheme(QLatin1String("go-up")), i18n("Move Entries Up"), this, &Worksheet::selectionMoveUp);
- menu->addAction(QIcon::fromTheme(QLatin1String("go-down")), i18n("Move Entries Down"), this, &Worksheet::selectionMoveDown);
- menu->addAction(QIcon::fromTheme(QLatin1String("media-playback-start")), i18n("Evaluate Entries"), this, &Worksheet::selectionEvaluate);
- menu->addSeparator();
- menu->addAction(QIcon::fromTheme(QLatin1String("edit-delete")), i18n("Delete Entries"), this, &Worksheet::selectionRemove);
-
- bool isAnyCommandEntryInSelection = false;
- for (auto* entry : m_selectedEntries)
- if (entry->type() == CommandEntry::Type)
- {
- isAnyCommandEntryInSelection = true;
- break;
- }
-
- if (isAnyCommandEntryInSelection)
- {
- menu->addSeparator();
- menu->addAction(QIcon(), i18n("Collapse Command Entry Results"), this, &Worksheet::collapseSelectionResults);
- menu->addAction(QIcon(), i18n("Expand Command Entry Results"), this, &Worksheet::uncollapseSelectionResults);
- menu->addSeparator();
- menu->addAction(QIcon(), i18n("Delete Command Entry Results"), this, &Worksheet::removeSelectionResults);
- menu->addSeparator();
- menu->addAction(QIcon(), i18n("Exclude Command Entry From Execution"), this, &Worksheet::excludeFromExecutionSelection);
- menu->addAction(QIcon(), i18n("Add Command Entry To Execution"), this, &Worksheet::addToExectuionSelection);
- }
- }
-}
-
-void Worksheet::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
-{
- if (m_readOnly)
- return;
-
- // forward the event to the items
- QGraphicsScene::contextMenuEvent(event);
-
- if (!event->isAccepted()) {
- event->accept();
- QMenu* menu = createContextMenu();
- populateMenu(menu, event->scenePos());
-
- menu->popup(event->screenPos());
- }
-}
-
-void Worksheet::mousePressEvent(QGraphicsSceneMouseEvent* event)
-{
- /*
- if (event->button() == Qt::LeftButton && !focusItem() && lastEntry() &&
- event->scenePos().y() > lastEntry()->y() + lastEntry()->size().height())
- lastEntry()->focusEntry(WorksheetTextItem::BottomRight);
- */
- QGraphicsScene::mousePressEvent(event);
-
- if (!m_readOnly && event->buttons() & Qt::LeftButton)
- {
- auto* selectedEntry = entryAt(event->scenePos());
- if (event->modifiers() & Qt::ControlModifier)
- {
- clearFocus();
- resetEntryCursor();
-
- if (selectedEntry)
- {
- selectedEntry->setCellSelected(!selectedEntry->isCellSelected());
- selectedEntry->update();
-
- auto* lastSelectedEntry = m_circularFocusBuffer.size() > 0 ? m_circularFocusBuffer.last() : nullptr;
- if (lastSelectedEntry)
- {
- lastSelectedEntry->setCellSelected(!lastSelectedEntry->isCellSelected());
- lastSelectedEntry->update();
- m_circularFocusBuffer.clear();
- }
-
- for (auto* entry : {selectedEntry, lastSelectedEntry})
- if (entry)
- {
- if (entry->isCellSelected())
- m_selectedEntries.append(entry);
- else if (!entry->isCellSelected())
- m_selectedEntries.removeOne(entry);
- }
- }
- }
- else
- {
- for (auto* entry : m_selectedEntries)
- {
- if(isValidEntry(entry))
- {
- entry->setCellSelected(false);
- entry->update();
- }
- }
- m_selectedEntries.clear();
-
- if (selectedEntry)
- notifyEntryFocus(selectedEntry);
-
- updateEntryCursor(event);
- }
- }
-}
-
-void Worksheet::keyPressEvent(QKeyEvent* event)
-{
- if (m_readOnly)
- return;
-
- if ((event->modifiers() & Qt::ControlModifier) && (event->key() == Qt::Key_1))
- worksheetView()->actualSize();
- else if ((m_choosenCursorEntry || m_isCursorEntryAfterLastEntry) && !event->text().isEmpty())
- addEntryFromEntryCursor(); //add new entry when entry cursor is actived when user starts typing text
-
- QGraphicsScene::keyPressEvent(event);
-}
-
-void Worksheet::setActionCollection(KActionCollection* collection)
-{
- m_collection = collection;
-}
-
-void Worksheet::initActions()
-{
- // Mostly copied from KRichTextWidget::createActions(KActionCollection*)
- // It would be great if this wasn't necessary.
-
- // Text color
- /* This is "format-stroke-color" in KRichTextWidget */
- auto* action = new QAction(QIcon::fromTheme(QLatin1String("format-text-color")),
- i18nc("@action", "Text &Color..."), m_collection);
- action->setIconText(i18nc("@label text color", "Color"));
- action->setPriority(QAction::LowPriority);
- m_richTextActionList.append(action);
- connect(action, &QAction::triggered, this, &Worksheet::setTextForegroundColor);
-
- // Text color
- action = new QAction(QIcon::fromTheme(QLatin1String("format-fill-color")),
- i18nc("@action", "Text &Highlight..."), m_collection);
- action->setPriority(QAction::LowPriority);
- m_richTextActionList.append(action);
- connect(action, &QAction::triggered, this, &Worksheet::setTextBackgroundColor);
-
- // Font Family
- m_fontAction = new KFontAction(i18nc("@action", "&Font"), m_collection);
- m_richTextActionList.append(m_fontAction);
- connect(m_fontAction, &KFontAction::textTriggered, this, &Worksheet::setFontFamily);
-
- // Font Size
- m_fontSizeAction = new KFontSizeAction(i18nc("@action", "Font &Size"), m_collection);
- m_richTextActionList.append(m_fontSizeAction);
- connect(m_fontSizeAction, &KFontSizeAction::fontSizeChanged, this, &Worksheet::setFontSize);
-
- // Bold
- m_boldAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-text-bold")),
- i18nc("@action boldify selected text", "&Bold"),
- m_collection);
- m_boldAction->setPriority(QAction::LowPriority);
- QFont bold;
- bold.setBold(true);
- m_boldAction->setFont(bold);
- m_richTextActionList.append(m_boldAction);
- connect(m_boldAction, &QAction::triggered, this, &Worksheet::setTextBold);
-
- // Italic
- m_italicAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-text-italic")),
- i18nc("@action italicize selected text", "&Italic"),
- m_collection);
- m_italicAction->setPriority(QAction::LowPriority);
- QFont italic;
- italic.setItalic(true);
- m_italicAction->setFont(italic);
- m_richTextActionList.append(m_italicAction);
- connect(m_italicAction, &QAction::triggered, this, &Worksheet::setTextItalic);
-
- // Underline
- m_underlineAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-text-underline")),
- i18nc("@action underline selected text",
- "&Underline"),
- m_collection);
- m_underlineAction->setPriority(QAction::LowPriority);
- QFont underline;
- underline.setUnderline(true);
- m_underlineAction->setFont(underline);
- m_richTextActionList.append(m_underlineAction);
- connect(m_underlineAction, &QAction::triggered, this, &Worksheet::setTextUnderline);
-
- // Strike
- m_strikeOutAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-text-strikethrough")),
- i18nc("@action", "&Strike Out"),
- m_collection);
- m_strikeOutAction->setPriority(QAction::LowPriority);
- m_richTextActionList.append(m_strikeOutAction);
- connect(m_strikeOutAction, &QAction::triggered, this, &Worksheet::setTextStrikeOut);
-
- // Alignment
- auto* alignmentGroup = new QActionGroup(this);
-
- // Align left
- m_alignLeftAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-justify-left")),
- i18nc("@action", "Align &Left"),
- m_collection);
- m_alignLeftAction->setPriority(QAction::LowPriority);
- m_alignLeftAction->setIconText(i18nc("@label left justify", "Left"));
- m_richTextActionList.append(m_alignLeftAction);
- connect(m_alignLeftAction, &QAction::triggered, this, &Worksheet::setAlignLeft);
- alignmentGroup->addAction(m_alignLeftAction);
-
- // Align center
- m_alignCenterAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-justify-center")),
- i18nc("@action", "Align &Center"),
- m_collection);
- m_alignCenterAction->setPriority(QAction::LowPriority);
- m_alignCenterAction->setIconText(i18nc("@label center justify", "Center"));
- m_richTextActionList.append(m_alignCenterAction);
- connect(m_alignCenterAction, &QAction::triggered, this, &Worksheet::setAlignCenter);
- alignmentGroup->addAction(m_alignCenterAction);
-
- // Align right
- m_alignRightAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-justify-right")),
- i18nc("@action", "Align &Right"),
- m_collection);
- m_alignRightAction->setPriority(QAction::LowPriority);
- m_alignRightAction->setIconText(i18nc("@label right justify", "Right"));
- m_richTextActionList.append(m_alignRightAction);
- connect(m_alignRightAction, &QAction::triggered, this, &Worksheet::setAlignRight);
- alignmentGroup->addAction(m_alignRightAction);
-
- // Align justify
- m_alignJustifyAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-justify-fill")),
- i18nc("@action", "&Justify"),
- m_collection);
- m_alignJustifyAction->setPriority(QAction::LowPriority);
- m_alignJustifyAction->setIconText(i18nc("@label justify fill", "Justify"));
- m_richTextActionList.append(m_alignJustifyAction);
- connect(m_alignJustifyAction, &QAction::triggered, this, &Worksheet::setAlignJustify);
- alignmentGroup->addAction(m_alignJustifyAction);
-
- if (m_collection)
- {
- m_collection->addAction(QLatin1String("format_text_foreground_color"), action);
- m_collection->addAction(QLatin1String("format_text_background_color"), action);
- m_collection->addAction(QLatin1String("format_font_family"), m_fontAction);
- m_collection->addAction(QLatin1String("format_font_size"), m_fontSizeAction);
- m_collection->addAction(QLatin1String("format_text_bold"), m_boldAction);
- m_collection->setDefaultShortcut(m_boldAction, Qt::CTRL | Qt::Key_B);
- m_collection->addAction(QLatin1String("format_text_italic"), m_italicAction);
- m_collection->setDefaultShortcut(m_italicAction, Qt::CTRL | Qt::Key_I);
- m_collection->addAction(QLatin1String("format_text_underline"), m_underlineAction);
- m_collection->setDefaultShortcut(m_underlineAction, Qt::CTRL | Qt::Key_U);
- m_collection->addAction(QLatin1String("format_text_strikeout"), m_strikeOutAction);
- m_collection->setDefaultShortcut(m_strikeOutAction, Qt::CTRL | Qt::Key_L);
- m_collection->addAction(QLatin1String("format_align_left"), m_alignLeftAction);
- m_collection->addAction(QLatin1String("format_align_center"), m_alignCenterAction);
- m_collection->addAction(QLatin1String("format_align_right"), m_alignRightAction);
- m_collection->addAction(QLatin1String("format_align_justify"), m_alignJustifyAction);
- }
-
- /*
- // List style
- KSelectAction* selAction;
- selAction = new KSelectAction(QIcon::fromTheme("format-list-unordered"),
- i18nc("@title:menu", "List Style"),
- collection);
- QStringList listStyles;
- listStyles << i18nc("@item:inmenu no list style", "None")
- << i18nc("@item:inmenu disc list style", "Disc")
- << i18nc("@item:inmenu circle list style", "Circle")
- << i18nc("@item:inmenu square list style", "Square")
- << i18nc("@item:inmenu numbered lists", "123")
- << i18nc("@item:inmenu lowercase abc lists", "abc")
- << i18nc("@item:inmenu uppercase abc lists", "ABC");
- selAction->setItems(listStyles);
- selAction->setCurrentItem(0);
- action = selAction;
- m_richTextActionList.append(action);
- collection->addAction("format_list_style", action);
- connect(action, SIGNAL(triggered(int)),
- this, &Worksheet::_k_setListStyle(int)));
- connect(action, &QAction::triggered,
- this, &Worksheet::_k_updateMiscActions()));
-
- // Indent
- action = new QAction(QIcon::fromTheme("format-indent-more"),
- i18nc("@action", "Increase Indent"), collection);
- action->setPriority(QAction::LowPriority);
- m_richTextActionList.append(action);
- collection->addAction("format_list_indent_more", action);
- connect(action, &QAction::triggered,
- this, &Worksheet::indentListMore()));
- connect(action, &QAction::triggered,
- this, &Worksheet::_k_updateMiscActions()));
-
- // Dedent
- action = new QAction(QIcon::fromTheme("format-indent-less"),
- i18nc("@action", "Decrease Indent"), collection);
- action->setPriority(QAction::LowPriority);
- m_richTextActionList.append(action);
- collection->addAction("format_list_indent_less", action);
- connect(action, &QAction::triggered, this, &Worksheet::indentListLess()));
- connect(action, &QAction::triggered, this, &Worksheet::_k_updateMiscActions()));
- */
-}
-
-WorksheetTextEditorItem* Worksheet::LastFocusedTextItem()
-{
- return m_lastFocusedTextItem;
-}
-
-WorksheetTextItem* Worksheet::lastFocusedTextItem()
-{
- return m_legacylastFocusedTextItem;
-}
-
-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);
- disconnect(this, &Worksheet::undo, m_lastFocusedTextItem, &WorksheetTextEditorItem::undo);
- disconnect(this, &Worksheet::redo, m_lastFocusedTextItem, &WorksheetTextEditorItem::redo);
- disconnect(m_lastFocusedTextItem, &WorksheetTextEditorItem::cutAvailable, this, &Worksheet::cutAvailable);
- disconnect(m_lastFocusedTextItem, &WorksheetTextEditorItem::copyAvailable, this, &Worksheet::copyAvailable);
- disconnect(m_lastFocusedTextItem, &WorksheetTextEditorItem::pasteAvailable, this, &Worksheet::pasteAvailable);
- disconnect(this, &Worksheet::cut, m_lastFocusedTextItem, &WorksheetTextEditorItem::cut);
- disconnect(this, &Worksheet::copy, m_lastFocusedTextItem, &WorksheetTextEditorItem::copy);
- m_lastFocusedTextItem = nullptr;
- }
-
- if (m_legacylastFocusedTextItem && m_legacylastFocusedTextItem != newItem) {
- disconnect(m_legacylastFocusedTextItem, &WorksheetTextItem::undoAvailable, this, &Worksheet::undoAvailable);
- disconnect(m_legacylastFocusedTextItem, &WorksheetTextItem::redoAvailable, this, &Worksheet::redoAvailable);
- disconnect(this, &Worksheet::undo, m_legacylastFocusedTextItem, &WorksheetTextItem::undo);
- disconnect(this, &Worksheet::redo, m_legacylastFocusedTextItem, &WorksheetTextItem::redo);
- disconnect(m_legacylastFocusedTextItem, &WorksheetTextItem::cutAvailable, this, &Worksheet::cutAvailable);
- disconnect(m_legacylastFocusedTextItem, &WorksheetTextItem::copyAvailable, this, &Worksheet::copyAvailable);
- disconnect(m_legacylastFocusedTextItem, &WorksheetTextItem::pasteAvailable, this, &Worksheet::pasteAvailable);
- disconnect(this, &Worksheet::cut, m_legacylastFocusedTextItem, &WorksheetTextItem::cut);
- disconnect(this, &Worksheet::copy, m_legacylastFocusedTextItem, &WorksheetTextItem::copy);
- m_legacylastFocusedTextItem->clearSelection();
- }
-
- if (newItem) {
- WorksheetEntry* entry = qobject_cast<WorksheetEntry*>(newItem->parentObject());
- if (entry)
- setAcceptRichText(entry->acceptRichText());
-
- Q_EMIT undoAvailable(newItem->isUndoAvailable());
- Q_EMIT redoAvailable(newItem->isRedoAvailable());
- connect(newItem, &WorksheetTextItem::undoAvailable, this, &Worksheet::undoAvailable);
- connect(newItem, &WorksheetTextItem::redoAvailable, this, &Worksheet::redoAvailable);
- connect(this, &Worksheet::undo, newItem, &WorksheetTextItem::undo);
- connect(this, &Worksheet::redo, newItem, &WorksheetTextItem::redo);
- Q_EMIT cutAvailable(newItem->isCutAvailable());
- Q_EMIT copyAvailable(newItem->isCopyAvailable());
- Q_EMIT pasteAvailable(newItem->isPasteAvailable());
- connect(newItem, &WorksheetTextItem::cutAvailable, this, &Worksheet::cutAvailable);
- connect(newItem, &WorksheetTextItem::copyAvailable, this, &Worksheet::copyAvailable);
- connect(newItem, &WorksheetTextItem::pasteAvailable, this, &Worksheet::pasteAvailable);
- connect(this, &Worksheet::cut, newItem, &WorksheetTextItem::cut);
- connect(this, &Worksheet::copy, newItem, &WorksheetTextItem::copy);
- }
- else {
- setAcceptRichText(false);
- Q_EMIT undoAvailable(false);
- Q_EMIT redoAvailable(false);
- Q_EMIT cutAvailable(false);
- Q_EMIT copyAvailable(false);
- Q_EMIT pasteAvailable(false);
- }
- m_legacylastFocusedTextItem = 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);
- disconnect(this, &Worksheet::undo, m_legacylastFocusedTextItem, &WorksheetTextItem::undo);
- disconnect(this, &Worksheet::redo, m_legacylastFocusedTextItem, &WorksheetTextItem::redo);
- disconnect(m_legacylastFocusedTextItem, &WorksheetTextItem::cutAvailable, this, &Worksheet::cutAvailable);
- disconnect(m_legacylastFocusedTextItem, &WorksheetTextItem::copyAvailable, this, &Worksheet::copyAvailable);
- disconnect(m_legacylastFocusedTextItem, &WorksheetTextItem::pasteAvailable, this, &Worksheet::pasteAvailable);
- disconnect(this, &Worksheet::cut, m_legacylastFocusedTextItem, &WorksheetTextItem::cut);
- disconnect(this, &Worksheet::copy, m_legacylastFocusedTextItem, &WorksheetTextItem::copy);
- // m_legacylastFocusedTextItem = nullptr;
- }
-
- if (m_readOnly) {
- if (m_lastFocusedTextItem && m_lastFocusedTextItem != newItem) {
- disconnect(this, &Worksheet::copy, m_lastFocusedTextItem, &WorksheetTextEditorItem::copy);
- m_lastFocusedTextItem->clearSelection();
- }
- if (newItem && m_lastFocusedTextItem != newItem) {
- connect(this, &Worksheet::copy, newItem, &WorksheetTextEditorItem::copy);
- Q_EMIT copyAvailable(newItem->isCopyAvailable());
- }
- else if (!newItem)
- Q_EMIT copyAvailable(false);
- m_lastFocusedTextItem = newItem;
- return;
- }
-
- if (m_lastFocusedTextItem && m_lastFocusedTextItem != newItem) {
- disconnect(m_lastFocusedTextItem, &WorksheetTextEditorItem::undoAvailable, this, &Worksheet::undoAvailable);
- disconnect(m_lastFocusedTextItem, &WorksheetTextEditorItem::redoAvailable, this, &Worksheet::redoAvailable);
- disconnect(this, &Worksheet::undo, m_lastFocusedTextItem, &WorksheetTextEditorItem::undo);
- disconnect(this, &Worksheet::redo, m_lastFocusedTextItem, &WorksheetTextEditorItem::redo);
- disconnect(m_lastFocusedTextItem, &WorksheetTextEditorItem::cutAvailable, this, &Worksheet::cutAvailable);
- disconnect(m_lastFocusedTextItem, &WorksheetTextEditorItem::copyAvailable, this, &Worksheet::copyAvailable);
- disconnect(m_lastFocusedTextItem, &WorksheetTextEditorItem::pasteAvailable, this, &Worksheet::pasteAvailable);
- disconnect(this, &Worksheet::cut, m_lastFocusedTextItem, &WorksheetTextEditorItem::cut);
- disconnect(this, &Worksheet::copy, m_lastFocusedTextItem, &WorksheetTextEditorItem::copy);
- m_lastFocusedTextItem->clearSelection();
- }
-
- if (newItem && m_lastFocusedTextItem != newItem) {
- setAcceptRichText(false);
- Q_EMIT undoAvailable(newItem->isUndoAvailable());
- Q_EMIT redoAvailable(newItem->isRedoAvailable());
- connect(newItem, &WorksheetTextEditorItem::undoAvailable, this, &Worksheet::undoAvailable);
- connect(newItem, &WorksheetTextEditorItem::redoAvailable, this, &Worksheet::redoAvailable);
- connect(this, &Worksheet::undo, newItem, &WorksheetTextEditorItem::undo);
- connect(this, &Worksheet::redo, newItem, &WorksheetTextEditorItem::redo);
- Q_EMIT cutAvailable(newItem->isCutAvailable());
- Q_EMIT copyAvailable(newItem->isCopyAvailable());
- Q_EMIT pasteAvailable(newItem->isPasteAvailable());
- connect(newItem, &WorksheetTextEditorItem::cutAvailable, this, &Worksheet::cutAvailable);
- connect(newItem, &WorksheetTextEditorItem::copyAvailable, this, &Worksheet::copyAvailable);
- connect(newItem, &WorksheetTextEditorItem::pasteAvailable, this, &Worksheet::pasteAvailable);
- connect(this, &Worksheet::cut, newItem, &WorksheetTextEditorItem::cut);
- connect(this, &Worksheet::copy, newItem, &WorksheetTextEditorItem::copy);
- } else if (!newItem) {
- setAcceptRichText(false);
- Q_EMIT undoAvailable(false);
- Q_EMIT redoAvailable(false);
- Q_EMIT cutAvailable(false);
- Q_EMIT copyAvailable(false);
- Q_EMIT pasteAvailable(false);
- }
- m_lastFocusedTextItem = newItem;
-}
-
-
-/*!
- * handles the paste action triggered in cantor_part.
- * Pastes into the last focused text item.
- * In case the "new entry"-cursor is currently shown,
- * a new entry is created first which the content will be pasted into.
- */
-void Worksheet::paste() {
- if (m_choosenCursorEntry || m_isCursorEntryAfterLastEntry)
- addEntryFromEntryCursor();
-
- if (m_lastFocusedTextItem)
- m_lastFocusedTextItem->paste();
- else if (m_legacylastFocusedTextItem)
- m_legacylastFocusedTextItem->paste();
-}
-
-void Worksheet::setRichTextInformation(const RichTextInfo& info)
-{
- if (!m_boldAction)
- return;
-
- m_boldAction->setChecked(info.bold);
- m_italicAction->setChecked(info.italic);
- m_underlineAction->setChecked(info.underline);
- m_strikeOutAction->setChecked(info.strikeOut);
- m_fontAction->setFont(info.font);
- if (info.fontSize > 0)
- m_fontSizeAction->setFontSize(info.fontSize);
-
- if (info.align & Qt::AlignLeft)
- m_alignLeftAction->setChecked(true);
- else if (info.align & Qt::AlignCenter)
- m_alignCenterAction->setChecked(true);
- else if (info.align & Qt::AlignRight)
- m_alignRightAction->setChecked(true);
- else if (info.align & Qt::AlignJustify)
- m_alignJustifyAction->setChecked(true);
-}
-
-void Worksheet::setAcceptRichText(bool b)
-{
- if (!m_readOnly)
- {
- for(auto* action : m_richTextActionList)
- {
- if (!action) continue;
- action->setVisible(b);
- }
- }
-}
-
-WorksheetTextEditorItem* Worksheet::currentTextItem()
-{
- auto* item = focusItem();
- if (!item)
- item = m_lastFocusedTextItem;
- while (item && item->type() != WorksheetTextEditorItem::Type)
- item = item->parentItem();
-
- return qgraphicsitem_cast<WorksheetTextEditorItem*>(item);
-}
-
-void Worksheet::setTextForegroundColor()
-{
- if (m_legacylastFocusedTextItem)
- m_legacylastFocusedTextItem->setTextForegroundColor();
-}
-
-void Worksheet::setTextBackgroundColor()
-{
- if (m_lastFocusedTextItem)
- m_lastFocusedTextItem->setTextBackgroundColor();
- else if (m_legacylastFocusedTextItem)
- m_legacylastFocusedTextItem->setTextBackgroundColor();
-}
-
-void Worksheet::setTextBold(bool b)
-{
- if (m_lastFocusedTextItem)
- m_lastFocusedTextItem->setTextBold(b);
- else if (m_legacylastFocusedTextItem)
- m_legacylastFocusedTextItem->setTextBold(b);
-}
-
-void Worksheet::setTextItalic(bool b)
-{
- if (m_lastFocusedTextItem)
- m_lastFocusedTextItem->setTextItalic(b);
- else if (m_legacylastFocusedTextItem)
- m_legacylastFocusedTextItem->setTextItalic(b);
-}
-
-void Worksheet::setTextUnderline(bool b)
-{
- if (m_lastFocusedTextItem)
- m_lastFocusedTextItem->setTextUnderline(b);
- else if (m_legacylastFocusedTextItem)
- m_legacylastFocusedTextItem->setTextUnderline(b);
-}
-
-void Worksheet::setTextStrikeOut(bool b)
-{
- if (m_lastFocusedTextItem)
- m_lastFocusedTextItem->setTextStrikeOut(b);
- else if (m_legacylastFocusedTextItem)
- m_legacylastFocusedTextItem->setTextStrikeOut(b);
-}
-
-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()));
+ // Italic
+ m_italicAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-text-italic")),
+ i18nc("@action italicize selected text", "&Italic"),
+ m_collection);
+ m_italicAction->setPriority(QAction::LowPriority);
+ QFont italic;
+ italic.setItalic(true);
+ m_italicAction->setFont(italic);
+ m_richTextActionList.append(m_italicAction);
+ connect(m_italicAction, &QAction::triggered, this, &Worksheet::setTextItalic);
- m_dragScrollTimer->start();
-}
+ // Underline
+ m_underlineAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-text-underline")),
+ i18nc("@action underline selected text",
+ "&Underline"),
+ m_collection);
+ m_underlineAction->setPriority(QAction::LowPriority);
+ QFont underline;
+ underline.setUnderline(true);
+ m_underlineAction->setFont(underline);
+ m_richTextActionList.append(m_underlineAction);
+ connect(m_underlineAction, &QAction::triggered, this, &Worksheet::setTextUnderline);
-void Worksheet::removeDragPlaceholder()
-{
- if (!m_placeholderEntry)
- return;
+ // Strike
+ m_strikeOutAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-text-strikethrough")),
+ i18nc("@action", "&Strike Out"),
+ m_collection);
+ m_strikeOutAction->setPriority(QAction::LowPriority);
+ m_richTextActionList.append(m_strikeOutAction);
+ connect(m_strikeOutAction, &QAction::triggered, this, &Worksheet::setTextStrikeOut);
- auto* placeholder = m_placeholderEntry;
- auto* previous = placeholder->previous();
- auto* next = placeholder->next();
+ // Alignment
+ auto* alignmentGroup = new QActionGroup(this);
- if (previous && previous->next() == placeholder)
- previous->setNext(next);
- else if (firstEntry() == placeholder)
- setFirstEntry(next);
+ // Align left
+ m_alignLeftAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-justify-left")),
+ i18nc("@action", "Align &Left"),
+ m_collection);
+ m_alignLeftAction->setPriority(QAction::LowPriority);
+ m_alignLeftAction->setIconText(i18nc("@label left justify", "Left"));
+ m_richTextActionList.append(m_alignLeftAction);
+ connect(m_alignLeftAction, &QAction::triggered, this, &Worksheet::setAlignLeft);
+ alignmentGroup->addAction(m_alignLeftAction);
- if (next && next->previous() == placeholder)
- next->setPrevious(previous);
- else if (lastEntry() == placeholder)
- setLastEntry(previous);
+ // Align center
+ m_alignCenterAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-justify-center")),
+ i18nc("@action", "Align &Center"),
+ m_collection);
+ m_alignCenterAction->setPriority(QAction::LowPriority);
+ m_alignCenterAction->setIconText(i18nc("@label center justify", "Center"));
+ m_richTextActionList.append(m_alignCenterAction);
+ connect(m_alignCenterAction, &QAction::triggered, this, &Worksheet::setAlignCenter);
+ alignmentGroup->addAction(m_alignCenterAction);
- placeholder->setPrevious(nullptr);
- placeholder->setNext(nullptr);
- placeholder->hide();
- placeholder->deleteLater();
+ // Align right
+ m_alignRightAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-justify-right")),
+ i18nc("@action", "Align &Right"),
+ m_collection);
+ m_alignRightAction->setPriority(QAction::LowPriority);
+ m_alignRightAction->setIconText(i18nc("@label right justify", "Right"));
+ m_richTextActionList.append(m_alignRightAction);
+ connect(m_alignRightAction, &QAction::triggered, this, &Worksheet::setAlignRight);
+ alignmentGroup->addAction(m_alignRightAction);
- m_placeholderEntry = nullptr;
-}
+ // Align justify
+ m_alignJustifyAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-justify-fill")),
+ i18nc("@action", "&Justify"),
+ m_collection);
+ m_alignJustifyAction->setPriority(QAction::LowPriority);
+ m_alignJustifyAction->setIconText(i18nc("@label justify fill", "Justify"));
+ m_richTextActionList.append(m_alignJustifyAction);
+ connect(m_alignJustifyAction, &QAction::triggered, this, &Worksheet::setAlignJustify);
+ alignmentGroup->addAction(m_alignJustifyAction);
-void Worksheet::updateEntryCursor(QGraphicsSceneMouseEvent* event)
-{
- // determine the worksheet entry near which the entry cursor will be shown
- resetEntryCursor();
- if (event->button() == Qt::LeftButton && !focusItem())
+ if (m_collection)
{
- 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;
- }
- }
+ m_collection->addAction(QLatin1String("format_text_foreground_color"), action);
+ m_collection->addAction(QLatin1String("format_text_background_color"), action);
+ m_collection->addAction(QLatin1String("format_font_family"), m_fontAction);
+ m_collection->addAction(QLatin1String("format_font_size"), m_fontSizeAction);
+ m_collection->addAction(QLatin1String("format_text_bold"), m_boldAction);
+ m_collection->setDefaultShortcut(m_boldAction, Qt::CTRL | Qt::Key_B);
+ m_collection->addAction(QLatin1String("format_text_italic"), m_italicAction);
+ m_collection->setDefaultShortcut(m_italicAction, Qt::CTRL | Qt::Key_I);
+ m_collection->addAction(QLatin1String("format_text_underline"), m_underlineAction);
+ m_collection->setDefaultShortcut(m_underlineAction, Qt::CTRL | Qt::Key_U);
+ m_collection->addAction(QLatin1String("format_text_strikeout"), m_strikeOutAction);
+ m_collection->setDefaultShortcut(m_strikeOutAction, Qt::CTRL | Qt::Key_L);
+ m_collection->addAction(QLatin1String("format_align_left"), m_alignLeftAction);
+ m_collection->addAction(QLatin1String("format_align_center"), m_alignCenterAction);
+ m_collection->addAction(QLatin1String("format_align_right"), m_alignRightAction);
+ m_collection->addAction(QLatin1String("format_align_justify"), m_alignJustifyAction);
}
- 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_choosenCursorEntry || m_isCursorEntryAfterLastEntry) && m_entryCursorItem)
- m_entryCursorItem->setVisible(!m_entryCursorItem->isVisible());
-}
-
-void Worksheet::resetEntryCursor()
-{
- m_choosenCursorEntry = nullptr;
- m_isCursorEntryAfterLastEntry = false;
- m_entryCursorItem->hide();
-}
-
-void Worksheet::drawEntryCursor()
-{
- 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();
- }
+ /*
+ // List style
+ KSelectAction* selAction;
+ selAction = new KSelectAction(QIcon::fromTheme("format-list-unordered"),
+ i18nc("@title:menu", "List Style"),
+ collection);
+ QStringList listStyles;
+ listStyles << i18nc("@item:inmenu no list style", "None")
+ << i18nc("@item:inmenu disc list style", "Disc")
+ << i18nc("@item:inmenu circle list style", "Circle")
+ << i18nc("@item:inmenu square list style", "Square")
+ << i18nc("@item:inmenu numbered lists", "123")
+ << i18nc("@item:inmenu lowercase abc lists", "abc")
+ << i18nc("@item:inmenu uppercase abc lists", "ABC");
+ selAction->setItems(listStyles);
+ selAction->setCurrentItem(0);
+ action = selAction;
+ m_richTextActionList.append(action);
+ collection->addAction("format_list_style", action);
+ connect(action, SIGNAL(triggered(int)),
+ this, &Worksheet::_k_setListStyle(int)));
+ connect(action, &QAction::triggered,
+ this, &Worksheet::_k_updateMiscActions()));
+
+ // Indent
+ action = new QAction(QIcon::fromTheme("format-indent-more"),
+ i18nc("@action", "Increase Indent"), collection);
+ action->setPriority(QAction::LowPriority);
+ m_richTextActionList.append(action);
+ collection->addAction("format_list_indent_more", action);
+ connect(action, &QAction::triggered,
+ this, &Worksheet::indentListMore()));
+ connect(action, &QAction::triggered,
+ this, &Worksheet::_k_updateMiscActions()));
+
+ // Dedent
+ action = new QAction(QIcon::fromTheme("format-indent-less"),
+ i18nc("@action", "Decrease Indent"), collection);
+ action->setPriority(QAction::LowPriority);
+ m_richTextActionList.append(action);
+ collection->addAction("format_list_indent_less", action);
+ connect(action, &QAction::triggered, this, &Worksheet::indentListLess()));
+ connect(action, &QAction::triggered, this, &Worksheet::_k_updateMiscActions()));
+ */
}
-void Worksheet::setType(Worksheet::Type type)
+WorksheetTextEditorItem* Worksheet::LastFocusedTextItem()
{
- m_type = type;
+ return m_lastFocusedTextItem;
}
-Worksheet::Type Worksheet::type() const
+WorksheetTextItem* Worksheet::lastFocusedTextItem()
{
- return m_type;
+ return m_legacylastFocusedTextItem;
}
-void Worksheet::changeEntryType(WorksheetEntry* target, int newType)
+void Worksheet::updateFocusedTextItem(WorksheetTextItem* newItem)
{
- if (target && target->type() != newType)
+ if (newItem)
{
- bool animation_state = m_animationsEnabled;
- m_animationsEnabled = false;
+ auto* entry = qobject_cast<WorksheetEntry*>(newItem->parentObject());
- QString content;
+ if (entry && isValidEntry(entry))
+ updateCurrentHierarchyFromEntry(entry);
+ }
- 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();
- }
+ if (m_lastFocusedTextItem) {
+ disconnect(m_lastFocusedTextItem, &WorksheetTextEditorItem::undoAvailable, this, &Worksheet::undoAvailable);
+ disconnect(m_lastFocusedTextItem, &WorksheetTextEditorItem::redoAvailable, this, &Worksheet::redoAvailable);
+ disconnect(this, &Worksheet::undo, m_lastFocusedTextItem, &WorksheetTextEditorItem::undo);
+ disconnect(this, &Worksheet::redo, m_lastFocusedTextItem, &WorksheetTextEditorItem::redo);
+ disconnect(m_lastFocusedTextItem, &WorksheetTextEditorItem::cutAvailable, this, &Worksheet::cutAvailable);
+ disconnect(m_lastFocusedTextItem, &WorksheetTextEditorItem::copyAvailable, this, &Worksheet::copyAvailable);
+ disconnect(m_lastFocusedTextItem, &WorksheetTextEditorItem::pasteAvailable, this, &Worksheet::pasteAvailable);
+ disconnect(this, &Worksheet::cut, m_lastFocusedTextItem, &WorksheetTextEditorItem::cut);
+ disconnect(this, &Worksheet::copy, m_lastFocusedTextItem, &WorksheetTextEditorItem::copy);
+ m_lastFocusedTextItem = nullptr;
+ }
- auto* newEntry = WorksheetEntry::create(newType, this);
- if (newEntry)
- {
- newEntry->setContent(content);
- auto* tmp = target;
+ if (m_legacylastFocusedTextItem && m_legacylastFocusedTextItem != newItem) {
+ disconnect(m_legacylastFocusedTextItem, &WorksheetTextItem::undoAvailable, this, &Worksheet::undoAvailable);
+ disconnect(m_legacylastFocusedTextItem, &WorksheetTextItem::redoAvailable, this, &Worksheet::redoAvailable);
+ disconnect(this, &Worksheet::undo, m_legacylastFocusedTextItem, &WorksheetTextItem::undo);
+ disconnect(this, &Worksheet::redo, m_legacylastFocusedTextItem, &WorksheetTextItem::redo);
+ disconnect(m_legacylastFocusedTextItem, &WorksheetTextItem::cutAvailable, this, &Worksheet::cutAvailable);
+ disconnect(m_legacylastFocusedTextItem, &WorksheetTextItem::copyAvailable, this, &Worksheet::copyAvailable);
+ disconnect(m_legacylastFocusedTextItem, &WorksheetTextItem::pasteAvailable, this, &Worksheet::pasteAvailable);
+ disconnect(this, &Worksheet::cut, m_legacylastFocusedTextItem, &WorksheetTextItem::cut);
+ disconnect(this, &Worksheet::copy, m_legacylastFocusedTextItem, &WorksheetTextItem::copy);
+ m_legacylastFocusedTextItem->clearSelection();
+ }
- newEntry->setPrevious(tmp->previous());
- newEntry->setNext(tmp->next());
+ if (newItem) {
+ WorksheetEntry* entry = qobject_cast<WorksheetEntry*>(newItem->parentObject());
+ if (entry)
+ setAcceptRichText(entry->acceptRichText());
- tmp->setPrevious(nullptr);
- tmp->setNext(nullptr);
- tmp->clearFocus();
- tmp->forceRemove();
+ Q_EMIT undoAvailable(newItem->isUndoAvailable());
+ Q_EMIT redoAvailable(newItem->isRedoAvailable());
+ connect(newItem, &WorksheetTextItem::undoAvailable, this, &Worksheet::undoAvailable);
+ connect(newItem, &WorksheetTextItem::redoAvailable, this, &Worksheet::redoAvailable);
+ connect(this, &Worksheet::undo, newItem, &WorksheetTextItem::undo);
+ connect(this, &Worksheet::redo, newItem, &WorksheetTextItem::redo);
+ Q_EMIT cutAvailable(newItem->isCutAvailable());
+ Q_EMIT copyAvailable(newItem->isCopyAvailable());
+ Q_EMIT pasteAvailable(newItem->isPasteAvailable());
+ connect(newItem, &WorksheetTextItem::cutAvailable, this, &Worksheet::cutAvailable);
+ connect(newItem, &WorksheetTextItem::copyAvailable, this, &Worksheet::copyAvailable);
+ connect(newItem, &WorksheetTextItem::pasteAvailable, this, &Worksheet::pasteAvailable);
+ connect(this, &Worksheet::cut, newItem, &WorksheetTextItem::cut);
+ connect(this, &Worksheet::copy, newItem, &WorksheetTextItem::copy);
+ }
+ else {
+ setAcceptRichText(false);
+ Q_EMIT undoAvailable(false);
+ Q_EMIT redoAvailable(false);
+ Q_EMIT cutAvailable(false);
+ Q_EMIT copyAvailable(false);
+ Q_EMIT pasteAvailable(false);
+ }
+ m_legacylastFocusedTextItem = newItem;
+}
- if (newEntry->previous())
- newEntry->previous()->setNext(newEntry);
- else
- setFirstEntry(newEntry);
+void Worksheet::updateFocusedTextItem(WorksheetTextEditorItem* newItem)
+{
+ if (newItem)
+ {
+ auto* entry = qobject_cast<WorksheetEntry*>(newItem->parentObject());
- if (newEntry->next())
- newEntry->next()->setPrevious(newEntry);
- else
- setLastEntry(newEntry);
+ 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);
+ disconnect(this, &Worksheet::undo, m_legacylastFocusedTextItem, &WorksheetTextItem::undo);
+ disconnect(this, &Worksheet::redo, m_legacylastFocusedTextItem, &WorksheetTextItem::redo);
+ disconnect(m_legacylastFocusedTextItem, &WorksheetTextItem::cutAvailable, this, &Worksheet::cutAvailable);
+ disconnect(m_legacylastFocusedTextItem, &WorksheetTextItem::copyAvailable, this, &Worksheet::copyAvailable);
+ disconnect(m_legacylastFocusedTextItem, &WorksheetTextItem::pasteAvailable, this, &Worksheet::pasteAvailable);
+ disconnect(this, &Worksheet::cut, m_legacylastFocusedTextItem, &WorksheetTextItem::cut);
+ disconnect(this, &Worksheet::copy, m_legacylastFocusedTextItem, &WorksheetTextItem::copy);
+ // m_legacylastFocusedTextItem = nullptr;
+ }
- if (newType == HierarchyEntry::Type || targetEntryType == HierarchyEntry::Type)
- updateHierarchyLayout();
- updateLayout();
- makeVisible(newEntry);
- focusEntry(newEntry);
- setModified();
- newEntry->focusEntry();
+ if (m_readOnly) {
+ if (m_lastFocusedTextItem && m_lastFocusedTextItem != newItem) {
+ disconnect(this, &Worksheet::copy, m_lastFocusedTextItem, &WorksheetTextEditorItem::copy);
+ m_lastFocusedTextItem->clearSelection();
}
- m_animationsEnabled = animation_state;
+ if (newItem && m_lastFocusedTextItem != newItem) {
+ connect(this, &Worksheet::copy, newItem, &WorksheetTextEditorItem::copy);
+ Q_EMIT copyAvailable(newItem->isCopyAvailable());
+ }
+ else if (!newItem)
+ Q_EMIT copyAvailable(false);
+ m_lastFocusedTextItem = newItem;
+ return;
+ }
+
+ if (m_lastFocusedTextItem && m_lastFocusedTextItem != newItem) {
+ disconnect(m_lastFocusedTextItem, &WorksheetTextEditorItem::undoAvailable, this, &Worksheet::undoAvailable);
+ disconnect(m_lastFocusedTextItem, &WorksheetTextEditorItem::redoAvailable, this, &Worksheet::redoAvailable);
+ disconnect(this, &Worksheet::undo, m_lastFocusedTextItem, &WorksheetTextEditorItem::undo);
+ disconnect(this, &Worksheet::redo, m_lastFocusedTextItem, &WorksheetTextEditorItem::redo);
+ disconnect(m_lastFocusedTextItem, &WorksheetTextEditorItem::cutAvailable, this, &Worksheet::cutAvailable);
+ disconnect(m_lastFocusedTextItem, &WorksheetTextEditorItem::copyAvailable, this, &Worksheet::copyAvailable);
+ disconnect(m_lastFocusedTextItem, &WorksheetTextEditorItem::pasteAvailable, this, &Worksheet::pasteAvailable);
+ disconnect(this, &Worksheet::cut, m_lastFocusedTextItem, &WorksheetTextEditorItem::cut);
+ disconnect(this, &Worksheet::copy, m_lastFocusedTextItem, &WorksheetTextEditorItem::copy);
+ m_lastFocusedTextItem->clearSelection();
+ }
+
+ if (newItem && m_lastFocusedTextItem != newItem) {
+ setAcceptRichText(false);
+ Q_EMIT undoAvailable(newItem->isUndoAvailable());
+ Q_EMIT redoAvailable(newItem->isRedoAvailable());
+ connect(newItem, &WorksheetTextEditorItem::undoAvailable, this, &Worksheet::undoAvailable);
+ connect(newItem, &WorksheetTextEditorItem::redoAvailable, this, &Worksheet::redoAvailable);
+ connect(this, &Worksheet::undo, newItem, &WorksheetTextEditorItem::undo);
+ connect(this, &Worksheet::redo, newItem, &WorksheetTextEditorItem::redo);
+ Q_EMIT cutAvailable(newItem->isCutAvailable());
+ Q_EMIT copyAvailable(newItem->isCopyAvailable());
+ Q_EMIT pasteAvailable(newItem->isPasteAvailable());
+ connect(newItem, &WorksheetTextEditorItem::cutAvailable, this, &Worksheet::cutAvailable);
+ connect(newItem, &WorksheetTextEditorItem::copyAvailable, this, &Worksheet::copyAvailable);
+ connect(newItem, &WorksheetTextEditorItem::pasteAvailable, this, &Worksheet::pasteAvailable);
+ connect(this, &Worksheet::cut, newItem, &WorksheetTextEditorItem::cut);
+ connect(this, &Worksheet::copy, newItem, &WorksheetTextEditorItem::copy);
+ } else if (!newItem) {
+ setAcceptRichText(false);
+ Q_EMIT undoAvailable(false);
+ Q_EMIT redoAvailable(false);
+ Q_EMIT cutAvailable(false);
+ Q_EMIT copyAvailable(false);
+ Q_EMIT pasteAvailable(false);
}
+ m_lastFocusedTextItem = newItem;
}
-bool Worksheet::isValidEntry(WorksheetEntry* entry)
-{
- for (auto* iter = firstEntry(); iter; iter = iter->next())
- if (entry == iter)
- return true;
- return false;
+/*!
+ * handles the paste action triggered in cantor_part.
+ * Pastes into the last focused text item.
+ * In case the "new entry"-cursor is currently shown,
+ * a new entry is created first which the content will be pasted into.
+ */
+void Worksheet::paste() {
+ if (m_choosenCursorEntry || m_isCursorEntryAfterLastEntry)
+ addEntryFromEntryCursor();
+
+ if (m_lastFocusedTextItem)
+ m_lastFocusedTextItem->paste();
+ else if (m_legacylastFocusedTextItem)
+ m_legacylastFocusedTextItem->paste();
}
-void Worksheet::selectionRemove()
+void Worksheet::setRichTextInformation(const RichTextInfo& info)
{
- if (m_selectedEntries.isEmpty())
+ if (!m_boldAction)
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());
+ m_boldAction->setChecked(info.bold);
+ m_italicAction->setChecked(info.italic);
+ m_underlineAction->setChecked(info.underline);
+ m_strikeOutAction->setChecked(info.strikeOut);
+ m_fontAction->setFont(info.font);
+ if (info.fontSize > 0)
+ m_fontSizeAction->setFontSize(info.fontSize);
- if (result != KMessageBox::PrimaryAction)
- return;
- }
+ if (info.align & Qt::AlignLeft)
+ m_alignLeftAction->setChecked(true);
+ else if (info.align & Qt::AlignCenter)
+ m_alignCenterAction->setChecked(true);
+ else if (info.align & Qt::AlignRight)
+ m_alignRightAction->setChecked(true);
+ else if (info.align & Qt::AlignJustify)
+ m_alignJustifyAction->setChecked(true);
+}
- for (auto* entry : m_selectedEntries)
+void Worksheet::setAcceptRichText(bool b)
+{
+ if (!m_readOnly)
{
- if (isValidEntry(entry))
- entry->startRemoving(false);
+ for(auto* action : m_richTextActionList)
+ {
+ if (!action) continue;
+ action->setVisible(b);
+ }
}
-
- m_selectedEntries.clear();
}
-void Worksheet::selectionEvaluate()
+WorksheetTextEditorItem* Worksheet::currentTextItem()
{
- // 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();
+ auto* item = focusItem();
+ if (!item)
+ item = m_lastFocusedTextItem;
+ while (item && item->type() != WorksheetTextEditorItem::Type)
+ item = item->parentItem();
+
+ return qgraphicsitem_cast<WorksheetTextEditorItem*>(item);
}
-void Worksheet::selectionMoveUp()
+void Worksheet::setTextForegroundColor()
{
- 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 (m_legacylastFocusedTextItem)
+ m_legacylastFocusedTextItem->setTextForegroundColor();
}
-void Worksheet::selectionMoveDown()
+void Worksheet::setTextBackgroundColor()
{
- 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();
+ if (m_lastFocusedTextItem)
+ m_lastFocusedTextItem->setTextBackgroundColor();
+ else if (m_legacylastFocusedTextItem)
+ m_legacylastFocusedTextItem->setTextBackgroundColor();
}
-void Worksheet::notifyEntryFocus(WorksheetEntry* entry)
+void Worksheet::setTextBold(bool b)
{
- if (entry)
- {
- m_circularFocusBuffer.enqueue(entry);
+ if (m_lastFocusedTextItem)
+ m_lastFocusedTextItem->setTextBold(b);
+ else if (m_legacylastFocusedTextItem)
+ m_legacylastFocusedTextItem->setTextBold(b);
+}
- if (m_circularFocusBuffer.size() > 2)
- m_circularFocusBuffer.dequeue();
- }
- else
- m_circularFocusBuffer.clear();
+void Worksheet::setTextItalic(bool b)
+{
+ if (m_lastFocusedTextItem)
+ m_lastFocusedTextItem->setTextItalic(b);
+ else if (m_legacylastFocusedTextItem)
+ m_legacylastFocusedTextItem->setTextItalic(b);
}
-void Worksheet::collapseAllResults()
+void Worksheet::setTextUnderline(bool b)
{
- for (auto* entry = firstEntry(); entry; entry = entry->next())
- if (entry->type() == CommandEntry::Type)
- static_cast<CommandEntry*>(entry)->collapseResults();
+ if (m_lastFocusedTextItem)
+ m_lastFocusedTextItem->setTextUnderline(b);
+ else if (m_legacylastFocusedTextItem)
+ m_legacylastFocusedTextItem->setTextUnderline(b);
}
-void Worksheet::uncollapseAllResults()
+void Worksheet::setTextStrikeOut(bool b)
{
- for (auto* entry = firstEntry(); entry; entry = entry->next())
- if (entry->type() == CommandEntry::Type)
- static_cast<CommandEntry*>(entry)->expandResults();
+ if (m_lastFocusedTextItem)
+ m_lastFocusedTextItem->setTextStrikeOut(b);
+ else if (m_legacylastFocusedTextItem)
+ m_legacylastFocusedTextItem->setTextStrikeOut(b);
}
-void Worksheet::removeAllResults()
+void Worksheet::setAlignLeft()
{
- bool remove = false;
+ if (m_lastFocusedTextItem)
+ m_lastFocusedTextItem->setAlignment(Qt::AlignLeft);
+ else if (m_legacylastFocusedTextItem)
+ m_legacylastFocusedTextItem->setAlignment(Qt::AlignLeft);
+}
- 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);
- }
- else
- remove = true;
+void Worksheet::setAlignRight()
+{
+ if (m_lastFocusedTextItem)
+ m_lastFocusedTextItem->setAlignment(Qt::AlignRight);
+ else if (m_legacylastFocusedTextItem)
+ m_legacylastFocusedTextItem->setAlignment(Qt::AlignRight);
+}
- if (remove)
- {
- for (auto *entry = firstEntry(); entry; entry = entry->next())
- if (entry->type() == CommandEntry::Type)
- static_cast<CommandEntry*>(entry)->removeResults();
- }
+void Worksheet::setAlignCenter()
+{
+ if (m_lastFocusedTextItem)
+ m_lastFocusedTextItem->setAlignment(Qt::AlignCenter);
+ else if (m_legacylastFocusedTextItem)
+ m_legacylastFocusedTextItem->setAlignment(Qt::AlignCenter);
}
-void Worksheet::addToExectuionSelection()
+void Worksheet::setAlignJustify()
{
- for (auto* entry : m_selectedEntries)
- if (entry->type() == CommandEntry::Type)
- static_cast<CommandEntry*>(entry)->addToExecution();
+ if (m_lastFocusedTextItem)
+ m_lastFocusedTextItem->setAlignment(Qt::AlignJustify);
+ else if (m_legacylastFocusedTextItem)
+ m_legacylastFocusedTextItem->setAlignment(Qt::AlignJustify);
}
-void Worksheet::excludeFromExecutionSelection()
+void Worksheet::setFontFamily(const QString& font)
{
- for (auto* entry : m_selectedEntries)
- if (entry->type() == CommandEntry::Type)
- static_cast<CommandEntry*>(entry)->excludeFromExecution();
+ if (m_lastFocusedTextItem)
+ m_lastFocusedTextItem->setFontFamily(font);
+ else if (m_legacylastFocusedTextItem)
+ m_legacylastFocusedTextItem->setFontFamily(font);
}
-void Worksheet::collapseSelectionResults()
+void Worksheet::setFontSize(int size)
{
- for (auto* entry : m_selectedEntries)
- if (entry->type() == CommandEntry::Type)
- static_cast<CommandEntry*>(entry)->collapseResults();
+ if (m_lastFocusedTextItem)
+ m_lastFocusedTextItem->setFontSize(size);
+ else if (m_legacylastFocusedTextItem)
+ m_legacylastFocusedTextItem->setFontSize(size);
}
-void Worksheet::uncollapseSelectionResults()
+
+bool Worksheet::isShortcut(const QKeySequence& sequence)
{
- for (auto* entry : m_selectedEntries)
- if (entry->type() == CommandEntry::Type)
- static_cast<CommandEntry*>(entry)->expandResults();
+ return m_shortcuts.contains(sequence);
}
-void Worksheet::removeSelectionResults()
+void Worksheet::registerShortcut(QAction* action)
{
- for (auto* entry : m_selectedEntries)
- if (entry->type() == CommandEntry::Type)
- static_cast<CommandEntry*>(entry)->removeResults();
+ for (auto& shortcut : action->shortcuts())
+ m_shortcuts.insert(shortcut, action);
+
+ connect(action, &QAction::changed, this, &Worksheet::updateShortcut);
}
-void Worksheet::navigateToTocNode(QString nodeId)
+void Worksheet::updateShortcut()
{
- QString plotCommandId;
- QString plotResultId;
- if (parsePlotNodeId(nodeId, &plotCommandId, &plotResultId))
- {
- const CommandSearchResult commandSearch = findCommandEntryById(plotCommandId);
- if (!commandSearch.entry)
- {
- scheduleTocStructureRefresh();
- return;
- }
+ QAction* action = qobject_cast<QAction*>(sender());
+ if (!action)
+ return;
- const bool expanded = expandHierarchyAncestors(commandSearch.collapsedAncestors);
- if (expanded)
- {
- updateHierarchyLayout();
- updateLayout();
- }
+ // 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);
+}
- if (!navigateToPlotResult(commandSearch.entry, plotResultId))
- scheduleTocStructureRefresh();
+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;
}
- QString commandId;
- if (parseCommandNodeId(nodeId, &commandId))
- {
- const CommandSearchResult commandSearch = findCommandEntryById(commandId);
- if (!commandSearch.entry)
- return;
+ event->accept();
+ removeDragPlaceholder();
+ updateLayout();
+}
- const bool expanded = expandHierarchyAncestors(commandSearch.collapsedAncestors);
- if (expanded)
- {
- updateHierarchyLayout();
- 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;
}
+ }
- auto* commandEntry = commandSearch.entry;
- updateCurrentHierarchyFromEntry(commandEntry);
-
- worksheetView()->scrollTo(qRound(commandEntry->scenePos().y()));
-
- worksheetView()->setFocus();
+ const bool dragWithHierarchy = !m_hierarchySubentriesDrag.empty();
- commandEntry->focusEntry(WorksheetTextItem::TopLeft);
+ if (m_placeholderEntry)
+ {
+ if (prev == m_placeholderEntry)
+ prev = m_placeholderEntry->previous();
- resetEntryCursor();
- return;
+ if (next == m_placeholderEntry)
+ next = m_placeholderEntry->next();
}
- const HierarchySearchResult hierarchySearch = findHierarchyEntryById(nodeId);
- if (hierarchySearch.entry)
+ if (prev || next)
{
- const bool expanded = expandHierarchyAncestors(hierarchySearch.collapsedAncestors);
+ const QSizeF placeholderSize = dragWithHierarchy ? m_hierarchyDragSize : m_dragEntry->size();
+
+ if (!m_placeholderEntry)
+ m_placeholderEntry = new PlaceHolderEntry(this, QSizeF(0, 0));
- if (expanded)
+ const bool positionChanged = m_placeholderEntry->previous() != prev || m_placeholderEntry->next() != next;
+
+ if (positionChanged)
{
- updateHierarchyLayout();
- updateLayout();
- }
+ auto* oldPrevious = m_placeholderEntry->previous();
+ auto* oldNext = m_placeholderEntry->next();
- auto* hierarchyEntry = hierarchySearch.entry;
- updateCurrentHierarchyFromEntry(hierarchyEntry);
+ if (oldPrevious && oldPrevious->next() == m_placeholderEntry)
+ oldPrevious->setNext(oldNext);
+ else if (firstEntry() == m_placeholderEntry)
+ setFirstEntry(oldNext);
- worksheetView()->scrollTo(qRound(hierarchyEntry->scenePos().y()));
+ if (oldNext && oldNext->previous() == m_placeholderEntry)
+ oldNext->setPrevious(oldPrevious);
+ else if (lastEntry() == m_placeholderEntry)
+ setLastEntry(oldPrevious);
- worksheetView()->setFocus();
+ m_placeholderEntry->setPrevious(prev);
+ m_placeholderEntry->setNext(next);
- hierarchyEntry->focusEntry(WorksheetTextItem::BottomRight);
+ if (prev)
+ prev->setNext(m_placeholderEntry);
+ else
+ setFirstEntry(m_placeholderEntry);
- resetEntryCursor();
- }
-}
+ if (next)
+ next->setPrevious(m_placeholderEntry);
+ else
+ setLastEntry(m_placeholderEntry);
+ }
-bool Worksheet::navigateToPlotResult(CommandEntry* commandEntry, const QString& resultId)
-{
- if (!commandEntry || resultId.isEmpty())
- return false;
+ m_placeholderEntry->changeSize(placeholderSize);
- 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();
+ 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();
+ }
- m_hierarchyTrackingSource = HierarchyTrackingSource::FocusedEntry;
- setCurrentTocNode(nodeId);
+ event->accept();
+}
- resetEntryCursor();
- return true;
+void Worksheet::dropEvent(QGraphicsSceneDragDropEvent* event)
+{
+ if (!m_dragEntry)
+ QGraphicsScene::dropEvent(event);
+ event->accept();
}
-void Worksheet::updateCurrentTocNodeFromResult(CommandEntry* commandEntry, Cantor::Result* result)
+void Worksheet::updateDragScrollTimer()
{
- if (!commandEntry || !result || !isValidEntry(commandEntry))
+ if (!m_dragScrollTimer)
return;
- m_hierarchyTrackingSource = HierarchyTrackingSource::FocusedEntry;
+ 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 (isPlotResult(result))
- setCurrentTocNode(buildPlotNodeId(commandEntry->commandId(), result->resultId()));
+ if (viewPos.y() < 10)
+ worksheetView()->scrollBy(-10*(10 - viewPos.y()));
else
- updateCurrentHierarchyFromEntry(commandEntry);
+ worksheetView()->scrollBy(10*(viewHeight - viewPos.y()));
+
+ m_dragScrollTimer->start();
}
-void Worksheet::renamePlot(const QString& commandId, const QString& resultId, const QString& newTitle)
+void Worksheet::removeDragPlaceholder()
{
- if (m_readOnly || commandId.isEmpty() || resultId.isEmpty())
+ if (!m_placeholderEntry)
return;
- QString normalizedTitle = newTitle;
- normalizedTitle.replace(QLatin1Char('\r'), QLatin1Char(' '));
- normalizedTitle.replace(QLatin1Char('\n'), QLatin1Char(' '));
- normalizedTitle = normalizedTitle.trimmed();
+ auto* placeholder = m_placeholderEntry;
+ auto* previous = placeholder->previous();
+ auto* next = placeholder->next();
- const CommandSearchResult commandSearch = findCommandEntryById(commandId);
- auto* commandEntry = commandSearch.entry;
- auto* expression = commandEntry ? commandEntry->expression() : nullptr;
+ if (previous && previous->next() == placeholder)
+ previous->setNext(next);
+ else if (firstEntry() == placeholder)
+ setFirstEntry(next);
- if (!commandEntry || !expression)
- return;
+ if (next && next->previous() == placeholder)
+ next->setPrevious(previous);
+ else if (lastEntry() == placeholder)
+ setLastEntry(previous);
- for (auto* result : expression->results())
- {
- if (!result || result->resultId() != resultId || !isPlotResult(result))
- continue;
+ placeholder->setPrevious(nullptr);
+ placeholder->setNext(nullptr);
+ placeholder->hide();
+ placeholder->deleteLater();
- if (result->displayName() == normalizedTitle)
- return;
+ m_placeholderEntry = nullptr;
+}
- result->setDisplayName(normalizedTitle);
- setModified();
- scheduleTocStructureRefresh();
- return;
+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::deletePlot(const QString& commandId, const QString& resultId)
+void Worksheet::addEntryFromEntryCursor()
{
- if (m_readOnly || commandId.isEmpty() || resultId.isEmpty())
- return;
+ qDebug() << "Add new entry from entry cursor";
+ if (m_isCursorEntryAfterLastEntry)
+ insertCommandEntry(lastEntry());
+ else
+ insertCommandEntryBefore(m_choosenCursorEntry);
+ resetEntryCursor();
+}
- const CommandSearchResult commandSearch = findCommandEntryById(commandId);
- auto* commandEntry = commandSearch.entry;
- auto* expression = commandEntry ? commandEntry->expression() : nullptr;
+void Worksheet::animateEntryCursor()
+{
+ if ((m_choosenCursorEntry || m_isCursorEntryAfterLastEntry) && m_entryCursorItem)
+ m_entryCursorItem->setVisible(!m_entryCursorItem->isVisible());
+}
- if (!commandEntry || !expression)
- return;
+void Worksheet::resetEntryCursor()
+{
+ m_choosenCursorEntry = nullptr;
+ m_isCursorEntryAfterLastEntry = false;
+ m_entryCursorItem->hide();
+}
- for (auto* result : expression->results())
+void Worksheet::drawEntryCursor()
+{
+ if (m_entryCursorItem && (m_choosenCursorEntry || (m_isCursorEntryAfterLastEntry && lastEntry())))
{
- if (!result || result->resultId() != resultId || !isPlotResult(result))
- continue;
-
- const QString plotNodeId = buildPlotNodeId(commandId, resultId);
- const bool wasCurrentNode = m_currentTocNodeId == plotNodeId;
-
- expression->removeResult(result);
-
- if (wasCurrentNode)
- setCurrentTocNode(buildCommandNodeId(commandEntry));
-
- setModified();
- scheduleTocStructureRefresh();
- return;
+ 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::renameHierarchyEntry(const QString& hierarchyId, const QString& newName)
+void Worksheet::setType(Worksheet::Type type)
{
- if (m_readOnly || hierarchyId.isEmpty())
- return;
-
- 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);
+ m_type = type;
}
-void Worksheet::changeHierarchyLevel(QString hierarchyId, int levelDelta)
+Worksheet::Type Worksheet::type() const
{
- if (m_readOnly || hierarchyId.isEmpty() || (levelDelta != -1 && levelDelta != 1))
- return;
-
- 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)
- {
- if (currentRootLevel <= minimumLevel)
- return;
+ return m_type;
+}
- expanded = expandHierarchyAncestors(hierarchySearch.collapsedAncestors);
- }
- else
+void Worksheet::changeEntryType(WorksheetEntry* target, int newType)
+{
+ if (target && target->type() != newType)
{
- if (currentRootLevel >= maximumLevel)
- return;
-
- expanded = expandHierarchyAncestors(hierarchySearch.collapsedAncestors);
+ bool animation_state = m_animationsEnabled;
+ m_animationsEnabled = false;
- bool hasPreviousSibling = false;
+ QString content;
- for (auto* entry = targetEntry->previous(); entry; entry = entry->previous())
+ int targetEntryType = target->type();
+ switch(targetEntryType)
{
- if (entry->type() != HierarchyEntry::Type)
- continue;
-
- const int entryLevel = static_cast<int>(static_cast<HierarchyEntry*>(entry)->level());
-
- if (entryLevel < currentRootLevel)
+ case CommandEntry::Type:
+ content = static_cast<CommandEntry*>(target)->command();
break;
-
- if (entryLevel == currentRootLevel)
- {
- hasPreviousSibling = true;
+ case MarkdownEntry::Type:
+ content = static_cast<MarkdownEntry*>(target)->plainText();
break;
- }
- }
-
- 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)
- {
- updateHierarchyLayout();
- if (expanded)
- updateLayout();
- return;
- }
+ case TextEntry::Type:
+ content = static_cast<TextEntry*>(target)->text();
+ break;
+ case LatexEntry::Type:
+ content = static_cast<LatexEntry*>(target)->plain();
}
- }
- const auto shiftHierarchyEntry = [levelDelta](HierarchyEntry* hierarchyEntry)
- {
- if (!hierarchyEntry)
- return;
+ auto* newEntry = WorksheetEntry::create(newType, this);
+ if (newEntry)
+ {
+ newEntry->setContent(content);
+ auto* tmp = target;
- const int newLevel = static_cast<int>(hierarchyEntry->level()) + levelDelta;
+ newEntry->setPrevious(tmp->previous());
+ newEntry->setNext(tmp->next());
- hierarchyEntry->setLevel(static_cast<HierarchyEntry::HierarchyLevel>(newLevel));
- };
+ tmp->setPrevious(nullptr);
+ tmp->setNext(nullptr);
+ tmp->clearFocus();
+ tmp->forceRemove();
- shiftHierarchyEntry(targetEntry);
+ if (newEntry->previous())
+ newEntry->previous()->setNext(newEntry);
+ else
+ setFirstEntry(newEntry);
- for (auto* entry : subentries)
- {
- if (!entry || entry->type() != HierarchyEntry::Type)
- continue;
+ if (newEntry->next())
+ newEntry->next()->setPrevious(newEntry);
+ else
+ setLastEntry(newEntry);
- shiftHierarchyEntry(static_cast<HierarchyEntry*>(entry));
+ if (newType == HierarchyEntry::Type || targetEntryType == HierarchyEntry::Type)
+ updateHierarchyLayout();
+ updateLayout();
+ makeVisible(newEntry);
+ focusEntry(newEntry);
+ setModified();
+ newEntry->focusEntry();
+ }
+ m_animationsEnabled = animation_state;
}
-
- updateHierarchyLayout();
- updateLayout();
-
- setModified();
}
-void Worksheet::deleteHierarchyEntry(const QString& hierarchyId, bool deleteContents)
+bool Worksheet::isValidEntry(WorksheetEntry* entry)
{
- if (m_readOnly || hierarchyId.isEmpty())
- return;
+ for (auto* iter = firstEntry(); iter; iter = iter->next())
+ if (entry == iter)
+ return true;
- const HierarchySearchResult hierarchySearch = findHierarchyEntryById(hierarchyId);
- HierarchyEntry* targetEntry = hierarchySearch.entry;
+ return false;
+}
- if (!targetEntry)
+void Worksheet::selectionRemove()
+{
+ if (m_selectedEntries.isEmpty())
return;
- QString headingLabel = targetEntry->text().trimmed();
-
- if (headingLabel.isEmpty())
- headingLabel = targetEntry->hierarchyText();
-
- QString warningText;
- QString dialogTitle;
-
- if (deleteContents)
- {
- 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
- {
- 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");
- }
-
if (Settings::warnAboutEntryDelete())
{
- const auto result = KMessageBox::warningTwoActions(
- worksheetView(),
- warningText,
- dialogTitle,
+ 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());
@@ -4137,204 +2981,194 @@ void Worksheet::deleteHierarchyEntry(const QString& hierarchyId, bool deleteCont
return;
}
- expandHierarchyAncestors(hierarchySearch.collapsedAncestors);
- expandHierarchyForStructureChange(targetEntry);
-
- const std::vector<WorksheetEntry*>subentries = hierarchySubelements(targetEntry);
-
- clearAllSelections();
- notifyEntryFocus(nullptr);
-
- QList<WorksheetEntry*> entriesToRemove;
- entriesToRemove.append(targetEntry);
-
- if (deleteContents)
- {
- for (auto* entry : subentries)
- entriesToRemove.append(entry);
- }
- else
+ for (auto* entry : m_selectedEntries)
{
- const int minimumLevel = static_cast<int>(HierarchyEntry::HierarchyLevel::Chapter);
-
- for (auto* entry : subentries)
- {
- 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));
- }
+ if (isValidEntry(entry))
+ entry->startRemoving(false);
}
- WorksheetEntry* previousEntry = targetEntry->previous();
- WorksheetEntry* lastRemovedEntry = entriesToRemove.constLast();
- WorksheetEntry* nextEntry = lastRemovedEntry->next();
-
- if (previousEntry)
- previousEntry->setNext(nextEntry);
- else
- setFirstEntry(nextEntry);
+ m_selectedEntries.clear();
+}
- if (nextEntry)
- nextEntry->setPrevious(previousEntry);
- else
- setLastEntry(previousEntry);
+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();
+}
- clearFocus();
+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();
+}
- updateFocusedTextItem(static_cast<WorksheetTextItem*>(nullptr));
- updateFocusedTextItem(static_cast<WorksheetTextEditorItem*>(nullptr));
+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();
+}
- for (auto* entry : entriesToRemove)
+void Worksheet::notifyEntryFocus(WorksheetEntry* entry)
+{
+ if (entry)
{
- if (!entry)
- continue;
-
- entry->setPrevious(nullptr);
- entry->setNext(nullptr);
- entry->clearFocus();
- entry->hide();
- entry->deleteLater();
+ m_circularFocusBuffer.enqueue(entry);
+
+ if (m_circularFocusBuffer.size() > 2)
+ m_circularFocusBuffer.dequeue();
}
+ else
+ m_circularFocusBuffer.clear();
+}
- WorksheetEntry* focusTarget = nextEntry ? nextEntry : previousEntry;
+void Worksheet::collapseAllResults()
+{
+ for (auto* entry = firstEntry(); entry; entry = entry->next())
+ if (entry->type() == CommandEntry::Type)
+ static_cast<CommandEntry*>(entry)->collapseResults();
+}
- if (!firstEntry())
- focusTarget = appendCommandEntry();
+void Worksheet::uncollapseAllResults()
+{
+ for (auto* entry = firstEntry(); entry; entry = entry->next())
+ if (entry->type() == CommandEntry::Type)
+ static_cast<CommandEntry*>(entry)->expandResults();
+}
- updateHierarchyLayout();
- updateLayout();
+void Worksheet::removeAllResults()
+{
+ bool remove = false;
- if (focusTarget)
+ if (KMessageBox::shouldBeShownContinue(QLatin1String("WarnAboutAllResultsRemoving")))
{
- focusTarget->focusEntry();
- makeVisible(focusTarget);
- updateCurrentHierarchyFromEntry(focusTarget);
+ 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);
}
else
+ remove = true;
+
+ if (remove)
{
- m_hierarchyTrackingSource = HierarchyTrackingSource::FocusedEntry;
- updateCurrentHierarchy(nullptr);
+ for (auto *entry = firstEntry(); entry; entry = entry->next())
+ if (entry->type() == CommandEntry::Type)
+ static_cast<CommandEntry*>(entry)->removeResults();
}
-
- resetEntryCursor();
- setModified();
}
-WorksheetEntry* Worksheet::cutSubentriesForHierarchy(HierarchyEntry* hierarchyEntry)
+void Worksheet::addToExectuionSelection()
{
- if (!hierarchyEntry || !hierarchyEntry->next())
- return nullptr;
+ for (auto* entry : m_selectedEntries)
+ if (entry->type() == CommandEntry::Type)
+ static_cast<CommandEntry*>(entry)->addToExecution();
+}
- 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;
+void Worksheet::excludeFromExecutionSelection()
+{
+ for (auto* entry : m_selectedEntries)
+ if (entry->type() == CommandEntry::Type)
+ static_cast<CommandEntry*>(entry)->excludeFromExecution();
+}
- const int hierarchyLevel = static_cast<int>(hierarchyEntry->level());
+void Worksheet::collapseSelectionResults()
+{
+ for (auto* entry : m_selectedEntries)
+ if (entry->type() == CommandEntry::Type)
+ static_cast<CommandEntry*>(entry)->collapseResults();
+}
- while (cutEnd->next())
- {
- WorksheetEntry* candidate = cutEnd->next();
+void Worksheet::uncollapseSelectionResults()
+{
+ for (auto* entry : m_selectedEntries)
+ if (entry->type() == CommandEntry::Type)
+ static_cast<CommandEntry*>(entry)->expandResults();
+}
- if (candidate->type() == HierarchyEntry::Type)
- {
- const int candidateLevel = static_cast<int>(static_cast<HierarchyEntry*>(candidate)->level());
+void Worksheet::removeSelectionResults()
+{
+ for (auto* entry : m_selectedEntries)
+ if (entry->type() == CommandEntry::Type)
+ static_cast<CommandEntry*>(entry)->removeResults();
+}
- if (candidateLevel <= hierarchyLevel)
- break;
- }
+void Worksheet::navigateToTocNode(QString nodeId)
+{
+ m_hierarchyManager->navigateToTocNode(nodeId);
+}
- cutEnd = candidate;
- }
+void Worksheet::updateCurrentTocNodeFromResult(CommandEntry* commandEntry, Cantor::Result* result)
+{
+ m_hierarchyManager->updateCurrentTocNodeFromResult(commandEntry, result);
+}
- WorksheetEntry* entryAfterSection = cutEnd->next();
+void Worksheet::renamePlot(const QString& commandId, const QString& resultId, const QString& newTitle)
+{
+ m_hierarchyManager->renamePlot(commandId, resultId, newTitle);
+}
- hierarchyEntry->setNext(entryAfterSection);
+void Worksheet::deletePlot(const QString& commandId, const QString& resultId)
+{
+ m_hierarchyManager->deletePlot(commandId, resultId);
+}
- if (entryAfterSection)
- entryAfterSection->setPrevious(hierarchyEntry);
- else
- setLastEntry(hierarchyEntry);
+void Worksheet::renameHierarchyEntry(const QString& hierarchyId, const QString& newName)
+{
+ m_hierarchyManager->renameHierarchyEntry(hierarchyId, newName);
+}
- cutBegin->setPrevious(nullptr);
- cutEnd->setNext(nullptr);
+void Worksheet::changeHierarchyLevel(QString hierarchyId, int levelDelta)
+{
+ m_hierarchyManager->changeHierarchyLevel(hierarchyId, levelDelta);
+}
- for (auto* entry = cutBegin; entry; entry = entry->next())
- entry->hide();
+void Worksheet::deleteHierarchyEntry(const QString& hierarchyId, bool deleteContents)
+{
+ m_hierarchyManager->deleteHierarchyEntry(hierarchyId, deleteContents);
+}
- return cutBegin;
+WorksheetEntry* Worksheet::cutSubentriesForHierarchy(HierarchyEntry* hierarchyEntry)
+{
+ return m_hierarchyManager->cutSubentriesForHierarchy(hierarchyEntry);
}
void Worksheet::insertSubentriesForHierarchy(HierarchyEntry* hierarchyEntry, WorksheetEntry* storedSubentriesBegin)
{
- if (!hierarchyEntry || !storedSubentriesBegin)
- return;
-
- WorksheetEntry* previousNext = hierarchyEntry->next();
-
- hierarchyEntry->setNext(storedSubentriesBegin);
- storedSubentriesBegin->setPrevious(hierarchyEntry);
-
- WorksheetEntry* storedEnd = storedSubentriesBegin;
-
- for (auto* entry = storedSubentriesBegin; entry; entry = entry->next())
- {
- entry->show();
- storedEnd = entry;
- }
-
- storedEnd->setNext(previousNext);
-
- if (previousNext)
- previousNext->setPrevious(storedEnd);
- else
- setLastEntry(storedEnd);
+ m_hierarchyManager->insertSubentriesForHierarchy(hierarchyEntry, storedSubentriesBegin);
}
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;
+ return m_hierarchyManager->expandHierarchyForStructureChange(hierarchyEntry);
}
void Worksheet::handleSettingsChanges()
diff --git a/src/worksheet.h b/src/worksheet.h
index 6fb142da..262f90d1 100644
--- a/src/worksheet.h
+++ b/src/worksheet.h
@@ -17,8 +17,6 @@
#include <QQueue>
#include <QVariantList>
-#include <functional>
-
#include "lib/renderer.h"
#include "mathrender.h"
#include "worksheetcursor.h"
@@ -34,6 +32,7 @@ class WorksheetEntry;
class CommandEntry;
class WorksheetView;
class HierarchyEntry;
+class WorksheetHierarchyManager;
class PlaceHolderEntry;
class WorksheetTextItem;
@@ -59,12 +58,6 @@ class Worksheet : public QGraphicsScene
JupyterNotebook
};
- enum class HierarchyTrackingSource
- {
- Viewport,
- FocusedEntry
- };
-
Worksheet(Cantor::Backend*, QWidget*, bool useDeafultWorksheetParameters = true);
~Worksheet() override;
@@ -325,38 +318,7 @@ 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);
+ friend class WorksheetHierarchyManager;
private Q_SLOTS:
//void checkEntriesForSanity();
@@ -390,9 +352,6 @@ class Worksheet : public QGraphicsScene
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;
@@ -401,6 +360,8 @@ class Worksheet : public QGraphicsScene
static const double EntryCursorLength;
static const double EntryCursorWidth;
+ WorksheetHierarchyManager* m_hierarchyManager{nullptr};
+
Cantor::Session* m_session{nullptr};
Cantor::Renderer m_renderer;
MathRenderer m_mathRenderer;
@@ -461,9 +422,6 @@ class Worksheet : public QGraphicsScene
QVector<WorksheetEntry*> m_selectedEntries;
QQueue<WorksheetEntry*> m_circularFocusBuffer;
- size_t m_hierarchyMaxDepth{0};
- QVariantList m_tocNodeSnapshot;
- bool m_tocRefreshScheduled{false};
};
#endif // WORKSHEET_H
diff --git a/src/worksheethierarchymanager.cpp b/src/worksheethierarchymanager.cpp
new file mode 100644
index 00000000..c97edc87
--- /dev/null
+++ b/src/worksheethierarchymanager.cpp
@@ -0,0 +1,1308 @@
+#include "worksheethierarchymanager.h"
+
+#include "commandentry.h"
+#include "hierarchyentry.h"
+#include "placeholderentry.h"
+#include "resultitem.h"
+#include "settings.h"
+#include "worksheet.h"
+#include "worksheetentry.h"
+#include "worksheettextitem.h"
+#include "worksheetview.h"
+#include "lib/animationresult.h"
+#include "lib/expression.h"
+#include "lib/imageresult.h"
+#include "lib/pdfresult.h"
+#include "lib/result.h"
+
+#include <KLocalizedString>
+#include <KMessageBox>
+#include <KStandardGuiItem>
+
+#include <QList>
+#include <QRectF>
+#include <QSet>
+#include <QTimer>
+#include <QVariantMap>
+
+#include <algorithm>
+
+namespace
+{
+ const QLatin1String TocNodeTypeChapter("chapter");
+ const QLatin1String TocNodeTypeSection("section");
+ const QLatin1String TocNodeTypeCommand("command");
+ const QLatin1String TocNodeTypePlot("plot");
+}
+
+WorksheetHierarchyManager::WorksheetHierarchyManager(Worksheet* worksheet)
+ : QObject(worksheet)
+ , m_worksheet(worksheet)
+{
+ Q_ASSERT(m_worksheet);
+}
+
+size_t WorksheetHierarchyManager::hierarchyMaxDepth() const
+{
+ return m_hierarchyMaxDepth;
+}
+
+void WorksheetHierarchyManager::refreshTocStructure()
+{
+ if (m_worksheet->m_isClosing || m_worksheet->m_isLoadingFromFile)
+ return;
+
+ m_tocRefreshScheduled = false;
+ m_tocNodeSnapshot = collectTocNodes();
+
+ if (!m_currentTocNodeId.isEmpty())
+ {
+ bool currentNodeStillExists = false;
+ for (const QVariant& nodeValue : m_tocNodeSnapshot)
+ {
+ if (nodeValue.toMap().value(QStringLiteral("id")).toString() == m_currentTocNodeId)
+ {
+ currentNodeStillExists = true;
+ break;
+ }
+ }
+
+ if (!currentNodeStillExists)
+ setCurrentTocNode(QString());
+ }
+
+ Q_EMIT m_worksheet->tocNodesChanged(m_tocNodeSnapshot);
+}
+
+void WorksheetHierarchyManager::scheduleTocStructureRefresh()
+{
+ if (m_worksheet->m_isClosing || m_worksheet->m_isLoadingFromFile || m_tocRefreshScheduled)
+ return;
+
+ m_tocRefreshScheduled = true;
+ QTimer::singleShot(0, this, [this]()
+ {
+ if (!m_tocRefreshScheduled)
+ return;
+
+ refreshTocStructure();
+ });
+}
+
+void WorksheetHierarchyManager::emitTocNodeSnapshot()
+{
+ if (m_worksheet->m_isClosing)
+ return;
+
+ if (m_tocNodeSnapshot.isEmpty() && m_worksheet->firstEntry())
+ m_tocNodeSnapshot = collectTocNodes();
+
+ Q_EMIT m_worksheet->tocNodesChanged(m_tocNodeSnapshot);
+}
+
+QVariantList WorksheetHierarchyManager::collectTocNodes() const
+{
+ QVariantList nodes;
+ QVector<QString> hierarchyNodeIds;
+ QVector<int> hierarchyDepths;
+
+ visitLogicalEntries([&](WorksheetEntry* entry)
+ {
+ if (entry->type() == HierarchyEntry::Type)
+ {
+ 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)
+ {
+ hierarchyDepths.removeLast();
+ hierarchyNodeIds.removeLast();
+ }
+
+ 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())
+ {
+ 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;
+ });
+
+ return nodes;
+}
+
+QString WorksheetHierarchyManager::buildCommandNodeId(CommandEntry* entry) const
+{
+ if (!entry)
+ return QString();
+
+ const QString& commandId = entry->commandId();
+ if (commandId.isEmpty())
+ return QString();
+
+ return QStringLiteral("command:") + commandId;
+}
+
+QString WorksheetHierarchyManager::buildPlotNodeId(const QString& commandId, const QString& resultId) const
+{
+ if (commandId.isEmpty() || resultId.isEmpty())
+ return QString();
+
+ return QStringLiteral("plot:%1:%2").arg(commandId, resultId);
+}
+
+bool WorksheetHierarchyManager::parseCommandNodeId(const QString& nodeId, QString* commandId) const
+{
+ const QString prefix = QStringLiteral("command:");
+ if (!nodeId.startsWith(prefix))
+ return false;
+
+ const QString parsedCommandId = nodeId.mid(prefix.size());
+ if (parsedCommandId.isEmpty() || parsedCommandId.contains(QLatin1Char(':')))
+ return false;
+
+ if (commandId)
+ *commandId = parsedCommandId;
+
+ return true;
+}
+
+bool WorksheetHierarchyManager::parsePlotNodeId(const QString& nodeId, QString* commandId, QString* resultId) const
+{
+ const QString prefix = QStringLiteral("plot:");
+ if (!nodeId.startsWith(prefix))
+ return false;
+
+ const QStringList parts = nodeId.mid(prefix.size()).split(QLatin1Char(':'));
+ if (parts.size() != 2 || parts.at(0).isEmpty() || parts.at(1).isEmpty())
+ return false;
+
+ if (commandId)
+ *commandId = parts.at(0);
+ if (resultId)
+ *resultId = parts.at(1);
+
+ return true;
+}
+
+QString WorksheetHierarchyManager::commandTocTitle() const
+{
+ return i18n("Command");
+}
+
+QString WorksheetHierarchyManager::commandTocDisplayText(CommandEntry* entry) const
+{
+ auto* expression = entry ? entry->expression() : nullptr;
+ const QString title = commandTocTitle();
+ if (m_worksheet->m_showExpressionIds && expression && expression->id() != -1)
+ return i18n("%1 %2", title, expression->id());
+
+ return title;
+}
+
+QString WorksheetHierarchyManager::plotTocTitle(Cantor::Result* result) const
+{
+ if (result && !result->displayName().isEmpty())
+ return result->displayName();
+
+ return i18n("Plot");
+}
+
+QString WorksheetHierarchyManager::plotTocDisplayText(CommandEntry* entry, Cantor::Result* result, int plotOrdinal, int plotCount) const
+{
+ const QString title = plotTocTitle(result);
+ auto* expression = entry ? entry->expression() : nullptr;
+ if (m_worksheet->m_showExpressionIds && expression && expression->id() != -1)
+ {
+ if (plotCount > 1)
+ return i18n("%1 %2.%3", title, expression->id(), plotOrdinal);
+
+ return i18n("%1 %2", title, expression->id());
+ }
+
+ if (plotCount > 1)
+ return i18n("%1 %2", title, plotOrdinal);
+
+ return title;
+}
+
+bool WorksheetHierarchyManager::isPlotResult(Cantor::Result* result) const
+{
+ if (!result || result->role() != Cantor::Result::Role::Plot)
+ return false;
+
+ return result->type() == Cantor::ImageResult::Type
+ || result->type() == Cantor::AnimationResult::Type
+ || result->type() == Cantor::PdfResult::Type;
+}
+
+bool WorksheetHierarchyManager::visitLogicalEntries(WorksheetEntry* first, const std::function<bool(WorksheetEntry*)>& visitor) const
+{
+ 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;
+}
+
+bool WorksheetHierarchyManager::visitLogicalEntries(const std::function<bool(WorksheetEntry*)>& visitor) const
+{
+ return visitLogicalEntries(m_worksheet->firstEntry(), visitor);
+}
+
+bool WorksheetHierarchyManager::findHierarchyEntryById(WorksheetEntry* first, const QString& hierarchyId, QVector<HierarchyEntry*> collapsedAncestors, HierarchySearchResult& result) const
+{
+ for (auto* entry = first; entry; entry = entry->next())
+ {
+ if (entry->type() != HierarchyEntry::Type)
+ continue;
+
+ auto* hierarchyEntry = static_cast<HierarchyEntry*>(entry);
+
+ if (hierarchyEntry->hierarchyId() == hierarchyId)
+ {
+ result.entry = hierarchyEntry;
+ result.collapsedAncestors = collapsedAncestors;
+ return true;
+ }
+
+ if (auto* hiddenEntry = hierarchyEntry->hiddenSubentries())
+ {
+ auto childAncestors = collapsedAncestors;
+ childAncestors.append(hierarchyEntry);
+
+ if (findHierarchyEntryById(hiddenEntry, hierarchyId, childAncestors, result))
+ return true;
+ }
+ }
+
+ return false;
+}
+
+WorksheetHierarchyManager::HierarchySearchResult WorksheetHierarchyManager::findHierarchyEntryById(const QString& hierarchyId) const
+{
+ HierarchySearchResult result;
+
+ if (hierarchyId.isEmpty())
+ return result;
+
+ findHierarchyEntryById(m_worksheet->firstEntry(), hierarchyId, {}, result);
+ return result;
+}
+
+bool WorksheetHierarchyManager::findCommandEntryById(WorksheetEntry* first, const QString& commandId, QVector<HierarchyEntry*> collapsedAncestors, CommandSearchResult& result) const
+{
+ 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;
+ }
+ }
+
+ 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 false;
+}
+
+WorksheetHierarchyManager::CommandSearchResult WorksheetHierarchyManager::findCommandEntryById(const QString& commandId) const
+{
+ CommandSearchResult result;
+
+ if (commandId.isEmpty())
+ return result;
+
+ findCommandEntryById(m_worksheet->firstEntry(), commandId, {}, result);
+ return result;
+}
+
+bool WorksheetHierarchyManager::expandHierarchyAncestors(const QVector<HierarchyEntry*>& ancestors)
+{
+ bool expanded = false;
+
+ for (auto* ancestor : ancestors)
+ {
+ if (!ancestor || !ancestor->hasHiddenSubentries())
+ continue;
+
+ WorksheetEntry* hiddenSubentries = ancestor->takeHiddenSubentries();
+
+ if (!hiddenSubentries)
+ continue;
+
+ insertSubentriesForHierarchy(ancestor, hiddenSubentries);
+ expanded = true;
+ }
+
+ return expanded;
+}
+
+void WorksheetHierarchyManager::updateHierarchyLayout()
+{
+ QSet<QString> usedHierarchyIds;
+ QSet<QString> usedCommandIds;
+ QSet<QString> usedResultIds;
+
+ m_hierarchyMaxDepth = 0;
+ std::vector<int> hierarchyNumbers;
+
+ 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 WorksheetHierarchyManager::updateHierarchyControlsLayout(WorksheetEntry* startEntry)
+{
+ Q_UNUSED(startEntry);
+
+ std::vector<HierarchyEntry*> levelEntries;
+ const int numerationBegin = static_cast<int>(HierarchyEntry::HierarchyLevel::Chapter);
+ const int numerationEnd = static_cast<int>(HierarchyEntry::HierarchyLevel::EndValue);
+
+ for (int i = numerationBegin; i < numerationEnd; ++i)
+ levelEntries.push_back(nullptr);
+
+ WorksheetEntry* lastRealEntry = nullptr;
+
+ for (auto* entry = m_worksheet->firstEntry(); entry; entry = entry->next())
+ {
+ if (entry->type() == PlaceHolderEntry::Type || entry->aboutToBeRemoved())
+ continue;
+
+ lastRealEntry = entry;
+
+ if (entry->type() != HierarchyEntry::Type)
+ continue;
+
+ auto* hierarchyEntry = static_cast<HierarchyEntry*>(entry);
+
+ const int index = static_cast<int>(hierarchyEntry->level()) - numerationBegin;
+
+ if (index < 0 || index >= static_cast<int>(levelEntries.size()))
+ continue;
+
+ if (!levelEntries[index])
+ {
+ levelEntries[index] = hierarchyEntry;
+ continue;
+ }
+
+ // Close previous controls at this level and below.
+ for (int i = index; i < static_cast<int>(levelEntries.size()); ++i)
+ {
+ auto* openEntry = levelEntries[i];
+
+ 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);
+
+ levelEntries[i] = nullptr;
+ }
+
+ levelEntries[index] = hierarchyEntry;
+ }
+
+ if (!lastRealEntry)
+ return;
+
+ 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);
+ }
+}
+
+std::vector<WorksheetEntry*> WorksheetHierarchyManager::hierarchySubelements(HierarchyEntry* hierarchyEntry) const
+{
+ std::vector<WorksheetEntry*> subentries;
+
+ Q_ASSERT(hierarchyEntry);
+
+ 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;
+}
+
+WorksheetEntry* WorksheetHierarchyManager::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
+ m_worksheet->setLastEntry(hierarchyEntry);
+
+ cutBegin->setPrevious(nullptr);
+ cutEnd->setNext(nullptr);
+
+ for (auto* entry = cutBegin; entry; entry = entry->next())
+ entry->hide();
+
+ return cutBegin;
+}
+
+void WorksheetHierarchyManager::insertSubentriesForHierarchy(HierarchyEntry* hierarchyEntry, WorksheetEntry* storedSubentriesBegin)
+{
+ if (!hierarchyEntry || !storedSubentriesBegin)
+ return;
+
+ WorksheetEntry* previousNext = hierarchyEntry->next();
+
+ hierarchyEntry->setNext(storedSubentriesBegin);
+ storedSubentriesBegin->setPrevious(hierarchyEntry);
+
+ WorksheetEntry* storedEnd = storedSubentriesBegin;
+
+ for (auto* entry = storedSubentriesBegin; entry; entry = entry->next())
+ {
+ entry->show();
+ storedEnd = entry;
+ }
+
+ storedEnd->setNext(previousNext);
+
+ if (previousNext)
+ previousNext->setPrevious(storedEnd);
+ else
+ m_worksheet->setLastEntry(storedEnd);
+}
+
+bool WorksheetHierarchyManager::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 WorksheetHierarchyManager::navigateToTocNode(QString nodeId)
+{
+ QString plotCommandId;
+ QString plotResultId;
+ if (parsePlotNodeId(nodeId, &plotCommandId, &plotResultId))
+ {
+ const CommandSearchResult commandSearch = findCommandEntryById(plotCommandId);
+ if (!commandSearch.entry)
+ {
+ scheduleTocStructureRefresh();
+ return;
+ }
+
+ const bool expanded = expandHierarchyAncestors(commandSearch.collapsedAncestors);
+ if (expanded)
+ {
+ updateHierarchyLayout();
+ m_worksheet->updateLayout();
+ }
+
+ 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();
+ m_worksheet->updateLayout();
+ }
+
+ auto* commandEntry = commandSearch.entry;
+ updateCurrentHierarchyFromEntry(commandEntry);
+
+ m_worksheet->worksheetView()->scrollTo(qRound(commandEntry->scenePos().y()));
+ m_worksheet->worksheetView()->setFocus();
+ commandEntry->focusEntry(WorksheetTextItem::TopLeft);
+
+ m_worksheet->resetEntryCursor();
+ return;
+ }
+
+ const HierarchySearchResult hierarchySearch = findHierarchyEntryById(nodeId);
+ auto* hierarchyEntry = hierarchySearch.entry;
+
+ if (hierarchyEntry)
+ {
+ const bool expanded = expandHierarchyAncestors(hierarchySearch.collapsedAncestors);
+ if (expanded)
+ {
+ updateHierarchyLayout();
+ m_worksheet->updateLayout();
+ }
+
+ updateCurrentHierarchyFromEntry(hierarchyEntry);
+
+ m_worksheet->worksheetView()->scrollTo(qRound(hierarchyEntry->scenePos().y()));
+ m_worksheet->worksheetView()->setFocus();
+ hierarchyEntry->focusEntry(WorksheetTextItem::BottomRight);
+
+ m_worksheet->resetEntryCursor();
+ }
+}
+
+bool WorksheetHierarchyManager::navigateToPlotResult(CommandEntry* commandEntry, const QString& resultId)
+{
+ if (!commandEntry || resultId.isEmpty())
+ return false;
+
+ if (commandEntry->isResultCollapsed())
+ {
+ commandEntry->expandResults();
+ m_worksheet->updateLayout();
+ }
+
+ auto* resultItem = commandEntry->resultItemById(resultId);
+ if (!resultItem || !resultItem->result() || !isPlotResult(resultItem->result()))
+ return false;
+
+ QGraphicsObject* object = resultItem->graphicsObject();
+ if (!object)
+ return false;
+
+ const QString nodeId = buildPlotNodeId(commandEntry->commandId(), resultId);
+
+ m_worksheet->worksheetView()->scrollTo(qRound(object->sceneBoundingRect().top()));
+ m_worksheet->worksheetView()->setFocus();
+ object->setFocus();
+
+ m_trackingSource = TrackingSource::FocusedEntry;
+ setCurrentTocNode(nodeId);
+
+ m_worksheet->resetEntryCursor();
+ return true;
+}
+
+void WorksheetHierarchyManager::updateCurrentTocNodeFromResult(CommandEntry* commandEntry, Cantor::Result* result)
+{
+ if (!commandEntry || !result || !m_worksheet->isValidEntry(commandEntry))
+ return;
+
+ m_trackingSource = TrackingSource::FocusedEntry;
+
+ if (isPlotResult(result))
+ setCurrentTocNode(buildPlotNodeId(commandEntry->commandId(), result->resultId()));
+ else
+ updateCurrentHierarchyFromEntry(commandEntry);
+}
+
+void WorksheetHierarchyManager::renamePlot(const QString& commandId, const QString& resultId, const QString& newTitle)
+{
+ if (m_worksheet->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())
+ {
+ if (!result || result->resultId() != resultId || !isPlotResult(result))
+ continue;
+
+ if (result->displayName() == normalizedTitle)
+ return;
+
+ result->setDisplayName(normalizedTitle);
+ m_worksheet->setModified();
+ scheduleTocStructureRefresh();
+ return;
+ }
+}
+
+void WorksheetHierarchyManager::deletePlot(const QString& commandId, const QString& resultId)
+{
+ if (m_worksheet->m_readOnly || commandId.isEmpty() || resultId.isEmpty())
+ return;
+
+ const CommandSearchResult commandSearch = findCommandEntryById(commandId);
+ auto* commandEntry = commandSearch.entry;
+ auto* expression = commandEntry ? commandEntry->expression() : nullptr;
+
+ if (!commandEntry || !expression)
+ return;
+
+ for (auto* result : expression->results())
+ {
+ if (!result || result->resultId() != resultId || !isPlotResult(result))
+ continue;
+
+ const QString plotNodeId = buildPlotNodeId(commandId, resultId);
+ const bool wasCurrentNode = m_currentTocNodeId == plotNodeId;
+
+ expression->removeResult(result);
+
+ if (wasCurrentNode)
+ setCurrentTocNode(buildCommandNodeId(commandEntry));
+
+ m_worksheet->setModified();
+ scheduleTocStructureRefresh();
+ return;
+ }
+}
+
+void WorksheetHierarchyManager::renameHierarchyEntry(const QString& hierarchyId, const QString& newName)
+{
+ if (m_worksheet->m_readOnly || hierarchyId.isEmpty())
+ return;
+
+ 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 WorksheetHierarchyManager::changeHierarchyLevel(const QString& hierarchyId, int levelDelta)
+{
+ if (m_worksheet->m_readOnly || hierarchyId.isEmpty() || (levelDelta != -1 && levelDelta != 1))
+ return;
+
+ 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)
+ {
+ if (currentRootLevel <= minimumLevel)
+ return;
+
+ expanded = expandHierarchyAncestors(hierarchySearch.collapsedAncestors);
+ }
+ else
+ {
+ if (currentRootLevel >= maximumLevel)
+ return;
+
+ expanded = expandHierarchyAncestors(hierarchySearch.collapsedAncestors);
+
+ bool hasPreviousSibling = false;
+
+ for (auto* entry = targetEntry->previous(); entry; entry = entry->previous())
+ {
+ if (entry->type() != HierarchyEntry::Type)
+ continue;
+
+ const int entryLevel = static_cast<int>(static_cast<HierarchyEntry*>(entry)->level());
+
+ if (entryLevel < currentRootLevel)
+ break;
+
+ if (entryLevel == currentRootLevel)
+ {
+ hasPreviousSibling = true;
+ break;
+ }
+ }
+
+ if (!hasPreviousSibling)
+ {
+ if (expanded)
+ {
+ updateHierarchyLayout();
+ m_worksheet->updateLayout();
+ }
+
+ 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)
+ {
+ updateHierarchyLayout();
+ if (expanded)
+ m_worksheet->updateLayout();
+ return;
+ }
+ }
+ }
+
+ const auto shiftHierarchyEntry = [levelDelta](HierarchyEntry* hierarchyEntry)
+ {
+ if (!hierarchyEntry)
+ return;
+
+ 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));
+ }
+
+ updateHierarchyLayout();
+ m_worksheet->updateLayout();
+
+ m_worksheet->setModified();
+}
+
+void WorksheetHierarchyManager::deleteHierarchyEntry(const QString& hierarchyId, bool deleteContents)
+{
+ if (m_worksheet->m_readOnly || hierarchyId.isEmpty())
+ return;
+
+ 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)
+ {
+ 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
+ {
+ 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");
+ }
+
+ if (Settings::warnAboutEntryDelete())
+ {
+ const auto result = KMessageBox::warningTwoActions(
+ m_worksheet->worksheetView(),
+ warningText,
+ dialogTitle,
+ KStandardGuiItem::remove(),
+ KStandardGuiItem::cancel());
+
+ if (result != KMessageBox::PrimaryAction)
+ return;
+ }
+
+ expandHierarchyAncestors(hierarchySearch.collapsedAncestors);
+ expandHierarchyForStructureChange(targetEntry);
+
+ const std::vector<WorksheetEntry*> subentries = hierarchySubelements(targetEntry);
+
+ m_worksheet->clearAllSelections();
+ m_worksheet->notifyEntryFocus(nullptr);
+
+ QList<WorksheetEntry*> entriesToRemove;
+ entriesToRemove.append(targetEntry);
+
+ if (deleteContents)
+ {
+ for (auto* entry : subentries)
+ entriesToRemove.append(entry);
+ }
+ else
+ {
+ const int minimumLevel = static_cast<int>(HierarchyEntry::HierarchyLevel::Chapter);
+
+ for (auto* entry : subentries)
+ {
+ 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* previousEntry = targetEntry->previous();
+ WorksheetEntry* lastRemovedEntry = entriesToRemove.constLast();
+ WorksheetEntry* nextEntry = lastRemovedEntry->next();
+
+ if (previousEntry)
+ previousEntry->setNext(nextEntry);
+ else
+ m_worksheet->setFirstEntry(nextEntry);
+
+ if (nextEntry)
+ nextEntry->setPrevious(previousEntry);
+ else
+ m_worksheet->setLastEntry(previousEntry);
+
+ m_worksheet->clearFocus();
+
+ m_worksheet->updateFocusedTextItem(static_cast<WorksheetTextItem*>(nullptr));
+ m_worksheet->updateFocusedTextItem(static_cast<WorksheetTextEditorItem*>(nullptr));
+
+ for (auto* entry : entriesToRemove)
+ {
+ if (!entry)
+ continue;
+
+ entry->setPrevious(nullptr);
+ entry->setNext(nullptr);
+ entry->clearFocus();
+ entry->hide();
+ entry->deleteLater();
+ }
+
+ WorksheetEntry* focusTarget = nextEntry ? nextEntry : previousEntry;
+
+ if (!m_worksheet->firstEntry())
+ focusTarget = m_worksheet->appendCommandEntry();
+
+ updateHierarchyLayout();
+ m_worksheet->updateLayout();
+
+ if (focusTarget)
+ {
+ focusTarget->focusEntry();
+ m_worksheet->makeVisible(focusTarget);
+ updateCurrentHierarchyFromEntry(focusTarget);
+ }
+ else
+ {
+ m_trackingSource = TrackingSource::FocusedEntry;
+ updateCurrentHierarchy(nullptr);
+ }
+
+ m_worksheet->resetEntryCursor();
+ m_worksheet->setModified();
+}
+
+QString WorksheetHierarchyManager::hierarchyIdForEntry(WorksheetEntry* entry) const
+{
+ for (auto* current = entry; current; current = current->previous())
+ {
+ if (current->type() == HierarchyEntry::Type)
+ return static_cast<HierarchyEntry*>(current)->hierarchyId();
+ }
+
+ return QString();
+}
+
+void WorksheetHierarchyManager::setCurrentTocNode(const QString& nodeId)
+{
+ if (m_currentTocNodeId == nodeId)
+ return;
+
+ m_currentTocNodeId = nodeId;
+
+ Q_EMIT m_worksheet->currentTocNodeChanged(nodeId);
+}
+
+void WorksheetHierarchyManager::updateCurrentHierarchy(WorksheetEntry* entry)
+{
+ QString nodeId;
+
+ if (entry && entry->type() == CommandEntry::Type)
+ nodeId = buildCommandNodeId(static_cast<CommandEntry*>(entry));
+ else
+ nodeId = hierarchyIdForEntry(entry);
+
+ setCurrentTocNode(nodeId);
+}
+
+void WorksheetHierarchyManager::updateCurrentHierarchyFromView(const QRectF& viewRect)
+{
+ if (m_trackingSource != TrackingSource::Viewport || m_worksheet->m_layoutUpdateInProgress || m_worksheet->m_isLoadingFromFile || viewRect.isEmpty())
+ return;
+
+ const qreal activationOffset = qMin<qreal>(48.0, viewRect.height() * 0.15);
+
+ const qreal activationY = viewRect.top() + activationOffset;
+
+ WorksheetEntry* activeEntry = nullptr;
+
+ for (auto* entry = m_worksheet->firstEntry(); entry; entry = entry->next())
+ {
+ if (!entry->isVisible())
+ continue;
+
+ if (entry->scenePos().y() > activationY)
+ break;
+
+ activeEntry = entry;
+ }
+
+ updateCurrentHierarchy(activeEntry);
+}
+
+void WorksheetHierarchyManager::updateCurrentHierarchyFromEntry(WorksheetEntry* entry)
+{
+ m_trackingSource = TrackingSource::FocusedEntry;
+
+ updateCurrentHierarchy(entry);
+}
+
+void WorksheetHierarchyManager::followHierarchyFromView()
+{
+ m_trackingSource = TrackingSource::Viewport;
+
+ // Defer until viewRect() reflects the final scroll position.
+ QTimer::singleShot(0, this, [this]() {
+ if (m_trackingSource != TrackingSource::Viewport)
+ return;
+
+ updateCurrentHierarchyFromView(m_worksheet->worksheetView()->viewRect());
+ });
+}
+
+void WorksheetHierarchyManager::normalizeDraggedHierarchyLevels(HierarchyEntry* rootEntry, WorksheetEntry* previousEntry, const std::vector<WorksheetEntry*>& subentries)
+{
+ if (!rootEntry)
+ return;
+
+ HierarchyEntry* previousHierarchyEntry = nullptr;
+
+ for (auto* entry = previousEntry; entry; entry = entry->previous())
+ {
+ if (entry->type() != HierarchyEntry::Type)
+ continue;
+
+ previousHierarchyEntry = static_cast<HierarchyEntry*>(entry);
+ break;
+ }
+
+ const int minimumLevel = static_cast<int>(HierarchyEntry::HierarchyLevel::Chapter);
+ const int maximumLevel = static_cast<int>(HierarchyEntry::HierarchyLevel::Subparagraph);
+
+ int maximumAllowedRootLevel = minimumLevel;
+
+ if (previousHierarchyEntry)
+ maximumAllowedRootLevel = qMin(maximumLevel, static_cast<int>(previousHierarchyEntry->level()) + 1);
+
+ const int oldRootLevel = static_cast<int>(rootEntry->level());
+ if (oldRootLevel <= maximumAllowedRootLevel)
+ return;
+
+ const int levelOffset = maximumAllowedRootLevel - oldRootLevel;
+
+ const auto shiftLevel = [levelOffset, minimumLevel, maximumLevel](HierarchyEntry* hierarchyEntry)
+ {
+ if (!hierarchyEntry)
+ return;
+
+ const int newLevel = qBound(minimumLevel, static_cast<int>(hierarchyEntry->level()) + levelOffset, maximumLevel);
+
+ hierarchyEntry->setLevel(static_cast<HierarchyEntry::HierarchyLevel>(newLevel));
+ };
+
+ shiftLevel(rootEntry);
+
+ for (auto* entry : subentries)
+ {
+ if (entry->type() != HierarchyEntry::Type)
+ continue;
+
+ shiftLevel(static_cast<HierarchyEntry*>(entry));
+ }
+}
diff --git a/src/worksheethierarchymanager.h b/src/worksheethierarchymanager.h
new file mode 100644
index 00000000..38a5db2a
--- /dev/null
+++ b/src/worksheethierarchymanager.h
@@ -0,0 +1,110 @@
+#ifndef WORKSHEETHIERARCHYMANAGER_H
+#define WORKSHEETHIERARCHYMANAGER_H
+
+#include <QObject>
+#include <QString>
+#include <QVariantList>
+#include <QVector>
+
+#include <functional>
+#include <vector>
+
+class QRectF;
+class Worksheet;
+class CommandEntry;
+class WorksheetEntry;
+class HierarchyEntry;
+
+namespace Cantor {
+ class Result;
+}
+
+/*
+ * Helper owned by Worksheet for managing the structure shown in the
+ * Table of Contents panel. This includes hierarchy entries, command
+ * entries, plot result nodes, navigation, and structure-related editing
+ * actions.
+ */
+class WorksheetHierarchyManager : public QObject
+{
+ public:
+ explicit WorksheetHierarchyManager(Worksheet* worksheet);
+
+ size_t hierarchyMaxDepth() const;
+
+ void refreshTocStructure();
+ void scheduleTocStructureRefresh();
+ void emitTocNodeSnapshot();
+
+ void updateHierarchyLayout();
+ void updateHierarchyControlsLayout(WorksheetEntry* startEntry = nullptr);
+
+ std::vector<WorksheetEntry*> hierarchySubelements(HierarchyEntry*) const;
+ WorksheetEntry* cutSubentriesForHierarchy(HierarchyEntry*);
+ void insertSubentriesForHierarchy(HierarchyEntry*, WorksheetEntry*);
+ bool expandHierarchyForStructureChange(HierarchyEntry*);
+
+ void renameHierarchyEntry(const QString& hierarchyId, const QString& newName);
+ void changeHierarchyLevel(const 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 updateCurrentHierarchy(WorksheetEntry* entry);
+ void updateCurrentHierarchyFromView(const QRectF& viewRect);
+ void updateCurrentHierarchyFromEntry(WorksheetEntry* entry);
+ void followHierarchyFromView();
+
+ void normalizeDraggedHierarchyLevels(HierarchyEntry* rootEntry, WorksheetEntry* previousEntry, const std::vector<WorksheetEntry*>& subentries);
+
+ private:
+ enum class TrackingSource
+ {
+ Viewport,
+ FocusedEntry
+ };
+
+ struct HierarchySearchResult
+ {
+ HierarchyEntry* entry{nullptr};
+ QVector<HierarchyEntry*> collapsedAncestors;
+ };
+
+ struct CommandSearchResult
+ {
+ CommandEntry* entry{nullptr};
+ QVector<HierarchyEntry*> collapsedAncestors;
+ };
+
+ QVariantList collectTocNodes() const;
+ bool visitLogicalEntries(WorksheetEntry* first, const std::function<bool(WorksheetEntry*)>& visitor) const;
+ bool visitLogicalEntries(const std::function<bool(WorksheetEntry*)>& visitor) const;
+ bool findHierarchyEntryById(WorksheetEntry* first, const QString& hierarchyId, QVector<HierarchyEntry*> collapsedAncestors, HierarchySearchResult& result) const;
+ HierarchySearchResult findHierarchyEntryById(const QString& hierarchyId) const;
+ bool findCommandEntryById(WorksheetEntry* first, const QString& commandId, QVector<HierarchyEntry*> collapsedAncestors, CommandSearchResult& result) const;
+ CommandSearchResult findCommandEntryById(const QString& commandId) const;
+ bool expandHierarchyAncestors(const QVector<HierarchyEntry*>& ancestors);
+ QString buildCommandNodeId(CommandEntry* entry) const;
+ 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) const;
+
+ Worksheet* m_worksheet;
+ QString m_currentTocNodeId;
+ TrackingSource m_trackingSource{TrackingSource::Viewport};
+ size_t m_hierarchyMaxDepth{0};
+ QVariantList m_tocNodeSnapshot;
+ bool m_tocRefreshScheduled{false};
+};
+
+#endif // WORKSHEETHIERARCHYMANAGER_H