Browse Source

support external nodes (#1718)

Le Tan 4 years ago
parent
commit
9895207dd4

+ 34 - 0
src/core/notebook/externalnode.cpp

@@ -0,0 +1,34 @@
+#include "externalnode.h"
+
+#include "node.h"
+#include <utils/pathutils.h>
+
+using namespace vnotex;
+
+ExternalNode::ExternalNode(Node *p_parent, const QString &p_name, Type p_type)
+    : m_parentNode(p_parent),
+      m_name(p_name),
+      m_type(p_type)
+{
+    Q_ASSERT(m_parentNode);
+}
+
+Node *ExternalNode::getNode() const
+{
+    return m_parentNode;
+}
+
+const QString &ExternalNode::getName() const
+{
+    return m_name;
+}
+
+bool ExternalNode::isFolder() const
+{
+    return m_type == Type::Folder;
+}
+
+QString ExternalNode::fetchAbsolutePath() const
+{
+    return PathUtils::concatenateFilePath(m_parentNode->fetchAbsolutePath(), m_name);
+}

+ 41 - 0
src/core/notebook/externalnode.h

@@ -0,0 +1,41 @@
+#ifndef EXTERNALNODE_H
+#define EXTERNALNODE_H
+
+#include <QString>
+
+namespace vnotex
+{
+    class Node;
+
+    // External node not managed by VNote.
+    class ExternalNode
+    {
+    public:
+        enum class Type
+        {
+            File,
+            Folder
+        };
+
+        ExternalNode(Node *p_parent, const QString &p_name, Type p_type);
+
+        Node *getNode() const;
+
+        const QString &getName() const;
+
+        bool isFolder() const;
+
+        QString fetchAbsolutePath() const;
+
+    private:
+        // Parent node.
+        // We support only one level further the external folder.
+        Node *m_parentNode = nullptr;
+
+        QString m_name;
+
+        Type m_type = Type::File;
+    };
+}
+
+#endif // EXTERNALNODE_H

+ 59 - 3
src/core/notebook/node.cpp

@@ -103,13 +103,13 @@ bool Node::containsChild(const QString &p_name, bool p_caseSensitive) const
 
 bool Node::containsChild(const QSharedPointer<Node> &p_node) const
 {
-    return getChildren().indexOf(p_node) != -1;
+    return m_children.indexOf(p_node) != -1;
 }
 
 QSharedPointer<Node> Node::findChild(const QString &p_name, bool p_caseSensitive) const
 {
     auto targetName = p_caseSensitive ? p_name : p_name.toLower();
-    for (auto &child : getChildren()) {
+    for (const auto &child : m_children) {
         if (p_caseSensitive ? child->getName() == targetName
                             : child->getName().toLower() == targetName) {
             return child;
@@ -164,7 +164,12 @@ void Node::setModifiedTimeUtc()
     m_modifiedTimeUtc = QDateTime::currentDateTimeUtc();
 }
 
-const QVector<QSharedPointer<Node>> &Node::getChildren() const
+const QVector<QSharedPointer<Node>> &Node::getChildrenRef() const
+{
+    return m_children;
+}
+
+QVector<QSharedPointer<Node>> Node::getChildren() const
 {
     return m_children;
 }
@@ -347,3 +352,54 @@ void Node::sortChildren(const QVector<int> &p_beforeIdx, const QVector<int> &p_a
 
     save();
 }
+
+QVector<QSharedPointer<ExternalNode>> Node::fetchExternalChildren() const
+{
+    return getConfigMgr()->fetchExternalChildren(const_cast<Node *>(this));
+}
+
+bool Node::containsContainerChild(const QString &p_name) const
+{
+    // TODO: we assume that m_children is sorted first the container children.
+    for (auto &child : m_children) {
+        if (!child->isContainer()) {
+            break;
+        }
+
+        if (child->getName() == p_name) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+bool Node::containsContentChild(const QString &p_name) const
+{
+    // TODO: we assume that m_children is sorted: first the container children then content children.
+    for (int i = m_children.size() - 1; i >= 0; --i) {
+        if (m_children[i]->isContainer()) {
+            break;
+        }
+
+        if (m_children[i]->getName() == p_name) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+bool Node::exists() const
+{
+    return m_flags & Flag::Exists;
+}
+
+void Node::setExists(bool p_exists)
+{
+    if (p_exists) {
+        m_flags |= Flag::Exists;
+    } else {
+        m_flags &= ~Flag::Exists;
+    }
+}

+ 19 - 2
src/core/notebook/node.h

@@ -15,6 +15,7 @@ namespace vnotex
     class INotebookConfigMgr;
     class INotebookBackend;
     class File;
+    class ExternalNode;
 
     // Used when add/new a node.
     struct NodeParameters
@@ -35,7 +36,9 @@ namespace vnotex
             Content = 0x1,
             // A node with children.
             Container = 0x2,
-            ReadOnly = 0x4
+            ReadOnly = 0x4,
+            // Whether a node exists on disk.
+            Exists = 0x10
         };
         Q_DECLARE_FLAGS(Flags, Flag)
 
@@ -86,6 +89,11 @@ namespace vnotex
 
         bool hasContent() const;
 
+        // Whether the node exists on disk.
+        bool exists() const;
+
+        void setExists(bool p_exists);
+
         Node::Flags getFlags() const;
 
         Node::Use getUse() const;
@@ -98,7 +106,8 @@ namespace vnotex
         const QDateTime &getModifiedTimeUtc() const;
         void setModifiedTimeUtc();
 
-        const QVector<QSharedPointer<Node>> &getChildren() const;
+        const QVector<QSharedPointer<Node>> &getChildrenRef() const;
+        QVector<QSharedPointer<Node>> getChildren() const;
         int getChildrenCount() const;
 
         QSharedPointer<Node> findChild(const QString &p_name, bool p_caseSensitive = true) const;
@@ -107,12 +116,20 @@ namespace vnotex
 
         bool containsChild(const QSharedPointer<Node> &p_node) const;
 
+        // Case sensitive.
+        bool containsContainerChild(const QString &p_name) const;
+
+        // Case sensitive.
+        bool containsContentChild(const QString &p_name) const;
+
         void addChild(const QSharedPointer<Node> &p_node);
 
         void insertChild(int p_idx, const QSharedPointer<Node> &p_node);
 
         void removeChild(const QSharedPointer<Node> &p_node);
 
+        QVector<QSharedPointer<ExternalNode>> fetchExternalChildren() const;
+
         void setParent(Node *p_parent);
         Node *getParent() const;
 

+ 16 - 20
src/core/notebook/notebook.cpp

@@ -161,7 +161,7 @@ const QSharedPointer<Node> &Notebook::getRootNode() const
 QSharedPointer<Node> Notebook::getRecycleBinNode() const
 {
     auto root = getRootNode();
-    auto children = root->getChildren();
+    const auto &children = root->getChildrenRef();
     auto it = std::find_if(children.begin(),
                            children.end(),
                            [this](const QSharedPointer<Node> &p_node) {
@@ -203,7 +203,7 @@ QSharedPointer<Node> Notebook::loadNodeByPath(const QString &p_path)
         relativePath = p_path;
     }
 
-    return m_configMgr->loadNodeByPath(m_root, relativePath);
+    return m_configMgr->loadNodeByPath(getRootNode(), relativePath);
 }
 
 QSharedPointer<Node> Notebook::copyNodeAsChildOf(const QSharedPointer<Node> &p_src, Node *p_dest, bool p_move)
@@ -227,17 +227,15 @@ QSharedPointer<Node> Notebook::copyNodeAsChildOf(const QSharedPointer<Node> &p_s
 
 void Notebook::removeNode(const QSharedPointer<Node> &p_node, bool p_force, bool p_configOnly)
 {
+    Q_ASSERT(p_node && !p_node->isRoot());
     Q_ASSERT(p_node->getNotebook() == this);
     m_configMgr->removeNode(p_node, p_force, p_configOnly);
 }
 
-void Notebook::removeNode(const Node *p_node, bool p_force, bool p_configOnly)
+void Notebook::removeNode(Node *p_node, bool p_force, bool p_configOnly)
 {
-    Q_ASSERT(p_node && !p_node->isRoot());
-    auto children = p_node->getParent()->getChildren();
-    auto it = std::find(children.begin(), children.end(), p_node);
-    Q_ASSERT(it != children.end());
-    removeNode(*it, p_force, p_configOnly);
+    Q_ASSERT(p_node);
+    removeNode(p_node->sharedFromThis(), p_force, p_configOnly);
 }
 
 bool Notebook::isRecycleBinNode(const Node *p_node) const
@@ -261,22 +259,14 @@ bool Notebook::isNodeInRecycleBin(const Node *p_node) const
     return false;
 }
 
-void Notebook::moveNodeToRecycleBin(const Node *p_node)
+void Notebook::moveNodeToRecycleBin(Node *p_node)
 {
-    Q_ASSERT(p_node && !p_node->isRoot());
-    auto children = p_node->getParent()->getChildren();
-    for (auto &child : children) {
-        if (p_node == child) {
-            moveNodeToRecycleBin(child);
-            return;
-        }
-    }
-
-    Q_ASSERT(false);
+    moveNodeToRecycleBin(p_node->sharedFromThis());
 }
 
 void Notebook::moveNodeToRecycleBin(const QSharedPointer<Node> &p_node)
 {
+    Q_ASSERT(p_node && !p_node->isRoot());
     auto destNode = getOrCreateRecycleBinDateNode();
     copyNodeAsChildOf(p_node, destNode.data(), true);
 }
@@ -299,8 +289,9 @@ QSharedPointer<Node> Notebook::getOrCreateRecycleBinDateNode()
 
 void Notebook::emptyNode(const Node *p_node, bool p_force)
 {
+    // Copy the children.
     auto children = p_node->getChildren();
-    for (auto &child : children) {
+    for (const auto &child : children) {
         removeNode(child, p_force);
     }
 }
@@ -355,3 +346,8 @@ QSharedPointer<Node> Notebook::copyAsNode(Node *p_parent,
 {
     return m_configMgr->copyAsNode(p_parent, p_flags, p_path);
 }
+
+void Notebook::reloadNode(Node *p_node)
+{
+    m_configMgr->reloadNode(p_node);
+}

+ 4 - 2
src/core/notebook/notebook.h

@@ -99,11 +99,11 @@ namespace vnotex
         // @p_configOnly: if true, will just remove node from config.
         void removeNode(const QSharedPointer<Node> &p_node, bool p_force = false, bool p_configOnly = false);
 
-        void removeNode(const Node *p_node, bool p_force = false, bool p_configOnly = false);
+        void removeNode(Node *p_node, bool p_force = false, bool p_configOnly = false);
 
         void moveNodeToRecycleBin(const QSharedPointer<Node> &p_node);
 
-        void moveNodeToRecycleBin(const Node *p_node);
+        void moveNodeToRecycleBin(Node *p_node);
 
         // Move @p_filePath to the recycle bin, without adding it as a child node.
         void moveFileToRecycleBin(const QString &p_filePath);
@@ -127,6 +127,8 @@ namespace vnotex
 
         bool isBuiltInFolder(const Node *p_node, const QString &p_name) const;
 
+        void reloadNode(Node *p_node);
+
         static const QString c_defaultAttachmentFolder;
 
         static const QString c_defaultImageFolder;

+ 2 - 0
src/core/notebook/notebook.pri

@@ -1,4 +1,5 @@
 SOURCES += \
+    $$PWD/externalnode.cpp \
     $$PWD/notebook.cpp \
     $$PWD/bundlenotebookfactory.cpp \
     $$PWD/notebookparameters.cpp \
@@ -8,6 +9,7 @@ SOURCES += \
     $$PWD/vxnodefile.cpp
 
 HEADERS += \
+    $$PWD/externalnode.h \
     $$PWD/notebook.h \
     $$PWD/inotebookfactory.h \
     $$PWD/bundlenotebookfactory.h \

+ 4 - 0
src/core/notebookbackend/inotebookbackend.h

@@ -67,6 +67,10 @@ namespace vnotex
 
         virtual bool exists(const QString &p_path) const = 0;
 
+        virtual bool existsFile(const QString &p_path) const = 0;
+
+        virtual bool existsDir(const QString &p_path) const = 0;
+
         virtual bool childExistsCaseInsensitive(const QString &p_dirPath, const QString &p_name) const = 0;
 
         virtual bool isFile(const QString &p_path) const = 0;

+ 12 - 0
src/core/notebookbackend/localnotebookbackend.cpp

@@ -86,6 +86,18 @@ bool LocalNotebookBackend::exists(const QString &p_path) const
     return QFileInfo::exists(getFullPath(p_path));
 }
 
+bool LocalNotebookBackend::existsFile(const QString &p_path) const
+{
+    QFileInfo fi(getFullPath(p_path));
+    return fi.exists() && fi.isFile();
+}
+
+bool LocalNotebookBackend::existsDir(const QString &p_path) const
+{
+    QFileInfo fi(getFullPath(p_path));
+    return fi.exists() && fi.isDir();
+}
+
 bool LocalNotebookBackend::childExistsCaseInsensitive(const QString &p_dirPath, const QString &p_name) const
 {
     return FileUtils::childExistsCaseInsensitive(getFullPath(p_dirPath), p_name);

+ 4 - 0
src/core/notebookbackend/localnotebookbackend.h

@@ -47,6 +47,10 @@ namespace vnotex
 
         bool exists(const QString &p_path) const Q_DECL_OVERRIDE;
 
+        bool existsFile(const QString &p_path) const Q_DECL_OVERRIDE;
+
+        bool existsDir(const QString &p_path) const Q_DECL_OVERRIDE;
+
         bool childExistsCaseInsensitive(const QString &p_dirPath, const QString &p_name) const Q_DECL_OVERRIDE;
 
         bool isFile(const QString &p_path) const Q_DECL_OVERRIDE;

+ 5 - 1
src/core/notebookconfigmgr/inotebookconfigmgr.h

@@ -36,7 +36,7 @@ namespace vnotex
 
         const QSharedPointer<INotebookBackend> &getBackend() const;
 
-        virtual QSharedPointer<Node> loadRootNode() const = 0;
+        virtual QSharedPointer<Node> loadRootNode() = 0;
 
         virtual void loadNode(Node *p_node) const = 0;
         virtual void saveNode(const Node *p_node) = 0;
@@ -75,6 +75,10 @@ namespace vnotex
 
         virtual QString fetchNodeAttachmentFolderPath(Node *p_node) = 0;
 
+        virtual QVector<QSharedPointer<ExternalNode>> fetchExternalChildren(Node *p_node) const = 0;
+
+        virtual void reloadNode(Node *p_node) = 0;
+
     protected:
         // Version of the config processing code.
         virtual QString getCodeVersion() const;

+ 146 - 33
src/core/notebookconfigmgr/vxnotebookconfigmgr.cpp

@@ -8,6 +8,7 @@
 #include <notebookbackend/inotebookbackend.h>
 #include <notebook/notebookparameters.h>
 #include <notebook/vxnode.h>
+#include <notebook/externalnode.h>
 #include <notebook/bundlenotebook.h>
 #include <utils/utils.h>
 #include <utils/fileutils.h>
@@ -205,11 +206,12 @@ void VXNotebookConfigMgr::createEmptyRootNode()
     writeNodeConfig(c_nodeConfigName, node);
 }
 
-QSharedPointer<Node> VXNotebookConfigMgr::loadRootNode() const
+QSharedPointer<Node> VXNotebookConfigMgr::loadRootNode()
 {
     auto nodeConfig = readNodeConfig("");
     QSharedPointer<Node> root = nodeConfigToNode(*nodeConfig, "", nullptr);
     root->setUse(Node::Use::Root);
+    root->setExists(true);
     Q_ASSERT(root->isLoaded());
 
     if (!markRecycleBinNode(root)) {
@@ -219,11 +221,16 @@ QSharedPointer<Node> VXNotebookConfigMgr::loadRootNode() const
     return root;
 }
 
-bool VXNotebookConfigMgr::markRecycleBinNode(const QSharedPointer<Node> &p_root) const
+bool VXNotebookConfigMgr::markRecycleBinNode(const QSharedPointer<Node> &p_root)
 {
     auto node = p_root->findChild(c_recycleBinFolderName,
                                   FileUtils::isPlatformNameCaseSensitive());
     if (node) {
+        if (!node->exists()) {
+            removeNode(node, true, true);
+            return false;
+        }
+
         node->setUse(Node::Use::RecycleBin);
         markNodeReadOnly(node.data());
         return true;
@@ -239,7 +246,7 @@ void VXNotebookConfigMgr::markNodeReadOnly(Node *p_node) const
     }
 
     p_node->setReadOnly(true);
-    for (auto &child : p_node->getChildren()) {
+    for (const auto &child : p_node->getChildrenRef()) {
         markNodeReadOnly(child.data());
     }
 }
@@ -305,16 +312,30 @@ void VXNotebookConfigMgr::loadFolderNode(Node *p_node, const NodeConfig &p_confi
 {
     QVector<QSharedPointer<Node>> children;
     children.reserve(p_config.m_files.size() + p_config.m_folders.size());
+    const auto basePath = p_node->fetchPath();
 
     for (const auto &folder : p_config.m_folders) {
+        if (folder.m_name.isEmpty()) {
+            // Skip empty name node.
+            qWarning() << "skipped loading node with empty name under" << p_node->fetchPath();
+            continue;
+        }
+
         auto folderNode = QSharedPointer<VXNode>::create(folder.m_name,
                                                          getNotebook(),
                                                          p_node);
         inheritNodeFlags(p_node, folderNode.data());
+        folderNode->setExists(getBackend()->existsDir(PathUtils::concatenateFilePath(basePath, folder.m_name)));
         children.push_back(folderNode);
     }
 
     for (const auto &file : p_config.m_files) {
+        if (file.m_name.isEmpty()) {
+            // Skip empty name node.
+            qWarning() << "skipped loading node with empty name under" << p_node->fetchPath();
+            continue;
+        }
+
         auto fileNode = QSharedPointer<VXNode>::create(file.m_id,
                                                        file.m_name,
                                                        file.m_createdTimeUtc,
@@ -324,6 +345,7 @@ void VXNotebookConfigMgr::loadFolderNode(Node *p_node, const NodeConfig &p_confi
                                                        getNotebook(),
                                                        p_node);
         inheritNodeFlags(p_node, fileNode.data());
+        fileNode->setExists(getBackend()->existsFile(PathUtils::concatenateFilePath(basePath, file.m_name)));
         children.push_back(fileNode);
     }
 
@@ -338,7 +360,7 @@ QSharedPointer<Node> VXNotebookConfigMgr::newNode(Node *p_parent,
                                                   Node::Flags p_flags,
                                                   const QString &p_name)
 {
-    Q_ASSERT(p_parent && p_parent->isContainer());
+    Q_ASSERT(p_parent && p_parent->isContainer() && !p_name.isEmpty());
 
     QSharedPointer<Node> node;
 
@@ -359,6 +381,7 @@ QSharedPointer<Node> VXNotebookConfigMgr::addAsNode(Node *p_parent,
 {
     Q_ASSERT(p_parent && p_parent->isContainer());
 
+    // TODO: reuse the config if available.
     QSharedPointer<Node> node;
     if (p_flags & Node::Flag::Content) {
         Q_ASSERT(!(p_flags & Node::Flag::Container));
@@ -407,6 +430,9 @@ QSharedPointer<Node> VXNotebookConfigMgr::newFileNode(Node *p_parent,
     // Write empty file.
     if (p_create) {
         getBackend()->writeFile(node->fetchPath(), QString());
+        node->setExists(true);
+    } else {
+        node->setExists(getBackend()->existsFile(node->fetchPath()));
     }
 
     addChildNode(p_parent, node);
@@ -433,6 +459,9 @@ QSharedPointer<Node> VXNotebookConfigMgr::newFolderNode(Node *p_parent,
     // Make folder.
     if (p_create) {
         getBackend()->makePath(node->fetchPath());
+        node->setExists(true);
+    } else {
+        node->setExists(getBackend()->existsDir(node->fetchPath()));
     }
 
     writeNodeConfig(node.data());
@@ -452,7 +481,7 @@ QSharedPointer<VXNotebookConfigMgr::NodeConfig> VXNotebookConfigMgr::nodeToNodeC
                                                      p_node->getCreatedTimeUtc(),
                                                      p_node->getModifiedTimeUtc());
 
-    for (const auto &child : p_node->getChildren()) {
+    for (const auto &child : p_node->getChildrenRef()) {
         if (child->hasContent()) {
             NodeFileConfig fileConfig;
             fileConfig.m_name = child->getName();
@@ -477,7 +506,7 @@ QSharedPointer<VXNotebookConfigMgr::NodeConfig> VXNotebookConfigMgr::nodeToNodeC
 
 void VXNotebookConfigMgr::loadNode(Node *p_node) const
 {
-    if (p_node->isLoaded()) {
+    if (p_node->isLoaded() || !p_node->exists()) {
         return;
     }
 
@@ -513,7 +542,7 @@ void VXNotebookConfigMgr::addChildNode(Node *p_parent, const QSharedPointer<Node
 {
     if (p_child->isContainer()) {
         int idx = 0;
-        auto children = p_parent->getChildren();
+        const auto &children = p_parent->getChildrenRef();
         for (; idx < children.size(); ++idx) {
             if (!children[idx]->isContainer()) {
                 break;
@@ -558,6 +587,13 @@ QSharedPointer<Node> VXNotebookConfigMgr::copyNodeAsChildOf(const QSharedPointer
 {
     Q_ASSERT(p_dest->isContainer());
 
+    if (!p_src->exists()) {
+        if (p_move) {
+            p_src->getNotebook()->removeNode(p_src);
+        }
+        return nullptr;
+    }
+
     QSharedPointer<Node> node;
     if (p_src->isContainer()) {
         node = copyFolderNodeAsChildOf(p_src, p_dest, p_move);
@@ -605,6 +641,7 @@ QSharedPointer<Node> VXNotebookConfigMgr::copyFileNodeAsChildOf(const QSharedPoi
                                                    attachmentFolder,
                                                    notebook,
                                                    p_dest);
+    destNode->setExists(true);
     addChildNode(p_dest, destNode);
     writeNodeConfig(p_dest);
 
@@ -643,6 +680,7 @@ QSharedPointer<Node> VXNotebookConfigMgr::copyFolderNodeAsChildOf(const QSharedP
                                p_src->getModifiedTimeUtc(),
                                QStringList(),
                                QVector<QSharedPointer<Node>>());
+    destNode->setExists(true);
 
     writeNodeConfig(destNode.data());
 
@@ -650,7 +688,8 @@ QSharedPointer<Node> VXNotebookConfigMgr::copyFolderNodeAsChildOf(const QSharedP
     writeNodeConfig(p_dest);
 
     // Copy children node.
-    for (const auto &childNode : p_src->getChildren()) {
+    auto children = p_src->getChildren();
+    for (const auto &childNode : children) {
         copyNodeAsChildOf(childNode, destNode.data(), p_move);
     }
 
@@ -664,13 +703,18 @@ QSharedPointer<Node> VXNotebookConfigMgr::copyFolderNodeAsChildOf(const QSharedP
 void VXNotebookConfigMgr::removeNode(const QSharedPointer<Node> &p_node, bool p_force, bool p_configOnly)
 {
     auto parentNode = p_node->getParent();
-    if (!p_configOnly) {
+    if (!p_configOnly && p_node->exists()) {
         // Remove all children.
-        for (auto &childNode : p_node->getChildren()) {
+        auto children = p_node->getChildren();
+        for (const auto &childNode : children) {
             removeNode(childNode, p_force, p_configOnly);
         }
 
-        removeFilesOfNode(p_node.data(), p_force);
+        try {
+            removeFilesOfNode(p_node.data(), p_force);
+        } catch (Exception &p_e) {
+            qWarning() << "failed to remove files of node" << p_node->fetchPath() << p_e.what();
+        }
     }
 
     if (parentNode) {
@@ -771,7 +815,9 @@ bool VXNotebookConfigMgr::isBuiltInFolder(const Node *p_node, const QString &p_n
     const auto name = p_name.toLower();
     if (name == c_recycleBinFolderName
         || name == getNotebook()->getImageFolder().toLower()
-        || name == getNotebook()->getAttachmentFolder().toLower()) {
+        || name == getNotebook()->getAttachmentFolder().toLower()
+        || name == QStringLiteral("_v_images")
+        || name == QStringLiteral("_v_attachments")) {
         return true;
     }
     return BundleNotebookConfigMgr::isBuiltInFolder(p_node, p_name);
@@ -779,25 +825,36 @@ bool VXNotebookConfigMgr::isBuiltInFolder(const Node *p_node, const QString &p_n
 
 QSharedPointer<Node> VXNotebookConfigMgr::copyFileAsChildOf(const QString &p_srcPath, Node *p_dest)
 {
-    // Copy source file itself.
-    auto destFilePath = PathUtils::concatenateFilePath(p_dest->fetchPath(),
+    // Skip copy if it already locates in dest folder.
+    auto destFilePath = PathUtils::concatenateFilePath(p_dest->fetchAbsolutePath(),
                                                        PathUtils::fileName(p_srcPath));
-    destFilePath = getBackend()->renameIfExistsCaseInsensitive(destFilePath);
-    getBackend()->copyFile(p_srcPath, destFilePath);
+    if (!PathUtils::areSamePaths(p_srcPath, destFilePath)) {
+        // Copy source file itself.
+        destFilePath = getBackend()->renameIfExistsCaseInsensitive(destFilePath);
+        getBackend()->copyFile(p_srcPath, destFilePath);
 
-    // Copy media files fetched from content.
-    ContentMediaUtils::copyMediaFiles(p_srcPath, getBackend().data(), destFilePath);
+        // Copy media files fetched from content.
+        ContentMediaUtils::copyMediaFiles(p_srcPath, getBackend().data(), destFilePath);
+    }
+
+    const auto name = PathUtils::fileName(destFilePath);
+    auto destNode = p_dest->findChild(name, true);
+    if (destNode) {
+        // Already have the node.
+        return destNode;
+    }
 
     // Create a file node.
     auto currentTime = QDateTime::currentDateTimeUtc();
-    auto destNode = QSharedPointer<VXNode>::create(getNotebook()->getAndUpdateNextNodeId(),
-                                                   PathUtils::fileName(destFilePath),
-                                                   currentTime,
-                                                   currentTime,
-                                                   QStringList(),
-                                                   QString(),
-                                                   getNotebook(),
-                                                   p_dest);
+    destNode = QSharedPointer<VXNode>::create(getNotebook()->getAndUpdateNextNodeId(),
+                                              name,
+                                              currentTime,
+                                              currentTime,
+                                              QStringList(),
+                                              QString(),
+                                              getNotebook(),
+                                              p_dest);
+    destNode->setExists(true);
     addChildNode(p_dest, destNode);
     writeNodeConfig(p_dest);
 
@@ -806,24 +863,33 @@ QSharedPointer<Node> VXNotebookConfigMgr::copyFileAsChildOf(const QString &p_src
 
 QSharedPointer<Node> VXNotebookConfigMgr::copyFolderAsChildOf(const QString &p_srcPath, Node *p_dest)
 {
-    auto destFolderPath = PathUtils::concatenateFilePath(p_dest->fetchPath(),
+    // Skip copy if it already locates in dest folder.
+    auto destFolderPath = PathUtils::concatenateFilePath(p_dest->fetchAbsolutePath(),
                                                          PathUtils::fileName(p_srcPath));
-    destFolderPath = getBackend()->renameIfExistsCaseInsensitive(destFolderPath);
+    if (!PathUtils::areSamePaths(p_srcPath, destFolderPath)) {
+        destFolderPath = getBackend()->renameIfExistsCaseInsensitive(destFolderPath);
 
-    // Copy folder.
-    getBackend()->copyDir(p_srcPath, destFolderPath);
+        // Copy folder.
+        getBackend()->copyDir(p_srcPath, destFolderPath);
+    }
+
+    const auto name = PathUtils::fileName(destFolderPath);
+    auto destNode = p_dest->findChild(name, true);
+    if (destNode) {
+        // Already have the node.
+        return destNode;
+    }
 
     // Create a folder node.
     auto notebook = getNotebook();
-    auto destNode = QSharedPointer<VXNode>::create(PathUtils::fileName(destFolderPath),
-                                                   notebook,
-                                                   p_dest);
+    destNode = QSharedPointer<VXNode>::create(name, notebook, p_dest);
     auto currentTime = QDateTime::currentDateTimeUtc();
     destNode->loadCompleteInfo(notebook->getAndUpdateNextNodeId(),
                                currentTime,
                                currentTime,
                                QStringList(),
                                QVector<QSharedPointer<Node>>());
+    destNode->setExists(true);
 
     writeNodeConfig(destNode.data());
 
@@ -839,3 +905,50 @@ void VXNotebookConfigMgr::inheritNodeFlags(const Node *p_node, Node *p_child) co
         markNodeReadOnly(p_child);
     }
 }
+
+QVector<QSharedPointer<ExternalNode>> VXNotebookConfigMgr::fetchExternalChildren(Node *p_node) const
+{
+    Q_ASSERT(p_node->isContainer());
+    QVector<QSharedPointer<ExternalNode>> externalNodes;
+
+    auto dir = p_node->toDir();
+
+    // Folders.
+    {
+        const auto folders = dir.entryList(QDir::Dirs | QDir::NoSymLinks | QDir::NoDotAndDotDot);
+        for (const auto &folder : folders) {
+            if (isBuiltInFolder(p_node, folder)) {
+                continue;
+            }
+
+            if (p_node->containsContainerChild(folder)) {
+                continue;
+            }
+
+            externalNodes.push_back(QSharedPointer<ExternalNode>::create(p_node, folder, ExternalNode::Type::Folder));
+        }
+    }
+
+    // Files.
+    {
+        const auto files = dir.entryList(QDir::Files);
+        for (const auto &file : files) {
+            if (isBuiltInFile(p_node, file)) {
+                continue;
+            }
+
+            if (p_node->containsContentChild(file)) {
+                continue;
+            }
+
+            externalNodes.push_back(QSharedPointer<ExternalNode>::create(p_node, file, ExternalNode::Type::File));
+        }
+    }
+
+    return externalNodes;
+}
+
+void VXNotebookConfigMgr::reloadNode(Node *p_node)
+{
+    // TODO.
+}

+ 6 - 2
src/core/notebookconfigmgr/vxnotebookconfigmgr.h

@@ -31,7 +31,7 @@ namespace vnotex
 
         void createEmptySkeleton(const NotebookParameters &p_paras) Q_DECL_OVERRIDE;
 
-        QSharedPointer<Node> loadRootNode() const Q_DECL_OVERRIDE;
+        QSharedPointer<Node> loadRootNode() Q_DECL_OVERRIDE;
 
         void loadNode(Node *p_node) const Q_DECL_OVERRIDE;
         void saveNode(const Node *p_node) Q_DECL_OVERRIDE;
@@ -68,6 +68,10 @@ namespace vnotex
 
         QString fetchNodeAttachmentFolderPath(Node *p_node) Q_DECL_OVERRIDE;
 
+        QVector<QSharedPointer<ExternalNode>> fetchExternalChildren(Node *p_node) const Q_DECL_OVERRIDE;
+
+        void reloadNode(Node *p_node) Q_DECL_OVERRIDE;
+
     private:
         // Config of a file child.
         struct NodeFileConfig
@@ -173,7 +177,7 @@ namespace vnotex
 
         void removeFilesOfNode(Node *p_node, bool p_force);
 
-        bool markRecycleBinNode(const QSharedPointer<Node> &p_root) const;
+        bool markRecycleBinNode(const QSharedPointer<Node> &p_root);
 
         void markNodeReadOnly(Node *p_node) const;
 

+ 3 - 0
src/core/vnotex.h

@@ -91,6 +91,9 @@ namespace vnotex
         // @m_response of @p_event: true to continue the rename, false to cancel the rename.
         void nodeAboutToRename(Node *p_node, const QSharedPointer<Event> &p_event);
 
+        // @m_response of @p_event: true to continue the reload, false to cancel the reload.
+        void nodeAboutToReload(Node *p_node, const QSharedPointer<Event> &p_event);
+
         // Requested to open @p_filePath.
         void openFileRequested(const QString &p_filePath, const QSharedPointer<FileOpenParameters> &p_paras);
 

+ 36 - 12
src/core/widgetconfig.cpp

@@ -24,8 +24,10 @@ void WidgetConfig::init(const QJsonObject &p_app,
 
     m_findAndReplaceOptions = static_cast<FindOptions>(READINT(QStringLiteral("find_and_replace_options")));
 
-    m_noteExplorerViewOrder = READINT(QStringLiteral("note_explorer_view_order"));
-    m_noteExplorerRecycleBinNodeShown = READBOOL(QStringLiteral("note_explorer_recycle_bin_node_shown"));
+    m_nodeExplorerViewOrder = READINT(QStringLiteral("node_explorer_view_order"));
+    m_nodeExplorerRecycleBinNodeVisible = READBOOL(QStringLiteral("node_explorer_recycle_bin_node_visible"));
+    m_nodeExplorerExternalFilesVisible = READBOOL(QStringLiteral("node_explorer_external_files_visible"));
+    m_nodeExplorerAutoImportExternalFilesEnabled = READBOOL(QStringLiteral("node_explorer_auto_import_external_files_enabled"));
 }
 
 QJsonObject WidgetConfig::toJson() const
@@ -33,8 +35,10 @@ QJsonObject WidgetConfig::toJson() const
     QJsonObject obj;
     obj[QStringLiteral("outline_auto_expanded_level")] = m_outlineAutoExpandedLevel;
     obj[QStringLiteral("find_and_replace_options")] = static_cast<int>(m_findAndReplaceOptions);
-    obj[QStringLiteral("note_explorer_view_order")] = m_noteExplorerViewOrder;
-    obj[QStringLiteral("note_explorer_recycle_bin_node_shown")] = m_noteExplorerRecycleBinNodeShown;
+    obj[QStringLiteral("node_explorer_view_order")] = m_nodeExplorerViewOrder;
+    obj[QStringLiteral("node_explorer_recycle_bin_node_visible")] = m_nodeExplorerRecycleBinNodeVisible;
+    obj[QStringLiteral("node_explorer_external_files_visible")] = m_nodeExplorerExternalFilesVisible;
+    obj[QStringLiteral("node_explorer_auto_import_external_files_enabled")] = m_nodeExplorerAutoImportExternalFilesEnabled;
     return obj;
 }
 
@@ -58,22 +62,42 @@ void WidgetConfig::setFindAndReplaceOptions(FindOptions p_options)
     updateConfig(m_findAndReplaceOptions, p_options, this);
 }
 
-int WidgetConfig::getNoteExplorerViewOrder() const
+int WidgetConfig::getNodeExplorerViewOrder() const
 {
-    return m_noteExplorerViewOrder;
+    return m_nodeExplorerViewOrder;
 }
 
-void WidgetConfig::setNoteExplorerViewOrder(int p_viewOrder)
+void WidgetConfig::setNodeExplorerViewOrder(int p_viewOrder)
 {
-    updateConfig(m_noteExplorerViewOrder, p_viewOrder, this);
+    updateConfig(m_nodeExplorerViewOrder, p_viewOrder, this);
 }
 
-bool WidgetConfig::isNoteExplorerRecycleBinNodeShown() const
+bool WidgetConfig::isNodeExplorerRecycleBinNodeVisible() const
 {
-    return m_noteExplorerRecycleBinNodeShown;
+    return m_nodeExplorerRecycleBinNodeVisible;
 }
 
-void WidgetConfig::setNoteExplorerRecycleBinNodeShown(bool p_shown)
+void WidgetConfig::setNodeExplorerRecycleBinNodeVisible(bool p_visible)
 {
-    updateConfig(m_noteExplorerRecycleBinNodeShown, p_shown, this);
+    updateConfig(m_nodeExplorerRecycleBinNodeVisible, p_visible, this);
+}
+
+bool WidgetConfig::isNodeExplorerExternalFilesVisible() const
+{
+    return m_nodeExplorerExternalFilesVisible;
+}
+
+void WidgetConfig::setNodeExplorerExternalFilesVisible(bool p_visible)
+{
+    updateConfig(m_nodeExplorerExternalFilesVisible, p_visible, this);
+}
+
+bool WidgetConfig::getNodeExplorerAutoImportExternalFilesEnabled() const
+{
+    return m_nodeExplorerAutoImportExternalFilesEnabled;
+}
+
+void WidgetConfig::setNodeExplorerAutoImportExternalFilesEnabled(bool p_enabled)
+{
+    updateConfig(m_nodeExplorerAutoImportExternalFilesEnabled, p_enabled, this);
 }

+ 16 - 6
src/core/widgetconfig.h

@@ -24,20 +24,30 @@ namespace vnotex
         FindOptions getFindAndReplaceOptions() const;
         void setFindAndReplaceOptions(FindOptions p_options);
 
-        int getNoteExplorerViewOrder() const;
-        void setNoteExplorerViewOrder(int p_viewOrder);
+        int getNodeExplorerViewOrder() const;
+        void setNodeExplorerViewOrder(int p_viewOrder);
 
-        bool isNoteExplorerRecycleBinNodeShown() const;
-        void setNoteExplorerRecycleBinNodeShown(bool p_shown);
+        bool isNodeExplorerRecycleBinNodeVisible() const;
+        void setNodeExplorerRecycleBinNodeVisible(bool p_visible);
+
+        bool isNodeExplorerExternalFilesVisible() const;
+        void setNodeExplorerExternalFilesVisible(bool p_visible);
+
+        bool getNodeExplorerAutoImportExternalFilesEnabled() const;
+        void setNodeExplorerAutoImportExternalFilesEnabled(bool p_enabled);
 
     private:
         int m_outlineAutoExpandedLevel = 6;
 
         FindOptions m_findAndReplaceOptions = FindOption::None;
 
-        int m_noteExplorerViewOrder = 0;
+        int m_nodeExplorerViewOrder = 0;
+
+        bool m_nodeExplorerRecycleBinNodeVisible = false;
+
+        bool m_nodeExplorerExternalFilesVisible = true;
 
-        bool m_noteExplorerRecycleBinNodeShown = false;
+        bool m_nodeExplorerAutoImportExternalFilesEnabled = true;
     };
 }
 

+ 5 - 3
src/data/core/vnotex.json

@@ -286,8 +286,10 @@
         "outline_auto_expanded_level" : 6,
         "//comment" : "Default find options in FindAndReplace",
         "find_and_replace_options" : 16,
-        "//comment" : "View order of the note explorer",
-        "note_explorer_view_order" : 0,
-        "note_explorer_recycle_bin_node_shown" : false
+        "//comment" : "View order of the node explorer",
+        "node_explorer_view_order" : 0,
+        "node_explorer_recycle_bin_node_visible" : false,
+        "node_explorer_external_files_visible" : true,
+        "node_explorer_auto_import_external_files_enabled" : true
     }
 }

+ 7 - 1
src/data/extra/themes/moonlight/palette.json

@@ -224,7 +224,13 @@
         },
         "notebookexplorer" : {
             "node_icon" : {
-                "fg" : "@base#icon#fg"
+                "fg" : "@base#icon#fg",
+                "invalid" : {
+                    "fg" : "@base#icon#warning#fg"
+                }
+            },
+            "external_node_icon" : {
+                "fg" : "@base#icon#inactive#fg"
             }
         },
         "viewsplit" : {

+ 7 - 1
src/data/extra/themes/native/palette.json

@@ -83,7 +83,13 @@
         },
         "notebookexplorer" : {
             "node_icon" : {
-                "fg" : "@base#icon#fg"
+                "fg" : "@base#icon#fg",
+                "invalid" : {
+                    "fg" : "@base#icon#warning#fg"
+                }
+            },
+            "external_node_icon" : {
+                "fg" : "@base#icon#disabled#fg"
             }
         },
         "viewsplit" : {

+ 7 - 1
src/data/extra/themes/pure/palette.json

@@ -220,7 +220,13 @@
         },
         "notebookexplorer" : {
             "node_icon" : {
-                "fg" : "@base#icon#fg"
+                "fg" : "@base#icon#fg",
+                "invalid" : {
+                    "fg" : "@base#icon#warning#fg"
+                }
+            },
+            "external_node_icon" : {
+                "fg" : "@base#icon#inactive#fg"
             }
         },
         "viewsplit" : {

+ 2 - 2
src/export/exporter.cpp

@@ -119,7 +119,7 @@ QStringList Exporter::doExport(const ExportOption &p_option, const QString &p_ou
     }
 
     p_folder->load();
-    const auto &children = p_folder->getChildren();
+    const auto &children = p_folder->getChildrenRef();
     emit progressUpdated(0, children.size());
     for (int i = 0; i < children.size(); ++i) {
         if (checkAskedToStop()) {
@@ -192,7 +192,7 @@ QStringList Exporter::doExport(const ExportOption &p_option, Notebook *p_noteboo
     auto rootNode = p_notebook->getRootNode();
     Q_ASSERT(rootNode->isLoaded());
 
-    const auto &children = rootNode->getChildren();
+    const auto &children = rootNode->getChildrenRef();
     emit progressUpdated(0, children.size());
     for (int i = 0; i < children.size(); ++i) {
         if (checkAskedToStop()) {

+ 2 - 1
src/export/webviewexporter.cpp

@@ -61,6 +61,7 @@ bool WebViewExporter::doExport(const ExportOption &p_option,
     m_webViewStates = WebViewState::Started;
 
     auto baseUrl = PathUtils::pathToUrl(p_file->getContentPath());
+    m_viewer->adapter()->reset();
     m_viewer->setHtml(m_htmlTemplate, baseUrl);
 
     auto textContent = p_file->read();
@@ -93,7 +94,7 @@ bool WebViewExporter::doExport(const ExportOption &p_option,
 
     switch (p_option.m_targetFormat) {
     case ExportFormat::HTML:
-        // TODO: not supported yet.
+        // TODO: MIME HTML format is not supported yet.
         Q_ASSERT(!p_option.m_htmlOption.m_useMimeHtmlFormat);
         ret = doExportHtml(p_option.m_htmlOption, p_outputFile, baseUrl);
         break;

+ 2 - 2
src/utils/pathutils.cpp

@@ -48,7 +48,7 @@ bool PathUtils::isEmptyDir(const QString &p_path)
 
 QString PathUtils::concatenateFilePath(const QString &p_dirPath, const QString &p_name)
 {
-    auto dirPath = cleanPath(p_dirPath);
+    QString dirPath = cleanPath(p_dirPath);
     if (p_name.isEmpty()) {
         return dirPath;
     }
@@ -57,7 +57,7 @@ QString PathUtils::concatenateFilePath(const QString &p_dirPath, const QString &
         return p_name;
     }
 
-    return dirPath + '/' + p_name;
+    return dirPath + "/" + p_name;
 }
 
 QString PathUtils::dirName(const QString &p_path)

+ 11 - 0
src/widgets/editors/markdownvieweradapter.cpp

@@ -356,3 +356,14 @@ void MarkdownViewerAdapter::setSavedContent(const QString &p_headContent,
 {
     emit contentReady(p_headContent, p_styleContent, p_content, p_bodyClassList);
 }
+
+void MarkdownViewerAdapter::reset()
+{
+    m_revision = 0;
+    m_viewerReady = false;
+    m_pendingData.reset();
+    m_topLineNumber = -1;
+    m_headings.clear();
+    m_currentHeadingIndex = -1;
+    m_crossCopyTargets.clear();
+}

+ 3 - 0
src/widgets/editors/markdownvieweradapter.h

@@ -123,6 +123,9 @@ namespace vnotex
 
         void saveContent();
 
+        // Should be called before WebViewer.setHtml().
+        void reset();
+
         // Functions to be called from web side.
     public slots:
         void setReady(bool p_ready);

+ 2 - 0
src/widgets/markdownviewwindow.cpp

@@ -497,10 +497,12 @@ void MarkdownViewWindow::syncViewerFromBuffer(bool p_syncPositionFromEditMode)
         // TODO: Check buffer for last position recover.
 
         // Use getPath() instead of getBasePath() to make in-page anchor work.
+        adapter()->reset();
         m_viewer->setHtml(HtmlTemplateHelper::getMarkdownViewerTemplate(),
                           PathUtils::pathToUrl(buffer->getContentPath()));
         adapter()->setText(m_bufferRevision, buffer->getContent(), lineNumber);
     } else {
+        adapter()->reset();
         m_viewer->setHtml("");
         adapter()->setText(0, "", -1);
     }

+ 41 - 22
src/widgets/notebookexplorer.cpp

@@ -63,10 +63,15 @@ void NotebookExplorer::setupUI()
             });
     mainLayout->addWidget(m_selector);
 
+    const auto &widgetConfig = ConfigMgr::getInst().getWidgetConfig();
     m_nodeExplorer = new NotebookNodeExplorer(this);
-    m_nodeExplorer->setRecycleBinNodeVisible(ConfigMgr::getInst().getWidgetConfig().isNoteExplorerRecycleBinNodeShown());
+    m_nodeExplorer->setRecycleBinNodeVisible(widgetConfig.isNodeExplorerRecycleBinNodeVisible());
+    m_nodeExplorer->setViewOrder(widgetConfig.getNodeExplorerViewOrder());
+    m_nodeExplorer->setExternalFilesVisible(widgetConfig.isNodeExplorerExternalFilesVisible());
     connect(m_nodeExplorer, &NotebookNodeExplorer::nodeActivated,
             &VNoteX::getInst(), &VNoteX::openNodeRequested);
+    connect(m_nodeExplorer, &NotebookNodeExplorer::fileActivated,
+            &VNoteX::getInst(), &VNoteX::openFileRequested);
     connect(m_nodeExplorer, &NotebookNodeExplorer::nodeAboutToMove,
             &VNoteX::getInst(), &VNoteX::nodeAboutToMove);
     connect(m_nodeExplorer, &NotebookNodeExplorer::nodeAboutToRemove,
@@ -78,6 +83,8 @@ void NotebookExplorer::setupUI()
 
 TitleBar *NotebookExplorer::setupTitleBar(QWidget *p_parent)
 {
+    const auto &widgetConfig = ConfigMgr::getInst().getWidgetConfig();
+
     auto titleBar = new TitleBar(tr("Notebook"),
                                  TitleBar::Action::Menu,
                                  p_parent);
@@ -95,11 +102,11 @@ TitleBar *NotebookExplorer::setupTitleBar(QWidget *p_parent)
     {
         auto btn = titleBar->addActionButton(QStringLiteral("recycle_bin.svg"), tr("Toggle Recycle Bin Node"));
         btn->defaultAction()->setCheckable(true);
-        btn->defaultAction()->setChecked(ConfigMgr::getInst().getWidgetConfig().isNoteExplorerRecycleBinNodeShown());
+        btn->defaultAction()->setChecked(widgetConfig.isNodeExplorerRecycleBinNodeVisible());
         connect(btn, &QToolButton::triggered,
                 this, [this](QAction *p_act) {
                     const bool checked = p_act->isChecked();
-                    ConfigMgr::getInst().getWidgetConfig().setNoteExplorerRecycleBinNodeShown(checked);
+                    ConfigMgr::getInst().getWidgetConfig().setNodeExplorerRecycleBinNodeVisible(checked);
                     m_nodeExplorer->setRecycleBinNodeVisible(checked);
                 });
     }
@@ -113,6 +120,33 @@ TitleBar *NotebookExplorer::setupTitleBar(QWidget *p_parent)
                                 dialog.exec();
                             });
 
+    titleBar->addMenuSeparator();
+
+    // External Files menu.
+    {
+        auto subMenu = titleBar->addMenuSubMenu(tr("External Files"));
+        auto showAct = titleBar->addMenuAction(
+            subMenu,
+            tr("Show External Files"),
+            titleBar,
+            [this](bool p_checked) {
+                ConfigMgr::getInst().getWidgetConfig().setNodeExplorerExternalFilesVisible(p_checked);
+                m_nodeExplorer->setExternalFilesVisible(p_checked);
+            });
+        showAct->setCheckable(true);
+        showAct->setChecked(widgetConfig.isNodeExplorerExternalFilesVisible());
+
+        auto importAct = titleBar->addMenuAction(
+            subMenu,
+            tr("Import External Files When Activated"),
+            titleBar,
+            [this](bool p_checked) {
+                ConfigMgr::getInst().getWidgetConfig().setNodeExplorerAutoImportExternalFilesEnabled(p_checked);
+            });
+        importAct->setCheckable(true);
+        importAct->setChecked(widgetConfig.getNodeExplorerAutoImportExternalFilesEnabled());
+    }
+
     return titleBar;
 }
 
@@ -206,21 +240,7 @@ void NotebookExplorer::newNote()
 
 Node *NotebookExplorer::currentExploredFolderNode() const
 {
-    if (!m_currentNotebook) {
-        return nullptr;
-    }
-
-    auto node = m_nodeExplorer->getCurrentNode();
-    if (node) {
-        if (!node->isContainer()) {
-            node = node->getParent();
-        }
-        Q_ASSERT(node && node->isContainer());
-    } else {
-        node = m_currentNotebook->getRootNode().data();
-    }
-
-    return node;
+    return m_nodeExplorer->currentExploredFolderNode();
 }
 
 Node *NotebookExplorer::checkNotebookAndGetCurrentExploredFolderNode() const
@@ -372,7 +392,7 @@ void NotebookExplorer::setupViewMenu(QMenu *p_menu)
     act->setData(NotebookNodeExplorer::ViewOrder::OrderedByModifiedTimeReversed);
     p_menu->addAction(act);
 
-    int viewOrder = ConfigMgr::getInst().getWidgetConfig().getNoteExplorerViewOrder();
+    int viewOrder = ConfigMgr::getInst().getWidgetConfig().getNodeExplorerViewOrder();
     for (const auto &act : ag->actions()) {
         if (act->data().toInt() == viewOrder) {
             act->setChecked(true);
@@ -382,8 +402,7 @@ void NotebookExplorer::setupViewMenu(QMenu *p_menu)
     connect(ag, &QActionGroup::triggered,
             this, [this](QAction *p_action) {
                 int order = p_action->data().toInt();
-                ConfigMgr::getInst().getWidgetConfig().setNoteExplorerViewOrder(order);
-
-                m_nodeExplorer->reload();
+                ConfigMgr::getInst().getWidgetConfig().setNodeExplorerViewOrder(order);
+                m_nodeExplorer->setViewOrder(order);
             });
 }

+ 361 - 63
src/widgets/notebooknodeexplorer.cpp

@@ -6,9 +6,11 @@
 #include <QTreeWidget>
 #include <QMenu>
 #include <QAction>
+#include <QSet>
 
 #include <notebook/notebook.h>
 #include <notebook/node.h>
+#include <notebook/externalnode.h>
 #include "exception.h"
 #include "messageboxhelper.h"
 #include "vnotex.h"
@@ -29,18 +31,23 @@
 #include <core/fileopenparameters.h>
 #include <core/events.h>
 #include <core/configmgr.h>
-#include <core/widgetconfig.h>
 
 using namespace vnotex;
 
-const QString NotebookNodeExplorer::c_nodeIconForegroundName = "widgets#notebookexplorer#node_icon#fg";
-
 QIcon NotebookNodeExplorer::s_folderNodeIcon;
 
 QIcon NotebookNodeExplorer::s_fileNodeIcon;
 
+QIcon NotebookNodeExplorer::s_invalidFolderNodeIcon;
+
+QIcon NotebookNodeExplorer::s_invalidFileNodeIcon;
+
 QIcon NotebookNodeExplorer::s_recycleBinNodeIcon;
 
+QIcon NotebookNodeExplorer::s_externalFolderNodeIcon;
+
+QIcon NotebookNodeExplorer::s_externalFileNodeIcon;
+
 NotebookNodeExplorer::NodeData::NodeData()
 {
 }
@@ -52,9 +59,9 @@ NotebookNodeExplorer::NodeData::NodeData(Node *p_node, bool p_loaded)
 {
 }
 
-NotebookNodeExplorer::NodeData::NodeData(const QString &p_name)
-    : m_type(NodeType::Attachment),
-      m_name(p_name),
+NotebookNodeExplorer::NodeData::NodeData(const QSharedPointer<ExternalNode> &p_externalNode)
+    : m_type(NodeType::ExternalNode),
+      m_externalNode(p_externalNode),
       m_loaded(true)
 {
 }
@@ -67,13 +74,12 @@ NotebookNodeExplorer::NodeData::NodeData(const NodeData &p_other)
         m_node = p_other.m_node;
         break;
 
-    case NodeType::Attachment:
-        m_name = p_other.m_name;
+    case NodeType::ExternalNode:
+        m_externalNode = p_other.m_externalNode;
         break;
 
     default:
-        m_node = p_other.m_node;
-        m_name = p_other.m_name;
+        Q_ASSERT(false);
         break;
     }
 
@@ -96,13 +102,12 @@ NotebookNodeExplorer::NodeData &NotebookNodeExplorer::NodeData::operator=(const
         m_node = p_other.m_node;
         break;
 
-    case NodeType::Attachment:
-        m_name = p_other.m_name;
+    case NodeType::ExternalNode:
+        m_externalNode = p_other.m_externalNode;
         break;
 
     default:
-        m_node = p_other.m_node;
-        m_name = p_other.m_name;
+        Q_ASSERT(false);
         break;
     }
 
@@ -121,9 +126,9 @@ bool NotebookNodeExplorer::NodeData::isNode() const
     return m_type == NodeType::Node;
 }
 
-bool NotebookNodeExplorer::NodeData::isAttachment() const
+bool NotebookNodeExplorer::NodeData::isExternalNode() const
 {
-    return m_type == NodeType::Attachment;
+    return m_type == NodeType::ExternalNode;
 }
 
 NotebookNodeExplorer::NodeData::NodeType NotebookNodeExplorer::NodeData::getType() const
@@ -137,17 +142,17 @@ Node *NotebookNodeExplorer::NodeData::getNode() const
     return m_node;
 }
 
-const QString &NotebookNodeExplorer::NodeData::getName() const
+ExternalNode *NotebookNodeExplorer::NodeData::getExternalNode() const
 {
-    Q_ASSERT(isAttachment());
-    return m_name;
+    Q_ASSERT(isExternalNode());
+    return m_externalNode.data();
 }
 
 void NotebookNodeExplorer::NodeData::clear()
 {
     m_type = NodeType::Invalid;
     m_node = nullptr;
-    m_name.clear();
+    m_externalNode.clear();
     m_loaded = false;
 }
 
@@ -180,15 +185,26 @@ void NotebookNodeExplorer::initNodeIcons() const
         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(c_nodeIconForegroundName);
+    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");
     const QString recycleBinIconName("recycle_bin.svg");
 
     s_folderNodeIcon = IconUtils::fetchIcon(themeMgr.getIconFile(folderIconName), fg);
     s_fileNodeIcon = IconUtils::fetchIcon(themeMgr.getIconFile(fileIconName), fg);
+    s_invalidFolderNodeIcon = IconUtils::fetchIcon(themeMgr.getIconFile(folderIconName), invalidFg);
+    s_invalidFileNodeIcon = IconUtils::fetchIcon(themeMgr.getIconFile(fileIconName), invalidFg);
     s_recycleBinNodeIcon = IconUtils::fetchIcon(themeMgr.getIconFile(recycleBinIconName), fg);
+    s_externalFolderNodeIcon = IconUtils::fetchIcon(themeMgr.getIconFile(folderIconName), externalFg);
+    s_externalFileNodeIcon = IconUtils::fetchIcon(themeMgr.getIconFile(fileIconName), externalFg);
 }
 
 void NotebookNodeExplorer::setupUI()
@@ -244,8 +260,8 @@ void NotebookNodeExplorer::setupMasterExplorer(QWidget *p_parent)
 
                     if (data.isNode()) {
                         createContextMenuOnNode(menu.data(), data.getNode());
-                    } else if (data.isAttachment()) {
-                        createContextMenuOnAttachment(menu.data(), data.getName());
+                    } else if (data.isExternalNode()) {
+                        createContextMenuOnExternalNode(menu.data(), data.getExternalNode());
                     }
                 }
 
@@ -263,9 +279,23 @@ void NotebookNodeExplorer::setupMasterExplorer(QWidget *p_parent)
                 }
 
                 if (data.isNode()) {
+                    if (checkInvalidNode(data.getNode())) {
+                        return;
+                    }
                     emit nodeActivated(data.getNode(), QSharedPointer<FileOpenParameters>::create());
-                } else if (data.isAttachment()) {
-                    // TODO.
+                } else if (data.isExternalNode()) {
+                    // Import to config first.
+                    if (m_autoImportExternalFiles) {
+                        auto importedNode = importToIndex(data.getExternalNode());
+                        if (importedNode) {
+                            emit nodeActivated(importedNode.data(), QSharedPointer<FileOpenParameters>::create());
+                        }
+                        return;
+                    }
+
+                    // Just open it.
+                    emit fileActivated(data.getExternalNode()->fetchAbsolutePath(),
+                                       QSharedPointer<FileOpenParameters>::create());
                 }
             });
 }
@@ -333,7 +363,7 @@ void NotebookNodeExplorer::generateNodeTree()
 
 void NotebookNodeExplorer::loadRootNode(const Node *p_node) const
 {
-    Q_ASSERT(p_node->isLoaded());
+    Q_ASSERT(p_node->isLoaded() && p_node->isContainer());
 
     // Render recycle bin node first.
     auto recycleBinNode = m_notebook->getRecycleBinNode();
@@ -341,9 +371,20 @@ void NotebookNodeExplorer::loadRootNode(const Node *p_node) const
         loadRecycleBinNode(recycleBinNode.data());
     }
 
+    // External children.
+    if (m_externalFilesVisible) {
+        auto externalChildren = p_node->fetchExternalChildren();
+        // TODO: Sort external children.
+        for (const auto &child : externalChildren) {
+            auto item = new QTreeWidgetItem(m_masterExplorer);
+            loadNode(item, child);
+        }
+    }
+
+    // Children.
     auto children = p_node->getChildren();
     sortNodes(children);
-    for (auto &child : children) {
+    for (const auto &child : children) {
         if (recycleBinNode == child) {
             continue;
         }
@@ -378,15 +419,34 @@ void NotebookNodeExplorer::loadNode(QTreeWidgetItem *p_item, Node *p_node, int p
     }
 }
 
+void NotebookNodeExplorer::loadNode(QTreeWidgetItem *p_item, const QSharedPointer<ExternalNode> &p_node) const
+{
+    clearTreeWigetItemChildren(p_item);
+
+    fillTreeItem(p_item, p_node);
+
+    // No children for external node.
+}
+
 void NotebookNodeExplorer::loadChildren(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) {
+            auto item = new QTreeWidgetItem(p_item);
+            loadNode(item, child);
+        }
+    }
+
     auto children = p_node->getChildren();
     sortNodes(children);
-    for (auto &child : children) {
+    for (const auto &child : children) {
         auto item = new QTreeWidgetItem(p_item);
         loadNode(item, child.data(), p_level);
     }
@@ -434,22 +494,33 @@ void NotebookNodeExplorer::fillTreeItem(QTreeWidgetItem *p_item, Node *p_node, b
     setItemNodeData(p_item, NodeData(p_node, p_loaded));
     p_item->setText(Column::Name, p_node->getName());
     p_item->setIcon(Column::Name, getNodeItemIcon(p_node));
-    p_item->setToolTip(Column::Name, p_node->getName());
+    p_item->setToolTip(Column::Name, p_node->exists() ? p_node->getName() : (tr("[Invalid] %1").arg(p_node->getName())));
+}
+
+void NotebookNodeExplorer::fillTreeItem(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, getNodeItemIcon(p_node.data()));
+    p_item->setToolTip(Column::Name, tr("[External] %1").arg(p_node->getName()));
 }
 
-QIcon NotebookNodeExplorer::getNodeItemIcon(const Node *p_node) const
+const QIcon &NotebookNodeExplorer::getNodeItemIcon(const Node *p_node) const
 {
     if (p_node->hasContent()) {
-        return s_fileNodeIcon;
+        return p_node->exists() ? s_fileNodeIcon : s_invalidFileNodeIcon;
     } else {
         if (p_node->getUse() == Node::Use::RecycleBin) {
             return s_recycleBinNodeIcon;
         }
 
-        return s_folderNodeIcon;
+        return p_node->exists() ? s_folderNodeIcon : s_invalidFolderNodeIcon;
     }
+}
 
-    return QIcon();
+const QIcon &NotebookNodeExplorer::getNodeItemIcon(const ExternalNode *p_node) const
+{
+    return p_node->isFolder() ? s_externalFolderNodeIcon : s_externalFileNodeIcon;
 }
 
 Node *NotebookNodeExplorer::getCurrentNode() const
@@ -457,7 +528,7 @@ Node *NotebookNodeExplorer::getCurrentNode() const
     auto item = m_masterExplorer->currentItem();
     if (item) {
         auto data = getItemNodeData(item);
-        while (data.isAttachment()) {
+        while (item && !data.isNode()) {
             item = item->parent();
             if (item) {
                 data = getItemNodeData(item);
@@ -687,6 +758,9 @@ void NotebookNodeExplorer::createContextMenuOnRoot(QMenu *p_menu)
 
     p_menu->addSeparator();
 
+    act = createAction(Action::Reload, p_menu);
+    p_menu->addAction(act);
+
     act = createAction(Action::OpenLocation, p_menu);
     p_menu->addAction(act);
 }
@@ -698,6 +772,9 @@ void NotebookNodeExplorer::createContextMenuOnNode(QMenu *p_menu, const Node *p_
 
     if (m_notebook->isRecycleBinNode(p_node)) {
         // Recycle bin node.
+        act = createAction(Action::Reload, p_menu);
+        p_menu->addAction(act);
+
         if (selectedSize == 1) {
             act = createAction(Action::EmptyRecycleBin, p_menu);
             p_menu->addAction(act);
@@ -707,12 +784,22 @@ void NotebookNodeExplorer::createContextMenuOnNode(QMenu *p_menu, const Node *p_
         }
     } else if (m_notebook->isNodeInRecycleBin(p_node)) {
         // Node in recycle bin.
+        act = createAction(Action::Open, p_menu);
+        p_menu->addAction(act);
+
+        p_menu->addSeparator();
+
         act = createAction(Action::Cut, p_menu);
         p_menu->addAction(act);
 
         act = createAction(Action::DeleteFromRecycleBin, p_menu);
         p_menu->addAction(act);
 
+        p_menu->addSeparator();
+
+        act = createAction(Action::Reload, p_menu);
+        p_menu->addAction(act);
+
         if (selectedSize == 1) {
             p_menu->addSeparator();
 
@@ -723,6 +810,11 @@ void NotebookNodeExplorer::createContextMenuOnNode(QMenu *p_menu, const Node *p_
             p_menu->addAction(act);
         }
     } else {
+        act = createAction(Action::Open, p_menu);
+        p_menu->addAction(act);
+
+        p_menu->addSeparator();
+
         act = createAction(Action::NewNote, p_menu);
         p_menu->addAction(act);
 
@@ -750,6 +842,9 @@ void NotebookNodeExplorer::createContextMenuOnNode(QMenu *p_menu, const Node *p_
 
         p_menu->addSeparator();
 
+        act = createAction(Action::Reload, p_menu);
+        p_menu->addAction(act);
+
         act = createAction(Action::Sort, p_menu);
         p_menu->addAction(act);
 
@@ -768,10 +863,28 @@ void NotebookNodeExplorer::createContextMenuOnNode(QMenu *p_menu, const Node *p_
     }
 }
 
-void NotebookNodeExplorer::createContextMenuOnAttachment(QMenu *p_menu, const QString &p_name)
+void NotebookNodeExplorer::createContextMenuOnExternalNode(QMenu *p_menu, const ExternalNode *p_node)
 {
-    Q_UNUSED(p_menu);
-    Q_UNUSED(p_name);
+    Q_UNUSED(p_node);
+
+    const int selectedSize = m_masterExplorer->selectedItems().size();
+    QAction *act = nullptr;
+
+    act = createAction(Action::Open, p_menu);
+    p_menu->addAction(act);
+
+    act = createAction(Action::ImportToConfig, p_menu);
+    p_menu->addAction(act);
+
+    if (selectedSize == 1) {
+        p_menu->addSeparator();
+
+        act = createAction(Action::CopyPath, p_menu);
+        p_menu->addAction(act);
+
+        act = createAction(Action::OpenLocation, p_menu);
+        p_menu->addAction(act);
+    }
 }
 
 static QIcon generateMenuActionIcon(const QString &p_name)
@@ -811,7 +924,7 @@ QAction *NotebookNodeExplorer::createAction(Action p_act, QObject *p_parent)
         connect(act, &QAction::triggered,
                 this, [this]() {
                     auto node = getCurrentNode();
-                    if (!node) {
+                    if (checkInvalidNode(node)) {
                         return;
                     }
 
@@ -834,15 +947,32 @@ QAction *NotebookNodeExplorer::createAction(Action p_act, QObject *p_parent)
         act = new QAction(tr("Open &Location"), p_parent);
         connect(act, &QAction::triggered,
                 this, [this]() {
+                    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);
                     QString locationPath;
-                    auto node = getCurrentNode();
-                    if (node) {
+                    if (data.isNode()) {
+                        auto node = data.getNode();
+                        if (checkInvalidNode(node)) {
+                            return;
+                        }
+
                         locationPath = node->fetchAbsolutePath();
                         if (!node->isContainer()) {
                             locationPath = PathUtils::parentDirPath(locationPath);
                         }
-                    } else if (m_notebook) {
-                        locationPath = m_notebook->getRootFolderAbsolutePath();
+                    } else if (data.isExternalNode()) {
+                        auto externalNode = data.getExternalNode();
+                        locationPath = externalNode->fetchAbsolutePath();
+                        if (!externalNode->isFolder()) {
+                            locationPath = PathUtils::parentDirPath(locationPath);
+                        }
                     }
 
                     if (!locationPath.isEmpty()) {
@@ -855,9 +985,22 @@ QAction *NotebookNodeExplorer::createAction(Action p_act, QObject *p_parent)
         act = new QAction(tr("Cop&y Path"), p_parent);
         connect(act, &QAction::triggered,
                 this, [this]() {
-                    auto node = getCurrentNode();
-                    if (node) {
-                        auto nodePath = node->fetchAbsolutePath();
+                    auto item = m_masterExplorer->currentItem();
+                    if (!item) {
+                        return;
+                    }
+                    auto data = getItemNodeData(item);
+                    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));
                     }
@@ -906,9 +1049,8 @@ QAction *NotebookNodeExplorer::createAction(Action p_act, QObject *p_parent)
                         m_notebook->emptyNode(rbNode, true);
                     } catch (Exception &p_e) {
                         MessageBoxHelper::notify(MessageBoxHelper::Critical,
-                                                 tr("Failed to empty recycle bin (%1) (%2).")
-                                                   .arg(rbNodePath, p_e.what()),
-                                                  VNoteX::getInst().getMainWindow());
+                                                 tr("Failed to empty recycle bin (%1) (%2).").arg(rbNodePath, p_e.what()),
+                                                 VNoteX::getInst().getMainWindow());
                     }
 
                     updateNode(rbNode);
@@ -935,18 +1077,42 @@ QAction *NotebookNodeExplorer::createAction(Action p_act, QObject *p_parent)
     case Action::RemoveFromConfig:
         act = new QAction(tr("&Remove From Index"), p_parent);
         connect(act, &QAction::triggered,
-                this, [this]() {
-                    removeSelectedNodesFromConfig();
-                });
+                this, &NotebookNodeExplorer::removeSelectedNodesFromConfig);
         break;
 
     case Action::Sort:
         act = new QAction(generateMenuActionIcon("sort.svg"), tr("&Sort"), p_parent);
+        connect(act, &QAction::triggered,
+                this, &NotebookNodeExplorer::manualSort);
+        break;
+
+    case Action::Reload:
+        act = new QAction(tr("Re&load"), p_parent);
         connect(act, &QAction::triggered,
                 this, [this]() {
-                    manualSort();
+                    auto node = currentExploredFolderNode();
+                    if (m_notebook && node) {
+                        // TODO: emit signals to notify other components.
+                        m_notebook->reloadNode(node);
+                    }
+                    updateNode(node);
+                });
+        break;
+
+    case Action::ImportToConfig:
+        act = new QAction(tr("&Import To Index"), p_parent);
+        connect(act, &QAction::triggered,
+                this, [this]() {
+                    auto nodes = getSelectedNodes().second;
+                    importToIndex(nodes);
                 });
         break;
+
+    case Action::Open:
+        act = new QAction(tr("&Open"), p_parent);
+        connect(act, &QAction::triggered,
+                this, &NotebookNodeExplorer::openSelectedNodes);
+        break;
     }
 
     return act;
@@ -954,7 +1120,7 @@ QAction *NotebookNodeExplorer::createAction(Action p_act, QObject *p_parent)
 
 void NotebookNodeExplorer::copySelectedNodes(bool p_move)
 {
-    auto nodes = getSelectedNodes();
+    auto nodes = getSelectedNodes().first;
     if (nodes.isEmpty()) {
         return;
     }
@@ -964,6 +1130,10 @@ void NotebookNodeExplorer::copySelectedNodes(bool p_move)
     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);
@@ -976,15 +1146,17 @@ void NotebookNodeExplorer::copySelectedNodes(bool p_move)
     VNoteX::getInst().showStatusMessageShort(tr("Copied %n item(s)", "", static_cast<int>(nrItems)));
 }
 
-QVector<Node *> NotebookNodeExplorer::getSelectedNodes() const
+QPair<QVector<Node *>, QVector<ExternalNode *>> NotebookNodeExplorer::getSelectedNodes() const
 {
-    QVector<Node *> nodes;
+    QPair<QVector<Node *>, QVector<ExternalNode *>> nodes;
 
     auto items = m_masterExplorer->selectedItems();
     for (auto &item : items) {
         auto data = getItemNodeData(item);
         if (data.isNode()) {
-            nodes.push_back(data.getNode());
+            nodes.first.push_back(data.getNode());
+        } else if (data.isExternalNode()) {
+            nodes.second.push_back(data.getExternalNode());
         }
     }
 
@@ -1051,6 +1223,8 @@ void NotebookNodeExplorer::pasteNodesFromClipboard()
         // Current node may be a file node.
         if (!destNode->isContainer()) {
             destNode = destNode->getParent();
+        } else if (checkInvalidNode(destNode)) {
+            return;
         }
     }
 
@@ -1088,6 +1262,8 @@ void NotebookNodeExplorer::pasteNodesFromClipboard()
     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();
@@ -1176,7 +1352,7 @@ QVector<Node *> NotebookNodeExplorer::confirmSelectedNodes(const QString &p_titl
                                                            const QString &p_text,
                                                            const QString &p_info) const
 {
-    auto nodes = getSelectedNodes();
+    auto nodes = getSelectedNodes().first;
     if (nodes.isEmpty()) {
         return nodes;
     }
@@ -1270,7 +1446,7 @@ void NotebookNodeExplorer::removeSelectedNodesFromConfig()
 
 void NotebookNodeExplorer::filterAwayChildrenNodes(QVector<Node *> &p_nodes)
 {
-    for (int i = p_nodes.size() - 1; i > 0; --i) {
+    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) {
@@ -1351,8 +1527,7 @@ void NotebookNodeExplorer::focusNormalNode()
 
 void NotebookNodeExplorer::sortNodes(QVector<QSharedPointer<Node>> &p_nodes) const
 {
-    int viewOrder = ConfigMgr::getInst().getWidgetConfig().getNoteExplorerViewOrder();
-    if (viewOrder == ViewOrder::OrderedByConfiguration) {
+    if (m_viewOrder == ViewOrder::OrderedByConfiguration) {
         return;
     }
 
@@ -1369,10 +1544,10 @@ void NotebookNodeExplorer::sortNodes(QVector<QSharedPointer<Node>> &p_nodes) con
     }
 
     // Sort containers.
-    sortNodes(p_nodes, 0, firstFileIndex, viewOrder);
+    sortNodes(p_nodes, 0, firstFileIndex, m_viewOrder);
 
     // Sort non-containers.
-    sortNodes(p_nodes, firstFileIndex, p_nodes.size(), viewOrder);
+    sortNodes(p_nodes, firstFileIndex, p_nodes.size(), m_viewOrder);
 }
 
 void NotebookNodeExplorer::sortNodes(QVector<QSharedPointer<Node>> &p_nodes, int p_start, int p_end, int p_viewOrder) const
@@ -1437,6 +1612,25 @@ void NotebookNodeExplorer::setRecycleBinNodeVisible(bool p_visible)
     reload();
 }
 
+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()
 {
     auto node = getCurrentNode();
@@ -1465,7 +1659,7 @@ void NotebookNodeExplorer::manualSort()
         treeWidget->setColumnCount(2);
         treeWidget->setHeaderLabels({tr("Name"), tr("Created Time"), tr("Modified Time")});
 
-        const auto &children = parentNode->getChildren();
+        const auto &children = parentNode->getChildrenRef();
         for (int i = 0; i < children.size(); ++i) {
             const auto &child = children[i];
             if (m_notebook->isRecycleBinNode(child.data())) {
@@ -1498,3 +1692,107 @@ void NotebookNodeExplorer::manualSort()
         updateNode(parentNode);
     }
 }
+
+Node *NotebookNodeExplorer::currentExploredFolderNode() const
+{
+    if (!m_notebook) {
+        return nullptr;
+    }
+
+    auto node = getCurrentNode();
+    if (node) {
+        if (!node->isContainer()) {
+            node = node->getParent();
+        }
+        Q_ASSERT(node && node->isContainer());
+    } else {
+        node = m_notebook->getRootNode().data();
+    }
+
+    return node;
+}
+
+void NotebookNodeExplorer::setViewOrder(int p_order)
+{
+    if (m_viewOrder == p_order) {
+        return;
+    }
+
+    m_viewOrder = p_order;
+    reload();
+}
+
+void NotebookNodeExplorer::openSelectedNodes()
+{
+    // Support nodes and external nodes.
+    // Do nothing for folders.
+    auto selectedNodes = getSelectedNodes();
+    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());
+        }
+    }
+}
+
+QSharedPointer<Node> NotebookNodeExplorer::importToIndex(const 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<ExternalNode *> &p_nodes)
+{
+    QSet<Node *> nodesToUpdate;
+    Node *currentNode = nullptr;
+
+    for (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(const Node *p_node) const
+{
+    if (!p_node) {
+        return true;
+    }
+
+    if (!p_node->exists()) {
+        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;
+}

+ 56 - 16
src/widgets/notebooknodeexplorer.h

@@ -5,6 +5,7 @@
 #include <QSharedPointer>
 #include <QHash>
 #include <QScopedPointer>
+#include <QPair>
 
 #include "qtreewidgetstatecache.h"
 #include "clipboarddata.h"
@@ -22,6 +23,7 @@ namespace vnotex
     class TreeWidget;
     struct FileOpenParameters;
     class Event;
+    class ExternalNode;
 
     class NotebookNodeExplorer : public QWidget
     {
@@ -32,13 +34,13 @@ namespace vnotex
         class NodeData
         {
         public:
-            enum class NodeType { Node, Attachment, Invalid };
+            enum class NodeType { Node, ExternalNode, Invalid };
 
             NodeData();
 
             explicit NodeData(Node *p_node, bool p_loaded);
 
-            explicit NodeData(const QString &p_name);
+            explicit NodeData(const QSharedPointer<ExternalNode> &p_externalNode);
 
             NodeData(const NodeData &p_other);
 
@@ -50,13 +52,13 @@ namespace vnotex
 
             bool isNode() const;
 
-            bool isAttachment() const;
+            bool isExternalNode() const;
 
             NodeData::NodeType getType() const;
 
             Node *getNode() const;
 
-            const QString &getName() const;
+            ExternalNode *getExternalNode() const;
 
             void clear();
 
@@ -67,11 +69,9 @@ namespace vnotex
         private:
             NodeType m_type = NodeType::Invalid;
 
-            union
-            {
-                Node *m_node = nullptr;
-                QString m_name;
-            };
+            Node *m_node = nullptr;
+
+            QSharedPointer<ExternalNode> m_externalNode;
 
             bool m_loaded = false;
         };
@@ -104,10 +104,18 @@ namespace vnotex
 
         void setRecycleBinNodeVisible(bool p_visible);
 
+        void setViewOrder(int p_order);
+
+        void setExternalFilesVisible(bool p_visible);
+
+        void setAutoImportExternalFiles(bool p_enabled);
+
+        Node *currentExploredFolderNode() const;
+
     signals:
         void nodeActivated(Node *p_node, const QSharedPointer<FileOpenParameters> &p_paras);
 
-        void fileActivated(const QString &p_path);
+        void fileActivated(const QString &p_path, const QSharedPointer<FileOpenParameters> &p_paras);
 
         // @m_response of @p_event: true to continue the move, false to cancel the move.
         void nodeAboutToMove(Node *p_node, const QSharedPointer<Event> &p_event);
@@ -132,7 +140,10 @@ namespace vnotex
             Delete,
             DeleteFromRecycleBin,
             RemoveFromConfig,
-            Sort
+            Sort,
+            Reload,
+            ImportToConfig,
+            Open
         };
 
         void setupUI();
@@ -149,13 +160,19 @@ namespace vnotex
 
         void loadChildren(QTreeWidgetItem *p_item, Node *p_node, int p_level) const;
 
+        void loadNode(QTreeWidgetItem *p_item, const QSharedPointer<ExternalNode> &p_node) const;
+
         void loadRecycleBinNode(Node *p_node) const;
 
         void loadRecycleBinNode(QTreeWidgetItem *p_item, Node *p_node, int p_level) const;
 
         void fillTreeItem(QTreeWidgetItem *p_item, Node *p_node, bool p_loaded) const;
 
-        QIcon getNodeItemIcon(const Node *p_node) const;
+        void fillTreeItem(QTreeWidgetItem *p_item, const QSharedPointer<ExternalNode> &p_node) const;
+
+        const QIcon &getNodeItemIcon(const Node *p_node) const;
+
+        const QIcon &getNodeItemIcon(const ExternalNode *p_node) const;
 
         void initNodeIcons() const;
 
@@ -177,7 +194,7 @@ namespace vnotex
 
         void createContextMenuOnNode(QMenu *p_menu, const Node *p_node);
 
-        void createContextMenuOnAttachment(QMenu *p_menu, const QString &p_name);
+        void createContextMenuOnExternalNode(QMenu *p_menu, const ExternalNode *p_node);
 
         // Factory function to create action.
         QAction *createAction(Action p_act, QObject *p_parent);
@@ -186,8 +203,7 @@ namespace vnotex
 
         void pasteNodesFromClipboard();
 
-        // Only return selected Nodes.
-        QVector<Node *> getSelectedNodes() const;
+        QPair<QVector<Node *>, QVector<ExternalNode *>> getSelectedNodes() const;
 
         void removeSelectedNodes(bool p_skipRecycleBin);
 
@@ -226,6 +242,16 @@ namespace vnotex
         // Sort nodes in config file.
         void manualSort();
 
+        void openSelectedNodes();
+
+        QSharedPointer<Node> importToIndex(const ExternalNode *p_node);
+
+        void importToIndex(const QVector<ExternalNode *> &p_nodes);
+
+        // Check whether @p_node is a valid node. Will notify user.
+        // Return true if it is invalid.
+        bool checkInvalidNode(const Node *p_node) const;
+
         static NotebookNodeExplorer::NodeData getItemNodeData(const QTreeWidgetItem *p_item);
 
         static void setItemNodeData(QTreeWidgetItem *p_item, const NodeData &p_data);
@@ -242,11 +268,25 @@ namespace vnotex
 
         bool m_recycleBinNodeVisible = false;
 
+        int m_viewOrder = ViewOrder::OrderedByConfiguration;
+
+        bool m_externalFilesVisible = true;
+
+        bool m_autoImportExternalFiles = true;
+
         static QIcon s_folderNodeIcon;
+
         static QIcon s_fileNodeIcon;
+
+        static QIcon s_invalidFolderNodeIcon;
+
+        static QIcon s_invalidFileNodeIcon;
+
         static QIcon s_recycleBinNodeIcon;
 
-        static const QString c_nodeIconForegroundName;
+        static QIcon s_externalFolderNodeIcon;
+
+        static QIcon s_externalFileNodeIcon;
     };
 }
 

+ 5 - 0
src/widgets/titlebar.cpp

@@ -127,6 +127,11 @@ QAction *TitleBar::addMenuAction(const QString &p_iconName, const QString &p_tex
     return act;
 }
 
+QMenu *TitleBar::addMenuSubMenu(const QString &p_text)
+{
+    return m_menu->addMenu(p_text);
+}
+
 void TitleBar::addMenuSeparator()
 {
     Q_ASSERT(m_menu);

+ 13 - 0
src/widgets/titlebar.h

@@ -39,6 +39,11 @@ namespace vnotex
         template <typename Functor>
         QAction *addMenuAction(const QString &p_text, const QObject *p_context, Functor p_functor);
 
+        template <typename Functor>
+        QAction *addMenuAction(QMenu *p_subMenu, const QString &p_text, const QObject *p_context, Functor p_functor);
+
+        QMenu *addMenuSubMenu(const QString &p_text);
+
         void addMenuSeparator();
 
     protected:
@@ -91,6 +96,14 @@ namespace vnotex
         auto act = m_menu->addAction(p_text, p_context, p_functor);
         return act;
     }
+
+    template <typename Functor>
+    QAction *TitleBar::addMenuAction(QMenu *p_subMenu, const QString &p_text, const QObject *p_context, Functor p_functor)
+    {
+        Q_ASSERT(p_subMenu->parent() == m_menu);
+        auto act = p_subMenu->addAction(p_text, p_context, p_functor);
+        return act;
+    }
 } // ns vnotex
 
 #endif // TITLEBAR_H