[education/cantor] src/panelplugins/tocpanel: Implement structure navigator panel
Alexander Semke <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit 761ff2170791128891a5899667de8359061146ac by Alexander Semke, on behalf of Nanhao Lv.
Committed on 26/07/2026 at 19:50.
Pushed by asemke into branch 'master'.
Implement structure navigator panel
M +3 -0 src/panelplugins/tocpanel/CMakeLists.txt
M +920 -32 src/panelplugins/tocpanel/tocpanelplugin.cpp
M +113 -16 src/panelplugins/tocpanel/tocpanelplugin.h
https://invent.kde.org/education/cantor/-/commit/761ff2170791128891a5899667de8359061146ac
diff --git a/src/panelplugins/tocpanel/CMakeLists.txt b/src/panelplugins/tocpanel/CMakeLists.txt
index e36f9c61..453ffcf7 100644
--- a/src/panelplugins/tocpanel/CMakeLists.txt
+++ b/src/panelplugins/tocpanel/CMakeLists.txt
@@ -3,3 +3,6 @@ set(FileBrowserPanelPlugin_SRCS
)
add_panel(tocpanelplugin ${FileBrowserPanelPlugin_SRCS})
+
+target_link_libraries(cantor_tocpanelplugin cantor_config)
+target_include_directories(cantor_tocpanelplugin PRIVATE ${CMAKE_BINARY_DIR}/src)
diff --git a/src/panelplugins/tocpanel/tocpanelplugin.cpp b/src/panelplugins/tocpanel/tocpanelplugin.cpp
index f23cdc70..a04937f9 100644
--- a/src/panelplugins/tocpanel/tocpanelplugin.cpp
+++ b/src/panelplugins/tocpanel/tocpanelplugin.cpp
@@ -5,17 +5,116 @@
#include "tocpanelplugin.h"
-#include <cassert>
-#include <QListView>
+#include <KPluginFactory>
+
+#include <QAbstractItemDelegate>
+#include <QAction>
#include <QDebug>
+#include <QItemSelectionModel>
+#include <QKeyEvent>
+#include <QLineEdit>
+#include <QMenu>
+#include <QModelIndex>
+#include <QScopedValueRollback>
+#include <QStandardItem>
+#include <QStyledItemDelegate>
+#include <QTreeView>
+#include <QTimer>
+#include <QVariantList>
+#include <QVariantMap>
#include <QWidget>
-#include <KPluginFactory>
+#include <KLocalizedString>
+#include <KMessageBox>
+#include <KStandardGuiItem>
+
+#include "settings.h"
+
+namespace
+{
+const QLatin1String TocStateVersion("toc-state-v2");
+const QLatin1String TocNodeTypeChapter("chapter");
+const QLatin1String TocNodeTypeSection("section");
+const QLatin1String TocNodeTypeCommand("command");
+const QLatin1String TocNodeTypePlot("plot");
+
+class HierarchyNameDelegate : public QStyledItemDelegate
+{
+public:
+ explicit HierarchyNameDelegate(int nameRole, int nodeIdRole, int hierarchyIdRole, int nodeTypeRole, int customTitleRole, int entryIdRole, int resultIdRole, QObject* parent = nullptr)
+ : QStyledItemDelegate(parent)
+ , m_nameRole(nameRole)
+ , m_nodeIdRole(nodeIdRole)
+ , m_hierarchyIdRole(hierarchyIdRole)
+ , m_nodeTypeRole(nodeTypeRole)
+ , m_customTitleRole(customTitleRole)
+ , m_entryIdRole(entryIdRole)
+ , m_resultIdRole(resultIdRole)
+ {
+ }
+
+ QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override
+ {
+ Q_UNUSED(option);
+ Q_UNUSED(index);
+
+ auto* editor = new QLineEdit(parent);
+ editor->setClearButtonEnabled(true);
+ return editor;
+ }
+
+ void setEditorData(QWidget* editor, const QModelIndex& index) const override
+ {
+ auto* lineEdit = qobject_cast<QLineEdit*>(editor);
+
+ if (!lineEdit)
+ {
+ QStyledItemDelegate::setEditorData(editor, index);
+ return;
+ }
+
+ const QString nodeType = index.data(m_nodeTypeRole).toString();
+ const bool isPlotNode = nodeType == TocNodeTypePlot;
+ const QString title = index.data(m_nameRole).toString();
+ const QString customTitle = index.data(m_customTitleRole).toString();
+ const QString editTitle = isPlotNode && !customTitle.isEmpty() ? customTitle : title;
+
+ lineEdit->setText(isPlotNode ? editTitle : title);
+ if (isPlotNode)
+ lineEdit->setPlaceholderText(title);
+
+ lineEdit->setProperty("tocNodeId", index.data(m_nodeIdRole));
+ lineEdit->setProperty("tocNodeType", nodeType);
+ lineEdit->setProperty("tocHierarchyId", index.data(m_hierarchyIdRole));
+ lineEdit->setProperty("tocCommandId", index.data(m_entryIdRole));
+ lineEdit->setProperty("tocResultId", index.data(m_resultIdRole));
+ lineEdit->setProperty("tocOriginalTitle", isPlotNode ? editTitle : title);
+ lineEdit->selectAll();
+ }
+
+ void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override
+ {
+ Q_UNUSED(editor);
+ Q_UNUSED(model);
+ Q_UNUSED(index);
+ }
+
+private:
+ int m_nameRole;
+ int m_nodeIdRole;
+ int m_hierarchyIdRole;
+ int m_nodeTypeRole;
+ int m_customTitleRole;
+ int m_entryIdRole;
+ int m_resultIdRole;
+};
+}
TableOfContentPanelPlugin::TableOfContentPanelPlugin(QObject* parent, const QList<QVariant>& args): Cantor::PanelPlugin(parent),
m_mainWidget(nullptr)
{
Q_UNUSED(args);
+ resetVisibilityToDefaults();
}
TableOfContentPanelPlugin::~TableOfContentPanelPlugin()
@@ -36,9 +135,15 @@ QWidget* TableOfContentPanelPlugin::widget()
void TableOfContentPanelPlugin::connectToShell(QObject* cantorShell)
{
- connect(cantorShell, SIGNAL(hierarchyChanged(QStringList, QStringList, QList<int>)), this, SLOT(handleHierarchyChanges(QStringList, QStringList, QList<int>)));
- connect(this, SIGNAL(requestScrollToHierarchyEntry(QString)), cantorShell, SIGNAL(requestScrollToHierarchyEntry(QString)));
- connect(cantorShell, SIGNAL(hierarhyEntryNameChange(QString, QString, int)), this, SLOT(handleHierarhyEntryNameChange(QString, QString, int)));
+ connect(cantorShell, SIGNAL(tocNodesChanged(QVariantList)), this, SLOT(handleTocNodeChanges(QVariantList)));
+ connect(this, SIGNAL(requestNavigateToTocNode(QString)), cantorShell, SIGNAL(requestNavigateToTocNode(QString)));
+ connect(this, SIGNAL(requestRenameHierarchyEntry(QString,QString)), cantorShell, SIGNAL(requestRenameHierarchyEntry(QString,QString)));
+ connect(cantorShell, SIGNAL(currentTocNodeChanged(QString)), this, SLOT(handleCurrentTocNodeChanged(QString)));
+ connect(this, SIGNAL(requestChangeHierarchyLevel(QString,int)), cantorShell, SIGNAL(requestChangeHierarchyLevel(QString,int)));
+ connect(this, SIGNAL(requestDeleteHierarchyEntry(QString,bool)), cantorShell, SIGNAL(requestDeleteHierarchyEntry(QString,bool)));
+ connect(this, SIGNAL(requestRenamePlot(QString,QString,QString)), cantorShell, SIGNAL(requestRenamePlot(QString,QString,QString)));
+ connect(this, SIGNAL(requestDeletePlot(QString,QString)), cantorShell, SIGNAL(requestDeletePlot(QString,QString)));
+ connect(cantorShell, SIGNAL(tocReadOnlyChanged(bool)), this, SLOT(handleReadOnlyChanged(bool)));
}
bool TableOfContentPanelPlugin::showOnStartup()
@@ -46,70 +151,853 @@ bool TableOfContentPanelPlugin::showOnStartup()
return false;
}
+void TableOfContentPanelPlugin::handleClicked(const QModelIndex& index)
+{
+ if (m_editorActive || !index.isValid())
+ return;
+
+ const QString nodeId = index.data(NodeIdRole).toString();
+ const bool navigable = index.data(NavigableRole).toBool();
+
+ if (nodeId.isEmpty() || !navigable)
+ return;
+
+ Q_EMIT requestNavigateToTocNode(nodeId);
+}
+
void TableOfContentPanelPlugin::handleDoubleClicked(const QModelIndex& index)
{
- qDebug() << "TableOfContentPanelPlugin::handleDoubleClicked";
- const QString& searchStringIndex = m_hierarchyPositionStringList[index.row()];
- Q_EMIT requestScrollToHierarchyEntry(searchStringIndex);
+ beginRename(index);
}
void TableOfContentPanelPlugin::constructMainWidget()
{
- QListView* view = new QListView(m_mainWidget);
+ auto* view = new QTreeView;
+
view->setEditTriggers(QAbstractItemView::NoEditTriggers);
- view->setSelectionBehavior(QAbstractItemView::SelectItems);
+ view->setSelectionBehavior(QAbstractItemView::SelectRows);
+ view->setSelectionMode(QAbstractItemView::SingleSelection);
+ view->setHeaderHidden(true);
+ view->setUniformRowHeights(true);
+ view->setItemsExpandable(true);
+ view->setExpandsOnDoubleClick(false);
+ view->setContextMenuPolicy(Qt::CustomContextMenu);
+ view->installEventFilter(this);
+ view->viewport()->installEventFilter(this);
+
+ auto* delegate = new HierarchyNameDelegate(NameRole, NodeIdRole, HierarchyIdRole, NodeTypeRole, CustomTitleRole, EntryIdRole, ResultIdRole, view);
+
+ view->setItemDelegate(delegate);
view->setModel(&m_model);
- connect(view, &QListView::doubleClicked, this, &TableOfContentPanelPlugin::handleDoubleClicked);
+ connect(view, &QTreeView::clicked, this, &TableOfContentPanelPlugin::handleClicked);
+ connect(view, &QTreeView::doubleClicked, this, &TableOfContentPanelPlugin::handleDoubleClicked);
+ connect(view, &QTreeView::customContextMenuRequested, this, &TableOfContentPanelPlugin::handleContextMenuRequested);
+ connect(view, &QTreeView::expanded, this, &TableOfContentPanelPlugin::handleExpanded);
+ connect(view, &QTreeView::collapsed, this, &TableOfContentPanelPlugin::handleCollapsed);
+ connect(delegate, &QAbstractItemDelegate::commitData, this, &TableOfContentPanelPlugin::handleEditorCommit);
+ connect(delegate, &QAbstractItemDelegate::closeEditor, this, &TableOfContentPanelPlugin::handleEditorClosed);
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)
+ {
+ auto* keyEvent = static_cast<QKeyEvent*>(event);
+ if (keyEvent->key() == Qt::Key_F2 && !m_readOnly)
+ {
+ beginRename(m_mainWidget->currentIndex());
+ return true;
+ }
+ }
+
+ return Cantor::PanelPlugin::eventFilter(watched, event);
+}
+
+void TableOfContentPanelPlugin::rebuildModel()
+{
+ QScopedValueRollback<bool> guard(m_updatingModel, true);
+
+ m_model.clear();
+ m_itemsByNodeId.clear();
+
+ QVector<QStandardItem*> visibleItems(m_nodes.size(), nullptr);
+
+ for (int i = 0; i < m_nodes.size(); ++i)
+ {
+ if (!shouldDisplayNode(i))
+ continue;
+
+ const TocNode& node = m_nodes.at(i);
+ const int parentIndex = findVisibleAncestorIndex(node.parentIndex);
+ QStandardItem* parentItem = parentIndex >= 0 ? visibleItems.at(parentIndex) : m_model.invisibleRootItem();
+
+ auto* item = new QStandardItem(node.displayText);
+ item->setEditable(node.editable && !m_readOnly);
+
+ item->setData(node.id, NodeIdRole);
+ item->setData(node.parentId, ParentNodeIdRole);
+ item->setData(node.type, NodeTypeRole);
+ item->setData(node.depth, DepthRole);
+ item->setData(node.title, NameRole);
+ item->setData(node.displayText, DisplayTextRole);
+ item->setData(node.hierarchyText, HierarchyTextRole);
+ item->setData(node.hierarchyId, HierarchyIdRole);
+ item->setData(node.entryId, EntryIdRole);
+ item->setData(node.resultId, ResultIdRole);
+ item->setData(node.customTitle, CustomTitleRole);
+ item->setData(node.resultIndex, ResultIndexRole);
+ item->setData(node.editable, EditableRole);
+ item->setData(node.navigable, NavigableRole);
+
+ parentItem->appendRow(item);
+ visibleItems[i] = item;
+
+ m_itemsByNodeId.insert(node.id, item);
+ }
+
+ restoreExpansionState();
+ updateCurrentNodeSelection();
+}
+
+void TableOfContentPanelPlugin::restoreExpansionState()
+{
+ if (!m_mainWidget)
+ return;
+
+ if (!m_expansionStateInitialized)
+ {
+ m_mainWidget->expandAll();
+
+ for (auto it = m_itemsByNodeId.cbegin(); it != m_itemsByNodeId.cend(); ++it)
+ {
+ if (it.value() && it.value()->hasChildren())
+ m_expandedNodeIds.insert(it.key());
+ }
+
+ m_expansionStateInitialized = true;
+ return;
+ }
+
+ for (auto it = m_itemsByNodeId.cbegin(); it != m_itemsByNodeId.cend(); ++it)
+ {
+ if (!it.value())
+ continue;
+
+ const QModelIndex index = it.value()->index();
+
+ if (m_expandedNodeIds.contains(it.key()))
+ m_mainWidget->expand(index);
+ else
+ m_mainWidget->collapse(index);
+ }
+}
+
+void TableOfContentPanelPlugin::updateCurrentNodeSelection()
+{
+ if (!m_mainWidget || !m_mainWidget->selectionModel())
+ return;
+
+ auto* selectionModel = m_mainWidget->selectionModel();
+
+ auto* item = m_itemsByNodeId.value(m_currentNodeId, nullptr);
+ if (!item)
+ {
+ const int sourceIndex = m_nodeIndexById.value(m_currentNodeId, -1);
+ if (sourceIndex >= 0)
+ {
+ const int visibleAncestorIndex = findVisibleAncestorIndex(sourceIndex);
+ if (visibleAncestorIndex >= 0)
+ item = m_itemsByNodeId.value(m_nodes.at(visibleAncestorIndex).id, nullptr);
+ }
+ }
+
+ if (!item)
+ {
+ selectionModel->clearSelection();
+ selectionModel->setCurrentIndex(QModelIndex{}, QItemSelectionModel::NoUpdate);
+ return;
+ }
+
+ const QModelIndex index = item->index();
+
+ expandIndexParents(index);
+
+ selectionModel->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
+
+ m_mainWidget->scrollTo(index, QAbstractItemView::EnsureVisible);
}
void TableOfContentPanelPlugin::restoreState(const Cantor::PanelPlugin::State& state)
{
- if (state.inners.size() == 2)
+ cancelEditorSession();
+ clearNodes();
+
+ resetVisibilityToDefaults();
+ m_expandedNodeIds.clear();
+ m_currentNodeId.clear();
+ m_expansionStateInitialized = false;
+
+ bool restoredExpansionState = false;
+
+ if (state.inners.isEmpty())
{
- m_model.setStringList(state.inners[0].toStringList());
- m_hierarchyPositionStringList = state.inners[1].toStringList();
+ rebuildModel();
+ return;
+ }
+
+ if (state.inners.at(0).toString() == TocStateVersion)
+ {
+ if (state.inners.size() > 1)
+ m_showChapters = state.inners.at(1).toBool();
+ if (state.inners.size() > 2)
+ m_showSections = state.inners.at(2).toBool();
+ if (state.inners.size() > 3)
+ m_showCommandEntries = state.inners.at(3).toBool();
+ if (state.inners.size() > 4)
+ m_showPlots = state.inners.at(4).toBool();
+
+ const QStringList expandedIds = state.inners.size() > 5 ? state.inners.at(5).toStringList() : QStringList();
+ for (const QString& nodeId : expandedIds)
+ m_expandedNodeIds.insert(nodeId);
+
+ if (state.inners.size() > 6)
+ m_currentNodeId = state.inners.at(6).toString();
+
+ restoredExpansionState = true;
+ }
+ else if (state.inners.size() >= 10)
+ {
+ const QStringList expandedIds = state.inners.at(4).toStringList();
+ for (const QString& nodeId : expandedIds)
+ m_expandedNodeIds.insert(nodeId);
+
+ m_currentNodeId = state.inners.at(5).toString();
+ m_showChapters = state.inners.at(6).toBool();
+ m_showSections = state.inners.at(7).toBool();
+ m_showCommandEntries = state.inners.at(8).toBool();
+ m_showPlots = state.inners.at(9).toBool();
+ restoredExpansionState = true;
+ }
+ else if (state.inners.size() >= 6)
+ {
+ const QStringList expandedIds = state.inners.at(4).toStringList();
+ for (const QString& nodeId : expandedIds)
+ m_expandedNodeIds.insert(nodeId);
+
+ m_currentNodeId = state.inners.at(5).toString();
+ restoredExpansionState = true;
}
+
+ m_expansionStateInitialized = restoredExpansionState;
+
+ rebuildModel();
+}
+
+void TableOfContentPanelPlugin::expandIndexParents(const QModelIndex& index)
+{
+ if (!m_mainWidget)
+ return;
+
+ QList<QModelIndex> parents;
+
+ for (QModelIndex parent = index.parent(); parent.isValid(); parent = parent.parent())
+ parents.prepend(parent);
+
+ for (const QModelIndex& parent : parents)
+ m_mainWidget->expand(parent);
}
Cantor::PanelPlugin::State TableOfContentPanelPlugin::saveState()
{
Cantor::PanelPlugin::State state;
- state.inners.append(m_model.stringList());
- state.inners.append(m_hierarchyPositionStringList);
+ state.inners.append(QString(TocStateVersion));
+ state.inners.append(m_showChapters);
+ state.inners.append(m_showSections);
+ state.inners.append(m_showCommandEntries);
+ state.inners.append(m_showPlots);
+ state.inners.append(QStringList(m_expandedNodeIds.values()));
+ state.inners.append(m_currentNodeId);
return state;
}
-void TableOfContentPanelPlugin::handleHierarchyChanges(QStringList names, QStringList searchStrings, QList<int> depths)
+void TableOfContentPanelPlugin::handleTocNodeChanges(const QVariantList& nodes)
+{
+ if (m_editorActive)
+ {
+ m_pendingNodeSnapshot = nodes;
+ m_hasPendingNodeSnapshot = true;
+ return;
+ }
+
+ applyTocNodeChanges(nodes);
+}
+
+void TableOfContentPanelPlugin::applyTocNodeChanges(const QVariantList& nodes)
+{
+ clearNodes();
+ m_nodes.reserve(nodes.size());
+
+ QSet<QString> seenNodeIds;
+
+ const auto nodeTypeFromValue = [](const QVariant& value) -> QString
+ {
+ if (value.typeId() == QMetaType::QString)
+ return value.toString();
+
+ bool ok = false;
+ const int legacyType = value.toInt(&ok);
+ if (!ok)
+ return QString();
+
+ switch (legacyType)
+ {
+ case 0:
+ return TocNodeTypeChapter;
+ case 1:
+ return TocNodeTypeSection;
+ case 2:
+ return TocNodeTypeCommand;
+ case 3:
+ return TocNodeTypePlot;
+ default:
+ return QString();
+ }
+ };
+
+ for (const QVariant& value : nodes)
+ {
+ const QVariantMap node = value.toMap();
+
+ TocNode tocNode;
+ tocNode.id = node.value(QStringLiteral("id")).toString();
+ tocNode.parentId = node.value(QStringLiteral("parentId")).toString();
+ tocNode.type = nodeTypeFromValue(node.value(QStringLiteral("type")));
+ tocNode.title = node.value(QStringLiteral("title")).toString();
+ tocNode.displayText = node.value(QStringLiteral("displayText")).toString();
+ tocNode.hierarchyText = node.value(QStringLiteral("hierarchyText")).toString();
+ tocNode.hierarchyId = node.value(QStringLiteral("hierarchyId")).toString();
+ tocNode.entryId = node.value(QStringLiteral("entryId")).toString();
+ tocNode.resultId = node.value(QStringLiteral("resultId")).toString();
+ tocNode.customTitle = node.value(QStringLiteral("customTitle")).toString();
+ tocNode.resultIndex = node.value(QStringLiteral("resultIndex"), -1).toInt();
+ tocNode.depth = node.value(QStringLiteral("depth"), 0).toInt();
+ tocNode.editable = node.value(QStringLiteral("editable"), false).toBool();
+ tocNode.navigable = node.value(QStringLiteral("navigable"), false).toBool();
+
+ if (tocNode.displayText.isEmpty())
+ tocNode.displayText = tocNode.title;
+
+ if (tocNode.id.isEmpty() || tocNode.type.isEmpty() || seenNodeIds.contains(tocNode.id))
+ {
+ qWarning() << "Ignoring invalid TOC node" << node;
+ continue;
+ }
+
+ seenNodeIds.insert(tocNode.id);
+ m_nodeIndexById.insert(tocNode.id, m_nodes.size());
+ m_nodes.append(tocNode);
+ }
+
+ for (int i = 0; i < m_nodes.size(); ++i)
+ {
+ const QString& parentId = m_nodes.at(i).parentId;
+ m_nodes[i].parentIndex = parentId.isEmpty() ? -1 : m_nodeIndexById.value(parentId, -1);
+ }
+
+ cleanupStateAfterNodeChange();
+ rebuildModel();
+}
+
+void TableOfContentPanelPlugin::beginRename(const QModelIndex& index)
+{
+ if (!m_mainWidget || !index.isValid() || m_editorActive || m_readOnly)
+ return;
+
+ const QString nodeId = index.data(NodeIdRole).toString();
+ const QString hierarchyId = index.data(HierarchyIdRole).toString();
+ const QString nodeType = index.data(NodeTypeRole).toString();
+ const bool isPlotNode = nodeType == TocNodeTypePlot;
+
+ if (nodeId.isEmpty() || !index.data(EditableRole).toBool())
+ return;
+
+ if (!isPlotNode && hierarchyId.isEmpty())
+ return;
+
+ if (isPlotNode && (index.data(EntryIdRole).toString().isEmpty() || index.data(ResultIdRole).toString().isEmpty()))
+ return;
+
+ m_editorActive = true;
+ m_editingNodeId = nodeId;
+ m_hasPendingRename = false;
+ m_pendingRenameNodeType.clear();
+ m_pendingRenameHierarchyId.clear();
+ m_pendingRenameCommandId.clear();
+ m_pendingRenameResultId.clear();
+ m_pendingRenameTitle.clear();
+
+ m_mainWidget->setCurrentIndex(index);
+ m_mainWidget->edit(index);
+
+ QTimer::singleShot(0, this, [this, nodeId]()
+ {
+ if (!m_editorActive || m_editingNodeId != nodeId || !m_mainWidget)
+ return;
+
+ const auto editors = m_mainWidget->findChildren<QLineEdit*>();
+ for (auto* editor : editors)
+ {
+ if (editor && editor->property("tocNodeId").toString() == nodeId)
+ return;
+ }
+
+ finishEditorSession();
+ });
+}
+
+void TableOfContentPanelPlugin::handleEditorCommit(QWidget* editor)
{
- QStringList fullNames;
+ if (!m_editorActive || !editor)
+ return;
- assert(names.size() == searchStrings.size() && names.size() == depths.size());
+ auto* lineEdit = qobject_cast<QLineEdit*>(editor);
+ if (!lineEdit)
+ return;
- int size = names.size();
- for (int i = 0; i < size; i++)
- fullNames.append(QString::fromLatin1(" ").repeated(depths[i]) + searchStrings[i] + QLatin1String(" ") + names[i]);
+ const QString nodeId = lineEdit->property("tocNodeId").toString();
+ const QString nodeType = lineEdit->property("tocNodeType").toString();
+ const QString hierarchyId = lineEdit->property("tocHierarchyId").toString();
+ const QString commandId = lineEdit->property("tocCommandId").toString();
+ const QString resultId = lineEdit->property("tocResultId").toString();
- m_model.setStringList(fullNames);
- m_hierarchyPositionStringList = std::move(searchStrings);
+ if (nodeId != m_editingNodeId)
+ return;
+
+ QString newName = lineEdit->text();
+ newName.replace(QLatin1Char('\r'), QLatin1Char(' '));
+ newName.replace(QLatin1Char('\n'), QLatin1Char(' '));
+ newName = newName.trimmed();
+
+ QString oldName = lineEdit->property("tocOriginalTitle").toString();
+ const int position = m_nodeIndexById.value(nodeId, -1);
+ if (position >= 0 && position < m_nodes.size())
+ {
+ const TocNode& node = m_nodes.at(position);
+ oldName = nodeType == TocNodeTypePlot && !node.customTitle.isEmpty() ? node.customTitle : node.title;
+ }
+
+ if (newName == oldName)
+ return;
+
+ if (nodeType == TocNodeTypePlot)
+ {
+ if (commandId.isEmpty() || resultId.isEmpty())
+ return;
+
+ m_pendingRenameCommandId = commandId;
+ m_pendingRenameResultId = resultId;
+ }
+ else
+ {
+ if (hierarchyId.isEmpty())
+ return;
+
+ m_pendingRenameHierarchyId = hierarchyId;
+ }
+
+ m_pendingRenameNodeType = nodeType;
+ m_pendingRenameTitle = newName;
+ m_hasPendingRename = true;
}
-void TableOfContentPanelPlugin::handleHierarhyEntryNameChange(QString name, QString searchString, int deapth)
+void TableOfContentPanelPlugin::handleEditorClosed()
{
- int size = m_hierarchyPositionStringList.size();
- for (int i = 0; i < size; i++)
+ QTimer::singleShot(0, this, &TableOfContentPanelPlugin::finishEditorSession);
+}
+
+void TableOfContentPanelPlugin::finishEditorSession()
+{
+ const bool hadEditor = m_editorActive;
+ m_editorActive = false;
+ m_editingNodeId.clear();
+
+ if (!hadEditor && !m_hasPendingNodeSnapshot && !m_hasPendingRename)
+ return;
+
+ if (m_hasPendingNodeSnapshot)
{
- if (m_hierarchyPositionStringList[i] == searchString)
+ const QVariantList pendingNodes = m_pendingNodeSnapshot;
+ m_pendingNodeSnapshot.clear();
+ m_hasPendingNodeSnapshot = false;
+ applyTocNodeChanges(pendingNodes);
+ }
+
+ if (m_hasPendingRename)
+ {
+ const QString nodeType = m_pendingRenameNodeType;
+ const QString hierarchyId = m_pendingRenameHierarchyId;
+ const QString commandId = m_pendingRenameCommandId;
+ const QString resultId = m_pendingRenameResultId;
+ const QString title = m_pendingRenameTitle;
+
+ m_pendingRenameNodeType.clear();
+ m_pendingRenameHierarchyId.clear();
+ m_pendingRenameCommandId.clear();
+ m_pendingRenameResultId.clear();
+ m_pendingRenameTitle.clear();
+ m_hasPendingRename = false;
+
+ if (nodeType == TocNodeTypePlot)
+ Q_EMIT requestRenamePlot(commandId, resultId, title);
+ else
+ Q_EMIT requestRenameHierarchyEntry(hierarchyId, title);
+ }
+}
+
+void TableOfContentPanelPlugin::cancelEditorSession()
+{
+ m_editorActive = false;
+ m_editingNodeId.clear();
+ m_hasPendingNodeSnapshot = false;
+ m_hasPendingRename = false;
+ m_pendingRenameNodeType.clear();
+ m_pendingRenameHierarchyId.clear();
+ m_pendingRenameCommandId.clear();
+ m_pendingRenameResultId.clear();
+ m_pendingRenameTitle.clear();
+ m_pendingNodeSnapshot.clear();
+}
+
+bool TableOfContentPanelPlugin::shouldDisplayNode(int index) const
+{
+ if (index < 0 || index >= m_nodes.size())
+ return false;
+
+ const QString& type = m_nodes.at(index).type;
+
+ if (type == TocNodeTypeChapter)
+ return m_showChapters;
+ if (type == TocNodeTypeSection)
+ return m_showSections;
+ if (type == TocNodeTypeCommand)
+ return m_showCommandEntries;
+ if (type == TocNodeTypePlot)
+ return m_showPlots;
+
+ return true;
+}
+
+int TableOfContentPanelPlugin::findVisibleAncestorIndex(int index) const
+{
+ while (index >= 0)
+ {
+ if (shouldDisplayNode(index))
+ return index;
+
+ index = m_nodes.value(index).parentIndex;
+ }
+ return -1;
+}
+
+void TableOfContentPanelPlugin::clearNodes()
+{
+ m_nodes.clear();
+ m_nodeIndexById.clear();
+ m_itemsByNodeId.clear();
+}
+
+void TableOfContentPanelPlugin::resetVisibilityToDefaults()
+{
+ m_showChapters = Settings::showTocChaptersDefault();
+ m_showSections = Settings::showTocSectionsDefault();
+ m_showCommandEntries = Settings::showTocCommandEntriesDefault();
+ m_showPlots = Settings::showTocPlotsDefault();
+}
+
+void TableOfContentPanelPlugin::cleanupStateAfterNodeChange()
+{
+ for (auto it = m_expandedNodeIds.begin(); it != m_expandedNodeIds.end();)
+ {
+ if (!m_nodeIndexById.contains(*it))
+ it = m_expandedNodeIds.erase(it);
+ else
+ ++it;
+ }
+
+ if (!m_currentNodeId.isEmpty() && !m_nodeIndexById.contains(m_currentNodeId))
+ m_currentNodeId.clear();
+}
+
+void TableOfContentPanelPlugin::addVisibleItemsMenu(QMenu* menu)
+{
+ if (!menu)
+ return;
+
+ QMenu* visibleItemsMenu = menu->addMenu(i18n("Visible Items"));
+
+ QAction* showChaptersAction = visibleItemsMenu->addAction(i18n("Show Chapters"));
+ showChaptersAction->setCheckable(true);
+ showChaptersAction->setChecked(m_showChapters);
+ connect(showChaptersAction, &QAction::triggered, this, [this](bool checked)
+ {
+ m_showChapters = checked;
+ rebuildModel();
+ });
+
+ QAction* showSectionsAction = visibleItemsMenu->addAction(i18n("Show Sections"));
+ showSectionsAction->setCheckable(true);
+ showSectionsAction->setChecked(m_showSections);
+ connect(showSectionsAction, &QAction::triggered, this, [this](bool checked)
+ {
+ m_showSections = checked;
+ rebuildModel();
+ });
+
+ QAction* showCommandsAction = visibleItemsMenu->addAction(i18n("Show Command Entries"));
+ showCommandsAction->setCheckable(true);
+ showCommandsAction->setChecked(m_showCommandEntries);
+ connect(showCommandsAction, &QAction::triggered, this, [this](bool checked)
+ {
+ m_showCommandEntries = checked;
+ rebuildModel();
+ });
+
+ QAction* showPlotsAction = visibleItemsMenu->addAction(i18n("Show Plots"));
+ showPlotsAction->setCheckable(true);
+ showPlotsAction->setChecked(m_showPlots);
+ connect(showPlotsAction, &QAction::triggered, this, [this](bool checked)
+ {
+ m_showPlots = checked;
+ rebuildModel();
+ });
+
+ visibleItemsMenu->addSeparator();
+
+ QAction* resetAction = visibleItemsMenu->addAction(QIcon::fromTheme(QStringLiteral("edit-undo")), i18n("Reset to Defaults"));
+ connect(resetAction, &QAction::triggered, this, [this]()
+ {
+ resetVisibilityToDefaults();
+ rebuildModel();
+ });
+}
+
+void TableOfContentPanelPlugin::handleCurrentTocNodeChanged(const QString& nodeId)
+{
+ m_currentNodeId = nodeId;
+ updateCurrentNodeSelection();
+}
+
+void TableOfContentPanelPlugin::handleReadOnlyChanged(bool readOnly)
+{
+ if (m_readOnly == readOnly)
+ return;
+
+ m_readOnly = readOnly;
+
+ if (m_editorActive)
+ finishEditorSession();
+
+ rebuildModel();
+}
+
+void TableOfContentPanelPlugin::handleExpanded(const QModelIndex& index)
+{
+ const QString nodeId = index.data(NodeIdRole).toString();
+
+ if (!nodeId.isEmpty())
+ m_expandedNodeIds.insert(nodeId);
+}
+
+void TableOfContentPanelPlugin::handleCollapsed(const QModelIndex& index)
+{
+ const QString nodeId = index.data(NodeIdRole).toString();
+
+ if (!nodeId.isEmpty())
+ m_expandedNodeIds.remove(nodeId);
+}
+
+void TableOfContentPanelPlugin::handleContextMenuRequested(const QPoint& position)
+{
+ if (!m_mainWidget)
+ return;
+
+ if (m_editorActive)
+ return;
+
+ const QModelIndex index = m_mainWidget->indexAt(position);
+
+ QMenu menu(m_mainWidget);
+
+ if (index.isValid())
+ {
+ const QString nodeId = index.data(NodeIdRole).toString();
+ const QString hierarchyId = index.data(HierarchyIdRole).toString();
+ const QString nodeType = index.data(NodeTypeRole).toString();
+
+ auto* item = m_model.itemFromIndex(index);
+
+ const int depth = index.data(DepthRole).toInt();
+
+ if (!hierarchyId.isEmpty())
+ {
+ QAction* goToHeadingAction = menu.addAction(i18n("Go to Heading"));
+ goToHeadingAction->setEnabled(!nodeId.isEmpty() && index.data(NavigableRole).toBool());
+ connect(goToHeadingAction, &QAction::triggered, this, [this, nodeId]()
+ {
+ if (!nodeId.isEmpty())
+ Q_EMIT requestNavigateToTocNode(nodeId);
+ });
+
+ menu.addSeparator();
+
+ QAction* renameAction = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-rename")), i18n("Rename"));
+ renameAction->setEnabled(!m_readOnly);
+
+ connect(renameAction, &QAction::triggered, this, [this, nodeId]()
+ {
+ if (auto* item = m_itemsByNodeId.value(nodeId, nullptr))
+ beginRename(item->index());
+ });
+
+ menu.addSeparator();
+
+ QAction* promoteAction = menu.addAction(QIcon::fromTheme(QStringLiteral("format-indent-less")), i18n("Promote Heading"));
+ promoteAction->setEnabled(!m_readOnly && depth > 0);
+ 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);
+ connect(demoteAction, &QAction::triggered, this, [this, hierarchyId]()
+ {
+ Q_EMIT requestChangeHierarchyLevel(hierarchyId, 1);
+ });
+
+ menu.addSeparator();
+
+ QAction* deleteHeadingAction = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete Heading Only"));
+ deleteHeadingAction->setEnabled(!m_readOnly);
+ connect(deleteHeadingAction, &QAction::triggered, this, [this, hierarchyId]()
+ {
+ Q_EMIT requestDeleteHierarchyEntry(hierarchyId, false);
+ });
+
+ QAction* deleteSectionAction = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete Section and Contents"));
+ deleteSectionAction->setEnabled(!m_readOnly);
+ connect(deleteSectionAction, &QAction::triggered, this, [this, hierarchyId]()
+ {
+ Q_EMIT requestDeleteHierarchyEntry(hierarchyId, true);
+ });
+ }
+ else if (nodeType == TocNodeTypeCommand)
{
- QModelIndex index = m_model.index(i);
- const QString& fullName = QString::fromLatin1(" ").repeated(deapth) + searchString + QLatin1String(" ") + name;
- m_model.setData(index, fullName);
+ QAction* goToCommandAction = menu.addAction(i18n("Go to Command"));
+ goToCommandAction->setEnabled(!nodeId.isEmpty() && index.data(NavigableRole).toBool());
+ connect(goToCommandAction, &QAction::triggered, this, [this, nodeId]()
+ {
+ if (!nodeId.isEmpty())
+ Q_EMIT requestNavigateToTocNode(nodeId);
+ });
}
+ else if (nodeType == TocNodeTypePlot)
+ {
+ const QString commandId = index.data(EntryIdRole).toString();
+ const QString resultId = index.data(ResultIdRole).toString();
+ const QString displayText = index.data(DisplayTextRole).toString();
+ QAction* goToPlotAction = menu.addAction(i18n("Go to Plot"));
+ goToPlotAction->setEnabled(!nodeId.isEmpty() && index.data(NavigableRole).toBool());
+ connect(goToPlotAction, &QAction::triggered, this, [this, nodeId]()
+ {
+ if (!nodeId.isEmpty())
+ Q_EMIT requestNavigateToTocNode(nodeId);
+ });
+
+ menu.addSeparator();
+
+ QAction* renameAction = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-rename")), i18n("Rename Plot"));
+ renameAction->setEnabled(!m_readOnly && !nodeId.isEmpty() && !commandId.isEmpty() && !resultId.isEmpty());
+ connect(renameAction, &QAction::triggered, this, [this, nodeId]()
+ {
+ if (auto* item = m_itemsByNodeId.value(nodeId, nullptr))
+ beginRename(item->index());
+ });
+
+ QAction* deleteAction = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete Plot"));
+ deleteAction->setEnabled(!m_readOnly && !commandId.isEmpty() && !resultId.isEmpty());
+ connect(deleteAction, &QAction::triggered, this, [this, commandId, resultId, displayText]()
+ {
+ 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);
+ });
+ }
+
+ if (item && item->hasChildren())
+ {
+ menu.addSeparator();
+
+ if (m_mainWidget->isExpanded(index))
+ {
+ QAction* collapseAction = menu.addAction(QIcon::fromTheme(QStringLiteral("arrow-up-double")), i18n("Collapse Section"));
+ connect(collapseAction, &QAction::triggered, this, [this, nodeId]()
+ {
+ if (m_mainWidget)
+ {
+ if (auto* item = m_itemsByNodeId.value(nodeId, nullptr))
+ m_mainWidget->collapse(item->index());
+ }
+ });
+ }
+ else
+ {
+ QAction* expandAction = menu.addAction(QIcon::fromTheme(QStringLiteral("arrow-down-double")), i18n("Expand Section"));
+ connect(expandAction, &QAction::triggered, this, [this, nodeId]()
+ {
+ if (m_mainWidget)
+ {
+ if (auto* item = m_itemsByNodeId.value(nodeId, nullptr))
+ m_mainWidget->expandRecursively(item->index());
+ }
+ });
+ }
+ }
+
+ menu.addSeparator();
}
+
+ addVisibleItemsMenu(&menu);
+ menu.addSeparator();
+
+ QAction* expandAllAction = menu.addAction(QIcon::fromTheme(QStringLiteral("view-list-tree")), i18n("Expand All"));
+ connect(expandAllAction, &QAction::triggered, this, [this]()
+ {
+ if (m_mainWidget)
+ m_mainWidget->expandAll();
+ });
+
+ QAction* collapseAllAction = menu.addAction(QIcon::fromTheme(QStringLiteral("view-list-details")), i18n("Collapse All"));
+ connect(collapseAllAction, &QAction::triggered, this, [this]()
+ {
+ if (m_mainWidget)
+ m_mainWidget->collapseAll();
+ });
+
+ menu.exec(m_mainWidget->viewport()->mapToGlobal(position));
}
K_PLUGIN_FACTORY_WITH_JSON(tocpanelplugin, "tocpanelplugin.json", registerPlugin<TableOfContentPanelPlugin>();)
diff --git a/src/panelplugins/tocpanel/tocpanelplugin.h b/src/panelplugins/tocpanel/tocpanelplugin.h
index fed98b8c..3a26c9b0 100644
--- a/src/panelplugins/tocpanel/tocpanelplugin.h
+++ b/src/panelplugins/tocpanel/tocpanelplugin.h
@@ -6,17 +6,23 @@
#ifndef _FILEBROWSERPANELPLUGIN_H
#define _FILEBROWSERPANELPLUGIN_H
-#include <QStringListModel>
-
+#include <QHash>
+#include <QList>
+#include <QPoint>
#include <QPointer>
-
+#include <QSet>
+#include <QStandardItemModel>
+#include <QString>
+#include <QVariantList>
+#include <QVector>
#include "panelplugin.h"
class QWidget;
+class QEvent;
class QModelIndex;
-class QPushButton;
-class QLineEdit;
-class QComboBox;
+class QMenu;
+class QStandardItem;
+class QTreeView;
class TableOfContentPanelPlugin : public Cantor::PanelPlugin
{
@@ -35,22 +41,113 @@ class TableOfContentPanelPlugin : public Cantor::PanelPlugin
void restoreState(const State& state) override;
-
Q_SIGNALS:
- void requestScrollToHierarchyEntry(QString);
+ void requestNavigateToTocNode(QString nodeId);
+ void requestRenameHierarchyEntry(QString hierarchyId, QString newName);
+ void requestChangeHierarchyLevel(QString hierarchyId, int levelDelta);
+ void requestDeleteHierarchyEntry(QString hierarchyId, bool deleteContents);
+ void requestRenamePlot(QString commandId, QString resultId, QString newTitle);
+ void requestDeletePlot(QString commandId, QString resultId);
+
+ protected:
+ bool eventFilter(QObject* watched, QEvent* event) override;
private Q_SLOTS:
+ void handleClicked(const QModelIndex&);
void handleDoubleClicked(const QModelIndex&);
- void handleHierarchyChanges(QStringList names, QStringList searchStrings, QList<int> depths);
- void handleHierarhyEntryNameChange(QString name, QString searchString, int deapth);
+ void handleTocNodeChanges(const QVariantList& nodes);
+ void handleCurrentTocNodeChanged(const QString& nodeId);
+ void handleExpanded(const QModelIndex& index);
+ void handleCollapsed(const QModelIndex& index);
+ void handleContextMenuRequested(const QPoint& position);
+ void handleReadOnlyChanged(bool readOnly);
+
+private:
+ enum ItemRole
+ {
+ HierarchyIdRole = Qt::UserRole + 1,
+ NodeIdRole,
+ ParentNodeIdRole,
+ NodeTypeRole,
+ DepthRole,
+ NameRole,
+ DisplayTextRole,
+ HierarchyTextRole,
+ EntryIdRole,
+ ResultIdRole,
+ CustomTitleRole,
+ ResultIndexRole,
+ EditableRole,
+ NavigableRole
+ };
+
+ struct TocNode
+ {
+ QString id;
+ QString parentId;
+ QString type;
+ QString title;
+ QString displayText;
+ QString hierarchyText;
+ QString hierarchyId;
+ QString entryId;
+ QString resultId;
+ QString customTitle;
+ int resultIndex{-1};
+ int depth{0};
+ int parentIndex{-1};
+ bool editable{false};
+ bool navigable{false};
+ };
- private:
void constructMainWidget();
-
- private:
- QPointer<QWidget> m_mainWidget;
- QStringListModel m_model;
- QStringList m_hierarchyPositionStringList;
+ void rebuildModel();
+ void applyTocNodeChanges(const QVariantList& nodes);
+ void updateCurrentNodeSelection();
+ void restoreExpansionState();
+ void expandIndexParents(const QModelIndex& index);
+ bool shouldDisplayNode(int index) const;
+ int findVisibleAncestorIndex(int index) const;
+ void clearNodes();
+ void addVisibleItemsMenu(QMenu* menu);
+ void resetVisibilityToDefaults();
+ void cleanupStateAfterNodeChange();
+ void beginRename(const QModelIndex& index);
+ void handleEditorCommit(QWidget* editor);
+ void handleEditorClosed();
+ void finishEditorSession();
+ void cancelEditorSession();
+
+private:
+ QPointer<QTreeView> m_mainWidget;
+ QStandardItemModel m_model;
+
+ QVector<TocNode> m_nodes;
+ QHash<QString, int> m_nodeIndexById;
+
+ QHash<QString, QStandardItem*> m_itemsByNodeId;
+
+ bool m_showChapters{true};
+ bool m_showSections{true};
+ bool m_showCommandEntries{false};
+ bool m_showPlots{false};
+ bool m_readOnly{false};
+
+ QSet<QString> m_expandedNodeIds;
+
+ QString m_currentNodeId;
+ bool m_updatingModel{false};
+ bool m_expansionStateInitialized{false};
+ bool m_editorActive{false};
+ bool m_hasPendingNodeSnapshot{false};
+ bool m_hasPendingRename{false};
+ QString m_pendingRenameNodeType;
+ QString m_editingNodeId;
+ QString m_pendingRenameHierarchyId;
+ QString m_pendingRenameCommandId;
+ QString m_pendingRenameResultId;
+ QString m_pendingRenameTitle;
+ QVariantList m_pendingNodeSnapshot;
};
#endif /* _FILEBROWSERPANELPLUGIN_H */