Explorar o código

TagExplorer: add explorer for tags

Le Tan %!s(int64=7) %!d(string=hai) anos
pai
achega
13c2d143bb

+ 11 - 0
src/resources/icons/tag.svg

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
+<g>
+	<path fill="#000000" d="M464,32H304L48,320l160,160l256-288V32z M448,184L208.125,456L72.062,320L311.587,48H448V184z"/>
+	<path fill="#000000" d="M368,160c17.645,0,32-14.355,32-32s-14.355-32-32-32s-32,14.355-32,32S350.355,160,368,160z M368,112
+		c8.836,0,16,7.163,16,16s-7.164,16-16,16s-16-7.163-16-16S359.164,112,368,112z"/>
+</g>
+</svg>

+ 12 - 0
src/resources/icons/tag_explorer.svg

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
+<g>
+	<path fill="#000000" d="M448,64V32H288L32,320l160,160l23.471-23.904L240,480l240-272V64H448z M192,457.371L54.39,320L294.621,48H432v16v16
+		v105.377l-216.555,247.99l-11.34,11.363L192,457.371z M464,201.377L240,457.371l-13.182-12.65L448,192V80h16V201.377z"/>
+	<path fill="#000000" d="M352,160c17.645,0,32-14.355,32-32s-14.355-32-32-32s-32,14.355-32,32S334.355,160,352,160z M352,112
+		c8.836,0,16,7.163,16,16s-7.164,16-16,16s-16-7.163-16-16S343.164,112,352,112z"/>
+</g>
+</svg>

+ 4 - 2
src/src.pro

@@ -139,7 +139,8 @@ SOURCES += main.cpp\
     utils/vprocessutils.cpp \
     vtagpanel.cpp \
     valltagspanel.cpp \
-    vtaglabel.cpp
+    vtaglabel.cpp \
+    vtagexplorer.cpp
 
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
@@ -272,7 +273,8 @@ HEADERS  += vmainwindow.h \
     utils/vprocessutils.h \
     vtagpanel.h \
     valltagspanel.h \
-    vtaglabel.h
+    vtaglabel.h \
+    vtagexplorer.h
 
 RESOURCES += \
     vnote.qrc \

+ 16 - 0
src/vconfigmanager.h

@@ -194,6 +194,9 @@ public:
     const QByteArray getNotebookSplitterState() const;
     void setNotebookSplitterState(const QByteArray &p_state);
 
+    const QByteArray getTagExplorerSplitterState() const;
+    void setTagExplorerSplitterState(const QByteArray &p_state);
+
     bool getFindCaseSensitive() const;
     void setFindCaseSensitive(bool p_enabled);
 
@@ -1304,6 +1307,19 @@ inline void VConfigManager::setNotebookSplitterState(const QByteArray &p_state)
                                p_state);
 }
 
+inline const QByteArray VConfigManager::getTagExplorerSplitterState() const
+{
+    return getConfigFromSessionSettings("geometry",
+                                        "tag_explorer_splitter_state").toByteArray();
+}
+
+inline void VConfigManager::setTagExplorerSplitterState(const QByteArray &p_state)
+{
+    setConfigToSessionSettings("geometry",
+                               "tag_explorer_splitter_state",
+                               p_state);
+}
+
 inline bool VConfigManager::getFindCaseSensitive() const
 {
     return m_findCaseSensitive;

+ 1 - 1
src/vhistorylist.cpp

@@ -460,7 +460,7 @@ void VHistoryList::openItem(const QListWidgetItem *p_item) const
     g_mainWin->openFiles(files);
 }
 
