[education/cantor] src: Improve the worksheet structure navigator
Alexander Semke <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit fccedb016b0e3470d9ea7f280463252612a33531 by Alexander Semke, on behalf of Nanhao Lv.
Committed on 26/07/2026 at 19:50.
Pushed by asemke into branch 'master'.
Improve the worksheet structure navigator
M +2 -6 src/backends/R/rexpression.cpp
M +1 -3 src/backends/julia/juliaexpression.cpp
M +0 -1 src/backends/maxima/maximaexpression.cpp
M +0 -1 src/backends/octave/octaveexpression.cpp
M +0 -2 src/backends/python/pythonexpression.cpp
M +1 -3 src/backends/qalculate/qalculateexpression.cpp
M +2 -6 src/backends/sage/sageexpression.cpp
M +1 -3 src/backends/scilab/scilabexpression.cpp
M +9 -6 src/cantor.cpp
M +22 -10 src/commandentry.cpp
M +0 -1 src/commandentry.h
M +8 -1 src/lib/panelpluginhandler.cpp
M +0 -37 src/lib/result.cpp
M +0 -14 src/lib/result.h
M +256 -12 src/panelplugins/tocpanel/tocpanelplugin.cpp
M +17 -1 src/panelplugins/tocpanel/tocpanelplugin.h
M +13 -0 src/panelplugins/variablemgr/variablemanagerwidget.cpp
M +74 -1 src/worksheethierarchymanager.cpp
https://invent.kde.org/education/cantor/-/commit/fccedb016b0e3470d9ea7f280463252612a33531
diff --git a/src/backends/R/rexpression.cpp b/src/backends/R/rexpression.cpp
index 4c194683..426feefb 100644
--- a/src/backends/R/rexpression.cpp
+++ b/src/backends/R/rexpression.cpp
@@ -73,17 +73,13 @@ void RExpression::showFilesAsResult(const QStringList& files)
qDebug()<<"MimeType: "<<type.name();
if(type.name() == QLatin1String("application/pdf"))
{
- auto* result = new Cantor::ImageResult(QUrl::fromLocalFile(file));
- result->setRole(Cantor::Result::Role::Plot);
- setResult(result);
+ setResult(new Cantor::ImageResult(QUrl::fromLocalFile(file)));
setStatus(Cantor::Expression::Done);
}
else
if (type.name().contains(QLatin1String("image")))
{
- auto* result = new Cantor::ImageResult(QUrl::fromLocalFile(file));
- result->setRole(Cantor::Result::Role::Plot);
- setResult(result);
+ setResult(new Cantor::ImageResult(QUrl::fromLocalFile(file)));
setStatus(Cantor::Expression::Done);
}
else if(type.inherits(QLatin1String("text/plain"))
diff --git a/src/backends/julia/juliaexpression.cpp b/src/backends/julia/juliaexpression.cpp
index 374ebc1b..6d0bf915 100644
--- a/src/backends/julia/juliaexpression.cpp
+++ b/src/backends/julia/juliaexpression.cpp
@@ -69,9 +69,7 @@ void JuliaExpression::finalize(const QString& output, const QString& error, bool
} else {
if (!m_plot_filename.isEmpty() && QFileInfo(m_plot_filename).exists()) {
// If we have plot in result, show it
- auto* result = new Cantor::ImageResult(QUrl::fromLocalFile(m_plot_filename));
- result->setRole(Cantor::Result::Role::Plot);
- setResult(result);
+ setResult(new Cantor::ImageResult(QUrl::fromLocalFile(m_plot_filename)));
} else {
if (!output.isEmpty())
setResult(new Cantor::TextResult(output));
diff --git a/src/backends/maxima/maximaexpression.cpp b/src/backends/maxima/maximaexpression.cpp
index 47053239..8091ccb1 100644
--- a/src/backends/maxima/maximaexpression.cpp
+++ b/src/backends/maxima/maximaexpression.cpp
@@ -603,7 +603,6 @@ void MaximaExpression::loadPlotResult()
}
m_plotResult = plotResult;
- m_plotResult->setRole(Cantor::Result::Role::Plot);
if (m_plotResultIndex != -1)
{
diff --git a/src/backends/octave/octaveexpression.cpp b/src/backends/octave/octaveexpression.cpp
index 121f1803..d548ab4d 100644
--- a/src/backends/octave/octaveexpression.cpp
+++ b/src/backends/octave/octaveexpression.cpp
@@ -235,7 +235,6 @@ void OctaveExpression::imageChanged()
const QUrl& url = QUrl::fromLocalFile(m_plotFilename);
QByteArray pdfData = file.readAll();
auto* newResult = new Cantor::PdfResult(url, pdfData);
- newResult->setRole(Cantor::Result::Role::Plot);
bool found = false;
for (int i = 0; i < results().size(); i++)
diff --git a/src/backends/python/pythonexpression.cpp b/src/backends/python/pythonexpression.cpp
index 5a5d97e3..6f400578 100644
--- a/src/backends/python/pythonexpression.cpp
+++ b/src/backends/python/pythonexpression.cpp
@@ -143,7 +143,6 @@ void PythonExpression::imageChanged()
return;
auto* newResult = new Cantor::ImageResult(QUrl::fromLocalFile(m_tempFile->fileName()));
- newResult->setRole(Cantor::Result::Role::Plot);
if (result() == nullptr)
setResult(newResult);
else
@@ -154,7 +153,6 @@ void PythonExpression::imageChanged()
{
replaceResult(i, newResult);
found = true;
- break;
}
if (!found)
addResult(newResult);
diff --git a/src/backends/qalculate/qalculateexpression.cpp b/src/backends/qalculate/qalculateexpression.cpp
index 6eefcc4a..8ceaea1e 100644
--- a/src/backends/qalculate/qalculateexpression.cpp
+++ b/src/backends/qalculate/qalculateexpression.cpp
@@ -646,9 +646,7 @@ void QalculateExpression::evaluatePlotCommand()
deletePlotDataParameters(plotDataParameterList);
if (plotInline) {
- auto* result = new Cantor::ImageResult(QUrl::fromLocalFile(QString::fromStdString(plotParameters.filename)));
- result->setRole(Cantor::Result::Role::Plot);
- setResult(result);
+ setResult(new Cantor::ImageResult(QUrl::fromLocalFile(QString::fromStdString(plotParameters.filename))));
setStatus(Cantor::Expression::Done);
}
}
diff --git a/src/backends/sage/sageexpression.cpp b/src/backends/sage/sageexpression.cpp
index a6dedb21..b7addd4a 100644
--- a/src/backends/sage/sageexpression.cpp
+++ b/src/backends/sage/sageexpression.cpp
@@ -220,16 +220,12 @@ void SageExpression::evalFinished()
if(type.inherits(QLatin1String("image/gif")))
{
qDebug()<<"adding animation";
- auto* result = new Cantor::AnimationResult(QUrl::fromLocalFile(m_imagePath), i18n("Result of %1" , command() ) );
- result->setRole(Cantor::Result::Role::Plot);
- addResult(result);
+ addResult( new Cantor::AnimationResult(QUrl::fromLocalFile(m_imagePath), i18n("Result of %1" , command() ) ) );
}
else
{
qDebug()<<"adding image";
- auto* result = new Cantor::ImageResult(QUrl::fromLocalFile(m_imagePath ), i18n("Result of %1" , command() ) );
- result->setRole(Cantor::Result::Role::Plot);
- addResult(result);
+ addResult( new Cantor::ImageResult(QUrl::fromLocalFile(m_imagePath ), i18n("Result of %1" , command() ) ) );
}
}
setStatus(Cantor::Expression::Done);
diff --git a/src/backends/scilab/scilabexpression.cpp b/src/backends/scilab/scilabexpression.cpp
index 4477b9f2..5c60dcfe 100644
--- a/src/backends/scilab/scilabexpression.cpp
+++ b/src/backends/scilab/scilabexpression.cpp
@@ -97,9 +97,7 @@ void ScilabExpression::parsePlotFile(QString filename)
qDebug() << "parsePlotFile";
qDebug() << "ScilabExpression::parsePlotFile: " << filename;
- auto* result = new ScilabPlotResult(QUrl::fromLocalFile(filename));
- result->setRole(Cantor::Result::Role::Plot);
- setResult(result);
+ setResult(new ScilabPlotResult(QUrl::fromLocalFile(filename)));
setPlotPending(false);
diff --git a/src/cantor.cpp b/src/cantor.cpp
index 703008bb..f7aa6245 100644
--- a/src/cantor.cpp
+++ b/src/cantor.cpp
@@ -464,7 +464,8 @@ void CantorShell::addWorksheet(const QString& backendName)
m_tabWidget->setCurrentIndex(tab);
// Setting focus on worksheet view, because Qt clear focus of added widget inside addTab
// This fix https://bugs.kde.org/show_bug.cgi?id=395976
- part->widget()->findChild<QGraphicsView*>()->setFocus();
+ if (auto* worksheetView = part->widget()->findChild<QGraphicsView*>())
+ worksheetView->setFocus();
// Force run updateCaption for getting proper backend icon
QMetaObject::invokeMethod(part, "updateCaption");
@@ -500,11 +501,13 @@ void CantorShell::activateWorksheet(int index)
m_pluginsVisibility[m_part] = std::move(visiblePanelNames);
auto* wa = m_part->findChild<Cantor::WorksheetAccessInterface*>(Cantor::WorksheetAccessInterface::Name);
- assert(wa);
Cantor::PanelPluginHandler::PanelStates states;
- auto plugins = m_panelHandler.plugins(wa->session());
- for(auto* plugin : plugins)
- states.insert(plugin->name(), plugin->saveState());
+ if (wa)
+ {
+ const auto plugins = m_panelHandler.plugins(wa->session());
+ for (auto* plugin : plugins)
+ states.insert(plugin->name(), plugin->saveState());
+ }
m_pluginsStates[m_part] = std::move(states);
}
@@ -530,7 +533,7 @@ void CantorShell::activateWorksheet(int index)
//update the status bar
auto* wa = m_part->findChild<Cantor::WorksheetAccessInterface*>(Cantor::WorksheetAccessInterface::Name);
- if (wa->session())
+ if (wa && wa->session())
{
auto status = wa->session()->status();
switch (status) {
diff --git a/src/commandentry.cpp b/src/commandentry.cpp
index bf4e2115..939209be 100644
--- a/src/commandentry.cpp
+++ b/src/commandentry.cpp
@@ -15,6 +15,9 @@
#include "lib/jupyterutils.h"
#include "lib/result.h"
#include "lib/helpresult.h"
+#include "lib/imageresult.h"
+#include "lib/animationresult.h"
+#include "lib/pdfresult.h"
#include "lib/latexresult.h"
#include "lib/syntaxhelpobject.h"
#include "lib/session.h"
@@ -41,6 +44,19 @@
#include <KColorScheme>
#include <KSyntaxHighlighting/Definition>
+namespace
+{
+bool isVisualResult(Cantor::Result* result)
+{
+ if (!result)
+ return false;
+
+ return result->type() == Cantor::ImageResult::Type
+ || result->type() == Cantor::AnimationResult::Type
+ || result->type() == Cantor::PdfResult::Type;
+}
+}
+
const QString CommandEntry::Prompt = QLatin1String(">>> ");
const QString CommandEntry::MidPrompt = QLatin1String(">> ");
const QString CommandEntry::HidePrompt = QLatin1String("> ");
@@ -62,7 +78,6 @@ CommandEntry::CommandEntry(Worksheet* worksheet) : WorksheetEntry(worksheet),
m_backgroundColorActionGroup(nullptr),
m_backgroundColorMenu(nullptr),
m_textColorActionGroup(nullptr),
- m_themeActionGroup(nullptr),
m_textColorMenu(nullptr),
m_fontMenu(nullptr),
m_isExecutionEnabled(true)
@@ -83,11 +98,8 @@ CommandEntry::CommandEntry(Worksheet* worksheet) : WorksheetEntry(worksheet),
m_commandItem->setSyntaxHighlightingMode(highlightingMode);
- if (worksheet)
- {
- const auto& parentTheme = worksheet->theme();
- m_commandItem->setTheme(parentTheme.name());
- }
+ const auto& parentTheme = worksheet->theme();
+ m_commandItem->setTheme(parentTheme.name());
if (worksheet && worksheet->session() && worksheet->session()->variableModel())
{
@@ -476,7 +488,7 @@ void CommandEntry::cachePlotResultMetadata()
for (auto* result : m_expression->results())
{
- if (!result || result->role() != Cantor::Result::Role::Plot)
+ if (!isVisualResult(result))
continue;
m_plotResultMetadataToRestore.append({result->resultId(), result->displayName()});
@@ -485,7 +497,7 @@ void CommandEntry::cachePlotResultMetadata()
void CommandEntry::restorePlotResultMetadata(Cantor::Result* result, int plotIndex)
{
- if (!result || result->role() != Cantor::Result::Role::Plot)
+ if (!isVisualResult(result))
return;
if (plotIndex < 0 || plotIndex >= m_plotResultMetadataToRestore.size())
@@ -939,14 +951,14 @@ void CommandEntry::updateEntry()
for (int i = 0; i < m_resultItems.size() && i < expr->results().size(); ++i)
{
auto* result = expr->results().at(i);
- if (result && result->role() == Cantor::Result::Role::Plot)
+ if (isVisualResult(result))
++plotIndex;
}
for (int i = m_resultItems.size(); i < expr->results().size(); i++)
{
auto* result = expr->results()[i];
- if (result && result->role() == Cantor::Result::Role::Plot)
+ if (isVisualResult(result))
restorePlotResultMetadata(result, plotIndex++);
if (auto* resultItem = ResultItem::create(this, result))
diff --git a/src/commandentry.h b/src/commandentry.h
index 98a93038..ef078065 100644
--- a/src/commandentry.h
+++ b/src/commandentry.h
@@ -157,7 +157,6 @@ class CommandEntry : public WorksheetEntry
QActionGroup* m_backgroundColorActionGroup;
QMenu* m_backgroundColorMenu;
QActionGroup* m_textColorActionGroup;
- QActionGroup* m_themeActionGroup;
QColor m_defaultDefaultTextColor;
QMenu* m_textColorMenu;
QMenu* m_fontMenu;
diff --git a/src/lib/panelpluginhandler.cpp b/src/lib/panelpluginhandler.cpp
index 91902cb8..3bdf3016 100644
--- a/src/lib/panelpluginhandler.cpp
+++ b/src/lib/panelpluginhandler.cpp
@@ -106,7 +106,14 @@ QList<PanelPlugin*> PanelPluginHandler::activePluginsForSession(Session* session
}
if (previousPluginStates.contains(plugin->name()))
- plugin->restoreState(previousPluginStates[plugin->name()]);
+ {
+ PanelPlugin::State state = previousPluginStates[plugin->name()];
+ // A worksheet can replace its Session while loading a file. Panel
+ // UI state remains reusable, but the stored Session pointer may no
+ // longer be alive when returning to the worksheet.
+ state.session = session;
+ plugin->restoreState(state);
+ }
else
{
Cantor::PanelPlugin::State initState;
diff --git a/src/lib/result.cpp b/src/lib/result.cpp
index 3bdaec87..c1861d74 100644
--- a/src/lib/result.cpp
+++ b/src/lib/result.cpp
@@ -24,7 +24,6 @@ class Cantor::ResultPrivate
QJsonObject* jupyterMetadata{nullptr};
QString resultId;
QString displayName;
- Result::Role role{Result::Role::Generic};
int executionIndex{-1};
};
@@ -65,7 +64,6 @@ QJsonObject Cantor::Result::jupyterMetadata() const
QJsonObject cantorMetadata = metadata.value(JupyterUtils::cantorMetadataKey).toObject();
cantorMetadata.insert(QLatin1String("result-id"), d->resultId);
- cantorMetadata.insert(QLatin1String("result-role"), roleToString(d->role));
if (d->displayName.isEmpty())
cantorMetadata.remove(QLatin1String("result-title"));
else
@@ -86,8 +84,6 @@ void Cantor::Result::setJupyterMetadata(const QJsonObject& metadata)
if (!storedResultId.isEmpty())
d->resultId = storedResultId;
- d->role = roleFromString(cantorMetadata.value(QLatin1String("result-role")).toString());
-
const QJsonValue storedTitle = cantorMetadata.value(QLatin1String("result-title"));
if (storedTitle.isString())
d->displayName = storedTitle.toString().trimmed();
@@ -133,41 +129,9 @@ void Cantor::Result::setDisplayName(const QString& name)
d->displayName = name.trimmed();
}
-Cantor::Result::Role Cantor::Result::role() const
-{
- return d->role;
-}
-
-void Cantor::Result::setRole(Role role)
-{
- d->role = role;
-}
-
-QString Cantor::Result::roleToString(Role role)
-{
- switch (role)
- {
- case Role::Plot:
- return QStringLiteral("plot");
- case Role::Generic:
- return QStringLiteral("generic");
- }
-
- return QStringLiteral("generic");
-}
-
-Cantor::Result::Role Cantor::Result::roleFromString(const QString& roleName)
-{
- if (roleName == QLatin1String("plot"))
- return Role::Plot;
-
- return Role::Generic;
-}
-
void Cantor::Result::applyXmlResultMetadata(QDomElement& element) const
{
element.setAttribute(QLatin1String("result-id"), d->resultId);
- element.setAttribute(QLatin1String("result-role"), roleToString(d->role));
if (!d->displayName.isEmpty())
element.setAttribute(QLatin1String("result-title"), d->displayName);
}
@@ -178,6 +142,5 @@ void Cantor::Result::loadXmlResultMetadata(const QDomElement& element)
if (!storedResultId.isEmpty())
d->resultId = storedResultId;
- d->role = roleFromString(element.attribute(QLatin1String("result-role")));
d->displayName = element.attribute(QLatin1String("result-title")).trimmed();
}
diff --git a/src/lib/result.h b/src/lib/result.h
index b95e36d6..8e6ba12b 100644
--- a/src/lib/result.h
+++ b/src/lib/result.h
@@ -25,13 +25,6 @@ class ResultPrivate;
class CANTOR_EXPORT Result
{
public:
- /** Describes how UI code should treat this result. */
- enum class Role
- {
- Generic,
- Plot
- };
-
/**
* Default constructor
*/
@@ -113,13 +106,6 @@ class CANTOR_EXPORT Result
void setDisplayName(const QString& name);
- /** UI role assigned by the backend or loader. */
- Role role() const;
- void setRole(Role role);
-
- static QString roleToString(Role role);
- static Role roleFromString(const QString& roleName);
-
void applyXmlResultMetadata(QDomElement& element) const;
void loadXmlResultMetadata(const QDomElement& element);
diff --git a/src/panelplugins/tocpanel/tocpanelplugin.cpp b/src/panelplugins/tocpanel/tocpanelplugin.cpp
index 4f96fa7c..473537e2 100644
--- a/src/panelplugins/tocpanel/tocpanelplugin.cpp
+++ b/src/panelplugins/tocpanel/tocpanelplugin.cpp
@@ -12,16 +12,19 @@
#include <QDebug>
#include <QItemSelectionModel>
#include <QKeyEvent>
+#include <QLabel>
#include <QLineEdit>
#include <QMenu>
#include <QModelIndex>
#include <QScopedValueRollback>
+#include <QSignalBlocker>
#include <QStandardItem>
#include <QStyledItemDelegate>
#include <QTreeView>
#include <QTimer>
#include <QVariantList>
#include <QVariantMap>
+#include <QVBoxLayout>
#include <QWidget>
#include <KLocalizedString>
@@ -119,10 +122,8 @@ TableOfContentPanelPlugin::TableOfContentPanelPlugin(QObject* parent, const QLis
TableOfContentPanelPlugin::~TableOfContentPanelPlugin()
{
- if (m_mainWidget)
- {
- m_mainWidget->deleteLater();
- }
+ if (m_containerWidget)
+ m_containerWidget->deleteLater();
}
QWidget* TableOfContentPanelPlugin::widget()
@@ -130,7 +131,7 @@ QWidget* TableOfContentPanelPlugin::widget()
if (!m_mainWidget)
constructMainWidget();
- return m_mainWidget;
+ return m_containerWidget;
}
void TableOfContentPanelPlugin::connectToShell(QObject* cantorShell)
@@ -165,6 +166,12 @@ void TableOfContentPanelPlugin::handleClicked(const QModelIndex& index)
return;
Q_EMIT requestNavigateToTocNode(nodeId);
+
+ // Navigation focuses the target worksheet entry. Keep focus in the TOC
+ // when navigation originated here so subsequent TOC shortcuts keep
+ // operating on the selected node.
+ if (m_mainWidget)
+ m_mainWidget->setFocus(Qt::MouseFocusReason);
}
void TableOfContentPanelPlugin::handleDoubleClicked(const QModelIndex& index)
@@ -174,7 +181,20 @@ void TableOfContentPanelPlugin::handleDoubleClicked(const QModelIndex& index)
void TableOfContentPanelPlugin::constructMainWidget()
{
- auto* view = new QTreeView;
+ auto* container = new QWidget;
+ auto* layout = new QVBoxLayout(container);
+ layout->setContentsMargins(0, 0, 0, 0);
+ layout->setSpacing(4);
+
+ auto* searchEdit = new QLineEdit(container);
+ searchEdit->setClearButtonEnabled(true);
+ searchEdit->setPlaceholderText(i18n("Search Table of Contents"));
+
+ auto* view = new QTreeView(container);
+ auto* emptyLabel = new QLabel(container);
+ emptyLabel->setAlignment(Qt::AlignCenter);
+ emptyLabel->setWordWrap(true);
+ emptyLabel->hide();
view->setEditTriggers(QAbstractItemView::NoEditTriggers);
view->setSelectionBehavior(QAbstractItemView::SelectRows);
@@ -186,6 +206,7 @@ void TableOfContentPanelPlugin::constructMainWidget()
view->setContextMenuPolicy(Qt::CustomContextMenu);
view->installEventFilter(this);
view->viewport()->installEventFilter(this);
+ searchEdit->installEventFilter(this);
auto* delegate = new HierarchyNameDelegate(NameRole, NodeIdRole, HierarchyIdRole, NodeTypeRole, CustomTitleRole, EntryIdRole, ResultIdRole, view);
@@ -199,30 +220,168 @@ void TableOfContentPanelPlugin::constructMainWidget()
connect(view, &QTreeView::collapsed, this, &TableOfContentPanelPlugin::handleCollapsed);
connect(delegate, &QAbstractItemDelegate::commitData, this, &TableOfContentPanelPlugin::handleEditorCommit);
connect(delegate, &QAbstractItemDelegate::closeEditor, this, &TableOfContentPanelPlugin::handleEditorClosed);
+ connect(searchEdit, &QLineEdit::textChanged, this, [this](const QString& text)
+ {
+ m_searchText = text.trimmed();
+ rebuildModel();
+ });
+ layout->addWidget(searchEdit);
+ layout->addWidget(emptyLabel, 1);
+ layout->addWidget(view, 1);
+
+ m_containerWidget = container;
+ m_searchEdit = searchEdit;
+ m_emptyLabel = emptyLabel;
m_mainWidget = view;
rebuildModel();
}
bool TableOfContentPanelPlugin::eventFilter(QObject* watched, QEvent* event)
{
- if (m_mainWidget && (watched == m_mainWidget || watched == m_mainWidget->viewport()) && event->type() == QEvent::KeyPress)
+ const bool watchesToc = m_mainWidget && (watched == m_mainWidget || watched == m_mainWidget->viewport());
+
+ if (watchesToc && event->type() == QEvent::ShortcutOverride)
{
auto* keyEvent = static_cast<QKeyEvent*>(event);
+ const bool tocShortcut = keyEvent->matches(QKeySequence::Find)
+ || keyEvent->key() == Qt::Key_F2
+ || keyEvent->key() == Qt::Key_Delete
+ || keyEvent->key() == Qt::Key_Menu
+ || (keyEvent->key() == Qt::Key_F10 && keyEvent->modifiers() & Qt::ShiftModifier);
+
+ if (tocShortcut)
+ {
+ keyEvent->accept();
+ return true;
+ }
+ }
+
+ if (m_searchEdit && watched == m_searchEdit && event->type() == QEvent::KeyPress)
+ {
+ auto* keyEvent = static_cast<QKeyEvent*>(event);
+ if (keyEvent->key() == Qt::Key_Escape && !m_searchEdit->text().isEmpty())
+ {
+ m_searchEdit->clear();
+ return true;
+ }
+ }
+
+ if (watchesToc && event->type() == QEvent::KeyPress)
+ {
+ auto* keyEvent = static_cast<QKeyEvent*>(event);
+ if (keyEvent->matches(QKeySequence::Find) && m_searchEdit)
+ {
+ m_searchEdit->setFocus();
+ m_searchEdit->selectAll();
+ return true;
+ }
+
if (keyEvent->key() == Qt::Key_F2 && !m_readOnly)
{
beginRename(m_mainWidget->currentIndex());
return true;
}
+
+ const QModelIndex index = m_mainWidget->currentIndex();
+ if ((keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) && index.isValid())
+ {
+ handleClicked(index);
+ return true;
+ }
+
+ if ((keyEvent->key() == Qt::Key_Menu || (keyEvent->key() == Qt::Key_F10 && keyEvent->modifiers() & Qt::ShiftModifier)) && index.isValid())
+ {
+ showContextMenuForIndex(index);
+ return true;
+ }
+
+ if (keyEvent->key() == Qt::Key_Delete && !m_readOnly && index.isValid())
+ {
+ deleteItemAtIndex(index);
+ return true;
+ }
+
+ if (keyEvent->key() == Qt::Key_Escape && m_searchEdit && !m_searchEdit->text().isEmpty())
+ {
+ m_searchEdit->clear();
+ return true;
+ }
}
return Cantor::PanelPlugin::eventFilter(watched, event);
}
+void TableOfContentPanelPlugin::deleteItemAtIndex(const QModelIndex& index)
+{
+ if (!index.isValid() || m_readOnly)
+ return;
+
+ const QString hierarchyId = index.data(HierarchyIdRole).toString();
+ const QString nodeType = index.data(NodeTypeRole).toString();
+ const QString displayText = index.data(DisplayTextRole).toString();
+
+ if (!hierarchyId.isEmpty())
+ {
+ const QString headingTitle = displayText.isEmpty() ? i18n("Heading") : displayText;
+ const auto result = KMessageBox::warningTwoActions(
+ m_mainWidget,
+ i18n("Do you really want to delete the heading \"%1\"? Its contents will be kept. This action cannot be undone.", headingTitle),
+ i18n("Delete Heading"),
+ KStandardGuiItem::remove(),
+ KStandardGuiItem::cancel());
+
+ if (result == KMessageBox::PrimaryAction)
+ Q_EMIT requestDeleteHierarchyEntry(hierarchyId, false);
+ return;
+ }
+
+ const QString commandId = index.data(EntryIdRole).toString();
+ if (nodeType == TocNodeTypeCommand && !commandId.isEmpty())
+ {
+ if (Settings::warnAboutEntryDelete())
+ {
+ const QString commandTitle = displayText.isEmpty() ? i18n("Command") : displayText;
+ const auto result = KMessageBox::warningTwoActions(
+ m_mainWidget,
+ i18n("Do you really want to delete \"%1\"? This action cannot be undone.", commandTitle),
+ i18n("Delete Command"),
+ KStandardGuiItem::remove(),
+ KStandardGuiItem::cancel());
+
+ if (result != KMessageBox::PrimaryAction)
+ return;
+ }
+
+ Q_EMIT requestDeleteCommandEntry(commandId);
+ return;
+ }
+
+ const QString resultId = index.data(ResultIdRole).toString();
+ if (nodeType == TocNodeTypePlot && !commandId.isEmpty() && !resultId.isEmpty())
+ {
+ const QString plotTitle = displayText.isEmpty() ? i18n("Plot") : displayText;
+ const auto result = KMessageBox::warningTwoActions(
+ m_mainWidget,
+ i18n("Do you really want to delete \"%1\"? This action cannot be undone.", plotTitle),
+ i18n("Delete Plot"),
+ KStandardGuiItem::remove(),
+ KStandardGuiItem::cancel());
+
+ if (result == KMessageBox::PrimaryAction)
+ Q_EMIT requestDeletePlot(commandId, resultId);
+ }
+}
+
void TableOfContentPanelPlugin::rebuildModel()
{
+ if (!m_modelFilteredBySearch)
+ saveCurrentExpansionState();
+
QScopedValueRollback<bool> guard(m_updatingModel, true);
+ updateSearchVisibility();
+
m_model.clear();
m_itemsByNodeId.clear();
@@ -254,6 +413,8 @@ void TableOfContentPanelPlugin::rebuildModel()
item->setData(node.resultIndex, ResultIndexRole);
item->setData(node.editable, EditableRole);
item->setData(node.navigable, NavigableRole);
+ item->setData(node.canPromote, CanPromoteRole);
+ item->setData(node.canDemote, CanDemoteRole);
parentItem->appendRow(item);
visibleItems[i] = item;
@@ -262,7 +423,38 @@ void TableOfContentPanelPlugin::rebuildModel()
}
restoreExpansionState();
+ if (!m_searchText.isEmpty() && m_mainWidget)
+ m_mainWidget->expandAll();
updateCurrentNodeSelection();
+ const bool isEmpty = m_model.rowCount() == 0;
+ if (m_emptyLabel)
+ {
+ m_emptyLabel->setText(m_searchText.isEmpty()
+ ? i18n("No visible items in the Table of Contents")
+ : i18n("No matching items"));
+ m_emptyLabel->setVisible(isEmpty);
+ }
+ if (m_mainWidget)
+ m_mainWidget->setVisible(!isEmpty);
+ m_modelFilteredBySearch = !m_searchText.isEmpty();
+}
+
+void TableOfContentPanelPlugin::saveCurrentExpansionState()
+{
+ if (!m_mainWidget)
+ return;
+
+ for (auto it = m_itemsByNodeId.cbegin(); it != m_itemsByNodeId.cend(); ++it)
+ {
+ QStandardItem* item = it.value();
+ if (!item || !item->hasChildren())
+ continue;
+
+ if (m_mainWidget->isExpanded(item->index()))
+ m_expandedNodeIds.insert(it.key());
+ else
+ m_expandedNodeIds.remove(it.key());
+ }
}
void TableOfContentPanelPlugin::restoreExpansionState()
@@ -336,6 +528,15 @@ void TableOfContentPanelPlugin::updateCurrentNodeSelection()
void TableOfContentPanelPlugin::restoreState(const Cantor::PanelPlugin::State& state)
{
cancelEditorSession();
+
+ if (m_searchEdit)
+ {
+ const QSignalBlocker blocker(m_searchEdit);
+ m_searchEdit->clear();
+ }
+ m_searchText.clear();
+ m_modelFilteredBySearch = false;
+
clearNodes();
resetVisibilityToDefaults();
@@ -442,6 +643,9 @@ void TableOfContentPanelPlugin::handleTocNodeChanges(const QVariantList& nodes)
void TableOfContentPanelPlugin::applyTocNodeChanges(const QVariantList& nodes)
{
+ if (!m_modelFilteredBySearch)
+ saveCurrentExpansionState();
+
clearNodes();
m_nodes.reserve(nodes.size());
@@ -491,6 +695,8 @@ void TableOfContentPanelPlugin::applyTocNodeChanges(const QVariantList& nodes)
tocNode.depth = node.value(QStringLiteral("depth"), 0).toInt();
tocNode.editable = node.value(QStringLiteral("editable"), false).toBool();
tocNode.navigable = node.value(QStringLiteral("navigable"), false).toBool();
+ tocNode.canPromote = node.value(QStringLiteral("canPromote"), false).toBool();
+ tocNode.canDemote = node.value(QStringLiteral("canDemote"), false).toBool();
if (tocNode.displayText.isEmpty())
tocNode.displayText = tocNode.title;
@@ -695,7 +901,11 @@ bool TableOfContentPanelPlugin::shouldDisplayNode(int index) const
if (index < 0 || index >= m_nodes.size())
return false;
- const QString& type = m_nodes.at(index).type;
+ const TocNode& node = m_nodes.at(index);
+ if (!m_searchText.isEmpty() && !m_searchVisibleNodeIds.contains(node.id))
+ return false;
+
+ const QString& type = node.type;
if (type == TocNodeTypeChapter)
return m_showChapters;
@@ -709,6 +919,27 @@ bool TableOfContentPanelPlugin::shouldDisplayNode(int index) const
return true;
}
+void TableOfContentPanelPlugin::updateSearchVisibility()
+{
+ m_searchVisibleNodeIds.clear();
+ if (m_searchText.isEmpty())
+ return;
+
+ for (int index = 0; index < m_nodes.size(); ++index)
+ {
+ const TocNode& node = m_nodes.at(index);
+ if (!node.title.contains(m_searchText, Qt::CaseInsensitive)
+ && !node.displayText.contains(m_searchText, Qt::CaseInsensitive)
+ && !node.customTitle.contains(m_searchText, Qt::CaseInsensitive))
+ {
+ continue;
+ }
+
+ for (int visibleIndex = index; visibleIndex >= 0; visibleIndex = m_nodes.at(visibleIndex).parentIndex)
+ m_searchVisibleNodeIds.insert(m_nodes.at(visibleIndex).id);
+ }
+}
+
int TableOfContentPanelPlugin::findVisibleAncestorIndex(int index) const
{
while (index >= 0)
@@ -824,6 +1055,9 @@ void TableOfContentPanelPlugin::handleReadOnlyChanged(bool readOnly)
void TableOfContentPanelPlugin::handleExpanded(const QModelIndex& index)
{
+ if (m_updatingModel)
+ return;
+
const QString nodeId = index.data(NodeIdRole).toString();
if (!nodeId.isEmpty())
@@ -832,12 +1066,24 @@ void TableOfContentPanelPlugin::handleExpanded(const QModelIndex& index)
void TableOfContentPanelPlugin::handleCollapsed(const QModelIndex& index)
{
+ if (m_updatingModel)
+ return;
+
const QString nodeId = index.data(NodeIdRole).toString();
if (!nodeId.isEmpty())
m_expandedNodeIds.remove(nodeId);
}
+void TableOfContentPanelPlugin::showContextMenuForIndex(const QModelIndex& index)
+{
+ if (!m_mainWidget || !index.isValid())
+ return;
+
+ m_mainWidget->setCurrentIndex(index);
+ handleContextMenuRequested(m_mainWidget->visualRect(index).center());
+}
+
void TableOfContentPanelPlugin::handleContextMenuRequested(const QPoint& position)
{
if (!m_mainWidget)
@@ -858,8 +1104,6 @@ void TableOfContentPanelPlugin::handleContextMenuRequested(const QPoint& positio
auto* item = m_model.itemFromIndex(index);
- const int depth = index.data(DepthRole).toInt();
-
if (!hierarchyId.isEmpty())
{
QAction* goToHeadingAction = menu.addAction(i18n("Go to Heading"));
@@ -884,14 +1128,14 @@ void TableOfContentPanelPlugin::handleContextMenuRequested(const QPoint& positio
menu.addSeparator();
QAction* promoteAction = menu.addAction(QIcon::fromTheme(QStringLiteral("format-indent-less")), i18n("Promote Heading"));
- promoteAction->setEnabled(!m_readOnly && depth > 0);
+ promoteAction->setEnabled(!m_readOnly && index.data(CanPromoteRole).toBool());
connect(promoteAction, &QAction::triggered, this, [this, hierarchyId]()
{
Q_EMIT requestChangeHierarchyLevel(hierarchyId, -1);
});
QAction* demoteAction = menu.addAction(QIcon::fromTheme(QStringLiteral("format-indent-more")), i18n("Demote Heading"));
- demoteAction->setEnabled(!m_readOnly && depth < 5);
+ demoteAction->setEnabled(!m_readOnly && index.data(CanDemoteRole).toBool());
connect(demoteAction, &QAction::triggered, this, [this, hierarchyId]()
{
Q_EMIT requestChangeHierarchyLevel(hierarchyId, 1);
diff --git a/src/panelplugins/tocpanel/tocpanelplugin.h b/src/panelplugins/tocpanel/tocpanelplugin.h
index 6acc0335..1f45df9f 100644
--- a/src/panelplugins/tocpanel/tocpanelplugin.h
+++ b/src/panelplugins/tocpanel/tocpanelplugin.h
@@ -19,6 +19,8 @@
class QWidget;
class QEvent;
+class QLineEdit;
+class QLabel;
class QModelIndex;
class QMenu;
class QStandardItem;
@@ -80,7 +82,9 @@ private:
CustomTitleRole,
ResultIndexRole,
EditableRole,
- NavigableRole
+ NavigableRole,
+ CanPromoteRole,
+ CanDemoteRole
};
struct TocNode
@@ -100,6 +104,8 @@ private:
int parentIndex{-1};
bool editable{false};
bool navigable{false};
+ bool canPromote{false};
+ bool canDemote{false};
};
void constructMainWidget();
@@ -119,8 +125,15 @@ private:
void handleEditorClosed();
void finishEditorSession();
void cancelEditorSession();
+ void updateSearchVisibility();
+ void saveCurrentExpansionState();
+ void showContextMenuForIndex(const QModelIndex& index);
+ void deleteItemAtIndex(const QModelIndex& index);
private:
+ QPointer<QWidget> m_containerWidget;
+ QPointer<QLineEdit> m_searchEdit;
+ QPointer<QLabel> m_emptyLabel;
QPointer<QTreeView> m_mainWidget;
QStandardItemModel m_model;
@@ -136,9 +149,12 @@ private:
bool m_readOnly{false};
QSet<QString> m_expandedNodeIds;
+ QSet<QString> m_searchVisibleNodeIds;
QString m_currentNodeId;
+ QString m_searchText;
bool m_updatingModel{false};
+ bool m_modelFilteredBySearch{false};
bool m_expansionStateInitialized{false};
bool m_editorActive{false};
bool m_hasPendingNodeSnapshot{false};
diff --git a/src/panelplugins/variablemgr/variablemanagerwidget.cpp b/src/panelplugins/variablemgr/variablemanagerwidget.cpp
index ada4bfbb..c2675e03 100644
--- a/src/panelplugins/variablemgr/variablemanagerwidget.cpp
+++ b/src/panelplugins/variablemgr/variablemanagerwidget.cpp
@@ -107,7 +107,20 @@ VariableManagerWidget::VariableManagerWidget(Cantor::Session* session, QWidget*
void VariableManagerWidget::setSession(Cantor::Session* session)
{
+ if (m_model)
+ disconnect(m_model, nullptr, this, nullptr);
+
+ if (m_treeView)
+ m_treeView->setModel(nullptr);
+
m_session = session;
+ m_model = nullptr;
+
+ m_loadBtn->setEnabled(true);
+ m_saveBtn->setEnabled(true);
+ m_newBtn->setEnabled(true);
+ m_clearBtn->setEnabled(true);
+
if (session)
{
m_model = session->variableDataModel();
diff --git a/src/worksheethierarchymanager.cpp b/src/worksheethierarchymanager.cpp
index 3d0c272e..3c2066cf 100644
--- a/src/worksheethierarchymanager.cpp
+++ b/src/worksheethierarchymanager.cpp
@@ -125,6 +125,52 @@ QVariantList WorksheetHierarchyManager::collectTocNodes() const
const QString displayText = hierarchyEntry->hierarchyText().isEmpty()
? hierarchyEntry->text()
: hierarchyEntry->hierarchyText() + QLatin1Char(' ') + hierarchyEntry->text();
+ const int minimumLevel = static_cast<int>(HierarchyEntry::HierarchyLevel::Chapter);
+ const int maximumLevel = static_cast<int>(HierarchyEntry::HierarchyLevel::Subparagraph);
+ const int currentLevel = static_cast<int>(hierarchyEntry->level());
+
+ bool canDemote = currentLevel < maximumLevel;
+ if (canDemote)
+ {
+ bool hasPreviousSibling = false;
+ for (auto* previous = hierarchyEntry->previous(); previous; previous = previous->previous())
+ {
+ if (previous->type() != HierarchyEntry::Type)
+ continue;
+
+ const int previousLevel = static_cast<int>(static_cast<HierarchyEntry*>(previous)->level());
+ if (previousLevel < currentLevel)
+ break;
+ if (previousLevel == currentLevel)
+ {
+ hasPreviousSibling = true;
+ break;
+ }
+ }
+
+ canDemote = hasPreviousSibling;
+ }
+
+ if (canDemote)
+ {
+ WorksheetEntry* firstSubentry = hierarchyEntry->hasHiddenSubentries()
+ ? hierarchyEntry->hiddenSubentries()
+ : hierarchyEntry->next();
+ for (auto* child = firstSubentry; child; child = child->next())
+ {
+ if (child->type() != HierarchyEntry::Type)
+ continue;
+
+ const int childLevel = static_cast<int>(static_cast<HierarchyEntry*>(child)->level());
+ if (childLevel <= currentLevel)
+ break;
+ if (childLevel >= maximumLevel)
+ {
+ canDemote = false;
+ break;
+ }
+ }
+ }
QVariantMap node;
node.insert(QStringLiteral("id"), hierarchyId);
@@ -136,6 +182,8 @@ QVariantList WorksheetHierarchyManager::collectTocNodes() const
node.insert(QStringLiteral("depth"), depth);
node.insert(QStringLiteral("editable"), true);
node.insert(QStringLiteral("navigable"), true);
+ node.insert(QStringLiteral("canPromote"), currentLevel > minimumLevel);
+ node.insert(QStringLiteral("canDemote"), canDemote);
node.insert(QStringLiteral("hierarchyId"), hierarchyId);
node.insert(QStringLiteral("resultIndex"), -1);
node.insert(QStringLiteral("entryId"), hierarchyId);
@@ -311,7 +359,7 @@ QString WorksheetHierarchyManager::plotTocDisplayText(CommandEntry* entry, Canto
bool WorksheetHierarchyManager::isPlotResult(Cantor::Result* result) const
{
- if (!result || result->role() != Cantor::Result::Role::Plot)
+ if (!result)
return false;
return result->type() == Cantor::ImageResult::Type
@@ -1281,6 +1329,31 @@ void WorksheetHierarchyManager::updateCurrentHierarchyFromView(const QRectF& vie
activeEntry = entry;
}
+ if (activeEntry && activeEntry->type() == CommandEntry::Type)
+ {
+ auto* commandEntry = static_cast<CommandEntry*>(activeEntry);
+ QString activePlotNodeId;
+
+ for (int index = 0; index < commandEntry->resultItemCount(); ++index)
+ {
+ ResultItem* resultItem = commandEntry->resultItemAt(index);
+ if (!resultItem || !isPlotResult(resultItem->result()))
+ continue;
+
+ QGraphicsObject* object = resultItem->graphicsObject();
+ if (!object || !object->isVisible() || object->sceneBoundingRect().top() > activationY)
+ continue;
+
+ activePlotNodeId = buildPlotNodeId(commandEntry->commandId(), resultItem->result()->resultId());
+ }
+
+ if (!activePlotNodeId.isEmpty())
+ {
+ setCurrentTocNode(activePlotNodeId);
+ return;
+ }
+ }
+
updateCurrentHierarchy(activeEntry);
}