vnote/src/widgets/notebooknodeexplorer.cpp
2022-04-10 07:09:40 +08:00

2423 lines
74 KiB
C++

#include "notebooknodeexplorer.h"
#include <QVBoxLayout>
#include <QSplitter>
#include <QMenu>
#include <QAction>
#include <QSet>
#include <QShortcut>
#include <notebook/notebook.h>
#include <notebook/node.h>
#include <notebook/externalnode.h>
#include <notebook/nodeparameters.h>
#include <core/exception.h>
#include "messageboxhelper.h"
#include "vnotex.h"
#include "mainwindow.h"
#include <utils/iconutils.h>
#include <utils/docsutils.h>
#include "treewidget.h"
#include "listwidget.h"
#include "dialogs/notepropertiesdialog.h"
#include "dialogs/folderpropertiesdialog.h"
#include "dialogs/deleteconfirmdialog.h"
#include "dialogs/sortdialog.h"
#include "dialogs/viewtagsdialog.h"
#include <utils/widgetutils.h>
#include <utils/pathutils.h>
#include <utils/clipboardutils.h>
#include "notebookmgr.h"
#include "widgetsfactory.h"
#include "navigationmodemgr.h"
#include <core/fileopenparameters.h>
#include <core/events.h>
#include <core/configmgr.h>
#include <core/coreconfig.h>
#include <core/sessionconfig.h>
#include <core/widgetconfig.h>
#include <buffer/filetypehelper.h>
using namespace vnotex;
QIcon NotebookNodeExplorer::s_nodeIcons[NodeIcon::MaxIcons];
NotebookNodeExplorer::NodeData::NodeData()
{
}
NotebookNodeExplorer::NodeData::NodeData(Node *p_node, bool p_loaded)
: m_type(NodeType::Node),
m_node(p_node),
m_loaded(p_loaded)
{
}
NotebookNodeExplorer::NodeData::NodeData(const QSharedPointer<ExternalNode> &p_externalNode)
: m_type(NodeType::ExternalNode),
m_externalNode(p_externalNode),
m_loaded(true)
{
}
NotebookNodeExplorer::NodeData::NodeData(const NodeData &p_other)
{
m_type = p_other.m_type;
switch (m_type) {
case NodeType::Node:
m_node = p_other.m_node;
break;
case NodeType::ExternalNode:
m_externalNode = p_other.m_externalNode;
break;
default:
Q_ASSERT(false);
break;
}
m_loaded = p_other.m_loaded;
}
NotebookNodeExplorer::NodeData::~NodeData()
{
}
NotebookNodeExplorer::NodeData &NotebookNodeExplorer::NodeData::operator=(const NodeData &p_other)
{
if (&p_other == this) {
return *this;
}
m_type = p_other.m_type;
switch (m_type) {
case NodeType::Node:
m_node = p_other.m_node;
break;
case NodeType::ExternalNode:
m_externalNode = p_other.m_externalNode;
break;
default:
Q_ASSERT(false);
break;
}
m_loaded = p_other.m_loaded;
return *this;
}
bool NotebookNodeExplorer::NodeData::isValid() const
{
return m_type != NodeType::Invalid;
}
bool NotebookNodeExplorer::NodeData::isNode() const
{
return m_type == NodeType::Node;
}
bool NotebookNodeExplorer::NodeData::isExternalNode() const
{
return m_type == NodeType::ExternalNode;
}
NotebookNodeExplorer::NodeData::NodeType NotebookNodeExplorer::NodeData::getType() const
{
return m_type;
}
Node *NotebookNodeExplorer::NodeData::getNode() const
{
Q_ASSERT(isNode());
return m_node;
}
const QSharedPointer<ExternalNode> &NotebookNodeExplorer::NodeData::getExternalNode() const
{
Q_ASSERT(isExternalNode());
return m_externalNode;
}
void NotebookNodeExplorer::NodeData::clear()
{
m_type = NodeType::Invalid;
m_node = nullptr;
m_externalNode.clear();
m_loaded = false;
}
bool NotebookNodeExplorer::NodeData::matched(const Node *p_node) const
{
if (isNode() && m_node == p_node) {
return true;
}
return false;
}
bool NotebookNodeExplorer::NodeData::matched(const QString &p_name) const
{
if (isNode()) {
return m_node->getName() == p_name;
} else {
return m_externalNode->getName() == p_name;
}
}
bool NotebookNodeExplorer::NodeData::isLoaded() const
{
return m_loaded;
}
void NotebookNodeExplorer::CacheData::clear()
{
if (m_masterStateCache) {
m_masterStateCache->clear();
}
m_currentSlaveName.clear();
}
NotebookNodeExplorer::NotebookNodeExplorer(QWidget *p_parent)
: QWidget(p_parent)
{
initNodeIcons();
setupUI();
setupShortcuts();
}
void NotebookNodeExplorer::initNodeIcons() const
{
if (!s_nodeIcons[0].isNull()) {
return;
}
const QString nodeIconFgName = "widgets#notebookexplorer#node_icon#fg";
const QString invalidNodeIconFgName = "widgets#notebookexplorer#node_icon#invalid#fg";
const QString externalNodeIconFgName = "widgets#notebookexplorer#external_node_icon#fg";
const auto &themeMgr = VNoteX::getInst().getThemeMgr();
const auto fg = themeMgr.paletteColor(nodeIconFgName);
const auto invalidFg = themeMgr.paletteColor(invalidNodeIconFgName);
const auto externalFg = themeMgr.paletteColor(externalNodeIconFgName);
const QString folderIconName("folder_node.svg");
const QString fileIconName("file_node.svg");
s_nodeIcons[NodeIcon::FolderNode] = IconUtils::fetchIcon(themeMgr.getIconFile(folderIconName), fg);
s_nodeIcons[NodeIcon::FileNode] = IconUtils::fetchIcon(themeMgr.getIconFile(fileIconName), fg);
s_nodeIcons[NodeIcon::InvalidFolderNode] = IconUtils::fetchIcon(themeMgr.getIconFile(folderIconName), invalidFg);
s_nodeIcons[NodeIcon::InvalidFileNode] = IconUtils::fetchIcon(themeMgr.getIconFile(fileIconName), invalidFg);
s_nodeIcons[NodeIcon::ExternalFolderNode] = IconUtils::fetchIcon(themeMgr.getIconFile(folderIconName), externalFg);
s_nodeIcons[NodeIcon::ExternalFileNode] = IconUtils::fetchIcon(themeMgr.getIconFile(fileIconName), externalFg);
}
void NotebookNodeExplorer::setupUI()
{
auto mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
m_splitter = new QSplitter(this);
mainLayout->addWidget(m_splitter);
setupMasterExplorer(m_splitter);
m_splitter->addWidget(m_masterExplorer);
setFocusProxy(m_masterExplorer);
}
void NotebookNodeExplorer::setupMasterExplorer(QWidget *p_parent)
{
m_masterExplorer = new TreeWidget(TreeWidget::ClickSpaceToClearSelection, p_parent);
TreeWidget::setupSingleColumnHeaderlessTree(m_masterExplorer, true, true);
TreeWidget::showHorizontalScrollbar(m_masterExplorer);
m_masterNavigationWrapper.reset(new NavigationModeWrapper<QTreeWidget, QTreeWidgetItem>(m_masterExplorer));
NavigationModeMgr::getInst().registerNavigationTarget(m_masterNavigationWrapper.data());
connect(m_masterExplorer, &QTreeWidget::itemExpanded,
this, &NotebookNodeExplorer::loadMasterItemChildren);
connect(m_masterExplorer, &QTreeWidget::customContextMenuRequested,
this, [this](const QPoint &pos) {
if (!m_notebook) {
return;
}
auto item = m_masterExplorer->itemAt(pos);
QScopedPointer<QMenu> menu(WidgetsFactory::createMenu());
if (!item) {
createMasterContextMenuOnRoot(menu.data());
} else {
if (!isMasterAllSelectedItemsSameType()) {
return;
}
auto data = getItemNodeData(item);
if (data.isNode()) {
createContextMenuOnNode(menu.data(), data.getNode(), true);
} else if (data.isExternalNode()) {
createContextMenuOnExternalNode(menu.data(), data.getExternalNode().data(), true);
}
}
if (!menu->isEmpty()) {
menu->exec(m_masterExplorer->mapToGlobal(pos));
}
});
connect(m_masterExplorer, &QTreeWidget::itemActivated,
this, [this](QTreeWidgetItem *p_item, int p_column) {
Q_UNUSED(p_column);
if (!isCombinedExploreMode()) {
return;
}
auto data = getItemNodeData(p_item);
activateItemNode(data);
});
}
void NotebookNodeExplorer::activateItemNode(const NodeData &p_data)
{
if (!p_data.isValid()) {
return;
}
if (p_data.isNode()) {
if (checkInvalidNode(p_data.getNode())) {
return;
}
emit nodeActivated(p_data.getNode(), QSharedPointer<FileOpenParameters>::create());
} else if (p_data.isExternalNode()) {
// Import to config first.
if (m_autoImportExternalFiles) {
auto importedNode = importToIndex(p_data.getExternalNode());
if (importedNode) {
emit nodeActivated(importedNode.data(), QSharedPointer<FileOpenParameters>::create());
}
return;
}
// Just open it.
emit fileActivated(p_data.getExternalNode()->fetchAbsolutePath(), QSharedPointer<FileOpenParameters>::create());
}
}
void NotebookNodeExplorer::setupSlaveExplorer()
{
Q_ASSERT(!m_slaveExplorer);
m_slaveExplorer = new ListWidget(m_splitter);
m_splitter->addWidget(m_slaveExplorer);
m_slaveExplorer->setContextMenuPolicy(Qt::CustomContextMenu);
m_slaveExplorer->setSelectionMode(QAbstractItemView::ExtendedSelection);
connect(m_slaveExplorer, &QListWidget::customContextMenuRequested,
this, [this](const QPoint &pos) {
Q_ASSERT(!isCombinedExploreMode());
if (!m_notebook) {
return;
}
auto item = m_slaveExplorer->itemAt(pos);
QScopedPointer<QMenu> menu(WidgetsFactory::createMenu());
if (!item) {
createSlaveContextMenuOnMasterNode(menu.data());
} else {
if (!isSlaveAllSelectedItemsSameType()) {
return;
}
auto data = getItemNodeData(item);
if (data.isNode()) {
createContextMenuOnNode(menu.data(), data.getNode(), false);
} else if (data.isExternalNode()) {
createContextMenuOnExternalNode(menu.data(), data.getExternalNode().data(), false);
}
}
if (!menu->isEmpty()) {
menu->exec(m_slaveExplorer->mapToGlobal(pos));
}
});
connect(m_slaveExplorer, &QListWidget::itemActivated,
this, [this](QListWidgetItem *p_item) {
Q_ASSERT(!isCombinedExploreMode());
auto data = getItemNodeData(p_item);
activateItemNode(data);
});
m_slaveNavigationWrapper.reset(new NavigationModeWrapper<QListWidget, QListWidgetItem>(m_slaveExplorer));
NavigationModeMgr::getInst().registerNavigationTarget(m_slaveNavigationWrapper.data());
}
void NotebookNodeExplorer::setNotebook(const QSharedPointer<Notebook> &p_notebook)
{
if (p_notebook == m_notebook) {
return;
}
if (m_notebook) {
disconnect(m_notebook.data(), nullptr, this, nullptr);
}
cacheState(true);
m_notebook = p_notebook;
if (m_notebook) {
connect(m_notebook.data(), &Notebook::nodeUpdated,
this, [this](const Node *p_node) {
updateNode(p_node->getParent());
});
}
generateMasterNodeTree();
}
void NotebookNodeExplorer::generateMasterNodeTree()
{
m_masterExplorer->clear();
if (!m_notebook) {
return;
}
try {
auto rootNode = m_notebook->getRootNode();
loadRootNode(rootNode.data());
} catch (Exception &p_e) {
QString msg = tr("Failed to load nodes of notebook (%1) (%2).")
.arg(m_notebook->getName(), p_e.what());
qCritical() << msg;
MessageBoxHelper::notify(MessageBoxHelper::Critical, msg, VNoteX::getInst().getMainWindow());
}
// Restore current item.
bool restored = false;
auto &cacheData = getCache();
auto curMasterNode = cacheData.m_masterStateCache->getCurrentItem();
if (curMasterNode) {
restored = true;
setCurrentMasterNode(curMasterNode);
} else if (!isCombinedExploreMode()) {
// Manually update slave explorer on first run of generation.
updateSlaveExplorer();
}
if (!cacheData.m_currentSlaveName.isEmpty() && !isCombinedExploreMode()) {
restored = true;
setCurrentSlaveNode(cacheData.m_currentSlaveName);
}
if (!restored) {
focusNormalNode();
}
cacheData.clear();
}
void NotebookNodeExplorer::loadRootNode(const Node *p_node) const
{
Q_ASSERT(p_node->isLoaded() && p_node->isContainer());
// External children.
if (m_externalFilesVisible) {
auto externalChildren = p_node->fetchExternalChildren();
// TODO: Sort external children.
for (const auto &child : externalChildren) {
if (!belongsToMasterExplorer(child.data())) {
continue;
}
auto item = new QTreeWidgetItem(m_masterExplorer);
loadMasterExternalNode(item, child);
}
}
// Children.
auto children = p_node->getChildren();
sortNodes(children);
for (const auto &child : children) {
if (!belongsToMasterExplorer(child.data())) {
continue;
}
auto item = new QTreeWidgetItem(m_masterExplorer);
loadMasterNode(item, child.data(), 1);
}
}
static void clearTreeWidgetItemChildren(QTreeWidgetItem *p_item)
{
auto children = p_item->takeChildren();
for (auto &child : children) {
delete child;
}
}
void NotebookNodeExplorer::loadMasterNode(QTreeWidgetItem *p_item, Node *p_node, int p_level) const
{
if (!p_node->isLoaded()) {
p_node->load();
}
clearTreeWidgetItemChildren(p_item);
fillMasterItem(p_item, p_node, p_level > 0);
loadMasterNodeChildren(p_item, p_node, p_level - 1);
if (getCache().m_masterStateCache->contains(p_item) && p_item->childCount() > 0) {
if (p_item->isExpanded()) {
loadMasterItemChildren(p_item);
} else {
// itemExpanded() will trigger loadMasterItemChildren().
p_item->setExpanded(true);
}
}
}
void NotebookNodeExplorer::loadMasterExternalNode(QTreeWidgetItem *p_item, const QSharedPointer<ExternalNode> &p_node) const
{
clearTreeWidgetItemChildren(p_item);
fillMasterItem(p_item, p_node);
// No children for external node.
}
void NotebookNodeExplorer::loadMasterNodeChildren(QTreeWidgetItem *p_item, Node *p_node, int p_level) const
{
if (p_level < 0) {
return;
}
// External children.
if (m_externalFilesVisible && p_node->isContainer()) {
auto externalChildren = p_node->fetchExternalChildren();
// TODO: Sort external children.
for (const auto &child : externalChildren) {
if (!belongsToMasterExplorer(child.data())) {
continue;
}
auto item = new QTreeWidgetItem(p_item);
loadMasterExternalNode(item, child);
}
}
auto children = p_node->getChildren();
sortNodes(children);
for (const auto &child : children) {
if (!belongsToMasterExplorer(child.data())) {
continue;
}
auto item = new QTreeWidgetItem(p_item);
loadMasterNode(item, child.data(), p_level);
}
}
void NotebookNodeExplorer::fillMasterItem(QTreeWidgetItem *p_item, Node *p_node, bool p_loaded) const
{
setItemNodeData(p_item, NodeData(p_node, p_loaded));
p_item->setText(Column::Name, p_node->getName());
p_item->setIcon(Column::Name, getIcon(p_node));
p_item->setToolTip(Column::Name, generateToolTip(p_node));
}
void NotebookNodeExplorer::fillMasterItem(QTreeWidgetItem *p_item, const QSharedPointer<ExternalNode> &p_node) const
{
setItemNodeData(p_item, NodeData(p_node));
p_item->setText(Column::Name, p_node->getName());
p_item->setIcon(Column::Name, getIcon(p_node.data()));
p_item->setToolTip(Column::Name, tr("[External] %1").arg(p_node->getName()));
}
void NotebookNodeExplorer::fillSlaveItem(QListWidgetItem *p_item, Node *p_node) const
{
setItemNodeData(p_item, NodeData(p_node, true));
p_item->setText(p_node->getName());
p_item->setIcon(getIcon(p_node));
p_item->setToolTip(generateToolTip(p_node));
}
void NotebookNodeExplorer::fillSlaveItem(QListWidgetItem *p_item, const QSharedPointer<ExternalNode> &p_node) const
{
setItemNodeData(p_item, NodeData(p_node));
p_item->setText(p_node->getName());
p_item->setIcon(getIcon(p_node.data()));
p_item->setToolTip(tr("[External] %1").arg(p_node->getName()));
}
const QIcon &NotebookNodeExplorer::getIcon(const Node *p_node) const
{
if (p_node->hasContent()) {
return p_node->exists() ? s_nodeIcons[NodeIcon::FileNode] : s_nodeIcons[NodeIcon::InvalidFileNode];
} else {
return p_node->exists() ? s_nodeIcons[NodeIcon::FolderNode] : s_nodeIcons[NodeIcon::InvalidFolderNode];
}
}
const QIcon &NotebookNodeExplorer::getIcon(const ExternalNode *p_node) const
{
return p_node->isFolder() ? s_nodeIcons[NodeIcon::ExternalFolderNode] : s_nodeIcons[NodeIcon::ExternalFileNode];
}
Node *NotebookNodeExplorer::getCurrentMasterNode() const
{
auto item = m_masterExplorer->currentItem();
if (item) {
auto data = getItemNodeData(item);
while (item && !data.isNode()) {
item = item->parent();
if (item) {
data = getItemNodeData(item);
} else {
data.clear();
}
}
if (data.isNode()) {
return data.getNode();
}
}
return nullptr;
}
Node *NotebookNodeExplorer::getCurrentSlaveNode() const
{
auto item = m_slaveExplorer->currentItem();
if (item) {
auto data = getItemNodeData(item);
if (data.isNode()) {
return data.getNode();
}
}
return nullptr;
}
NotebookNodeExplorer::NodeData NotebookNodeExplorer::getCurrentMasterNodeData() const
{
auto item = m_masterExplorer->currentItem();
if (item) {
return getItemNodeData(item);
}
return NodeData();
}
NotebookNodeExplorer::NodeData NotebookNodeExplorer::getCurrentSlaveNodeData() const
{
auto item = m_slaveExplorer->currentItem();
if (item) {
return getItemNodeData(item);
}
return NodeData();
}
void NotebookNodeExplorer::setItemNodeData(QTreeWidgetItem *p_item, const NodeData &p_data)
{
p_item->setData(Column::Name, Qt::UserRole, QVariant::fromValue(p_data));
}
NotebookNodeExplorer::NodeData NotebookNodeExplorer::getItemNodeData(const QTreeWidgetItem *p_item)
{
if (!p_item) {
return NodeData();
}
return p_item->data(Column::Name, Qt::UserRole).value<NotebookNodeExplorer::NodeData>();
}
void NotebookNodeExplorer::setItemNodeData(QListWidgetItem *p_item, const NodeData &p_data)
{
p_item->setData(Qt::UserRole, QVariant::fromValue(p_data));
}
NotebookNodeExplorer::NodeData NotebookNodeExplorer::getItemNodeData(const QListWidgetItem *p_item)
{
if (!p_item) {
return NodeData();
}
return p_item->data(Qt::UserRole).value<NotebookNodeExplorer::NodeData>();
}
void NotebookNodeExplorer::updateNode(Node *p_node)
{
if (p_node && p_node->getNotebook() != m_notebook) {
return;
}
if (p_node && !belongsToMasterExplorer(p_node)) {
updateSlaveExplorer();
return;
}
auto item = findMasterNode(p_node);
if (item) {
bool expanded = item->isExpanded();
item->setExpanded(false);
loadMasterNode(item, p_node, 1);
item->setExpanded(expanded);
if (!isCombinedExploreMode()) {
updateSlaveExplorer();
}
} else {
cacheState(false);
generateMasterNodeTree();
}
}
// TODO: we could do it faster by going from the root node directly.
QTreeWidgetItem *NotebookNodeExplorer::findMasterNode(const Node *p_node) const
{
if (!p_node) {
return nullptr;
}
auto cnt = m_masterExplorer->topLevelItemCount();
for (int i = 0; i < cnt; ++i) {
auto item = findMasterNode(m_masterExplorer->topLevelItem(i), p_node);
if (item) {
return item;
}
}
return nullptr;
}
QTreeWidgetItem *NotebookNodeExplorer::findMasterNode(QTreeWidgetItem *p_item, const Node *p_node) const
{
auto data = getItemNodeData(p_item);
if (data.matched(p_node)) {
return p_item;
}
auto cnt = p_item->childCount();
for (int i = 0; i < cnt; ++i) {
auto item = findMasterNode(p_item->child(i), p_node);
if (item) {
return item;
}
}
return nullptr;
}
QListWidgetItem *NotebookNodeExplorer::findSlaveNode(const Node *p_node) const
{
for (int i = 0; i < m_slaveExplorer->count(); ++i) {
auto data = getItemNodeData(m_slaveExplorer->item(i));
if (data.matched(p_node)) {
return m_slaveExplorer->item(i);
}
}
return nullptr;
}
void NotebookNodeExplorer::setCurrentNode(Node *p_node)
{
if (!p_node) {
m_masterExplorer->setCurrentItem(nullptr);
return;
}
Q_ASSERT(p_node->getNotebook() == m_notebook);
if (belongsToMasterExplorer(p_node)) {
setCurrentMasterNode(p_node);
} else {
setCurrentMasterNode(p_node->getParent());
setCurrentSlaveNode(p_node);
}
}
void NotebookNodeExplorer::setCurrentMasterNode(Node *p_node)
{
if (!p_node || p_node->isRoot()) {
m_masterExplorer->setCurrentItem(nullptr);
return;
}
Q_ASSERT(p_node && belongsToMasterExplorer(p_node));
// (rootNode, p_node].
QList<Node *> nodes;
auto node = p_node;
while (!node->isRoot()) {
nodes.push_front(node);
node = node->getParent();
}
QList<QTreeWidgetItem *> items;
auto nodeIt = nodes.constBegin();
auto item = findMasterNodeInTopLevelItems(m_masterExplorer, *nodeIt);
if (!item) {
return;
}
items.push_back(item);
++nodeIt;
while (nodeIt != nodes.constEnd()) {
if (!item) {
return;
}
// Find *nodeIt in children of item.
auto data = getItemNodeData(item);
Q_ASSERT(data.isNode());
if (!data.isLoaded()) {
loadMasterNode(item, data.getNode(), 1);
}
auto childItem = findMasterNodeInDirectChildren(item, *nodeIt);
if (!childItem) {
return;
}
item = childItem;
items.push_back(item);
++nodeIt;
}
Q_ASSERT(getItemNodeData(item).getNode() == p_node);
// Do not expand the last item.
for (int i = 0; i < items.size() - 1; ++i) {
items[i]->setExpanded(true);
}
m_masterExplorer->setCurrentItem(item);
}
QTreeWidgetItem *NotebookNodeExplorer::findMasterNodeInDirectChildren(QTreeWidgetItem *p_item, const Node *p_node) const
{
auto cnt = p_item->childCount();
for (int i = 0; i < cnt; ++i) {
auto child = p_item->child(i);
auto data = getItemNodeData(child);
if (data.matched(p_node)) {
return child;
}
}
return nullptr;
}
QTreeWidgetItem *NotebookNodeExplorer::findMasterNodeInTopLevelItems(QTreeWidget *p_tree, const Node *p_node) const
{
auto cnt = p_tree->topLevelItemCount();
for (int i = 0; i < cnt; ++i) {
auto child = p_tree->topLevelItem(i);
auto data = getItemNodeData(child);
if (data.matched(p_node)) {
return child;
}
}
return nullptr;
}
void NotebookNodeExplorer::setCurrentSlaveNode(const Node *p_node)
{
if (!p_node) {
m_slaveExplorer->setCurrentItem(nullptr);
return;
}
Q_ASSERT(!belongsToMasterExplorer(p_node));
ListWidget::forEachItem(m_slaveExplorer, [this, p_node](QListWidgetItem *item) {
auto data = getItemNodeData(item);
if (data.matched(p_node)) {
m_slaveExplorer->setCurrentItem(item);
return false;
}
return true;
});
if (m_slaveExplorer->currentItem() && m_masterExplorer->hasFocus()) {
// To get focus after creating a new note.
m_slaveExplorer->setFocus();
}
}
void NotebookNodeExplorer::setCurrentSlaveNode(const QString &p_name)
{
if (p_name.isEmpty()) {
m_slaveExplorer->setCurrentItem(nullptr);
return;
}
ListWidget::forEachItem(m_slaveExplorer, [this, &p_name](QListWidgetItem *item) {
auto data = getItemNodeData(item);
if (data.matched(p_name)) {
m_slaveExplorer->setCurrentItem(item);
return false;
}
return true;
});
}
void NotebookNodeExplorer::cacheState(bool p_saveCurrent)
{
if (m_notebook) {
auto &cacheData = getCache();
cacheData.m_masterStateCache->save(m_masterExplorer, p_saveCurrent);
if (p_saveCurrent && !isCombinedExploreMode()) {
cacheData.m_currentSlaveName.clear();
auto item = m_slaveExplorer->currentItem();
if (item) {
auto data = getItemNodeData(item);
if (data.isNode()) {
cacheData.m_currentSlaveName = data.getNode()->getName();
} else {
cacheData.m_currentSlaveName = data.getExternalNode()->getName();
}
}
}
}
}
NotebookNodeExplorer::CacheData &NotebookNodeExplorer::getCache() const
{
Q_ASSERT(m_notebook);
auto it = const_cast<NotebookNodeExplorer *>(this)->m_cache.find(m_notebook.data());
if (it == const_cast<NotebookNodeExplorer *>(this)->m_cache.end()) {
auto keyFunc = [](const QTreeWidgetItem *p_item, bool &p_ok) {
auto data = NotebookNodeExplorer::getItemNodeData(p_item);
if (data.isNode()) {
p_ok = true;
return data.getNode();
}
p_ok = false;
return static_cast<Node *>(nullptr);
};
it = const_cast<NotebookNodeExplorer *>(this)->m_cache.insert(m_notebook.data(), CacheData());
it.value().m_masterStateCache = QSharedPointer<QTreeWidgetStateCache<Node *>>::create(keyFunc);
}
return it.value();
}
void NotebookNodeExplorer::clearCache(const Notebook *p_notebook)
{
auto it = m_cache.find(p_notebook);
if (it != m_cache.end()) {
it.value().clear();
}
}
void NotebookNodeExplorer::createMasterContextMenuOnRoot(QMenu *p_menu)
{
createAndAddAction(Action::NewNote, p_menu);
createAndAddAction(Action::NewFolder, p_menu);
if (isPasteOnNodeAvailable(nullptr)) {
p_menu->addSeparator();
createAndAddAction(Action::Paste, p_menu);
}
p_menu->addSeparator();
createAndAddAction(Action::Reload, p_menu);
createAndAddAction(Action::ReloadIndex, p_menu);
createAndAddAction(Action::OpenLocation, p_menu);
}
void NotebookNodeExplorer::createContextMenuOnNode(QMenu *p_menu, const Node *p_node, bool p_master)
{
const int selectedSize = p_master ? m_masterExplorer->selectedItems().size() : m_slaveExplorer->selectedItems().size();
createAndAddAction(Action::Open, p_menu, p_master);
addOpenWithMenu(p_menu, p_master);
p_menu->addSeparator();
if (selectedSize == 1 && p_node->isContainer()) {
createAndAddAction(Action::ExpandAll, p_menu, p_master);
}
p_menu->addSeparator();
createAndAddAction(Action::NewNote, p_menu, p_master);
createAndAddAction(Action::NewFolder, p_menu, p_master);
p_menu->addSeparator();
createAndAddAction(Action::Copy, p_menu, p_master);
createAndAddAction(Action::Cut, p_menu, p_master);
if (selectedSize == 1 && isPasteOnNodeAvailable(p_node)) {
createAndAddAction(Action::Paste, p_menu, p_master);
}
createAndAddAction(Action::Delete, p_menu, p_master);
createAndAddAction(Action::RemoveFromConfig, p_menu, p_master);
p_menu->addSeparator();
createAndAddAction(Action::Reload, p_menu, p_master);
createAndAddAction(Action::Sort, p_menu, p_master);
if (selectedSize == 1
&& m_notebook->tag()
&& !p_node->isContainer()) {
p_menu->addSeparator();
createAndAddAction(Action::Tag, p_menu, p_master);
}
p_menu->addSeparator();
createAndAddAction(Action::PinToQuickAccess, p_menu, p_master);
if (selectedSize == 1) {
createAndAddAction(Action::CopyPath, p_menu, p_master);
createAndAddAction(Action::OpenLocation, p_menu, p_master);
createAndAddAction(Action::Properties, p_menu, p_master);
}
}
void NotebookNodeExplorer::createContextMenuOnExternalNode(QMenu *p_menu, const ExternalNode *p_node, bool p_master)
{
Q_UNUSED(p_node);
const int selectedSize = p_master ? m_masterExplorer->selectedItems().size() : m_slaveExplorer->selectedItems().size();
createAndAddAction(Action::Open, p_menu, p_master);
addOpenWithMenu(p_menu, p_master);
p_menu->addSeparator();
createAndAddAction(Action::ImportToConfig, p_menu, p_master);
p_menu->addSeparator();
createAndAddAction(Action::PinToQuickAccess, p_menu, p_master);
if (selectedSize == 1) {
createAndAddAction(Action::CopyPath, p_menu, p_master);
createAndAddAction(Action::OpenLocation, p_menu, p_master);
}
}
void NotebookNodeExplorer::createSlaveContextMenuOnMasterNode(QMenu *p_menu)
{
auto masterNode = getSlaveExplorerMasterNode();
if (!masterNode) {
// Current master node may be an external node.
return;
}
createAndAddAction(Action::NewNote, p_menu, false);
createAndAddAction(Action::NewFolder, p_menu, false);
if (isPasteOnNodeAvailable(masterNode)) {
p_menu->addSeparator();
createAndAddAction(Action::Paste, p_menu, false);
}
p_menu->addSeparator();
createAndAddAction(Action::Reload, p_menu, false);
createAndAddAction(Action::OpenLocation, p_menu, false);
}
static QIcon generateMenuActionIcon(const QString &p_name)
{
const auto &themeMgr = VNoteX::getInst().getThemeMgr();
return IconUtils::fetchIconWithDisabledState(themeMgr.getIconFile(p_name));
}
QAction *NotebookNodeExplorer::createAction(Action p_act, QObject *p_parent, bool p_master)
{
const auto &coreConfig = ConfigMgr::getInst().getCoreConfig();
QAction *act = nullptr;
switch (p_act) {
case Action::NewNote:
act = new QAction(generateMenuActionIcon("new_note.svg"),
tr("New &Note"),
p_parent);
connect(act, &QAction::triggered,
this, []() {
emit VNoteX::getInst().newNoteRequested();
});
break;
case Action::NewFolder:
act = new QAction(generateMenuActionIcon("new_folder.svg"),
tr("New &Folder"),
p_parent);
connect(act, &QAction::triggered,
this, []() {
emit VNoteX::getInst().newFolderRequested();
});
break;
case Action::Properties:
act = new QAction(generateMenuActionIcon("properties.svg"),
tr("&Properties (Rename)"),
p_parent);
connect(act, &QAction::triggered,
this, [this, p_master]() {
openCurrentNodeProperties(p_master);
});
WidgetUtils::addActionShortcutText(act, coreConfig.getShortcut(CoreConfig::Properties));
break;
case Action::OpenLocation:
act = new QAction(tr("Open Locat&ion"), p_parent);
connect(act, &QAction::triggered,
this, [this]() {
// Always use the master node no matter it is in master/slave explorer.
auto item = m_masterExplorer->currentItem();
if (!item) {
if (m_notebook) {
auto locationPath = m_notebook->getRootFolderAbsolutePath();
WidgetUtils::openUrlByDesktop(QUrl::fromLocalFile(locationPath));
}
return;
}
auto data = getItemNodeData(item);
Node *node = nullptr;
if (data.isNode()) {
node = data.getNode();
} else {
// Open the parent folder of the external node.
node = data.getExternalNode()->getNode();
}
if (checkInvalidNode(node)) {
return;
}
auto locationPath = node->fetchAbsolutePath();
if (!node->isContainer()) {
locationPath = PathUtils::parentDirPath(locationPath);
}
if (!locationPath.isEmpty()) {
WidgetUtils::openUrlByDesktop(QUrl::fromLocalFile(locationPath));
}
});
break;
case Action::CopyPath:
act = new QAction(tr("Cop&y Path"), p_parent);
connect(act, &QAction::triggered,
this, [this, p_master]() {
NodeData data = p_master ? getCurrentMasterNodeData() : getCurrentSlaveNodeData();
if (!data.isValid()) {
return;
}
QString nodePath;
if (data.isNode()) {
auto node = data.getNode();
if (checkInvalidNode(node)) {
return;
}
nodePath = node->fetchAbsolutePath();
} else if (data.isExternalNode()) {
nodePath = data.getExternalNode()->fetchAbsolutePath();
}
if (!nodePath.isEmpty()) {
ClipboardUtils::setTextToClipboard(nodePath);
VNoteX::getInst().showStatusMessageShort(tr("Copied path: %1").arg(nodePath));
}
});
break;
case Action::Copy:
act = new QAction(tr("&Copy"), p_parent);
connect(act, &QAction::triggered,
this, [this, p_master]() {
copySelectedNodes(false, p_master);
});
WidgetUtils::addActionShortcutText(act, coreConfig.getShortcut(CoreConfig::Copy));
break;
case Action::Cut:
act = new QAction(tr("C&ut"), p_parent);
connect(act, &QAction::triggered,
this, [this, p_master]() {
copySelectedNodes(true, p_master);
});
break;
case Action::Paste:
act = new QAction(tr("&Paste"), p_parent);
connect(act, &QAction::triggered,
this, &NotebookNodeExplorer::pasteNodesFromClipboard);
WidgetUtils::addActionShortcutText(act, coreConfig.getShortcut(CoreConfig::Paste));
break;
case Action::Delete:
act = new QAction(tr("&Delete"), p_parent);
connect(act, &QAction::triggered,
this, [this, p_master]() {
removeSelectedNodes(p_master);
});
break;
case Action::RemoveFromConfig:
act = new QAction(tr("&Remove From Index"), p_parent);
connect(act, &QAction::triggered,
this, [this, p_master]() {
removeSelectedNodesFromConfig(p_master);
});
break;
case Action::Sort:
act = new QAction(generateMenuActionIcon("sort.svg"), tr("&Sort"), p_parent);
connect(act, &QAction::triggered,
this, [this, p_master]() {
manualSort(p_master);
});
break;
case Action::Reload:
act = new QAction(tr("Re&load"), p_parent);
connect(act, &QAction::triggered,
this, [this]() {
auto node = currentExploredFolderNode();
updateNode(node);
});
break;
case Action::ReloadIndex:
act = new QAction(tr("Relo&ad Index Of Notebook From Disk"), p_parent);
connect(act, &QAction::triggered,
this, [this]() {
if (!m_notebook) {
return;
}
auto event = QSharedPointer<Event>::create();
emit nodeAboutToReload(m_notebook->getRootNode().data(), event);
if (!event->m_response.toBool()) {
return;
}
m_notebook->reloadNodes();
reload();
});
break;
case Action::ImportToConfig:
act = new QAction(tr("&Import To Index"), p_parent);
connect(act, &QAction::triggered,
this, [this, p_master]() {
auto nodes = p_master ? getMasterSelectedNodesAndExternalNodes().second : getSlaveSelectedNodesAndExternalNodes().second;
importToIndex(nodes);
});
break;
case Action::Open:
act = new QAction(tr("&Open"), p_parent);
connect(act, &QAction::triggered,
this, [this, p_master]() {
// Support nodes and external nodes.
// Do nothing for folders.
auto selectedNodes = p_master ? getMasterSelectedNodesAndExternalNodes() : getSlaveSelectedNodesAndExternalNodes();
for (const auto &externalNode : selectedNodes.second) {
if (!externalNode->isFolder()) {
emit fileActivated(externalNode->fetchAbsolutePath(), QSharedPointer<FileOpenParameters>::create());
}
}
for (const auto &node : selectedNodes.first) {
if (checkInvalidNode(node)) {
continue;
}
if (node->hasContent()) {
emit nodeActivated(node, QSharedPointer<FileOpenParameters>::create());
}
}
});
break;
case Action::ExpandAll:
act = new QAction(tr("&Expand All\t*"), p_parent);
connect(act, &QAction::triggered,
this, [this]() {
auto item = m_masterExplorer->currentItem();
if (!item || item->childCount() == 0) {
return;
}
auto data = getItemNodeData(item);
if (!data.isNode()) {
return;
}
TreeWidget::expandRecursively(item);
});
break;
case Action::PinToQuickAccess:
act = new QAction(generateMenuActionIcon(QStringLiteral("quick_access_menu.svg")),
tr("Pin To &Quick Access"),
p_parent);
connect(act, &QAction::triggered,
this, [this, p_master]() {
auto nodes = p_master ? getMasterSelectedNodesAndExternalNodes() : getSlaveSelectedNodesAndExternalNodes();
QStringList files;
for (const auto &node : nodes.first) {
files.push_back(node->fetchAbsolutePath());
}
for (const auto &node : nodes.second) {
files.push_back(node->fetchAbsolutePath());
}
if (!files.isEmpty()) {
emit VNoteX::getInst().pinToQuickAccessRequested(files);
}
});
break;
case Action::Tag:
act = new QAction(generateMenuActionIcon(QStringLiteral("tag.svg")), tr("&Tags"), p_parent);
connect(act, &QAction::triggered,
this, [this, p_master]() {
Q_ASSERT(m_notebook->tag());
auto node = p_master ? getCurrentMasterNode() : getCurrentSlaveNode();
Q_ASSERT(node);
if (checkInvalidNode(node)) {
return;
}
ViewTagsDialog dialog(node, VNoteX::getInst().getMainWindow());
dialog.exec();
});
break;
default:
Q_ASSERT(false);
break;
}
return act;
}
QAction *NotebookNodeExplorer::createAndAddAction(Action p_act, QMenu *p_menu, bool p_master)
{
auto act = createAction(p_act, p_menu, p_master);
p_menu->addAction(act);
return act;
}
void NotebookNodeExplorer::copySelectedNodes(bool p_move, bool p_master)
{
auto nodes = p_master ? getMasterSelectedNodesAndExternalNodes().first : getSlaveSelectedNodesAndExternalNodes().first;
if (nodes.isEmpty()) {
return;
}
filterAwayChildrenNodes(nodes);
ClipboardData cdata(VNoteX::getInst().getInstanceId(),
p_move ? ClipboardData::MoveNode : ClipboardData::CopyNode);
for (auto node : nodes) {
if (checkInvalidNode(node)) {
continue;
}
auto item = QSharedPointer<NodeClipboardDataItem>::create(node->getNotebook()->getId(),
node->fetchPath());
cdata.addItem(item);
}
auto text = cdata.toJsonText();
ClipboardUtils::setTextToClipboard(text);
size_t nrItems = cdata.getData().size();
VNoteX::getInst().showStatusMessageShort(tr("Copied %n item(s)", "", static_cast<int>(nrItems)));
}
QPair<QVector<Node *>, QVector<QSharedPointer<ExternalNode>>> NotebookNodeExplorer::getMasterSelectedNodesAndExternalNodes() const
{
QPair<QVector<Node *>, QVector<QSharedPointer<ExternalNode>>> nodes;
auto items = m_masterExplorer->selectedItems();
for (auto &item : items) {
auto data = getItemNodeData(item);
if (data.isNode()) {
nodes.first.push_back(data.getNode());
} else if (data.isExternalNode()) {
nodes.second.push_back(data.getExternalNode());
}
}
return nodes;
}
QPair<QVector<Node *>, QVector<QSharedPointer<ExternalNode>>> NotebookNodeExplorer::getSlaveSelectedNodesAndExternalNodes() const
{
QPair<QVector<Node *>, QVector<QSharedPointer<ExternalNode>>> nodes;
auto items = m_slaveExplorer->selectedItems();
for (auto &item : items) {
auto data = getItemNodeData(item);
if (data.isNode()) {
nodes.first.push_back(data.getNode());
} else if (data.isExternalNode()) {
nodes.second.push_back(data.getExternalNode());
}
}
return nodes;
}
QSharedPointer<ClipboardData> NotebookNodeExplorer::tryFetchClipboardData()
{
auto text = ClipboardUtils::getTextFromClipboard();
return ClipboardData::fromJsonText(text);
}
static bool isValidClipboardData(const ClipboardData *p_data)
{
if (!p_data) {
return false;
}
if (p_data->getInstanceId() != VNoteX::getInst().getInstanceId()) {
return false;
}
if (p_data->getData().isEmpty()) {
return false;
}
auto act = p_data->getAction();
if (act != ClipboardData::CopyNode && act != ClipboardData::MoveNode) {
return false;
}
return true;
}
bool NotebookNodeExplorer::isPasteOnNodeAvailable(const Node *p_node) const
{
Q_UNUSED(p_node);
auto cdata = tryFetchClipboardData();
return isValidClipboardData(cdata.data());
}
static QSharedPointer<Node> getNodeFromClipboardDataItem(const NodeClipboardDataItem *p_item)
{
Q_ASSERT(p_item);
auto notebook = VNoteX::getInst().getNotebookMgr().findNotebookById(p_item->m_notebookId);
if (!notebook) {
Exception::throwOne(Exception::Type::InvalidArgument,
QString("failed to find notebook by ID (%1)").arg(p_item->m_notebookId));
return nullptr;
}
auto node = notebook->loadNodeByPath(p_item->m_nodeRelativePath);
Q_ASSERT(!node || node->fetchPath() == p_item->m_nodeRelativePath);
return node;
}
void NotebookNodeExplorer::pasteNodesFromClipboard()
{
// Identify the dest node.
auto destNode = getCurrentMasterNode();
if (!destNode) {
if (!m_notebook) {
return;
}
destNode = m_notebook->getRootNode().data();
} else {
// Current node may be a file node.
if (!destNode->isContainer()) {
destNode = destNode->getParent();
} else if (checkInvalidNode(destNode)) {
return;
}
}
Q_ASSERT(destNode && destNode->isContainer());
// Fetch source nodes from clipboard.
auto cdata = tryFetchClipboardData();
if (!isValidClipboardData(cdata.data())) {
MessageBoxHelper::notify(MessageBoxHelper::Warning,
tr("Invalid clipboard data to paste."),
VNoteX::getInst().getMainWindow());
return;
}
QVector<QSharedPointer<Node>> srcNodes;
auto items = cdata->getData();
for (auto &item : items) {
auto nodeItem = dynamic_cast<NodeClipboardDataItem *>(item.data());
Q_ASSERT(nodeItem);
auto src = getNodeFromClipboardDataItem(nodeItem);
if (!src) {
continue;
} else if (src == destNode) {
MessageBoxHelper::notify(MessageBoxHelper::Warning,
tr("Destination is detected in sources (%1). Operation is cancelled.")
.arg(destNode->fetchAbsolutePath()),
VNoteX::getInst().getMainWindow());
return;
}
srcNodes.push_back(src);
}
bool isMove = cdata->getAction() == ClipboardData::MoveNode;
QVector<const Node *> pastedNodes;
QSet<Node *> nodesNeedUpdate;
for (auto srcNode : srcNodes) {
Q_ASSERT(srcNode->exists());
if (isMove) {
// Notice the view area to close any opened view windows.
auto event = QSharedPointer<Event>::create();
emit nodeAboutToMove(srcNode.data(), event);
if (!event->m_response.toBool()) {
continue;
}
}
auto srcPath = srcNode->fetchAbsolutePath();
auto srcParentNode = srcNode->getParent();
try {
auto notebook = destNode->getNotebook();
auto pastedNode = notebook->copyNodeAsChildOf(srcNode, destNode, isMove);
pastedNodes.push_back(pastedNode.data());
} catch (Exception &p_e) {
MessageBoxHelper::notify(MessageBoxHelper::Critical,
tr("Failed to copy source (%1) to destination (%2) (%3).")
.arg(srcPath, destNode->fetchAbsolutePath(), p_e.what()),
VNoteX::getInst().getMainWindow());
}
if (isMove) {
nodesNeedUpdate.insert(srcParentNode);
}
}
for (auto node : nodesNeedUpdate) {
updateNode(node);
// Deleted src nodes may be the current node in cache. Clear the cache.
clearCache(node->getNotebook());
}
// Update and expand dest node. Select all pasted nodes.
updateAndExpandNode(destNode);
selectNodes(pastedNodes);
if (isMove) {
ClipboardUtils::clearClipboard();
}
VNoteX::getInst().showStatusMessageShort(tr("Pasted %n item(s)", "", pastedNodes.size()));
}
void NotebookNodeExplorer::setMasterNodeExpanded(const Node *p_node, bool p_expanded)
{
auto item = findMasterNode(p_node);
if (item) {
item->setExpanded(p_expanded);
}
}
void NotebookNodeExplorer::selectNodes(const QVector<const Node *> &p_nodes)
{
if (p_nodes.isEmpty()) {
return;
}
// All the nodes should either belong to master or slave explorer.
if (belongsToMasterExplorer(p_nodes[0])) {
bool firstItem = true;
for (auto node : p_nodes) {
auto item = findMasterNode(node);
if (item) {
auto flags = firstItem ? QItemSelectionModel::ClearAndSelect : QItemSelectionModel::Select;
m_masterExplorer->setCurrentItem(item, 0, flags);
firstItem = false;
}
}
} else {
bool firstItem = true;
for (auto node : p_nodes) {
auto item = findSlaveNode(node);
if (item) {
auto flags = firstItem ? QItemSelectionModel::ClearAndSelect : QItemSelectionModel::Select;
m_slaveExplorer->setCurrentItem(item, flags);
firstItem = false;
}
}
}
}
void NotebookNodeExplorer::removeSelectedNodes(bool p_master)
{
const QString text = tr("Delete these folders and notes?");
const QString info = tr("Deleted files could be found in the recycle bin of notebook.");
auto nodes = confirmSelectedNodes(tr("Confirm Deletion"), text, info, p_master);
removeNodes(nodes, false);
}
QVector<Node *> NotebookNodeExplorer::confirmSelectedNodes(const QString &p_title,
const QString &p_text,
const QString &p_info,
bool p_master) const
{
auto nodes = p_master ? getMasterSelectedNodesAndExternalNodes().first : getSlaveSelectedNodesAndExternalNodes().first;
if (nodes.isEmpty()) {
return nodes;
}
QVector<ConfirmItemInfo> items;
for (const auto &node : nodes) {
items.push_back(ConfirmItemInfo(getIcon(node),
node->getName(),
node->fetchAbsolutePath(),
node->fetchAbsolutePath(),
(void *)node));
}
DeleteConfirmDialog dialog(p_title,
p_text,
p_info,
items,
DeleteConfirmDialog::Flag::None,
false,
VNoteX::getInst().getMainWindow());
QVector<Node *> nodesToDelete;
if (dialog.exec()) {
items = dialog.getConfirmedItems();
for (const auto &item : items) {
nodesToDelete.push_back(static_cast<Node *>(item.m_data));
}
}
return nodesToDelete;
}
void NotebookNodeExplorer::removeNodes(QVector<Node *> p_nodes, bool p_configOnly)
{
if (p_nodes.isEmpty()) {
return;
}
filterAwayChildrenNodes(p_nodes);
int nrDeleted = 0;
QSet<Node *> nodesNeedUpdate;
for (auto node : p_nodes) {
auto srcName = node->getName();
auto srcPath = node->fetchAbsolutePath();
auto srcParentNode = node->getParent();
try {
auto event = QSharedPointer<Event>::create();
emit nodeAboutToRemove(node, event);
if (!event->m_response.toBool()) {
continue;
}
if (p_configOnly) {
m_notebook->removeNode(node, false, true);
} else {
m_notebook->moveNodeToRecycleBin(node);
}
++nrDeleted;
} catch (Exception &p_e) {
MessageBoxHelper::notify(MessageBoxHelper::Critical,
tr("Failed to delete/remove item (%1) (%2) (%3).")
.arg(srcName, srcPath, p_e.what()),
VNoteX::getInst().getMainWindow());
}
nodesNeedUpdate.insert(srcParentNode);
}
for (auto node : nodesNeedUpdate) {
updateNode(node);
}
VNoteX::getInst().showStatusMessageShort(tr("Deleted/Removed %n item(s)", "", nrDeleted));
}
void NotebookNodeExplorer::removeSelectedNodesFromConfig(bool p_master)
{
auto nodes = confirmSelectedNodes(tr("Confirm Removal"),
tr("Remove these folders and notes from index?"),
tr("Files are not touched but just removed from notebook index."),
p_master);
removeNodes(nodes, true);
}
void NotebookNodeExplorer::filterAwayChildrenNodes(QVector<Node *> &p_nodes)
{
for (int i = p_nodes.size() - 1; i >= 0; --i) {
// Check if j is i's ancestor.
for (int j = p_nodes.size() - 1; j >= 0; --j) {
if (i == j) {
continue;
}
if (Node::isAncestor(p_nodes[j], p_nodes[i])) {
p_nodes.remove(i);
break;
}
}
}
}
void NotebookNodeExplorer::updateAndExpandNode(Node *p_node)
{
setMasterNodeExpanded(p_node, false);
updateNode(p_node);
setMasterNodeExpanded(p_node, true);
}
bool NotebookNodeExplorer::isMasterAllSelectedItemsSameType() const
{
auto items = m_masterExplorer->selectedItems();
if (items.size() < 2) {
return true;
}
auto type = getItemNodeData(items.first()).getType();
for (int i = 1; i < items.size(); ++i) {
auto itype = getItemNodeData(items[i]).getType();
if (itype != type) {
return false;
}
}
return true;
}
bool NotebookNodeExplorer::isSlaveAllSelectedItemsSameType() const
{
auto items = m_slaveExplorer->selectedItems();
if (items.size() < 2) {
return true;
}
auto type = getItemNodeData(items.first()).getType();
for (int i = 1; i < items.size(); ++i) {
auto itype = getItemNodeData(items[i]).getType();
if (itype != type) {
return false;
}
}
return true;
}
void NotebookNodeExplorer::reload()
{
updateNode(nullptr);
}
void NotebookNodeExplorer::focusNormalNode()
{
if (isCombinedExploreMode()) {
m_masterExplorer->setCurrentItem(m_masterExplorer->topLevelItem(0));
} else {
if (m_slaveExplorer->count() > 0) {
m_slaveExplorer->setCurrentRow(0);
}
}
}
void NotebookNodeExplorer::sortNodes(QVector<QSharedPointer<Node>> &p_nodes) const
{
if (m_viewOrder == ViewOrder::OrderedByConfiguration) {
return;
}
// Put containers first.
int firstFileIndex = p_nodes.size();
for (int i = 0; i < p_nodes.size(); ++i) {
if (p_nodes[i]->isContainer()) {
// If it is a container, load it to set its created time and modified time.
p_nodes[i]->load();
} else {
firstFileIndex = i;
break;
}
}
// Sort containers.
sortNodes(p_nodes, 0, firstFileIndex, m_viewOrder);
// Sort non-containers.
sortNodes(p_nodes, firstFileIndex, p_nodes.size(), m_viewOrder);
}
void NotebookNodeExplorer::sortNodes(QVector<QSharedPointer<Node>> &p_nodes, int p_start, int p_end, ViewOrder p_viewOrder) const
{
if (p_start >= p_end) {
return;
}
bool reversed = false;
switch (p_viewOrder) {
case ViewOrder::OrderedByNameReversed:
reversed = true;
Q_FALLTHROUGH();
case ViewOrder::OrderedByName:
std::sort(p_nodes.begin() + p_start, p_nodes.begin() + p_end, [reversed](const QSharedPointer<Node> &p_a, const QSharedPointer<Node> p_b) {
if (reversed) {
return p_b->getName().toLower() < p_a->getName().toLower();
} else {
return p_a->getName().toLower() < p_b->getName().toLower();
}
});
break;
case ViewOrder::OrderedByCreatedTimeReversed:
reversed = true;
Q_FALLTHROUGH();
case ViewOrder::OrderedByCreatedTime:
std::sort(p_nodes.begin() + p_start, p_nodes.begin() + p_end, [reversed](const QSharedPointer<Node> &p_a, const QSharedPointer<Node> p_b) {
if (reversed) {
return p_b->getCreatedTimeUtc() < p_a->getCreatedTimeUtc();
} else {
return p_a->getCreatedTimeUtc() < p_b->getCreatedTimeUtc();
}
});
break;
case ViewOrder::OrderedByModifiedTimeReversed:
reversed = true;
Q_FALLTHROUGH();
case ViewOrder::OrderedByModifiedTime:
std::sort(p_nodes.begin() + p_start, p_nodes.begin() + p_end, [reversed](const QSharedPointer<Node> &p_a, const QSharedPointer<Node> p_b) {
if (reversed) {
return p_b->getModifiedTimeUtc() < p_a->getModifiedTimeUtc();
} else {
return p_a->getModifiedTimeUtc() < p_b->getModifiedTimeUtc();
}
});
break;
default:
break;
}
}
void NotebookNodeExplorer::setExternalFilesVisible(bool p_visible)
{
if (m_externalFilesVisible == p_visible) {
return;
}
m_externalFilesVisible = p_visible;
reload();
}
void NotebookNodeExplorer::setAutoImportExternalFiles(bool p_enabled)
{
if (m_autoImportExternalFiles == p_enabled) {
return;
}
m_autoImportExternalFiles = p_enabled;
}
void NotebookNodeExplorer::manualSort(bool p_master)
{
auto node = p_master ? getCurrentMasterNode() : getCurrentSlaveNode();
if (!node) {
return;
}
auto parentNode = node->getParent();
bool isNotebook = parentNode->isRoot();
// Check whether sort files or folders based on current node type.
bool sortFolders = node->isContainer();
SortDialog sortDlg(sortFolders ? tr("Sort Folders") : tr("Sort Notes"),
tr("Sort nodes under %1 (%2) in the configuration file.").arg(
isNotebook ? tr("notebook") : tr("folder"),
isNotebook ? m_notebook->getName() : parentNode->getName()),
VNoteX::getInst().getMainWindow());
QVector<int> selectedIdx;
// Update the tree.
{
auto treeWidget = sortDlg.getTreeWidget();
treeWidget->clear();
treeWidget->setColumnCount(2);
treeWidget->setHeaderLabels({tr("Name"), tr("Created Time"), tr("Modified Time")});
const auto &children = parentNode->getChildrenRef();
for (int i = 0; i < children.size(); ++i) {
const auto &child = children[i];
const bool selected = sortFolders ? child->isContainer() : !child->isContainer();
if (selected) {
selectedIdx.push_back(i);
QStringList cols {child->getName(),
Utils::dateTimeString(child->getCreatedTimeUtc().toLocalTime()),
Utils::dateTimeString(child->getModifiedTimeUtc().toLocalTime())};
QStringList comparisonCols {QString(),
Utils::dateTimeStringUniform(child->getCreatedTimeUtc().toLocalTime()),
Utils::dateTimeStringUniform(child->getModifiedTimeUtc().toLocalTime())};
auto item = sortDlg.addItem(cols, comparisonCols);
item->setData(0, Qt::UserRole, i);
}
}
sortDlg.updateTreeWidget();
}
if (sortDlg.exec() == QDialog::Accepted) {
const auto data = sortDlg.getSortedData();
Q_ASSERT(data.size() == selectedIdx.size());
QVector<int> sortedIdx(data.size(), -1);
for (int i = 0; i < data.size(); ++i) {
sortedIdx[i] = data[i].toInt();
}
parentNode->sortChildren(selectedIdx, sortedIdx);
updateNode(parentNode);
}
}
Node *NotebookNodeExplorer::currentExploredFolderNode() const
{
if (!m_notebook) {
return nullptr;
}
auto node = getCurrentMasterNode();
if (node) {
if (!node->isContainer()) {
node = node->getParent();
}
Q_ASSERT(node && node->isContainer());
} else {
node = m_notebook->getRootNode().data();
}
return node;
}
Node *NotebookNodeExplorer::currentExploredNode() const
{
if (!m_notebook) {
return nullptr;
}
if (isCombinedExploreMode()) {
return getCurrentMasterNode();
} else {
auto node = getCurrentSlaveNode();
if (!node) {
node = getCurrentMasterNode();
}
return node;
}
}
void NotebookNodeExplorer::setViewOrder(int p_order)
{
if (m_viewOrder == p_order) {
return;
}
if (p_order >= 0 && p_order < ViewOrder::ViewOrderMax) {
m_viewOrder = static_cast<ViewOrder>(p_order);
reload();
}
}
void NotebookNodeExplorer::setExploreMode(int p_mode)
{
if (m_exploreMode == p_mode) {
return;
}
if (p_mode >= 0 && p_mode < ExploreMode::ExploreModeMax) {
m_exploreMode = static_cast<ExploreMode>(p_mode);
switch (m_exploreMode) {
case ExploreMode::Combined:
setFocusProxy(m_masterExplorer);
WidgetUtils::distributeWidgetsOfSplitter(m_splitter);
Q_ASSERT(m_slaveExplorer);
m_slaveExplorer->clear();
m_slaveExplorer->hide();
disconnect(m_masterExplorer, &QTreeWidget::currentItemChanged,
this, &NotebookNodeExplorer::updateSlaveExplorer);
break;
case ExploreMode::SeparateSingle:
Q_FALLTHROUGH();
case ExploreMode::SeparateDouble:
if (!m_slaveExplorer) {
setupSlaveExplorer();
}
setFocusProxy(m_slaveExplorer);
m_slaveExplorer->show();
m_splitter->setOrientation(m_exploreMode == ExploreMode::SeparateSingle ? Qt::Vertical : Qt::Horizontal);
WidgetUtils::distributeWidgetsOfSplitter(m_splitter);
connect(m_masterExplorer, &QTreeWidget::currentItemChanged,
this, &NotebookNodeExplorer::updateSlaveExplorer);
break;
default:
Q_ASSERT(false);
return;
}
reload();
}
}
QStringList NotebookNodeExplorer::getSelectedNodesPath(bool p_master) const
{
QStringList files;
// Support nodes and external nodes.
auto selectedNodes = p_master ? getMasterSelectedNodesAndExternalNodes() : getSlaveSelectedNodesAndExternalNodes();
for (const auto &externalNode : selectedNodes.second) {
files << externalNode->fetchAbsolutePath();
}
for (const auto &node : selectedNodes.first) {
if (checkInvalidNode(node)) {
continue;
}
files << node->fetchAbsolutePath();
}
return files;
}
QSharedPointer<Node> NotebookNodeExplorer::importToIndex(QSharedPointer<ExternalNode> p_node)
{
auto node = m_notebook->addAsNode(p_node->getNode(),
p_node->isFolder() ? Node::Flag::Container : Node::Flag::Content,
p_node->getName(),
NodeParameters());
updateNode(p_node->getNode());
if (node) {
setCurrentNode(node.data());
}
return node;
}
void NotebookNodeExplorer::importToIndex(const QVector<QSharedPointer<ExternalNode>> &p_nodes)
{
QSet<Node *> nodesToUpdate;
Node *currentNode = nullptr;
for (const auto &externalNode : p_nodes) {
auto node = m_notebook->addAsNode(externalNode->getNode(),
externalNode->isFolder() ? Node::Flag::Container : Node::Flag::Content,
externalNode->getName(),
NodeParameters());
nodesToUpdate.insert(externalNode->getNode());
currentNode = node.data();
}
for (auto node : nodesToUpdate) {
updateNode(node);
}
if (currentNode) {
setCurrentNode(currentNode);
}
}
bool NotebookNodeExplorer::checkInvalidNode(Node *p_node) const
{
if (!p_node) {
return true;
}
bool nodeExists = p_node->exists();
if (nodeExists) {
p_node->checkExists();
nodeExists = p_node->exists();
}
if (!nodeExists) {
MessageBoxHelper::notify(MessageBoxHelper::Warning,
tr("Invalid node (%1).").arg(p_node->getName()),
tr("Please check if the node exists on the disk."),
p_node->fetchAbsolutePath(),
VNoteX::getInst().getMainWindow());
return true;
}
return false;
}
void NotebookNodeExplorer::addOpenWithMenu(QMenu *p_menu, bool p_master)
{
auto subMenu = p_menu->addMenu(tr("Open &With"));
const auto &types = FileTypeHelper::getInst().getAllFileTypes();
for (const auto &ft : types) {
if (ft.m_type == FileType::Others) {
continue;
}
QAction *act = subMenu->addAction(ft.m_displayName);
connect(act, &QAction::triggered,
this, [this, act, p_master]() {
openSelectedNodesWithProgram(act->data().toString(), p_master);
});
act->setData(ft.m_typeName);
}
subMenu->addSeparator();
{
const auto &sessionConfig = ConfigMgr::getInst().getSessionConfig();
for (const auto &pro : sessionConfig.getExternalPrograms()) {
QAction *act = subMenu->addAction(pro.m_name);
connect(act, &QAction::triggered,
this, [this, act, p_master]() {
openSelectedNodesWithProgram(act->data().toString(), p_master);
});
act->setData(pro.m_name);
WidgetUtils::addActionShortcutText(act, pro.m_shortcut);
}
}
subMenu->addSeparator();
{
auto defaultAct = subMenu->addAction(tr("System Default Program"));
connect(defaultAct, &QAction::triggered,
this, [this, defaultAct, p_master]() {
openSelectedNodesWithProgram(QString(), p_master);
});
const auto &coreConfig = ConfigMgr::getInst().getCoreConfig();
WidgetUtils::addActionShortcutText(defaultAct, coreConfig.getShortcut(CoreConfig::OpenWithDefaultProgram));
}
subMenu->addAction(tr("Add External Program"),
this,
[]() {
const auto file = DocsUtils::getDocFile(QStringLiteral("external_programs.md"));
if (!file.isEmpty()) {
auto paras = QSharedPointer<FileOpenParameters>::create();
paras->m_readOnly = true;
emit VNoteX::getInst().openFileRequested(file, paras);
}
});
}
// Shortcut auxiliary, it can also be used to determine the browser.
bool NotebookNodeExplorer::isActionFromMaster() const
{
if (!isCombinedExploreMode()) {
return m_masterExplorer->hasFocus();
}
return true;
}
void NotebookNodeExplorer::setupShortcuts()
{
const auto &coreConfig = ConfigMgr::getInst().getCoreConfig();
// OpenWithDefaultProgram.
{
auto shortcut = WidgetUtils::createShortcut(coreConfig.getShortcut(CoreConfig::OpenWithDefaultProgram), this);
if (shortcut) {
connect(shortcut, &QShortcut::activated,
this, [this]() {
openSelectedNodesWithProgram(QString(), isActionFromMaster());
});
}
}
// Copy
{
auto shortcut = WidgetUtils::createShortcut(coreConfig.getShortcut(CoreConfig::Copy), this);
if (shortcut) {
connect(shortcut, &QShortcut::activated,
this, [this]() {
copySelectedNodes(false, isActionFromMaster());
});
}
}
// Paste
{
auto shortcut = WidgetUtils::createShortcut(coreConfig.getShortcut(CoreConfig::Paste), this);
if (shortcut) {
connect(shortcut, &QShortcut::activated,
this, &NotebookNodeExplorer::pasteNodesFromClipboard);
}
}
// Properties
{
auto shortcut = WidgetUtils::createShortcut(coreConfig.getShortcut(CoreConfig::Properties), this);
if (shortcut) {
connect(shortcut, &QShortcut::activated,
this, [this]() {
openCurrentNodeProperties(isActionFromMaster());
});
}
}
const auto &sessionConfig = ConfigMgr::getInst().getSessionConfig();
for (const auto &pro : sessionConfig.getExternalPrograms()) {
auto shortcut = WidgetUtils::createShortcut(pro.m_shortcut, this);
const auto &name = pro.m_name;
if (shortcut) {
connect(shortcut, &QShortcut::activated,
this, [this, name]() {
bool isMaster = true;
if (!isCombinedExploreMode()) {
isMaster = m_masterExplorer->hasFocus();
}
openSelectedNodesWithProgram(name, isMaster);
});
}
}
}
void NotebookNodeExplorer::openSelectedNodesWithProgram(const QString &p_name, bool p_master)
{
const bool closeBefore = ConfigMgr::getInst().getWidgetConfig().getNodeExplorerCloseBeforeOpenWithEnabled();
const auto files = getSelectedNodesPath(p_master);
for (const auto &file : files) {
if (file.isEmpty()) {
continue;
}
if (closeBefore) {
auto event = QSharedPointer<Event>::create();
emit closeFileRequested(file, event);
if (!event->m_response.toBool()) {
continue;
}
}
if (p_name.isEmpty()) {
WidgetUtils::openUrlByDesktop(QUrl::fromLocalFile(file));
} else {
auto paras = QSharedPointer<FileOpenParameters>::create();
paras->m_fileType = p_name;
emit VNoteX::getInst().openFileRequested(file, paras);
}
}
}
void NotebookNodeExplorer::openCurrentNodeProperties(bool p_master)
{
const int selectedSize = p_master ? m_masterExplorer->selectedItems().size() : m_slaveExplorer->selectedItems().size();
if (selectedSize != 1) {
return;
}
auto node = p_master ? getCurrentMasterNode() : getCurrentSlaveNode();
if (checkInvalidNode(node)) {
return;
}
int ret = QDialog::Rejected;
if (node->hasContent()) {
NotePropertiesDialog dialog(node, VNoteX::getInst().getMainWindow());
ret = dialog.exec();
} else {
FolderPropertiesDialog dialog(node, VNoteX::getInst().getMainWindow());
ret = dialog.exec();
}
if (ret == QDialog::Accepted) {
setCurrentNode(node);
}
}
void NotebookNodeExplorer::loadMasterItemChildren(QTreeWidgetItem *p_item) const
{
auto cnt = p_item->childCount();
for (int i = 0; i < cnt; ++i) {
auto child = p_item->child(i);
auto data = getItemNodeData(child);
if (data.isNode() && !data.isLoaded()) {
loadMasterNode(child, data.getNode(), 1);
}
}
}
QString NotebookNodeExplorer::generateToolTip(const Node *p_node)
{
Q_ASSERT(p_node->isLoaded());
QString tip = p_node->exists() ? p_node->getName() : (tr("[Invalid] %1").arg(p_node->getName()));
tip += QLatin1String("\n\n");
if (!p_node->getTags().isEmpty()) {
const auto &tags = p_node->getTags();
QString tagString = tags.first();
for (int i = 1; i < tags.size(); ++i) {
tagString += QLatin1String("; ") + tags[i];
}
tip += tr("Tags: %1\n").arg(tagString);
}
tip += tr("Created Time: %1\n").arg(Utils::dateTimeString(p_node->getCreatedTimeUtc().toLocalTime()));
tip += tr("Modified Time: %1").arg(Utils::dateTimeString(p_node->getModifiedTimeUtc().toLocalTime()));
return tip;
}
QByteArray NotebookNodeExplorer::saveState() const
{
return m_splitter->saveState();
}
void NotebookNodeExplorer::restoreState(const QByteArray &p_data)
{
m_splitter->restoreState(p_data);
}
bool NotebookNodeExplorer::belongsToMasterExplorer(const Node *p_node) const
{
switch (m_exploreMode) {
case ExploreMode::Combined:
return true;
case ExploreMode::SeparateSingle:
Q_FALLTHROUGH();
case ExploreMode::SeparateDouble:
return p_node ? p_node->isContainer() : true;
break;
default:
Q_ASSERT(false);
break;
}
return true;
}
bool NotebookNodeExplorer::belongsToMasterExplorer(const ExternalNode *p_node) const
{
if (isCombinedExploreMode()) {
return true;
} else {
return p_node ? p_node->isFolder() : false;
}
}
void NotebookNodeExplorer::updateSlaveExplorer()
{
Q_ASSERT(!isCombinedExploreMode());
m_slaveExplorer->clear();
const Node *masterNode = nullptr;
auto item = m_masterExplorer->currentItem();
if (item) {
const int selectedSize = m_masterExplorer->selectedItems().size();
if (selectedSize > 1) {
return;
}
auto data = getItemNodeData(item);
if (data.isNode()) {
masterNode = data.getNode();
Q_ASSERT(masterNode->isContainer());
}
} else {
// Root node.
masterNode = m_notebook ? m_notebook->getRootNode().data() : nullptr;
}
if (!masterNode) {
return;
}
Q_ASSERT(masterNode->isContainer() && masterNode->isLoaded());
// External children.
if (m_externalFilesVisible) {
auto externalChildren = masterNode->fetchExternalChildren();
// TODO: Sort external children.
for (const auto &child : externalChildren) {
if (child->isFolder()) {
continue;
}
auto item = new QListWidgetItem(m_slaveExplorer);
fillSlaveItem(item, child);
}
}
auto children = masterNode->getChildren();
sortNodes(children);
for (const auto &child : children) {
if (child->isContainer()) {
continue;
}
Q_ASSERT(child->isLoaded());
auto item = new QListWidgetItem(m_slaveExplorer);
fillSlaveItem(item, child.data());
}
}
bool NotebookNodeExplorer::isCombinedExploreMode() const
{
return m_exploreMode == ExploreMode::Combined;
}
Node *NotebookNodeExplorer::getSlaveExplorerMasterNode() const
{
Q_ASSERT(!isCombinedExploreMode());
auto item = m_masterExplorer->currentItem();
if (item) {
const int selectedSize = m_masterExplorer->selectedItems().size();
if (selectedSize > 1) {
return nullptr;
}
return getCurrentMasterNode();
} else {
// Root node.
return (m_notebook ? m_notebook->getRootNode().data() : nullptr);
}
}