-void VHistoryList::locateCurrentItem()
+void VHistoryList::locateCurrentItem() const
 {
     auto item = m_itemList->currentItem();
     if (!item) {

+ 1 - 1
src/vhistorylist.h

@@ -48,7 +48,7 @@ private slots:
 
     void unpinSelectedItems();
 
-    void locateCurrentItem();
+    void locateCurrentItem() const;
 
     // Add selected files to Cart.
     void addFileToCart() const;

+ 13 - 4
src/vmainwindow.cpp

@@ -48,6 +48,7 @@
 #include "vhistorylist.h"
 #include "vexplorer.h"
 #include "vlistue.h"
+#include "vtagexplorer.h"
 
 extern VConfigManager *g_config;
 
@@ -291,6 +292,13 @@ void VMainWindow::setupNaviBox()
     m_naviBox->addItem(m_explorer,
                        ":/resources/icons/explorer.svg",
                        tr("Explorer"));
+
+    m_tagExplorer = new VTagExplorer();
+    m_naviBox->addItem(m_tagExplorer,
+                       ":/resources/icons/tag_explorer.svg",
+                       tr("Tags"));
+    connect(m_notebookSelector, &VNotebookSelector::curNotebookChanged,
+            m_tagExplorer, &VTagExplorer::setNotebook);
 }
 
 void VMainWindow::setupNotebookPanel()
@@ -2177,17 +2185,18 @@ void VMainWindow::saveStateAndGeometry()
     g_config->setSearchDockChecked(m_searchDock->isVisible());
     g_config->setNotebookSplitterState(m_nbSplitter->saveState());
     g_config->setMainSplitterState(m_mainSplitter->saveState());
+    m_tagExplorer->saveStateAndGeometry();
     g_config->setNaviBoxCurrentIndex(m_naviBox->currentIndex());
 }
 
 void VMainWindow::restoreStateAndGeometry()
 {
-    const QByteArray &geometry = g_config->getMainWindowGeometry();
+    const QByteArray geometry = g_config->getMainWindowGeometry();
     if (!geometry.isEmpty()) {
         restoreGeometry(geometry);
     }
 
-    const QByteArray &state = g_config->getMainWindowState();
+    const QByteArray state = g_config->getMainWindowState();
     if (!state.isEmpty()) {
         restoreState(state);
     }
@@ -2195,12 +2204,12 @@ void VMainWindow::restoreStateAndGeometry()
     m_toolDock->setVisible(g_config->getToolsDockChecked());
     m_searchDock->setVisible(g_config->getSearchDockChecked());
 
-    const QByteArray &splitterState = g_config->getMainSplitterState();
+    const QByteArray splitterState = g_config->getMainSplitterState();
     if (!splitterState.isEmpty()) {
         m_mainSplitter->restoreState(splitterState);
     }
 
-    const QByteArray &nbSplitterState = g_config->getNotebookSplitterState();
+    const QByteArray nbSplitterState = g_config->getNotebookSplitterState();
     if (!nbSplitterState.isEmpty()) {
         m_nbSplitter->restoreState(nbSplitterState);
     }

+ 3 - 0
src/vmainwindow.h

@@ -44,6 +44,7 @@ class QPrinter;
 class VUniversalEntry;
 class VHistoryList;
 class VExplorer;
+class VTagExplorer;
 
 enum class PanelViewState
 {
@@ -462,6 +463,8 @@ private:
 
     VExplorer *m_explorer;
 
+    VTagExplorer *m_tagExplorer;
+
     // Interval of the shared memory timer in ms.
     static const int c_sharedMemTimerInterval;
 };

+ 2 - 0
src/vnote.qrc

@@ -259,5 +259,7 @@
         <file>resources/themes/v_detorte/v_detorte_codeblock.css</file>
         <file>resources/themes/v_detorte/v_detorte_mermaid.css</file>
         <file>resources/icons/tags.svg</file>
+        <file>resources/icons/tag_explorer.svg</file>
+        <file>resources/icons/tag.svg</file>
     </qresource>
 </RCC>

+ 15 - 0
src/vnotebook.cpp

@@ -442,3 +442,18 @@ bool VNotebook::addTag(const QString &p_tag)
 
     return true;
 }
+
+void VNotebook::removeTag(const QString &p_tag)
+{
+    if (p_tag.isEmpty() || m_tags.isEmpty()) {
+        return;
+    }
+
+    int nr = m_tags.removeAll(p_tag);
+    if (nr > 0) {
+        if (!writeConfigNotebook()) {
+            qWarning() << "fail to update config of notebook" << m_name
+                       << "in directory" << m_path;
+        }
+    }
+}

+ 2 - 0
src/vnotebook.h

@@ -60,6 +60,8 @@ public:
 
     bool addTag(const QString &p_tag);
 
+    void removeTag(const QString &p_tag);
+
     bool hasTag(const QString &p_tag) const;
 
     static VNotebook *createNotebook(const QString &p_name,

+ 497 - 0
src/vtagexplorer.cpp

@@ -0,0 +1,497 @@
+#include "vtagexplorer.h"
+
+#include <QtWidgets>
+
+#include "utils/viconutils.h"
+#include "vmainwindow.h"
+#include "vlistwidget.h"
+#include "vnotebook.h"
+#include "vconfigmanager.h"
+#include "vsearch.h"
+#include "vnote.h"
+#include "vcart.h"
+#include "vhistorylist.h"
+#include "vnotefile.h"
+#include "utils/vutils.h"
+
+extern VMainWindow *g_mainWin;
+
+extern VConfigManager *g_config;
+
+extern VNote *g_vnote;
+
+#define MAX_DISPLAY_LENGTH 10
+
+VTagExplorer::VTagExplorer(QWidget *p_parent)
+    : QWidget(p_parent),
+      m_uiInitialized(false),
+      m_notebook(NULL),
+      m_notebookChanged(true),
+      m_search(NULL)
+{
+}
+
+void VTagExplorer::setupUI()
+{
+    if (m_uiInitialized) {
+        return;
+    }
+
+    m_uiInitialized = true;
+
+    m_notebookLabel = new QLabel(tr("Tags"), this);
+    m_notebookLabel->setProperty("TitleLabel", true);
+
+    m_tagList = new VListWidget(this);
+    m_tagList->setAttribute(Qt::WA_MacShowFocusRect, false);
+    connect(m_tagList, &QListWidget::itemActivated,
+            this, [this](const QListWidgetItem *p_item) {
+                QString tag;
+                if (p_item) {
+                    tag = p_item->text();
+                }
+
+                bool ret = activateTag(tag);
+                if (ret && !tag.isEmpty() && m_fileList->count() == 0) {
+                    promptToRemoveEmptyTag(tag);
+                }
+            });
+
+    m_tagLabel = new QLabel(tr("Notes"), this);
+    m_tagLabel->setProperty("TitleLabel", true);
+
+    m_fileList = new VListWidget(this);
+    m_fileList->setAttribute(Qt::WA_MacShowFocusRect, false);
+    m_fileList->setContextMenuPolicy(Qt::CustomContextMenu);
+    m_fileList->setSelectionMode(QAbstractItemView::ExtendedSelection);
+    connect(m_fileList, &QListWidget::itemActivated,
+            this, &VTagExplorer::openFileItem);
+    connect(m_fileList, &QListWidget::customContextMenuRequested,
+            this, &VTagExplorer::handleFileListContextMenuRequested);
+
+    QWidget *fileWidget = new QWidget(this);
+    QVBoxLayout *fileLayout = new QVBoxLayout();
+    fileLayout->addWidget(m_tagLabel);
+    fileLayout->addWidget(m_fileList);
+    fileLayout->setContentsMargins(0, 0, 0, 0);
+    fileWidget->setLayout(fileLayout);
+
+    m_splitter = new QSplitter(this);
+    m_splitter->setOrientation(Qt::Vertical);
+    m_splitter->setObjectName("TagExplorerSplitter");
+    m_splitter->addWidget(m_tagList);
+    m_splitter->addWidget(fileWidget);
+    m_splitter->setStretchFactor(0, 0);
+    m_splitter->setStretchFactor(1, 1);
+
+    QVBoxLayout *mainLayout = new QVBoxLayout();
+    mainLayout->addWidget(m_notebookLabel);
+    mainLayout->addWidget(m_splitter);
+    mainLayout->setContentsMargins(0, 0, 0, 0);
+
+    setLayout(mainLayout);
+
+    restoreStateAndGeometry();
+}
+
+void VTagExplorer::showEvent(QShowEvent *p_event)
+{
+    setupUI();
+    QWidget::showEvent(p_event);
+
+    updateContent();
+}
+
+void VTagExplorer::focusInEvent(QFocusEvent *p_event)
+{
+    setupUI();
+    QWidget::focusInEvent(p_event);
+    m_tagList->setFocus();
+}
+
+void VTagExplorer::setNotebook(VNotebook *p_notebook)
+{
+    if (p_notebook == m_notebook) {
+        return;
+    }
+
+    setupUI();
+
+    m_notebook = p_notebook;
+    m_notebookChanged = true;
+
+    if (!isVisible()) {
+        return;
+    }
+
+    updateContent();
+}
+
+void VTagExplorer::updateContent()
+{
+    if (m_notebook) {
+        updateNotebookLabel();
+        const QStringList &tags = m_notebook->getTags();
+        if (m_notebookChanged || tagListObsolete(tags)) {
+            updateTagList(tags);
+        }
+    } else {
+        clear();
+    }
+
+    m_notebookChanged = false;
+}
+
+void VTagExplorer::clear()
+{
+    setupUI();
+
+    m_fileList->clearAll();
+    m_tagList->clearAll();
+    updateTagLabel("");
+    updateNotebookLabel();
+}
+
+void VTagExplorer::updateNotebookLabel()
+{
+    QString text = tr("Tags");
+    QString tooltip;
+    if (m_notebook) {
+        QString name = m_notebook->getName();
+        tooltip = name;
+
+        if (name.size() > MAX_DISPLAY_LENGTH) {
+            name = name.left(MAX_DISPLAY_LENGTH) + QStringLiteral("...");
+        }
+
+        text = tr("Tags (%1)").arg(name);
+    }
+
+    m_notebookLabel->setText(text);
+    m_notebookLabel->setToolTip(tooltip);
+}
+
+bool VTagExplorer::tagListObsolete(const QStringList &p_tags) const
+{
+    if (m_tagList->count() != p_tags.size()) {
+        return true;
+    }
+
+    for (int i = 0; i < p_tags.size(); ++i) {
+        if (p_tags[i] != m_tagList->item(i)->text()) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+void VTagExplorer::updateTagLabel(const QString &p_tag)
+{
+    QString text = tr("Notes");
+    QString tooltip;
+    if (!p_tag.isEmpty()) {
+        QString name = p_tag;
+        tooltip = name;
+
+        if (name.size() > MAX_DISPLAY_LENGTH) {
+            name = name.left(MAX_DISPLAY_LENGTH) + QStringLiteral("...");
+        }
+
+        text = tr("Notes (%1)").arg(name);
+    }
+
+    m_tagLabel->setText(text);
+    m_tagLabel->setToolTip(tooltip);
+}
+
+bool VTagExplorer::activateTag(const QString &p_tag)
+{
+    updateTagLabel(p_tag);
+
+    m_fileList->clearAll();
+
+    if (p_tag.isEmpty()) {
+        return false;
+    }
+
+    // Search this tag within current notebook.
+    g_mainWin->showStatusMessage(tr("Searching for tag \"%1\"").arg(p_tag));
+
+    QVector<VNotebook *> notebooks;
+    notebooks.append(m_notebook);
+    getVSearch()->clear();
+    QSharedPointer<VSearchConfig> config(new VSearchConfig(VSearchConfig::CurrentNotebook,
+                                                           VSearchConfig::Tag,
+                                                           VSearchConfig::Note,
+                                                           VSearchConfig::Internal,
+                                                           VSearchConfig::NoneOption,
+                                                           p_tag,
+                                                           QString()));
+    getVSearch()->setConfig(config);
+    QSharedPointer<VSearchResult> result = getVSearch()->search(notebooks);
+
+    bool ret = result->m_state == VSearchState::Success;
+    handleSearchFinished(result);
+
+    return ret;
+}
+
+void VTagExplorer::updateTagList(const QStringList &p_tags)
+{
+    // Clear.
+    m_tagList->clearAll();
+    activateTag("");
+
+    for (auto const & tag : p_tags) {
+        addTagItem(tag);
+    }
+
+    if (m_tagList->count() > 0) {
+        m_tagList->setCurrentRow(0, QItemSelectionModel::ClearAndSelect);
+    }
+}
+
+void VTagExplorer::addTagItem(const QString &p_tag)
+{
+    QListWidgetItem *item = new QListWidgetItem(VIconUtils::treeViewIcon(":/resources/icons/tag.svg"),
+                                                p_tag);
+    item->setToolTip(p_tag);
+
+    m_tagList->addItem(item);
+}
+
+void VTagExplorer::saveStateAndGeometry()
+{
+    if (!m_uiInitialized) {
+        return;
+    }
+
+    g_config->setTagExplorerSplitterState(m_splitter->saveState());
+}
+
+void VTagExplorer::restoreStateAndGeometry()
+{
+    const QByteArray state = g_config->getTagExplorerSplitterState();
+    if (!state.isEmpty()) {
+        m_splitter->restoreState(state);
+    }
+}
+
+void VTagExplorer::initVSearch()
+{
+    m_search = new VSearch(this);
+    connect(m_search, &VSearch::resultItemAdded,
+            this, &VTagExplorer::handleSearchItemAdded);
+    connect(m_search, &VSearch::resultItemsAdded,
+            this, &VTagExplorer::handleSearchItemsAdded);
+    connect(m_search, &VSearch::finished,
+            this, &VTagExplorer::handleSearchFinished);
+
+    m_noteIcon = VIconUtils::treeViewIcon(":/resources/icons/note_item.svg");
+}
+
+void VTagExplorer::handleSearchItemAdded(const QSharedPointer<VSearchResultItem> &p_item)
+{
+    appendItemToFileList(p_item);
+}
+
+void VTagExplorer::appendItemToFileList(const QSharedPointer<VSearchResultItem> &p_item)
+{
+    Q_ASSERT(p_item->m_type == VSearchResultItem::Note);
+
+    QListWidgetItem *item = new QListWidgetItem(m_noteIcon,
+                                                p_item->m_text.isEmpty() ? p_item->m_path : p_item->m_text);
+    item->setData(Qt::UserRole, p_item->m_path);
+    item->setToolTip(p_item->m_path);
+    m_fileList->addItem(item);
+}
+
+void VTagExplorer::handleSearchItemsAdded(const QList<QSharedPointer<VSearchResultItem> > &p_items)
+{
+    for (auto const & it : p_items) {
+        appendItemToFileList(it);
+    }
+}
+
+void VTagExplorer::handleSearchFinished(const QSharedPointer<VSearchResult> &p_result)
+{
+    Q_ASSERT(p_result->m_state != VSearchState::Idle);
+    QString msg;
+    switch (p_result->m_state) {
+    case VSearchState::Busy:
+        // Only synchronized search.
+        Q_ASSERT(false);
+        msg = tr("Invalid busy state when searching for tag");
+        break;
+
+    case VSearchState::Success:
+        qDebug() << "search succeeded";
+        msg = tr("Search for tag succeeded");
+        break;
+
+    case VSearchState::Fail:
+        qDebug() << "search failed";
+        msg = tr("Search for tag failed");
+        break;
+
+    case VSearchState::Cancelled:
+        qDebug() << "search cancelled";
+        msg = tr("Search for tag calcelled");
+        break;
+
+    default:
+        break;
+    }
+
+    m_search->clear();
+
+    if (!msg.isEmpty()) {
+        g_mainWin->showStatusMessage(msg);
+    }
+}
+
+void VTagExplorer::openFileItem(QListWidgetItem *p_item) const
+{
+    if (!p_item) {
+        return;
+    }
+
+    QStringList files;
+    files << getFilePath(p_item);
+    g_mainWin->openFiles(files);
+}
+
+void VTagExplorer::openSelectedFileItems() const
+{
+    QStringList files;
+    QList<QListWidgetItem *> selectedItems = m_fileList->selectedItems();
+    for (auto it : selectedItems) {
+        files << getFilePath(it);
+    }
+
+    if (!files.isEmpty()) {
+        g_mainWin->openFiles(files);
+    }
+}
+
+QString VTagExplorer::getFilePath(const QListWidgetItem *p_item) const
+{
+    return p_item->data(Qt::UserRole).toString();
+}
+
+void VTagExplorer::handleFileListContextMenuRequested(QPoint p_pos)
+{
+    QListWidgetItem *item = m_fileList->itemAt(p_pos);
+    if (!item) {
+        return;
+    }
+
+    QMenu menu(this);
+    menu.setToolTipsVisible(true);
+
+    QAction *openAct = new QAction(tr("&Open"), &menu);
+    openAct->setToolTip(tr("Open selected notes"));
+    connect(openAct, &QAction::triggered,
+            this, &VTagExplorer::openSelectedFileItems);
+    menu.addAction(openAct);
+
+    QList<QListWidgetItem *> selectedItems = m_fileList->selectedItems();
+    if (selectedItems.size() == 1) {
+        QAction *locateAct = new QAction(VIconUtils::menuIcon(":/resources/icons/locate_note.svg"),
+                                         tr("&Locate To Folder"),
+                                         &menu);
+        locateAct->setToolTip(tr("Locate the folder of current note"));
+        connect(locateAct, &QAction::triggered,
+                this, &VTagExplorer::locateCurrentFileItem);
+        menu.addAction(locateAct);
+    }
+
+    menu.addSeparator();
+
+    QAction *addToCartAct = new QAction(VIconUtils::menuIcon(":/resources/icons/cart.svg"),
+                                        tr("Add To Cart"),
+                                        &menu);
+    addToCartAct->setToolTip(tr("Add selected notes to Cart for further processing"));
+    connect(addToCartAct, &QAction::triggered,
+            this, &VTagExplorer::addFileToCart);
+    menu.addAction(addToCartAct);
+
+    QAction *pinToHistoryAct = new QAction(VIconUtils::menuIcon(":/resources/icons/pin.svg"),
+                                           tr("Pin To History"),
+                                           &menu);
+    pinToHistoryAct->setToolTip(tr("Pin selected notes to History"));
+    connect(pinToHistoryAct, &QAction::triggered,
+            this, &VTagExplorer::pinFileToHistory);
+    menu.addAction(pinToHistoryAct);
+
+    menu.exec(m_fileList->mapToGlobal(p_pos));
+}
+
+void VTagExplorer::locateCurrentFileItem() const
+{
+    auto item = m_fileList->currentItem();
+    if (!item) {
+        return;
+    }
+
+    VFile *file = g_vnote->getInternalFile(getFilePath(item));
+    if (file) {
+        g_mainWin->locateFile(file);
+    }
+}
+
+void VTagExplorer::addFileToCart() const
+{
+    QList<QListWidgetItem *> items = m_fileList->selectedItems();
+    VCart *cart = g_mainWin->getCart();
+
+    for (int i = 0; i < items.size(); ++i) {
+        cart->addFile(getFilePath(items[i]));
+    }
+
+    g_mainWin->showStatusMessage(tr("%1 %2 added to Cart")
+                                   .arg(items.size())
+                                   .arg(items.size() > 1 ? tr("notes") : tr("note")));
+}
+
+void VTagExplorer::pinFileToHistory() const
+{
+    QList<QListWidgetItem *> items = m_fileList->selectedItems();
+
+    QStringList files;
+    for (int i = 0; i < items.size(); ++i) {
+        files << getFilePath(items[i]);
+    }
+
+    g_mainWin->getHistoryList()->pinFiles(files);
+
+    g_mainWin->showStatusMessage(tr("%1 %2 pinned to History")
+                                   .arg(items.size())
+                                   .arg(items.size() > 1 ? tr("notes") : tr("note")));
+}
+
+void VTagExplorer::promptToRemoveEmptyTag(const QString &p_tag)
+{
+    Q_ASSERT(!p_tag.isEmpty());
+
+    int ret = VUtils::showMessage(QMessageBox::Warning,
+                                  tr("Warning"),
+                                  tr("Empty tag detected! Do you want to remove it?"),
+                                  tr("The tag <span style=\"%1\">%2</span> seems not to "
+                                     "be assigned to any note currently.")
+                                    .arg(g_config->c_dataTextStyle)
+                                    .arg(p_tag),
+                                  QMessageBox::Ok | QMessageBox::Cancel,
+                                  QMessageBox::Cancel,
+                                  this,
+                                  MessageBoxType::Danger);
+
+    if (ret == QMessageBox::Cancel) {
+        return;
+    }
+
+    // Remove the tag from m_notebook.
+    m_notebook->removeTag(p_tag);
+    updateContent();
+}

+ 113 - 0
src/vtagexplorer.h

@@ -0,0 +1,113 @@
+#ifndef VTAGEXPLORER_H
+#define VTAGEXPLORER_H
+
+#include <QWidget>
+#include <QIcon>
+
+#include "vsearchconfig.h"
+
+class QLabel;
+class VListWidget;
+class QListWidgetItem;
+class QSplitter;
+class VNotebook;
+class VSearch;
+
+class VTagExplorer : public QWidget
+{
+    Q_OBJECT
+public:
+    explicit VTagExplorer(QWidget *p_parent = nullptr);
+
+    void clear();
+
+    void setNotebook(VNotebook *p_notebook);
+
+    void saveStateAndGeometry();
+
+protected:
+    void showEvent(QShowEvent *p_event) Q_DECL_OVERRIDE;
+
+    void focusInEvent(QFocusEvent *p_event) Q_DECL_OVERRIDE;
+
+private slots:
+    void handleSearchItemAdded(const QSharedPointer<VSearchResultItem> &p_item);
+
+    void handleSearchItemsAdded(const QList<QSharedPointer<VSearchResultItem> > &p_items);
+
+    void handleSearchFinished(const QSharedPointer<VSearchResult> &p_result);
+
+    void openFileItem(QListWidgetItem *p_item) const;
+
+    void openSelectedFileItems() const;
+
+    void locateCurrentFileItem() const;
+
+    void addFileToCart() const;
+
+    void pinFileToHistory() const;
+
+    void handleFileListContextMenuRequested(QPoint p_pos);
+
+private:
+    void setupUI();
+
+    void updateNotebookLabel();
+
+    void updateTagLabel(const QString &p_tag);
+
+    bool tagListObsolete(const QStringList &p_tags) const;
+
+    void updateTagList(const QStringList &p_tags);
+
+    void updateContent();
+
+    // Return ture if succeeded.
+    bool activateTag(const QString &p_tag);
+
+    void addTagItem(const QString &p_tag);
+
+    void restoreStateAndGeometry();
+
+    VSearch *getVSearch() const;
+
+    void initVSearch();
+
+    void appendItemToFileList(const QSharedPointer<VSearchResultItem> &p_item);
+
+    QString getFilePath(const QListWidgetItem *p_item) const;
+
+    void promptToRemoveEmptyTag(const QString &p_tag);
+
+    bool m_uiInitialized;
+
+    QLabel *m_notebookLabel;
+
+    QLabel *m_tagLabel;
+
+    VListWidget *m_tagList;
+
+    VListWidget *m_fileList;
+
+    QSplitter *m_splitter;
+
+    VNotebook *m_notebook;
+
+    bool m_notebookChanged;
+
+    QIcon m_noteIcon;
+
+    VSearch *m_search;
+};
+
+inline VSearch *VTagExplorer::getVSearch() const
+{
+    if (m_search) {
+        return m_search;
+    }
+
+    const_cast<VTagExplorer *>(this)->initVSearch();
+    return m_search;
+}
+
+#endif // VTAGEXPLORER_H